pax_global_header00006660000000000000000000000064147730335020014516gustar00rootroot0000000000000052 comment=157b19a29f11a5973270a14008360492c972e583 liquidsoap-2.3.2/000077500000000000000000000000001477303350200136745ustar00rootroot00000000000000liquidsoap-2.3.2/.codespellignore000066400000000000000000000001561477303350200170550ustar00rootroot00000000000000als ans fo hda mot nd ot ro ser writen WORS # https://github.com/codespell-project/codespell/issues/2508 nwe liquidsoap-2.3.2/.gitattributes000066400000000000000000000000721477303350200165660ustar00rootroot00000000000000doc/content/build.md binary doc/content/install.md binary liquidsoap-2.3.2/.github/000077500000000000000000000000001477303350200152345ustar00rootroot00000000000000liquidsoap-2.3.2/.github/FUNDING.yml000066400000000000000000000013541477303350200170540ustar00rootroot00000000000000# These are supported funding model platforms github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: ["http://paypal.me/LiquidsoapMedia"] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] liquidsoap-2.3.2/.github/ISSUE_TEMPLATE/000077500000000000000000000000001477303350200174175ustar00rootroot00000000000000liquidsoap-2.3.2/.github/ISSUE_TEMPLATE/bug_report.yml000066400000000000000000000252641477303350200223230ustar00rootroot00000000000000name: Bug report description: Document issues or problems encountered in the software to facilitate resolution and improve functionality. labels: - bug body: - type: textarea id: description attributes: label: Description description: A clear and concise description of what the bug is. validations: required: true - type: textarea id: reproduction attributes: label: Steps to reproduce description: A minimal step-by-step instructions to reproduce the bug. validations: required: true - type: textarea id: expected-behavior attributes: label: Expected behavior description: A clear and concise description of what you expected to happen. validations: required: true - type: textarea id: log attributes: label: Log extracts description: | Please provide extracts from your logs. Make sure to set `log.level := 4`. validations: required: false - type: textarea id: script attributes: label: Script extracts description: | Please provide extracts from your liquidsoap script. validations: required: false - type: textarea id: version attributes: label: Liquidsoap version description: Output of `liquidsoap --version`. render: text placeholder: | Liquidsoap 2.2.5 Copyright (c) 2003-2024 Savonet team Liquidsoap is open-source software, released under GNU General Public License. See for more information. validations: required: true - type: textarea id: build-config attributes: label: Liquidsoap build config description: Output of `liquidsoap --build-config`. render: text placeholder: | * Liquidsoap version : 2.2.5 * Compilation options - Release build : true - Git SHA : (none) - OCaml version : 4.14.1 - OS type : Unix - Libs versions : alsa=0.3.0 angstrom=0.15.0 ao=0.2.4 asetmap=0.8.1 asn1-combinators=0.2.6 astring=0.8.5 base64=3.5.1 bigarray=[distributed with Ocaml] bigarray-compat=1.1.0 bigstringaf=0.9.1 bjack=0.1.6 bos=0.2.1 bytes=[distributed with OCaml 4.02 or above] ca-certs=v0.2.3 camlimages.all_formats=4.2.6 camlimages.core=5.0.4 camlimages.exif=5.0.4 camlimages.gif=5.0.4 camlimages.jpeg=5.0.4 camlimages.png=5.0.4 camlimages.tiff=5.0.4 camlimages.xpm=5.0.4 camlp-streams camomile.lib=2.0 cohttp=5.3.0 cohttp-lwt=5.3.0 cohttp-lwt-unix=5.3.0 conduit=6.2.0 conduit-lwt=6.2.0 conduit-lwt-unix=6.2.0 cry=1.0.3 cstruct=6.2.0 ctypes=0.21.1 ctypes-foreign=0.21.1 ctypes.stubs=0.21.1 curl=0.9.2 domain-name=0.4.0 domain_shims dssi=0.1.5 dtools=0.4.5 dune-build-info=3.11.1 dune-private-libs.dune-section=3.11.1 dune-site=3.11.1 dune-site.private=3.11.1 duppy=0.9.4 eqaf=0.9 eqaf.bigstring=0.9 eqaf.cstruct=0.9 faad=0.5.2 fdkaac=0.3.3 ffmpeg-av=1.1.10 ffmpeg-avcodec=1.1.10 ffmpeg-avdevice=1.1.10 ffmpeg-avfilter=1.1.10 ffmpeg-avutil=1.1.10 ffmpeg-swresample=1.1.10 ffmpeg-swscale=1.1.10 fileutils=0.6.4 flac=0.5.1 flac.decoder=0.5.1 flac.ogg=0.5.1 fmt=0.9.0 fpath=0.7.3 frei0r=0.1.2 gd=1.0a5 gen=1.1 gmap=0.3.0 hkdf=1.0.4 inotify=2.4.1 integers ipaddr=5.5.0 ipaddr-sexp=5.5.0 ipaddr.unix=5.5.0 irc-client irc-client-unix jemalloc ladspa=0.2.2 lame=0.3.7 lastfm=0.3.4 lilv=0.1.0 liquidsoap-lang=2.2.5 liquidsoap-lang.console=2.2.5 liquidsoap_alsa=f0fdb0e-dirty liquidsoap_ao=f0fdb0e-dirty liquidsoap_bjack=f0fdb0e-dirty liquidsoap_builtins=f0fdb0e-dirty liquidsoap_camlimages=f0fdb0e-dirty liquidsoap_core=f0fdb0e-dirty liquidsoap_dssi=f0fdb0e-dirty liquidsoap_faad=f0fdb0e-dirty liquidsoap_fdkaac=f0fdb0e-dirty liquidsoap_ffmpeg=f0fdb0e-dirty liquidsoap_flac=f0fdb0e-dirty liquidsoap_frei0r=f0fdb0e-dirty liquidsoap_gd=f0fdb0e-dirty liquidsoap_irc=f0fdb0e-dirty liquidsoap_jemalloc=f0fdb0e-dirty liquidsoap_ladspa=f0fdb0e-dirty liquidsoap_lame=f0fdb0e-dirty liquidsoap_lastfm=f0fdb0e-dirty liquidsoap_lilv=f0fdb0e-dirty liquidsoap_lo=f0fdb0e-dirty liquidsoap_mad=f0fdb0e-dirty liquidsoap_mem_usage=f0fdb0e-dirty liquidsoap_memtrace=f0fdb0e-dirty liquidsoap_ogg=f0fdb0e-dirty liquidsoap_ogg_flac=f0fdb0e-dirty liquidsoap_optionals=f0fdb0e-dirty liquidsoap_opus=f0fdb0e-dirty liquidsoap_osc=f0fdb0e-dirty liquidsoap_oss=f0fdb0e-dirty liquidsoap_portaudio=f0fdb0e-dirty liquidsoap_posix_time=f0fdb0e-dirty liquidsoap_prometheus=f0fdb0e-dirty liquidsoap_pulseaudio=f0fdb0e-dirty liquidsoap_runtime=f0fdb0e-dirty liquidsoap_samplerate=f0fdb0e-dirty liquidsoap_sdl=f0fdb0e-dirty liquidsoap_shine=f0fdb0e-dirty liquidsoap_soundtouch=f0fdb0e-dirty liquidsoap_speex=f0fdb0e-dirty liquidsoap_srt=f0fdb0e-dirty liquidsoap_ssl=f0fdb0e-dirty liquidsoap_stereotool=f0fdb0e-dirty liquidsoap_taglib=f0fdb0e-dirty liquidsoap_theora=f0fdb0e-dirty liquidsoap_tls=f0fdb0e-dirty liquidsoap_vorbis=f0fdb0e-dirty liquidsoap_xmlplaylist=f0fdb0e-dirty liquidsoap_yaml=f0fdb0e-dirty lo=0.2.0 logs=0.7.0 logs.fmt=0.7.0 logs.lwt=0.7.0 lwt=5.7.0 lwt.unix=5.7.0 macaddr=5.5.0 mad=0.5.3 magic-mime=1.3.1 mem_usage=0.1.1 memtrace=0.2.3 menhirLib=20230608 metadata=0.3.0 mirage-crypto=0.11.2 mirage-crypto-ec=0.11.2 mirage-crypto-pk=0.11.2 mirage-crypto-rng=0.11.2 mirage-crypto-rng.unix=0.11.2 mm=0.8.5 mm.audio=0.8.5 mm.base=0.8.5 mm.image=0.8.5 mm.midi=0.8.5 mm.video=0.8.5 ocplib-endian ocplib-endian.bigstring ogg=0.7.4 ogg.decoder=0.7.4 opus=0.2.3 opus.decoder=0.2.3 osc osc-unix parsexp=v0.16.0 pbkdf pcre=7.5.0 portaudio=0.2.3 posix-base=5a7f328 posix-socket=5a7f328 posix-socket.constants=5a7f328 posix-socket.stubs=5a7f328 posix-socket.types=5a7f328 posix-time2=5a7f328 posix-time2.constants=5a7f328 posix-time2.stubs=5a7f328 posix-time2.types=5a7f328 posix-types=5a7f328 posix-types.constants=5a7f328 ppx_sexp_conv.runtime-lib=v0.16.0 prometheus=1.2 prometheus-app=1.2 ptime=1.1.0 ptime.clock.os=1.1.0 pulseaudio=0.1.6 re=1.11.0 result=1.5 rresult=0.7.0 samplerate=0.1.7 saturn_lockfree=0.4.1 sedlex=ccd3dea seq=[distributed with OCaml 4.07 or above] sexplib=v0.16.0 sexplib0=v0.16.0 shine=0.2.3 soundtouch=0.1.9 speex=0.4.2 speex.decoder=0.4.2 srt=0.3.1 srt.constants=0.3.1 srt.stubs=0.3.1 srt.stubs.locked=0.3.1 srt.types=0.3.1 ssl=0.7.0 stdlib-shims=0.3.0 stereotool=f0fdb0e-dirty str=[distributed with Ocaml] stringext=1.6.0 taglib=0.3.10 theora=0.4.1 theora.decoder=0.4.1 threads=[distributed with Ocaml] threads.posix=[internal] tls=0.17.1 tsdl=v1.0.0 tsdl-image=0.5 tsdl-ttf=0.6 unix=[distributed with Ocaml] unix-errno=52c6ecb unix-errno.errno_bindings=52c6ecb unix-errno.errno_types=52c6ecb unix-errno.errno_types_detected=52c6ecb unix-errno.unix=52c6ecb uri=4.4.0 uri-sexp=4.4.0 uri.services=4.4.0 vorbis=0.8.1 vorbis.decoder=0.8.1 x509=0.16.5 xmlm=1.4.0 xmlplaylist=0.1.5 yaml=3.1.0 yaml.bindings=3.1.0 yaml.bindings.types=3.1.0 yaml.c=3.1.0 yaml.ffi=3.1.0 yaml.types=3.1.0 zarith=1.13 - architecture : amd64 - host : x86_64-pc-linux-gnu - target : x86_64-pc-linux-gnu - system : linux - ocamlopt_cflags : -O2 -fno-strict-aliasing -fwrapv -pthread -fPIC - native_c_compiler : gcc -O2 -fno-strict-aliasing -fwrapv -pthread -fPIC -D_FILE_OFFSET_BITS=64 - native_c_libraries : -lm * Configured paths - mode : posix - standard library : /usr/share/liquidsoap/libs - scripted binaries : /usr/share/liquidsoap/bin - rundir : /var/run/liquidsoap - logdir : /var/log/liquidsoap - camomile files : /usr/share/liquidsoap/camomile * Supported input formats - MP3 : yes - AAC : yes - Ffmpeg : yes - Flac (native) : yes - Flac (ogg) : yes - Opus : yes - Speex : yes - Theora : yes - Vorbis : yes * Supported output formats - FDK-AAC : yes - Ffmpeg : yes - MP3 : yes - MP3 (fixed-point) : yes - Flac (native) : yes - Flac (ogg) : yes - Opus : yes - Speex : yes - Theora : yes - Vorbis : yes * Tags - Taglib (ID3 tags) : yes - Vorbis : yes * Input / output - ALSA : yes - AO : yes - FFmpeg : yes - GStreamer : no (requires gstreamer) - JACK : yes - OSS : yes - Portaudio : yes - Pulseaudio : yes - SRT : yes * Audio manipulation - FFmpeg : yes - LADSPA : yes - Lilv : yes - Samplerate : yes - SoundTouch : yes - StereoTool : yes * Video manipulation - camlimages : yes - FFmpeg : yes - frei0r : yes - ImageLib : no (requires imagelib) - SDL : yes * MIDI manipulation - DSSI : yes * Visualization - GD : yes - Graphics : no (requires graphics) - SDL : yes * Additional libraries - FFmpeg filters : yes - FFmpeg devices : yes - inotify : yes - irc : yes - jemalloc : yes - lastfm : yes - lo : yes - memtrace : yes - mem_usage : yes - osc : yes - ssl : yes - tls : yes - posix-time2 : yes - windows service : no (requires winsvc) - YAML support : yes - XML playlists : yes * Monitoring - Prometheus : yes validations: required: true - type: dropdown id: installation-method attributes: label: Installation method description: Specify how the software was installed. options: - From official container image - From official packages in the release artifacts - From distribution packages - From OPAM - From source/self-built default: 0 validations: required: true - type: textarea id: additional attributes: label: Additional Info description: | Please provide any additional information, such as logs, system info, similar issues, etc. For example, specify the distribution used for distribution packages, version of OPAM, or details of the self-built process. validations: required: false liquidsoap-2.3.2/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000002611477303350200214060ustar00rootroot00000000000000blank_issues_enabled: false contact_links: - name: Question url: https://github.com/savonet/liquidsoap/discussions/new about: Ask a question about using the software. liquidsoap-2.3.2/.github/ISSUE_TEMPLATE/feature_request.yml000066400000000000000000000016671477303350200233570ustar00rootroot00000000000000name: Feature request description: Submit suggestions for new features or improvements to enhance the software's capabilities and user experience. labels: - enhancement body: - type: textarea id: description attributes: label: Description description: A clear and concise description of the feature you are requesting. placeholder: validations: required: true - type: textarea id: preferable-solution attributes: label: Preferable solution description: A clear and concise description of what you want to happen. - type: textarea id: alternatives attributes: label: Alternative solutions description: A clear and concise description of any alternative solutions or features you've considered. - type: textarea id: additional attributes: label: Additional context description: Add any other context or screenshots about the feature request here. liquidsoap-2.3.2/.github/alpine/000077500000000000000000000000001477303350200165045ustar00rootroot00000000000000liquidsoap-2.3.2/.github/alpine/APKBUILD-minimal.in000066400000000000000000000016441477303350200216600ustar00rootroot00000000000000pkgname=@APK_PACKAGE@ pkgver=@APK_VERSION@ pkgrel=@APK_RELEASE@ pkgdesc="Swiss-army knife for multimedia streaming" url="https://github.com/savonet/liquidsoap" arch="all" license="GPL-2.0-only" install="@APK_PACKAGE@.post-install" options="!check textrels" package() { eval "$(opam env)" cd liquidsoap export LIQUIDSOAP_BUILD_TARGET=posix eval "$(opam config env)" export OCAMLPATH=$(cat ../.ocamlpath) dune build @install --release dune install --relocatable --prefix "${pkgdir}/usr" rm -rf "${pkgdir}/usr/share/liquidsoap-lang/libs/extra" rm -rf "$pkgdir/usr/lib" rm -rf "$pkgdir/usr/share/doc" rm -rf "$pkgdir/usr/share/man" mv "$pkgdir/usr/share/liquidsoap-lang" "$pkgdir/usr/share/liquidsoap" rm -rf "$pkgdir/usr/share/liquidsoap/libs/extra" cp -rf "$(opam var share)/camomile" "$pkgdir/usr/share/liquidsoap" } liquidsoap-2.3.2/.github/alpine/APKBUILD.in000066400000000000000000000015161477303350200202320ustar00rootroot00000000000000pkgname=@APK_PACKAGE@ pkgver=@APK_VERSION@ pkgrel=@APK_RELEASE@ pkgdesc="Swiss-army knife for multimedia streaming" url="https://github.com/savonet/liquidsoap" arch="all" license="GPL-2.0-only" install="@APK_PACKAGE@.post-install" options="!check textrels" depends="sdl2 sdl2_image sdl2_ttf" package() { eval "$(opam env)" cd liquidsoap export LIQUIDSOAP_BUILD_TARGET=posix eval "$(opam config env)" export OCAMLPATH=$(cat ../.ocamlpath) dune build @install --release dune install --relocatable --prefix "${pkgdir}/usr" rm -rf "$pkgdir/usr/lib" rm -rf "$pkgdir/usr/share/doc" rm -rf "$pkgdir/usr/share/man" mv "$pkgdir/usr/share/liquidsoap-lang" "$pkgdir/usr/share/liquidsoap" cp -rf "$(opam var share)/camomile" "$pkgdir/usr/share/liquidsoap" } liquidsoap-2.3.2/.github/alpine/liquidsoap.post-install000077500000000000000000000010041477303350200232270ustar00rootroot00000000000000#!/bin/sh addgroup -S liquidsoap 2> /dev/null adduser -S -D -h /var/liquidsoap -s /sbin/nologin -G liquidsoap -g liquidsoap liquidsoap 2> /dev/null addgroup liquidsoap audio 2> /dev/null mkdir -p /var/log/liquidsoap mkdir -p /var/cache/liquidspap echo "Generating cache for the standard library.." LIQ_CACHE_SYSTEM_DIR=/var/cache/liquidsoap liquidsoap --cache-only '()' chown -R liquidsoap:liquidsoap /var/log/liquidsoap chown liquidsoap:liquidsoap /var/cache/liquidsoap chmod -R +r /var/cache/liquidsoap exit 0 liquidsoap-2.3.2/.github/debian/000077500000000000000000000000001477303350200164565ustar00rootroot00000000000000liquidsoap-2.3.2/.github/debian/compat000066400000000000000000000000031477303350200176550ustar00rootroot0000000000000010 liquidsoap-2.3.2/.github/debian/control.in000066400000000000000000000061201477303350200204650ustar00rootroot00000000000000Source: @LIQ_PACKAGE@ Section: sound Priority: optional Maintainer: Romain Beauxis Build-Depends: debhelper (>= 10) Standards-Version: 4.3.0 Homepage: https://liquidsoap.info/ Package: @LIQ_PACKAGE@ Architecture: any Depends: ${shlibs:Depends}, ${misc:Depends}, libsdl2-2.0-0, libsdl2-image-2.0-0, libsdl2-ttf-2.0-0, bubblewrap, adduser Recommends: logrotate, ffmpeg Suggests: icecast2, awscli Provides: liquidsoap-snapshot Conflicts: liquidsoap-snapshot Replaces: liquidsoap, liquidsoap-snapshot, liquidsoap-plugin-all, liquidsoap-plugin-alsa [linux-any], liquidsoap-plugin-ao, liquidsoap-plugin-camlimages, liquidsoap-plugin-dssi, liquidsoap-plugin-faad, liquidsoap-plugin-flac, liquidsoap-plugin-frei0r, liquidsoap-plugin-gavl, liquidsoap-plugin-gd, liquidsoap-plugin-graphics, liquidsoap-plugin-gstreamer, liquidsoap-plugin-icecast, liquidsoap-plugin-jack, liquidsoap-plugin-ladspa, liquidsoap-plugin-lame, liquidsoap-plugin-lastfm, liquidsoap-plugin-lo, liquidsoap-plugin-mad, liquidsoap-plugin-ogg, liquidsoap-plugin-opus, liquidsoap-plugin-oss, liquidsoap-plugin-portaudio, liquidsoap-plugin-pulseaudio, liquidsoap-plugin-samplerate, liquidsoap-plugin-schroedinger, liquidsoap-plugin-sdl, liquidsoap-plugin-shine, liquidsoap-plugin-soundtouch, liquidsoap-plugin-speex, liquidsoap-plugin-taglib, liquidsoap-plugin-theora, liquidsoap-plugin-voaacenc, liquidsoap-plugin-vorbis, liquidsoap-plugin-xmlplaylist Breaks: liquidsoap, liquidsoap-plugin-all, liquidsoap-plugin-alsa [linux-any], liquidsoap-plugin-ao, liquidsoap-plugin-camlimages, liquidsoap-plugin-dssi, liquidsoap-plugin-faad, liquidsoap-plugin-flac, liquidsoap-plugin-frei0r, liquidsoap-plugin-gavl, liquidsoap-plugin-gd, liquidsoap-plugin-graphics, liquidsoap-plugin-gstreamer, liquidsoap-plugin-icecast, liquidsoap-plugin-jack, liquidsoap-plugin-ladspa, liquidsoap-plugin-lame, liquidsoap-plugin-lastfm, liquidsoap-plugin-lo, liquidsoap-plugin-mad, liquidsoap-plugin-ogg, liquidsoap-plugin-opus, liquidsoap-plugin-oss, liquidsoap-plugin-portaudio, liquidsoap-plugin-pulseaudio, liquidsoap-plugin-samplerate, liquidsoap-plugin-schroedinger, liquidsoap-plugin-sdl, liquidsoap-plugin-shine, liquidsoap-plugin-soundtouch, liquidsoap-plugin-speex, liquidsoap-plugin-taglib, liquidsoap-plugin-theora, liquidsoap-plugin-voaacenc, liquidsoap-plugin-vorbis, liquidsoap-plugin-xmlplaylist Description: audio and video streaming language Liquidsoap is a powerful tool for building complex audio and video streaming systems, typically targeting internet radios (e.g. icecast streams). . It consists of a simple script language, in which you can create, combine and transform audio sources. Its design makes liquidsoap flexible and easily extensible. . Some of the typical uses are: * dynamically generating playlists depending on the time or other factors * having smooth transitions between songs * adding jingles periodically * applying effects on the sound like volume normalization * reencoding the stream at various qualities * remotely controlling the stream liquidsoap-2.3.2/.github/debian/dirs000066400000000000000000000001211477303350200173340ustar00rootroot00000000000000usr/bin etc/init.d etc/bash_completion.d var/log/liquidsoap var/cache/liquidsoap liquidsoap-2.3.2/.github/debian/install000066400000000000000000000002711477303350200200470ustar00rootroot00000000000000bin/liquidsoap usr/bin share/bash_completion/completions/liquidsoap etc/bash_completion.d share/liquidsoap-lang/libs usr/share/liquidsoap share/liquidsoap/camomile usr/share/liquidsoap liquidsoap-2.3.2/.github/debian/manpages000066400000000000000000000000411477303350200201670ustar00rootroot00000000000000debian/tmp/man/man1/liquidsoap.1 liquidsoap-2.3.2/.github/debian/postinst000066400000000000000000000021521477303350200202640ustar00rootroot00000000000000#!/bin/sh # postinst script for liquidsoap set -e case "$1" in configure) ;; abort-upgrade | abort-remove | abort-deconfigure) exit 0 ;; *) echo "postinst called with unknown argument \`$1'" >&2 exit 1 ;; esac if ! getent group liquidsoap > /dev/null; then addgroup --system liquidsoap fi # Create the new system account id liquidsoap > dev/null 2>&1 || ( adduser --system --disabled-password --disabled-login \ --home /var/cache/liquidsoap --ingroup liquidsoap liquidsoap && usermod --append --groups audio liquidsoap ) # Add again /usr/share/liquidsoap if user was already created if ! test -d /usr/share/liquidsoap; then mkdir /usr/share/liquidsoap fi if ! test -d /var/cache/liquidsoap; then mkdir -p /var/cache/liquidsoap fi echo "Generating cache for the standard library.." LIQ_CACHE_SYSTEM_DIR=/var/cache/liquidsoap liquidsoap --cache-only '()' # Fix directories ownership chown -R liquidsoap:liquidsoap /var/log/liquidsoap chmod -R +r /var/log/liquidsoap chown -R liquidsoap:liquidsoap /var/cache/liquidsoap chown -R root:root /usr/share/liquidsoap #DEBHELPER# exit 0 liquidsoap-2.3.2/.github/debian/postrm000066400000000000000000000003351477303350200177260ustar00rootroot00000000000000#!/bin/sh # postrm script for liquidsoap set -e if [ "$1" = "purge" ] && [ -d /usr/share/liquidsoap ]; then if ! rmdir /usr/share/liquidsoap; then echo "leaving /usr/share/liquidsoap in place" fi fi #DEBHELPER# liquidsoap-2.3.2/.github/debian/rules000077500000000000000000000007541477303350200175440ustar00rootroot00000000000000#!/usr/bin/make -f DESTDIR := debian/tmp %: dh $@ override_dh_autoreconf: /bin/true override_dh_auto_configure: /bin/true override_dh_auto_build: /bin/true override_dh_auto_test: /bin/true override_dh_auto_install: export LIQUIDSOAP_BUILD_TARGET=posix dune build @install --release dune install --relocatable --prefix $(DESTDIR) mkdir -p $(DESTDIR)/share/liquidsoap cp -rf `opam var share`/camomile $(DESTDIR)/share/liquidsoap dh_install override_dh_auto_clean: /bin/true liquidsoap-2.3.2/.github/debian/rules-minimal000077500000000000000000000010401477303350200211550ustar00rootroot00000000000000#!/usr/bin/make -f DESTDIR := debian/tmp %: dh $@ override_dh_autoreconf: /bin/true override_dh_auto_configure: /bin/true override_dh_auto_build: /bin/true override_dh_auto_test: /bin/true override_dh_auto_install: export LIQUIDSOAP_BUILD_TARGET=posix dune build @install --release dune install --relocatable --prefix $(DESTDIR) rm -rf $(DESTDIR)/share/liquidsoap-lang/libs/extra mkdir -p $(DESTDIR)/share/liquidsoap cp -rf `opam var share`/camomile $(DESTDIR)/share/liquidsoap dh_install override_dh_auto_clean: /bin/true liquidsoap-2.3.2/.github/debian/source/000077500000000000000000000000001477303350200177565ustar00rootroot00000000000000liquidsoap-2.3.2/.github/debian/source/format000066400000000000000000000000141477303350200211640ustar00rootroot000000000000003.0 (quilt) liquidsoap-2.3.2/.github/docker/000077500000000000000000000000001477303350200165035ustar00rootroot00000000000000liquidsoap-2.3.2/.github/docker/alpine.dockerfile000066400000000000000000000007171477303350200220110ustar00rootroot00000000000000FROM alpine:edge AS downloader ARG APK_FILE COPY $APK_FILE /downloads/liquidsoap.apk FROM alpine:edge RUN --mount=type=bind,from=downloader,source=/downloads,target=/downloads \ set -eux; \ echo 'https://dl-cdn.alpinelinux.org/alpine/edge/testing' >> /etc/apk/repositories; \ apk add --allow-untrusted --no-cache \ /downloads/liquidsoap.apk \ ; USER liquidsoap RUN liquidsoap --cache-stdlib ENTRYPOINT ["/usr/bin/liquidsoap"] liquidsoap-2.3.2/.github/docker/debian.dockerfile000066400000000000000000000032571477303350200217650ustar00rootroot00000000000000FROM debian:12-slim AS downloader ARG DEB_FILE ARG DEB_DEBUG_FILE COPY $DEB_FILE /downloads/liquidsoap.deb COPY $DEB_DEBUG_FILE /downloads/liquidsoap-debug.deb ARG DEB_MULTIMEDIA_KEYRING="https://www.deb-multimedia.org/pool/main/d/deb-multimedia-keyring/deb-multimedia-keyring_2024.9.1_all.deb" ARG DEB_MULTIMEDIA_KEYRING_SHA256SUM="8dc6cbb266c701cfe58bd1d2eb9fe2245a1d6341c7110cfbfe3a5a975dcf97ca" RUN set -eux; \ apt-get update; \ apt-get install -y --no-install-recommends \ ca-certificates \ wget \ ; \ wget "$DEB_MULTIMEDIA_KEYRING" -O /downloads/deb-multimedia-keyring.deb; \ echo "$DEB_MULTIMEDIA_KEYRING_SHA256SUM /downloads/deb-multimedia-keyring.deb" | sha256sum -c -; FROM debian:12-slim ARG DEBIAN_FRONTEND=noninteractive # For ffmpeg with libfdk-aac RUN --mount=type=bind,from=downloader,source=/downloads,target=/downloads \ set -eux; \ apt-get update; \ apt-get install -y --no-install-recommends \ /downloads/deb-multimedia-keyring.deb \ ca-certificates \ ; \ echo 'deb https://www.deb-multimedia.org bookworm main non-free' > \ /etc/apt/sources.list.d/deb-multimedia.list; \ rm -rf \ /var/lib/apt/lists \ /var/lib/dpkg/status-old \ ; RUN --mount=type=bind,from=downloader,source=/downloads,target=/downloads \ set -eux; \ apt-get update; \ apt-get install -y --no-install-recommends \ /downloads/liquidsoap.deb \ /downloads/liquidsoap-debug.deb \ ; \ rm -rf \ /var/lib/apt/lists \ /var/lib/dpkg/status-old \ ; USER liquidsoap RUN liquidsoap --cache-stdlib ENTRYPOINT ["/usr/bin/liquidsoap"] liquidsoap-2.3.2/.github/docker/website.dockerfile000066400000000000000000000040111477303350200221720ustar00rootroot00000000000000FROM savonet/liquidsoap-ci:debian_bookworm_amd64 MAINTAINER The Savonet Team USER root # The following could also be interesting but compilation of the website takes # forever: amb-plugins caps cmt csladspa fomp lsp-plugins-ladspa lsp-plugins-lv2 RUN apt-get update && apt-get -y dist-upgrade && apt-get -y install openssh-client && apt-get install -y abgate calf-plugins swh-lv2 swh-plugins tap-plugins zam-plugins frei0r-plugins # This is until the next image rebuild: RUN apt-get -y install libcurl4-gnutls-dev USER opam # This is until the next image rebuild: RUN eval "$(opam config env)" && opam install -y ocurl js_of_ocaml js_of_ocaml-ppx WORKDIR /tmp/liquidsoap-full RUN rm -rf website/savonet.github.io RUN git remote set-url origin https://github.com/savonet/liquidsoap-full.git && \ git fetch --recurse-submodules=no && git checkout origin/master -- Makefile.git && \ git reset --hard && \ git pull && \ git submodule init ocaml-metadata && \ git submodule update ocaml-metadata && \ make public RUN eval "$(opam config env)" && make clean RUN eval "$(opam config env)" && cd ocaml-metadata && opam install -y . fileutils RUN eval "$(opam config env)" && \ git clone https://github.com/savonet/ocaml-posix.git && \ cd ocaml-posix && \ opam install -y . RUN cd liquidsoap && \ mv .git /tmp && \ rm -rf * && \ mv /tmp/.git . && \ git reset --hard RUN make public && make update # TODO: Remove gstreamer from liquidsoap-full RUN cat PACKAGES.default | grep -v gstreamer > PACKAGES RUN eval "$(opam config env)" && \ export PKG_CONFIG_PATH=/usr/share/pkgconfig/pkgconfig && \ touch liquidsoap/configure && \ ./configure --enable-graphics && \ rm liquidsoap/configure && \ export OCAMLPATH="$(cat .ocamlpath)" && \ cd liquidsoap && \ dune build && \ dune build --release src/js WORKDIR /tmp/liquidsoap-full/website RUN eval "$(opam config env)" && opam install -y odoc && make clean && git pull && make dist liquidsoap-2.3.2/.github/opam/000077500000000000000000000000001477303350200161705ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/liquidsoap-windows.opam000066400000000000000000000127731477303350200227220ustar00rootroot00000000000000# This file is generated by dune, edit dune-project instead opam-version: "2.0" synopsis: "Swiss-army knife for multimedia streaming" description: """ Liquidsoap is a powerful and flexible language for describing your streams. It offers a rich collection of operators that you can combine at will, giving you more power than you need for creating or transforming streams. But liquidsoap is still very light and easy to use, in the Unix tradition of simple strong components working together. """ maintainer: ["The Savonet Team "] authors: ["The Savonet Team "] license: "GPL-2.0-or-later" homepage: "https://github.com/savonet/liquidsoap" bug-reports: "https://github.com/savonet/liquidsoap/issues" depends: [ "dune" {>= "3.2"} "ocaml-windows" {>= "4.14"} "camomile-windows" {>= "2.0.0"} "camomile" {>= "2.0.0"} "dtools-windows" {>= "0.4.5"} "duppy-windows" {>= "0.9.4"} "mm-windows" {>= "0.8.4"} "re-windows" {>= "1.11.0"} "cry-windows" {>= "1.0.1"} "saturn_lockfree-windows" {>= "0.5.0"} "sedlex" {>= "3.2"} "sedlex-windows" {>= "3.2"} "magic-mime-windows" "menhir" "menhirLib-windows" "uri" "uri-windows" "fileutils" "fileutils-windows" "curl-windows" "xml-light-windows" "mem_usage-windows" {>= "0.1.1"} "metadata-windows" {>= "0.3.0"} "dune-site-windows" "dune-build-info-windows" "ppx_string" {build} "ppx_string-windows" {build} "ppx_hash" {build} "ppx_hash-windows" {build} ] depopts: [ "alsa-windows" "ao-windows" "bjack-windows" "camlimages-windows" "dssi-windows" "faad-windows" "fdkaac-windows" "ffmpeg-windows" "flac-windows" "frei0r-windows" "gd-windows" "graphics-windows" "inotify-windows" "irc-client-unix-windows" "ladspa-windows" "lame-windows" "lastfm-windows" "lilv-windows" "lo-windows" "mad-windows" "memtrace-windows" "curl-windows" "ogg-windows" "opus-windows" "osx-secure-transport-windows" "portaudio-windows" "posix-time2-windows" "posix-socket-windows" "pulseaudio-windows" "prometheus-liquidsoap-windows" "samplerate-windows" "shine-windows" "soundtouch-windows" "speex-windows" "srt-windows" "ssl-windows" "theora-windows" "tsdl-windows" "tsdl-image-windows" "tsdl-ttf-windows" "vorbis-windows" "xmlplaylist-windows" ] conflicts: [ "alsa-windows" {< "0.3.0"} "ao-windows" {< "0.2.0"} "bjack-windows" {< "0.1.3"} "dssi-windows" {< "0.1.3"} "faad-windows" {< "0.5.0"} "fdkaac-windows" {< "0.3.1"} "ffmpeg-windows" {< "1.2.0"} "ffmpeg-avutil-windows" {< "1.2.0"} "flac-windows" {< "0.3.0"} "frei0r-windows" {< "0.1.0"} "inotify-windows" {< "1.0"} "ladspa-windows" {< "0.2.0"} "lame-windows" {< "0.3.5"} "lastfm-windows" {< "0.3.0"} "lo-windows" {< "0.2.0"} "liquidsoap-windows" {< "2.2.0"} "mad-windows" {< "0.5.0"} "magic-windows" {< "0.6"} "curl-windows" {< "0.9.2"} "ogg-windows" {< "0.7.4"} "opus-windows" {< "0.2.0"} "portaudio-windows" {< "0.2.0"} "posix-socket-windows" {< "2.1.0"} "pulseaudio-windows" {< "0.1.4"} "samplerate-windows" {< "0.1.5"} "shine-windows" {< "0.2.0"} "soundtouch-windows" {< "0.1.9"} "speex-windows" {< "0.4.0"} "srt-windows" {< "0.3.3"} "ssl-windows" {< "0.5.2"} "sdl-liquidsoap-windows" {< "2"} "tsdl-image-windows" {< "0.3.2"} "theora-windows" {< "0.4.0"} "vorbis-windows" {< "0.8.0"} "xmlplaylist-windows" {< "0.1.3"} ] build: [ [ "env" "LIQUIDSOAP_BUILD_TARGET=standalone" "LIQUIDSOAP_SYS_CONFIG=mingw" "LIQUIDSOAP_ENABLE_BUILD_CONFIG=false" "LIQ_LDFLAGS=-lcurl -lwldap32 -ldl -lnghttp2 -lpsl -lssh2 -lidn2 -lzstd -lunistring -lbrotlicommon -lbrotlidec -lcrypt32 -liconv -lpthread -lz -lbcrypt -lwinmm -lksuser -link /usr/src/mxe/usr/x86_64-w64-mingw32.static/lib/libavutil.a" "dune" "build" "-x" "windows" "-p" "liquidsoap-lang,liquidsoap" "@install" "-j" jobs ] ] post-messages: [ """\ We're sorry that your liquidsoap install failed. Check out our installation instructions at: https://www.liquidsoap.info/doc-%{version}%/install.html#opam for more information.""" {failure} "✨ Congratulations on installing liquidsoap! ✨" {success} """\ We noticed that you did not install any mp3 decoder. This is a feature most users want. You might need to install the mad or ffmpeg package.""" {success & !mad-enabled & !ffmpeg-enabled} """\ We noticed that you did not install any mp3 encoder. This is a feature most users want. You might need to install the lame or shine package.""" {success & !lame-enabled & !shine-enabled & !ffmpeg-enabled} """\ We noticed that you did not install the samplerate package. We strongly recommend this package for audio samplerate conversion.""" {success & !samperate-enabled} """\ We noticed that you did not install the ocurl package. We strongly recommend this package for http request resolving support.""" {success & !curl-enabled} """\ We noticed that you did not install the cry package that provides icecast output. This is a feature most users want.""" {success & !cry-enabled} """\ We noticed that you did not install any ssl support package. Liquidsoap won't be able to use any HTTPS feature. You might want to install one of ssl or osx-secure-transport package.""" {success & !ssl-enabled & !secure-transport-enabled} ] depexts: ["coreutils"] {os = "macos" & os-distribution = "homebrew"} dev-repo: "git+https://github.com/savonet/liquidsoap.git" url { src: "https://github.com/savonet/liquidsoap/archive/@COMMIT_SHORT@.tar.gz" } liquidsoap-2.3.2/.github/opam/packages/000077500000000000000000000000001477303350200177465ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/ffmpeg-av-windows/000077500000000000000000000000001477303350200233065ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/ffmpeg-av-windows/ffmpeg-av-windows.1.2.0/000077500000000000000000000000001477303350200273035ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/ffmpeg-av-windows/ffmpeg-av-windows.1.2.0/opam000066400000000000000000000016061477303350200301650ustar00rootroot00000000000000opam-version: "2.0" synopsis: "Bindings for the ffmpeg libraries -- top-level helpers" maintainer: "Romain Beauxis " authors: "The Savonet Team " license: "LGPL-2.1-only" homepage: "https://github.com/savonet/ocaml-ffmpeg" bug-reports: "https://github.com/savonet/ocaml-ffmpeg/issues" depends: [ "conf-pkg-config" {build} "ocaml-windows" {>= "4.08.0"} "dune" {>= "3.6"} "dune-configurator" {build} "ffmpeg-avutil-windows" {= version} "ffmpeg-avcodec-windows" {= version} ] conflicts: [ "ffmpeg-windows" {< "0.5.0"} ] depexts: [ ["ffmpeg"] {os-distribution = "mxe"} ] build: [ [ "dune" "build" "-p" "ffmpeg-av" "-x" "windows" "-j" jobs "@install" ] ] dev-repo: "git+https://github.com/savonet/ocaml-ffmpeg.git" url { src: "https://github.com/savonet/ocaml-ffmpeg/archive/main.tar.gz" } liquidsoap-2.3.2/.github/opam/packages/ffmpeg-avcodec-windows/000077500000000000000000000000001477303350200243045ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/ffmpeg-avcodec-windows/ffmpeg-avcodec-windows.1.2.0/000077500000000000000000000000001477303350200312775ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/ffmpeg-avcodec-windows/ffmpeg-avcodec-windows.1.2.0/opam000066400000000000000000000015251477303350200321610ustar00rootroot00000000000000opam-version: "2.0" synopsis: "Bindings for the ffmpeg avcodec library" maintainer: "Romain Beauxis " authors: "The Savonet Team " license: "LGPL-2.1-only" homepage: "https://github.com/savonet/ocaml-ffmpeg" bug-reports: "https://github.com/savonet/ocaml-ffmpeg/issues" depends: [ "conf-pkg-config" {build} "ocaml-windows" {>= "4.08.0"} "dune" {>= "3.6"} "dune-configurator" {build} "ffmpeg-avutil-windows" {= version} ] conflicts: [ "ffmpeg-windows" {< "0.5.0"} ] depexts: [ ["ffmpeg"] {os-distribution = "mxe"} ] build: [ [ "dune" "build" "-p" "ffmpeg-avcodec" "-x" "windows" "-j" jobs "@install" ] ] dev-repo: "git+https://github.com/savonet/ocaml-ffmpeg.git" url { src: "https://github.com/savonet/ocaml-ffmpeg/archive/main.tar.gz" } liquidsoap-2.3.2/.github/opam/packages/ffmpeg-avdevice-windows/000077500000000000000000000000001477303350200244665ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/ffmpeg-avdevice-windows/ffmpeg-avdevice-windows.1.2.0/000077500000000000000000000000001477303350200316435ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/ffmpeg-avdevice-windows/ffmpeg-avdevice-windows.1.2.0/opam000066400000000000000000000015231477303350200325230ustar00rootroot00000000000000opam-version: "2.0" synopsis: "Bindings for the ffmpeg avdevice library" maintainer: "Romain Beauxis " authors: "The Savonet Team " license: "LGPL-2.1-only" homepage: "https://github.com/savonet/ocaml-ffmpeg" bug-reports: "https://github.com/savonet/ocaml-ffmpeg/issues" depends: [ "conf-pkg-config" {build} "ocaml-windows" {>= "4.08.0"} "dune" {>= "3.6"} "dune-configurator" {build} "ffmpeg-av-windows" {= version} ] conflicts: [ "ffmpeg-windows" {< "0.5.0"} ] depexts: [ ["ffmpeg"] {os-distribution = "mxe"} ] build: [ [ "dune" "build" "-p" "ffmpeg-avdevice" "-x" "windows" "-j" jobs "@install" ] ] dev-repo: "git+https://github.com/savonet/ocaml-ffmpeg.git" url { src: "https://github.com/savonet/ocaml-ffmpeg/archive/main.tar.gz" } liquidsoap-2.3.2/.github/opam/packages/ffmpeg-avfilter-windows/000077500000000000000000000000001477303350200245145ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/ffmpeg-avfilter-windows/ffmpeg-avfilter-windows.1.2.0/000077500000000000000000000000001477303350200317175ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/ffmpeg-avfilter-windows/ffmpeg-avfilter-windows.1.2.0/opam000066400000000000000000000015271477303350200326030ustar00rootroot00000000000000opam-version: "2.0" synopsis: "Bindings for the ffmpeg avfilter library" maintainer: "Romain Beauxis " authors: "The Savonet Team " license: "LGPL-2.1-only" homepage: "https://github.com/savonet/ocaml-ffmpeg" bug-reports: "https://github.com/savonet/ocaml-ffmpeg/issues" depends: [ "conf-pkg-config" {build} "ocaml-windows" {>= "4.08.0"} "dune" {>= "3.6"} "dune-configurator" {build} "ffmpeg-avutil-windows" {= version} ] conflicts: [ "ffmpeg-windows" {< "0.5.0"} ] depexts: [ ["ffmpeg"] {os-distribution = "mxe"} ] build: [ [ "dune" "build" "-p" "ffmpeg-avfilter" "-x" "windows" "-j" jobs "@install" ] ] dev-repo: "git+https://github.com/savonet/ocaml-ffmpeg.git" url { src: "https://github.com/savonet/ocaml-ffmpeg/archive/main.tar.gz" } liquidsoap-2.3.2/.github/opam/packages/ffmpeg-avutil-windows/000077500000000000000000000000001477303350200242045ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/ffmpeg-avutil-windows/ffmpeg-avutil-windows.1.2.0/000077500000000000000000000000001477303350200310775ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/ffmpeg-avutil-windows/ffmpeg-avutil-windows.1.2.0/opam000066400000000000000000000015001477303350200317520ustar00rootroot00000000000000opam-version: "2.0" synopsis: "Bindings for the ffmpeg avutil libraries" maintainer: "Romain Beauxis " authors: "The Savonet Team " license: "LGPL-2.1-only" homepage: "https://github.com/savonet/ocaml-ffmpeg" bug-reports: "https://github.com/savonet/ocaml-ffmpeg/issues" depends: [ "conf-pkg-config" {build} "ocaml-windows" {>= "4.08.0"} "dune" {>= "3.6"} "dune-configurator" {build} "base-threads" ] conflicts: [ "ffmpeg-windows" {< "0.5.0"} ] depexts: [ ["ffmpeg"] {os-distribution = "mxe"} ] build: [ [ "dune" "build" "-p" "ffmpeg-avutil" "-x" "windows" "-j" jobs "@install" ] ] dev-repo: "git+https://github.com/savonet/ocaml-ffmpeg.git" url { src: "https://github.com/savonet/ocaml-ffmpeg/archive/main.tar.gz" } liquidsoap-2.3.2/.github/opam/packages/ffmpeg-swresample-windows/000077500000000000000000000000001477303350200250625ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/ffmpeg-swresample-windows/ffmpeg-swresample-windows.1.2.0/000077500000000000000000000000001477303350200326335ustar00rootroot00000000000000opam000066400000000000000000000016021477303350200334320ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/ffmpeg-swresample-windows/ffmpeg-swresample-windows.1.2.0opam-version: "2.0" synopsis: "Bindings for the ffmpeg swresample library" maintainer: "Romain Beauxis " authors: "The Savonet Team " license: "LGPL-2.1-only" homepage: "https://github.com/savonet/ocaml-ffmpeg" bug-reports: "https://github.com/savonet/ocaml-ffmpeg/issues" depends: [ "conf-pkg-config" {build} "ocaml-windows" {>= "4.08.0"} "dune" {>= "3.6"} "dune-configurator" {build} "ffmpeg-avutil-windows" {= version} "ffmpeg-avcodec-windows" {= version} ] conflicts: [ "ffmpeg-windows" {< "0.5.0"} ] depexts: [ ["ffmpeg"] {os-distribution = "mxe"} ] build: [ [ "dune" "build" "-p" "ffmpeg-swresample" "-x" "windows" "-j" jobs "@install" ] ] dev-repo: "git+https://github.com/savonet/ocaml-ffmpeg.git" url { src: "https://github.com/savonet/ocaml-ffmpeg/archive/main.tar.gz" } liquidsoap-2.3.2/.github/opam/packages/ffmpeg-swscale-windows/000077500000000000000000000000001477303350200243415ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/ffmpeg-swscale-windows/ffmpeg-swscale-windows.1.2.0/000077500000000000000000000000001477303350200313715ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/ffmpeg-swscale-windows/ffmpeg-swscale-windows.1.2.0/opam000066400000000000000000000015251477303350200322530ustar00rootroot00000000000000opam-version: "2.0" synopsis: "Bindings for the ffmpeg swscale library" maintainer: "Romain Beauxis " authors: "The Savonet Team " license: "LGPL-2.1-only" homepage: "https://github.com/savonet/ocaml-ffmpeg" bug-reports: "https://github.com/savonet/ocaml-ffmpeg/issues" depends: [ "conf-pkg-config" {build} "ocaml-windows" {>= "4.08.0"} "dune" {>= "3.6"} "dune-configurator" {build} "ffmpeg-avutil-windows" {= version} ] conflicts: [ "ffmpeg-windows" {< "0.5.0"} ] depexts: [ ["ffmpeg"] {os-distribution = "mxe"} ] build: [ [ "dune" "build" "-p" "ffmpeg-swscale" "-x" "windows" "-j" jobs "@install" ] ] dev-repo: "git+https://github.com/savonet/ocaml-ffmpeg.git" url { src: "https://github.com/savonet/ocaml-ffmpeg/archive/main.tar.gz" } liquidsoap-2.3.2/.github/opam/packages/ffmpeg-windows/000077500000000000000000000000001477303350200227025ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/ffmpeg-windows/ffmpeg-windows.1.2.0/000077500000000000000000000000001477303350200262735ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/ffmpeg-windows/ffmpeg-windows.1.2.0/opam000066400000000000000000000016251477303350200271560ustar00rootroot00000000000000opam-version: "2.0" synopsis: "Bindings for the ffmpeg libraries" maintainer: "Romain Beauxis " authors: "The Savonet Team " license: "LGPL-2.1-only" homepage: "https://github.com/savonet/ocaml-ffmpeg" bug-reports: "https://github.com/savonet/ocaml-ffmpeg/issues" depends: [ "ocaml-windows" {>= "4.08.0"} "dune" {>= "3.6"} "ffmpeg-av-windows" {= version} "ffmpeg-avutil-windows" {= version} "ffmpeg-avcodec-windows" {= version} "ffmpeg-avfilter-windows" {= version} "ffmpeg-avdevice-windows" {= version} "ffmpeg-swscale-windows" {= version} "ffmpeg-swresample-windows" {= version} ] build: [ [ "dune" "build" "-p" "ffmpeg" "-x" "windows" "-j" jobs "@install" ] ] dev-repo: "git+https://github.com/savonet/ocaml-ffmpeg.git" url { src: "https://github.com/savonet/ocaml-ffmpeg/archive/main.tar.gz" } liquidsoap-2.3.2/.github/opam/packages/metadata/000077500000000000000000000000001477303350200215265ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/metadata/metadata.0.3.0/000077500000000000000000000000001477303350200237435ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/metadata/metadata.0.3.0/opam000066400000000000000000000017071477303350200246270ustar00rootroot00000000000000# This file is generated by dune, edit dune-project instead opam-version: "2.0" synopsis: "Read metadata from various file formats" description: "A pure OCaml library for reading files from various formats." maintainer: ["Samuel Mimram "] authors: ["Samuel Mimram "] license: "GPL-3.0-or-later" homepage: "https://github.com/savonet/ocaml-metadata" bug-reports: "https://github.com/savonet/ocaml-metadata/issues" depends: [ "dune" {>= "3.6"} "ocaml" {>= "4.14.0"} ] build: [ [ "dune" "build" "-p" "metadata" "-j" jobs "@install" ] ] dev-repo: "git+https://github.com/savonet/ocaml-metadata.git" url { src: "https://github.com/savonet/ocaml-metadata/archive/refs/tags/v0.3.0.tar.gz" checksum: [ "md5=368839ac027c397ce57cf3e4922888a7" "sha512=3e94f136f910d3484e688dd0afb3510a5b5d9618b2d991635416a69316e19653022ea21a236705be965a2980d1dc07370a115fd942b64e9d307103dbe02172ce" ] } liquidsoap-2.3.2/.github/opam/packages/posix-base-windows/000077500000000000000000000000001477303350200235105ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/posix-base-windows/posix-base-windows.2.2.0/000077500000000000000000000000001477303350200277105ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/posix-base-windows/posix-base-windows.2.2.0/opam000066400000000000000000000014071477303350200305710ustar00rootroot00000000000000# This file is generated by dune, edit dune-project instead opam-version: "2.0" synopsis: "Bindings for posix sockets" description: "posix-socket provides the types and bindings of posix sockets APIs available on both unix and windows." maintainer: ["romain.beauxis@gmail.com"] authors: ["Romain Beauxis"] license: "MIT" homepage: "https://github.com/savonet/ocaml-posix" bug-reports: "https://github.com/savonet/ocaml-posix/issues" depends: [ "dune" {>= "2.9"} "ocaml-windows" "ctypes" "ctypes-windows" ] build: [ [ "dune" "build" "-p" "posix-base" "-x" "windows" "-j" jobs "@install" ] ] dev-repo: "git+https://github.com/savonet/ocaml-posix.git" url { src: "https://github.com/savonet/ocaml-posix/archive/main.tar.gz" } liquidsoap-2.3.2/.github/opam/packages/posix-socket/000077500000000000000000000000001477303350200223765ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/posix-socket/posix-socket-windows.2.2.0/000077500000000000000000000000001477303350200271545ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/posix-socket/posix-socket-windows.2.2.0/opam000066400000000000000000000014111477303350200300300ustar00rootroot00000000000000# This file is generated by dune, edit dune-project instead opam-version: "2.0" synopsis: "Bindings for posix sockets" description: "posix-socket provides the types and bindings of posix sockets APIs available on both unix and windows." maintainer: ["romain.beauxis@gmail.com"] authors: ["Romain Beauxis"] license: "MIT" homepage: "https://github.com/savonet/ocaml-posix" bug-reports: "https://github.com/savonet/ocaml-posix/issues" depends: [ "dune" {>= "2.9"} "ocaml-windows" "ctypes" "ctypes-windows" ] build: [ [ "dune" "build" "-p" "posix-socket" "-x" "windows" "-j" jobs "@install" ] ] dev-repo: "git+https://github.com/savonet/ocaml-posix.git" url { src: "https://github.com/savonet/ocaml-posix/archive/main.tar.gz" } liquidsoap-2.3.2/.github/opam/packages/srt-windows/000077500000000000000000000000001477303350200222465ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/srt-windows/srt-windows.0.3.3/000077500000000000000000000000001477303350200252065ustar00rootroot00000000000000liquidsoap-2.3.2/.github/opam/packages/srt-windows/srt-windows.0.3.3/opam000066400000000000000000000021611477303350200260650ustar00rootroot00000000000000# This file is generated by dune, edit dune-project instead opam-version: "2.0" synopsis: "Binding for the Secure, Reliable, Transport protocol library" description: """ Secure Reliable Transport (SRT) is an open source transport technology that optimizes streaming performance across unpredictable networks, such as the Internet. This package provides OCaml bindings to the C implementation library. """ maintainer: ["The Savonet Team "] authors: ["The Savonet Team "] license: "GPL-2.0-only" homepage: "https://github.com/savonet/ocaml-srt" bug-reports: "https://github.com/savonet/ocaml-srt/issues" depends: [ "conf-pkg-config" {build} "dune" {> "2.0"} "dune-configurator" {build} "ctypes-foreign-windows" "integers-windows" "posix-socket-windows" {>= "2.2.0"} "posix-socket" ] build: [ ["dune" "subst"] {dev} [ "dune" "build" "-p" "srt" "-x" "windows" "-j" jobs "@install" ] ] depexts: [ ["libsrt"] {os-distribution = "mxe"} ] url { src: "https://github.com/savonet/ocaml-srt/archive/main.tar.gz" } liquidsoap-2.3.2/.github/opam/repo000066400000000000000000000000241477303350200170540ustar00rootroot00000000000000opam-version: "2.0" liquidsoap-2.3.2/.github/renovate.json000066400000000000000000000007711477303350200177570ustar00rootroot00000000000000{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:best-practices", "helpers:pinGitHubActionDigestsToSemver", ":automergeMinor" ], "packageRules": [ { "matchDepNames": [ "mknejp/delete-release-assets", "savonet/aws-s3-docker-action", "savonet/latest-tag" ], "matchManagers": ["github-actions"], "enabled": false }, { "matchCategories": ["docker"], "enabled": false } ] } liquidsoap-2.3.2/.github/scripts/000077500000000000000000000000001477303350200167235ustar00rootroot00000000000000liquidsoap-2.3.2/.github/scripts/add-local-opam-packages.sh000077500000000000000000000010051477303350200236040ustar00rootroot00000000000000#!/bin/sh set -e PWD=$(dirname "$0") BASE_DIR=$(cd "${PWD}/../.." && pwd) RELEASE=$GITHUB_SHA eval "$(opam config env)" cd "${BASE_DIR}" mkdir -p "./.github/opam/packages/liquidsoap-windows/liquidsoap-windows.${RELEASE}" cp ./.github/opam/liquidsoap-windows.opam "./.github/opam/packages/liquidsoap-windows/liquidsoap-windows.${RELEASE}/opam" sed -e "s#@COMMIT_SHORT@#$RELEASE#g" -i "./.github/opam/packages/liquidsoap-windows/liquidsoap-windows.${RELEASE}/opam" opam remote add liquidsoap-devel ./.github/opam liquidsoap-2.3.2/.github/scripts/build-apk.sh000077500000000000000000000055341477303350200211410ustar00rootroot00000000000000#!/bin/sh set -e BRANCH="$1" DOCKER_TAG="$2" ARCH="$3" ALPINE_ARCH="$4" IS_ROLLING_RELEASE="$5" IS_RELEASE="$6" MINIMAL_EXCLUDE_DEPS="$7" APK_RELEASE=0 cd /tmp/liquidsoap-full/liquidsoap APK_VERSION=$(opam show -f version ./liquidsoap.opam | cut -d'-' -f 1) COMMIT_SHORT=$(echo "${GITHUB_SHA}" | cut -c-7) export LIQUIDSOAP_BUILD_TARGET=posix export ABUILD_APK_INDEX_OPTS="--allow-untrusted" if [ -n "${IS_ROLLING_RELEASE}" ]; then APK_PACKAGE="liquidsoap-${COMMIT_SHORT}-${ALPINE_ARCH}" elif [ -n "${IS_RELEASE}" ]; then APK_PACKAGE="liquidsoap-${ALPINE_ARCH}" else TAG=$(echo "${BRANCH}" | tr '[:upper:]' '[:lower:]' | sed -e 's#[^0-9^a-z^A-Z^.^-]#-#g') APK_PACKAGE="liquidsoap-${TAG}-${ALPINE_ARCH}" fi echo "::group:: build ${APK_PACKAGE}.." cd /tmp/liquidsoap-full sed -e "s#@APK_PACKAGE@#${APK_PACKAGE}#" liquidsoap/.github/alpine/APKBUILD.in | sed -e "s#@APK_VERSION@#${APK_VERSION}#" | sed -e "s#@APK_RELEASE@#${APK_RELEASE}#" \ > APKBUILD cp "liquidsoap/.github/alpine/liquidsoap.post-install" "${APK_PACKAGE}.post-install" abuild-keygen -a -n abuild mv /home/opam/packages/tmp/"${ALPINE_ARCH}"/*.apk "/tmp/${GITHUB_RUN_NUMBER}/${DOCKER_TAG}_${ARCH}/alpine" echo "::endgroup::" if [ "${ARCH}" = "amd64" ]; then echo "::group:: save build config for ${APK_PACKAGE}.." eval "$(opam config env)" OCAMLPATH=$(cat .ocamlpath) export OCAMLPATH cd liquidsoap && ./liquidsoap --build-config > "/tmp/${GITHUB_RUN_NUMBER}/${DOCKER_TAG}_${ARCH}/alpine/${APK_PACKAGE}-${APK_VERSION}-r${APK_RELEASE}.config" echo "::endgroup::" fi rm -rf APKBUILD /home/opam/packages/tmp/"${ALPINE_ARCH}" echo "::group:: building ${APK_PACKAGE}-minimal.." # shellcheck disable=SC2086 opam remove -y --assume-depexts $MINIMAL_EXCLUDE_DEPS eval "$(opam config env)" cd /tmp/liquidsoap-full make clean cp PACKAGES.minimal-build PACKAGES cd liquidsoap ./.github/scripts/build-posix.sh 1 cd /tmp/liquidsoap-full OCAMLPATH=$(cat .ocamlpath) export OCAMLPATH sed -e "s#@APK_PACKAGE@#${APK_PACKAGE}-minimal#" liquidsoap/.github/alpine/APKBUILD-minimal.in | sed -e "s#@APK_VERSION@#${APK_VERSION}#" | sed -e "s#@APK_RELEASE@#${APK_RELEASE}#" \ > APKBUILD cp "liquidsoap/.github/alpine/liquidsoap.post-install" "${APK_PACKAGE}-minimal.post-install" abuild-keygen -a -n abuild mv /home/opam/packages/tmp/"${ALPINE_ARCH}"/*.apk "/tmp/${GITHUB_RUN_NUMBER}/${DOCKER_TAG}_${ARCH}/alpine" echo "::endgroup::" if [ "${ARCH}" = "amd64" ]; then echo "::group:: save build config for ${APK_PACKAGE}-minimal.." cd liquidsoap && ./liquidsoap --build-config > "/tmp/${GITHUB_RUN_NUMBER}/${DOCKER_TAG}_${ARCH}/alpine/${APK_PACKAGE}-minimal-${APK_VERSION}-r${APK_RELEASE}.config" fi echo "::endgroup::" { echo "basename=${APK_PACKAGE}-${APK_VERSION}-r${APK_RELEASE}.apk" echo "basename-minimal=${APK_PACKAGE}-minimal-${APK_VERSION}-r${APK_RELEASE}.apk" } >> "${GITHUB_OUTPUT}" liquidsoap-2.3.2/.github/scripts/build-deb.sh000077500000000000000000000056171477303350200211220ustar00rootroot00000000000000#!/bin/sh set -e GITHUB_SHA="$1" BRANCH="$2" DOCKER_TAG="$3" PLATFORM="$4" IS_ROLLING_RELEASE="$5" IS_RELEASE="$6" MINIMAL_EXCLUDE_DEPS="$7" DEB_RELEASE=1 ARCH=$(dpkg --print-architecture) COMMIT_SHORT=$(echo "${GITHUB_SHA}" | cut -c-7) export DEBFULLNAME="The Savonet Team" export DEBEMAIL="savonet-users@lists.sourceforge.net" export LIQUIDSOAP_BUILD_TARGET=posix cd /tmp/liquidsoap-full/liquidsoap eval "$(opam config env)" OCAMLPATH="$(cat ../.ocamlpath)" export OCAMLPATH LIQ_VERSION=$(opam show -f version ./liquidsoap.opam | cut -d'-' -f 1) LIQ_TAG=$(echo "${DOCKER_TAG}" | sed -e 's#_#-#g') if [ -n "${IS_ROLLING_RELEASE}" ]; then LIQ_PACKAGE="liquidsoap-${COMMIT_SHORT}" elif [ -n "${IS_RELEASE}" ]; then LIQ_PACKAGE="liquidsoap" else TAG=$(echo "${BRANCH}" | tr '[:upper:]' '[:lower:]' | sed -e 's#[^0-9^a-z^A-Z^.^-]#-#g') LIQ_PACKAGE="liquidsoap-${TAG}" fi echo "::group:: build ${LIQ_PACKAGE}.." cp -rf .github/debian . rm -rf debian/changelog cp -f debian/control.in debian/control sed -e "s#@LIQ_PACKAGE@#${LIQ_PACKAGE}#g" -i debian/control dch --create --distribution unstable --package "${LIQ_PACKAGE}" --newversion "1:${LIQ_VERSION}-${LIQ_TAG}-${DEB_RELEASE}" "Build ${COMMIT_SHORT}" fakeroot debian/rules binary echo "::endgroup::" if [ "${PLATFORM}" = "amd64" ]; then echo "::group:: save build config for ${LIQ_PACKAGE}.." ./liquidsoap --build-config > "/tmp/${GITHUB_RUN_NUMBER}/${DOCKER_TAG}_${PLATFORM}/debian/${LIQ_PACKAGE}_${LIQ_VERSION}-${LIQ_TAG}-${DEB_RELEASE}.config" mv /tmp/liquidsoap-full/*.deb "/tmp/${GITHUB_RUN_NUMBER}/${DOCKER_TAG}_${PLATFORM}/debian" fi echo "::endgroup::" echo "::group:: build ${LIQ_PACKAGE}-minimal.." # shellcheck disable=SC2086 opam remove -y --verbose $MINIMAL_EXCLUDE_DEPS cd /tmp/liquidsoap-full make clean cp PACKAGES.minimal-build PACKAGES rm .ocamlpath cd liquidsoap ./.github/scripts/build-posix.sh 1 OCAMLPATH="$(cat ../.ocamlpath)" export OCAMLPATH rm -rf debian cp -rf .github/debian . rm -rf debian/changelog cp -f debian/control.in debian/control sed -e "s#@LIQ_PACKAGE@#${LIQ_PACKAGE}-minimal#g" -i debian/control cp -rf debian/rules-minimal debian/rules dch --create --distribution unstable --package "${LIQ_PACKAGE}-minimal" --newversion "1:${LIQ_VERSION}-${LIQ_TAG}-${DEB_RELEASE}" "Build ${COMMIT_SHORT}" fakeroot debian/rules binary echo "::endgroup::" if [ "${PLATFORM}" = "amd64" ]; then echo "::group:: save build config for ${LIQ_PACKAGE}.." ./liquidsoap --build-config > "/tmp/${GITHUB_RUN_NUMBER}/${DOCKER_TAG}_${PLATFORM}/debian/${LIQ_PACKAGE}-minimal_${LIQ_VERSION}-${LIQ_TAG}-${DEB_RELEASE}.config" echo "::endgroup::" fi mv /tmp/liquidsoap-full/*.deb "/tmp/${GITHUB_RUN_NUMBER}/${DOCKER_TAG}_${PLATFORM}/debian" { echo "basename=${LIQ_PACKAGE}_${LIQ_VERSION}-${LIQ_TAG}-${DEB_RELEASE}_$ARCH" echo "basename-minimal=${LIQ_PACKAGE}-minimal_${LIQ_VERSION}-${LIQ_TAG}-${DEB_RELEASE}_$ARCH" } >> "${GITHUB_OUTPUT}" liquidsoap-2.3.2/.github/scripts/build-details.sh000077500000000000000000000047071477303350200220140ustar00rootroot00000000000000#!/bin/bash set -e if [ -n "${GITHUB_HEAD_REF}" ]; then BRANCH="${GITHUB_HEAD_REF#refs_heads_}" else BRANCH="${GITHUB_REF}" fi BRANCH="${BRANCH#refs_heads_}" BRANCH="${BRANCH#refs/heads/}" BRANCH="${BRANCH#refs/tags/}" BRANCH="${BRANCH//\//_}" echo "Detected branch: ${BRANCH}" if [ "${IS_FORK}" == "true" ]; then echo "Branch is from a fork" IS_FORK=true fi if [[ "${IS_FORK}" != "true" && ("${BRANCH}" =~ ^rolling-release\-v[0-9]\.[0-9]\.x || "${BRANCH}" =~ ^v[0-9]\.[0-9]\.[0-9]) ]]; then echo "Branch is release branch" IS_RELEASE=true echo "Branch has a docker release" DOCKER_RELEASE=true else echo "Branch is not release branch" IS_RELEASE= echo "Branch does not have a docker release" DOCKER_RELEASE= fi BUILD_OS='["debian_trixie", "debian_bookworm", "ubuntu_oracular", "ubuntu_noble", "alpine"]' BUILD_PLATFORM='["amd64", "arm64"]' BUILD_INCLUDE='[{"platform": "amd64", "runs-on": "depot-ubuntu-24.04-4", "alpine-arch": "x86_64", "docker-debian-os": "bookworm"}, {"platform": "arm64", "runs-on": "depot-ubuntu-24.04-arm-4", "alpine-arch": "aarch64", "docker-debian-os": "bookworm"}]' SHA=$(git rev-parse --short HEAD) if [[ "${BRANCH}" =~ "rolling-release-" ]]; then echo "Branch is rolling release" IS_ROLLING_RELEASE=true else IS_ROLLING_RELEASE= fi if [ "${IS_FORK}" != "true" ] && [ "${IS_RELEASE}" != "true" ] && [ "${IS_ROLLING_RELEASE}" != "true" ]; then echo "Save tests traces" SAVE_TRACES=true else echo "Disable tests traces upload" SAVE_TRACES= fi if [ "${IS_RELEASE}" != "true" ] || [ "${IS_ROLLING_RELEASE}" == "true" ]; then echo "Build is a snapshot" IS_SNAPSHOT=true else IS_SNAPSHOT= fi MINIMAL_EXCLUDE_DEPS="alsa ao bjack camlimages dssi faad fdkaac flac frei0r gd graphics gstreamer imagelib irc-client-unix ladspa lame lastfm lilv lo mad magic ogg opus osc-unix portaudio pulseaudio samplerate shine soundtouch speex srt tls theora tsdl sqlite3 vorbis" { echo "branch=${BRANCH}" echo "is_release=${IS_RELEASE}" echo "build_os=${BUILD_OS}" echo "build_platform=${BUILD_PLATFORM}" echo "build_include=${BUILD_INCLUDE}" echo "docker_release=${DOCKER_RELEASE}" echo "is_rolling_release=${IS_ROLLING_RELEASE}" echo "sha=${SHA}" echo "s3-artifact-basepath=s3://liquidsoap-artifacts/${GITHUB_WORKFLOW}/${GITHUB_RUN_NUMBER}" echo "is_fork=${IS_FORK}" echo "minimal_exclude_deps=${MINIMAL_EXCLUDE_DEPS}" echo "save_traces=${SAVE_TRACES}" echo "is_snapshot=${IS_SNAPSHOT}" } >> "${GITHUB_OUTPUT}" liquidsoap-2.3.2/.github/scripts/build-doc.sh000077500000000000000000000003431477303350200211240ustar00rootroot00000000000000#!/bin/sh set -e cd /tmp/liquidsoap-full/liquidsoap eval "$(opam config env)" OCAMLPATH="$(cat ../.ocamlpath)" export OCAMLPATH opam install -y odoc dune build @doc dune build --profile release ./src/js/interactive_js.bc.js liquidsoap-2.3.2/.github/scripts/build-posix.sh000077500000000000000000000033261477303350200215250ustar00rootroot00000000000000#!/bin/sh set -e CPU_CORES="$1" export CPU_CORES eval "$(opam config env)" echo "::group::Preparing bindings" cd /tmp/liquidsoap-full git remote set-url origin https://github.com/savonet/liquidsoap-full.git git fetch --recurse-submodules=no && git checkout origin/master -- Makefile.git git reset --hard git pull git pull make clean make public make update echo "::endgroup::" echo "::group::Checking out CI commit" cd /tmp/liquidsoap-full/liquidsoap git fetch origin "$GITHUB_SHA" git checkout "$GITHUB_SHA" mv .github /tmp rm -rf ./* mv /tmp/.github . git reset --hard echo "::endgroup::" echo "::group::Setting up specific dependencies" opam update opam install -y odoc.3.0.0~beta1 cd /tmp/liquidsoap-full/liquidsoap ./.github/scripts/checkout-deps.sh cd /tmp/liquidsoap-full export PKG_CONFIG_PATH=/usr/share/pkgconfig/pkgconfig echo "::endgroup::" echo "::group::Compiling" cd /tmp/liquidsoap-full test -f PACKAGES || cp PACKAGES.default PACKAGES # Workaround touch liquidsoap/configure ./configure --prefix=/usr \ --includedir="\${prefix}/include" \ --mandir="\${prefix}/share/man" \ --infodir="\${prefix}/share/info" \ --sysconfdir=/etc \ --localstatedir=/var \ --with-camomile-data-dir=/usr/share/liquidsoap/camomile \ CFLAGS=-g # Workaround rm liquidsoap/configure OCAMLPATH="$(cat .ocamlpath)" export OCAMLPATH cd /tmp/liquidsoap-full/liquidsoap dune build @doc @doc-private dune build --profile=release echo "::endgroup::" echo "::group::Print build config" dune exec -- liquidsoap --build-config echo "::endgroup::" echo "::group::Basic tests" cd /tmp/liquidsoap-full/liquidsoap dune exec -- liquidsoap --version dune exec -- liquidsoap --check 'print("hello world")' echo "::endgroup::" liquidsoap-2.3.2/.github/scripts/build-website.sh000077500000000000000000000006761477303350200220320ustar00rootroot00000000000000#!/bin/sh set -e PWD=$(dirname "$0") BASE_DIR=$(cd "${PWD}/../.." && pwd) DOCKER_IMAGE=savonet/liquidsoap-github-actions-website docker build --no-cache --tag "${DOCKER_IMAGE}" --file "${BASE_DIR}/.github/docker/website.dockerfile" . id="$(docker create "${DOCKER_IMAGE}")" docker cp "$id:/tmp/liquidsoap-full/website/html" html/ docker cp "$id:/tmp/liquidsoap-full/website/content/doc-dev/reference.md" html/reference.md docker rm -v "$id" liquidsoap-2.3.2/.github/scripts/build-win32.sh000077500000000000000000000044361477303350200213300ustar00rootroot00000000000000#!/bin/sh set -e export PKG_CONFIG_PATH=/usr/src/mxe/usr/x86_64-w64-mingw32.static/lib/pkgconfig SYSTEM="$1" BRANCH="$2" CPU_CORES="$3" IS_ROLLING_RELEASE="$4" IS_RELEASE="$5" GITHUB_SHA="$6" OPAM_PREFIX="$(opam var prefix)" VERSION="$(opam show -f version ./liquidsoap.opam | cut -d'-' -f 1)" PWD="$(dirname "$0")" BASE_DIR="$(cd "${PWD}/../.." && pwd)" COMMIT_SHORT="$(echo "${GITHUB_SHA}" | cut -c-7)" if [ -n "${IS_ROLLING_RELEASE}" ]; then TAG="${COMMIT_SHORT}-" elif [ -n "${IS_RELEASE}" ]; then TAG="" else TAG="${BRANCH}-" fi if [ "${SYSTEM}" = "x64" ]; then HOST="x86_64-w64-mingw32.static" BUILD="${TAG}${VERSION}-win64" PKG_CONFIG_PATH="/usr/src/mxe/usr/x86_64-w64-mingw32.static/lib/pkgconfig/" else # shellcheck disable=SC2034 HOST="i686-w64-mingw32.static" BUILD="${TAG}${VERSION}-win32" # shellcheck disable=SC2034 PKG_CONFIG_PATH="/usr/src/mxe/usr/i686-w64-mingw32.static/lib/pkgconfig/" fi export OPAMSOLVERTIMEOUT=480 export OPAMJOBS="$CPU_CORES" export CC="" echo "::group::Installing deps" eval "$(opam config env)" opam repository set-url windows https://github.com/ocaml-cross/opam-cross-windows.git opam update cd /tmp rm -rf ocaml-posix git clone https://github.com/savonet/ocaml-posix.git cd ocaml-posix opam pin -ny . opam install -y posix-socket.2.2.0 posix-base.2.2.0 opam install -y srt-windows.0.3.3 prometheus-app-windows cohttp-lwt-unix-windows echo "::endgroup::" echo "::group::Install liquidsoap-windows" opam install -y liquidsoap-windows echo "::endgroup::" echo "::group::Save build config" wine "${OPAM_PREFIX}/windows-sysroot/bin/liquidsoap" --build-config >> "/tmp/${GITHUB_RUN_NUMBER}/win32/dist/liquidsoap-$BUILD.config" echo "Build config:" cat "/tmp/${GITHUB_RUN_NUMBER}/win32/dist/liquidsoap-$BUILD.config" echo "::endgroup::" echo "::group::Bundling executable" cd ~ cp -rf "${BASE_DIR}/.github/win32" "liquidsoap-$BUILD" cp -rf "${BASE_DIR}/src/libs" "liquidsoap-$BUILD" cd "liquidsoap-$BUILD" cp "${OPAM_PREFIX}"/windows-sysroot/bin/liquidsoap ./liquidsoap.exe cp -rf "$(ocamlfind -toolchain windows ocamlc -where)/../../share/camomile" . cd .. zip -r "liquidsoap-$BUILD.zip" "liquidsoap-$BUILD" mv "liquidsoap-$BUILD.zip" "/tmp/${GITHUB_RUN_NUMBER}/win32/dist" echo "basename=liquidsoap-${BUILD}" >> "${GITHUB_OUTPUT}" echo "::endgroup::" liquidsoap-2.3.2/.github/scripts/checkout-deps.sh000077500000000000000000000007631477303350200220260ustar00rootroot00000000000000#!/bin/sh CWD="$(dirname "$0")" BASEDIR="$(cd "$CWD/../../.." && pwd)" for i in $(git log --reverse --pretty=format:%B origin/main..HEAD | grep '^DEPS=' | cut -d'=' -f 2 | tr ":" "\n"); do MODULE=$(echo "$i" | cut -d'#' -f 1) COMMIT=$(echo "$i" | cut -d'#' -f 2) echo "Checking out dep $MODULE on commit $COMMIT" cd "${BASEDIR}/${MODULE}" && git fetch origin "$COMMIT" && git reset --hard && git checkout "$COMMIT" && git submodule init && git submodule update done liquidsoap-2.3.2/.github/scripts/export-metrics.sh000077500000000000000000000006211477303350200222460ustar00rootroot00000000000000#!/bin/sh BRANCH=$1 METRICS_DIR=$2 TIME="$(date +%s)" METRICS_FILE="${METRICS_DIR}/${TIME}.yaml" git config --global --add safe.directory '*' cd /tmp/liquidsoap-full/liquidsoap || exit 1 mkdir -p "${METRICS_DIR}" touch /tmp/metrics.yaml cat /tmp/metrics.yaml > "${METRICS_FILE}" { echo "- commit: $(git rev-parse HEAD)" echo " branch: ${BRANCH}" echo " time: ${TIME}" } >> "${METRICS_FILE}" liquidsoap-2.3.2/.github/scripts/push-docker.sh000077500000000000000000000045011477303350200215060ustar00rootroot00000000000000#!/bin/sh set -e TAG=$1 USER=$2 PASSWORD=$3 GHCR_USER=$4 GHCR_PASSWORD=$5 GITHUB_SHA=$6 rm -rf ~/.docker/config.json mkdir -p ~/.docker echo "{ \"experimental\": \"enabled\" }" > ~/.docker/config.json COMMIT_SHORT=$(echo "${GITHUB_SHA}" | cut -c-7)$(echo "${GITHUB_SHA}" | cut -d'-' -f 2 -s | while read -r i; do echo "-$i"; done) docker login -u "$USER" -p "$PASSWORD" docker manifest create "savonet/liquidsoap:${TAG}" --amend "savonet/liquidsoap-ci-build:${TAG}_amd64" --amend "savonet/liquidsoap-ci-build:${TAG}_arm64" docker manifest push "savonet/liquidsoap:${TAG}" docker manifest create "savonet/liquidsoap:${COMMIT_SHORT}" --amend "savonet/liquidsoap-ci-build:${TAG}_amd64" --amend "savonet/liquidsoap-ci-build:${TAG}_arm64" docker manifest push "savonet/liquidsoap:${COMMIT_SHORT}" docker manifest create "savonet/liquidsoap-alpine:${TAG}" --amend "savonet/liquidsoap-ci-build:${TAG}_alpine_amd64" --amend "savonet/liquidsoap-ci-build:${TAG}_alpine_arm64" docker manifest push "savonet/liquidsoap-alpine:${TAG}" docker manifest create "savonet/liquidsoap-alpine:${COMMIT_SHORT}" --amend "savonet/liquidsoap-ci-build:${TAG}_alpine_amd64" --amend "savonet/liquidsoap-ci-build:${TAG}_alpine_arm64" docker manifest push "savonet/liquidsoap-alpine:${COMMIT_SHORT}" docker login ghcr.io -u "$GHCR_USER" -p "$GHCR_PASSWORD" docker manifest create "ghcr.io/savonet/liquidsoap:${TAG}" --amend "ghcr.io/savonet/liquidsoap-ci-build:${TAG}_amd64" --amend "ghcr.io/savonet/liquidsoap-ci-build:${TAG}_arm64" docker manifest push "ghcr.io/savonet/liquidsoap:${TAG}" docker manifest create "ghcr.io/savonet/liquidsoap:${COMMIT_SHORT}" --amend "ghcr.io/savonet/liquidsoap-ci-build:${TAG}_amd64" --amend "ghcr.io/savonet/liquidsoap-ci-build:${TAG}_arm64" docker manifest push "ghcr.io/savonet/liquidsoap:${COMMIT_SHORT}" docker manifest create "ghcr.io/savonet/liquidsoap-alpine:${TAG}" --amend "ghcr.io/savonet/liquidsoap-ci-build:${TAG}_alpine_amd64" --amend "ghcr.io/savonet/liquidsoap-ci-build:${TAG}_alpine_arm64" docker manifest push "ghcr.io/savonet/liquidsoap-alpine:${TAG}" docker manifest create "ghcr.io/savonet/liquidsoap-alpine:${COMMIT_SHORT}" --amend "ghcr.io/savonet/liquidsoap-ci-build:${TAG}_alpine_amd64" --amend "ghcr.io/savonet/liquidsoap-ci-build:${TAG}_alpine_arm64" docker manifest push "ghcr.io/savonet/liquidsoap-alpine:${COMMIT_SHORT}" liquidsoap-2.3.2/.github/scripts/stats-posix.sh000077500000000000000000000013611477303350200215610ustar00rootroot00000000000000#!/bin/sh set -e cd /tmp/liquidsoap-full/liquidsoap eval "$(opam config env)" OCAMLPATH="$(cat ../.ocamlpath)" export OCAMLPATH printf "Memory usage before loading all libraries: " dune exec --display=quiet -- src/bin/liquidsoap.exe --no-stdlib --check 'runtime.gc.full_major() print(runtime.memory.prettify_bytes(runtime.memory().process_private_memory))' printf "Memory usage after loading all libraries: " dune exec --display=quiet -- src/bin/liquidsoap.exe --check 'runtime.gc.full_major() print(runtime.memory().pretty.process_private_memory)' printf "Number of core functions: " dune exec --display=quiet -- src/bin/liquidsoap.exe --no-stdlib --list-functions | wc -l echo printf "Number of functions: " ./liquidsoap --list-functions | wc -l liquidsoap-2.3.2/.github/scripts/test-posix.sh000077500000000000000000000004121477303350200213760ustar00rootroot00000000000000#!/bin/sh set -e TARGET=$1 #export OPAMJOBS="$CPU_CORES" cd /tmp/liquidsoap-full/liquidsoap eval "$(opam config env)" OCAMLPATH="$(cat ../.ocamlpath)" export OCAMLPATH export CLICOLOR_FORCE=1 dune build -j 4 "${TARGET}" --error-reporting=twice --display=quiet liquidsoap-2.3.2/.github/stale.yml000066400000000000000000000016131477303350200170700ustar00rootroot00000000000000# Number of days of inactivity before an issue becomes stale daysUntilStale: 180 # Number of days of inactivity before a stale issue is closed daysUntilClose: 7 # Issues with these labels will never be considered stale exemptLabels: - enhancement - documentation - usage - bug # Label to use when marking an issue as stale staleLabel: stale # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. # Comment to post when closing a stale issue. Set to `false` to disable closeComment: > This issue was closed for lack of activity. If you believe that it is still relevant, please confirm that it applies to the latest released version of liquidsoap and re-open the ticket. Thanks! liquidsoap-2.3.2/.github/win32/000077500000000000000000000000001477303350200161765ustar00rootroot00000000000000liquidsoap-2.3.2/.github/win32/log/000077500000000000000000000000001477303350200167575ustar00rootroot00000000000000liquidsoap-2.3.2/.github/win32/log/.keep000066400000000000000000000000001477303350200176720ustar00rootroot00000000000000liquidsoap-2.3.2/.github/win32/run/000077500000000000000000000000001477303350200170025ustar00rootroot00000000000000liquidsoap-2.3.2/.github/win32/run/.keep000066400000000000000000000000001477303350200177150ustar00rootroot00000000000000liquidsoap-2.3.2/.github/win32/test.liq000066400000000000000000000011601477303350200176620ustar00rootroot00000000000000log.stdout.set(true) log.file.set(false) # Examples # A playlist source with a smart crossfade #s = smart_crossfade(playlist("mp3")) # A HTTP input s = input.http("http://wwoz-sc.streamguys1.com/wwoz-hi.mp3") # An output to a local file, encoding # in ogg/vorbis+ogg/theora #output.file(%ogg(%theora,%vorbis), # fallible=true, # "z:\tmp\output.ogv", # s) # An icecast output in AAC+ format #output.icecast(%aacplus(bitrate=32), # fallible=true, # mount="test", # s) # An output to the local soundcard output.ao(self_sync=false, fallible=true, s) liquidsoap-2.3.2/.github/workflows/000077500000000000000000000000001477303350200172715ustar00rootroot00000000000000liquidsoap-2.3.2/.github/workflows/ci.yml000066400000000000000000001113431477303350200204120ustar00rootroot00000000000000name: CI on: merge_group: pull_request: push: branches: - main tags: - rolling-release-* - v[0-9]+.[0-9]+.[0-9]+ - v[0-9]+.[0-9]+.[0-9]+-* paths: - ".github/**" - "**/*.ml*" - "**/*.liq" - "**/dune" - "**/dune.inc" - "doc/**" - "dune-project" - scripts/**" concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build_details: runs-on: depot-ubuntu-24.04-4 outputs: branch: ${{ steps.build_details.outputs.branch }} sha: ${{ steps.build_details.outputs.sha }} is_release: ${{ steps.build_details.outputs.is_release }} is_rolling_release: ${{ steps.build_details.outputs.is_rolling_release }} is_fork: ${{ steps.build_details.outputs.is_fork }} publish_docker_image: ${{ steps.build_details.outputs.is_fork != 'true' && github.event_name != 'merge_group' }} build_os: ${{ steps.build_details.outputs.build_os }} build_platform: ${{ steps.build_details.outputs.build_platform }} build_include: ${{ steps.build_details.outputs.build_include }} docker_release: ${{ steps.build_details.outputs.docker_release }} s3-artifact-basepath: ${{ steps.build_details.outputs.s3-artifact-basepath }} minimal_exclude_deps: ${{ steps.build_details.outputs.minimal_exclude_deps }} save_traces: ${{ steps.build_details.outputs.save_traces }} is_snapshot: ${{ steps.build_details.outputs.is_snapshot }} steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Get build details env: IS_FORK: ${{ github.event.pull_request.head.repo.fork == true }} run: .github/scripts/build-details.sh id: build_details build_no_depopts: runs-on: depot-ubuntu-24.04-4 needs: build_details container: image: savonet/liquidsoap-ci:debian_bookworm@sha256:c335eb95e0af0f177b790cbe74d3d7eff68546c05c57efe4042b4b4d105dff1a options: --user opam env: HOME: /home/opam steps: - name: Get number of CPU cores uses: savonet/github-actions-cpu-cores-docker@f72bcfaa219a2f60deaf8b26d0707b1d9c67d274 # v1 id: cpu_cores - name: Checkout code run: | cd /tmp/liquidsoap-full/liquidsoap git remote set-url origin https://github.com/savonet/liquidsoap.git git fetch origin ${{ github.sha }} git checkout ${{ github.sha }} - name: Build run: | echo "::group::Preparing build" cd /tmp/liquidsoap-full git remote set-url origin https://github.com/savonet/liquidsoap-full.git git fetch --recurse-submodules=no git checkout origin/master -- Makefile.git make public git reset --hard git submodule foreach 'git reset --hard' git pull cp PACKAGES.minimal PACKAGES opam update opam pin -yn . opam info -f "depopts:" liquidsoap | grep -v osx-secure-transport | xargs opam remove -y inotify ffmpeg-avutil cohttp-lwt-unix prometheus-app ${{ needs.build_details.outputs.minimal_exclude_deps }} echo "::endgroup::" opam install -y mem_usage cd liquidsoap ./.github/scripts/build-posix.sh "${{ steps.cpu_cores.outputs.count }}" env: LIQ_BUILD_MIN: true - name: Build doc run: | cd /tmp/liquidsoap-full/liquidsoap ./.github/scripts/build-doc.sh build_js: runs-on: depot-ubuntu-24.04-4 container: image: savonet/liquidsoap-ci:debian_bookworm@sha256:c335eb95e0af0f177b790cbe74d3d7eff68546c05c57efe4042b4b4d105dff1a options: --user opam env: HOME: /home/opam steps: - name: Checkout code run: | cd /tmp/liquidsoap-full/liquidsoap git remote set-url origin https://github.com/savonet/liquidsoap.git git fetch origin ${{ github.sha }} git checkout ${{ github.sha }} mv .git /tmp rm -rf ./* mv /tmp/.git . git reset --hard - name: Build JS run: | cd /tmp/liquidsoap-full/liquidsoap eval "$(opam config env)" opam update dune build --profile release ./src/js/interactive_js.bc.js tree_sitter_parse: runs-on: depot-ubuntu-24.04-4 needs: build_details steps: - name: Checkout latest code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup node uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: latest - name: Parse using tree-sitter run: | git clone https://github.com/savonet/tree-sitter-liquidsoap.git cd tree-sitter-liquidsoap npm install npm exec tree-sitter -- parse -q -s ../../**/*.liq lezer_parse: runs-on: depot-ubuntu-24.04-4 needs: build_details steps: - name: Checkout latest code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup node uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: latest - name: Parse using liquidsoap-lezer-print-tree run: | # This one has unicode variable name that isn't supported yet. rm -rf src/libs/list.liq git clone https://github.com/savonet/codemirror-lang-liquidsoap.git cd codemirror-lang-liquidsoap npm install npm exec liquidsoap-lezer-print-tree -- -q ../../**/*.liq update_doc: runs-on: depot-ubuntu-24.04-4 needs: build_details if: github.event_name != 'pull_request' && github.repository_owner == 'savonet' && needs.build_details.outputs.branch == 'main' container: image: savonet/liquidsoap-ci:debian_bookworm@sha256:c335eb95e0af0f177b790cbe74d3d7eff68546c05c57efe4042b4b4d105dff1a options: --user root -v ${{ github.workspace }}/${{ github.run_number }}:/tmp/${{ github.run_number }} env: HOME: /home/opam steps: - name: Get number of CPU cores uses: savonet/github-actions-cpu-cores-docker@f72bcfaa219a2f60deaf8b26d0707b1d9c67d274 # v1 id: cpu_cores - name: Checkout code run: | cd /tmp/liquidsoap-full/liquidsoap rm -rf doc/content/build.md doc/content/install.md sudo -u opam -E git remote set-url origin https://github.com/savonet/liquidsoap.git sudo -u opam -E git fetch origin ${{ github.sha }} sudo -u opam -E git checkout ${{ github.sha }} - name: Install node run: | apt-get update apt-get install -y npm - name: Build doc env: CPU_CORES: ${{ steps.cpu_cores.outputs.count }} run: | cd /tmp/liquidsoap-full/liquidsoap # TMP sudo -u opam -E ./.github/scripts/add-local-opam-packages.sh sudo -u opam -E ./.github/scripts/build-posix.sh "${{ steps.cpu_cores.outputs.count }}" cd /tmp/liquidsoap-full/liquidsoap/ mkdir -p /tmp/${{ github.run_number }}/odoc cp -rf _build/default/_doc/_html/* /tmp/${{ github.run_number }}/odoc eval $(opam config env) dune build src/js cd /tmp/liquidsoap-full/website make dist mkdir -p /tmp/${{ github.run_number }}/html cp -rf html/* /tmp/${{ github.run_number }}/html - name: Push doc content if: success() && github.repository_owner == 'savonet' uses: crazy-max/ghaction-github-pages@df5cc2bfa78282ded844b354faee141f06b41865 # v4.2.0 with: repo: savonet/savonet.github.io target_branch: master build_dir: ${{ github.run_number }}/html fqdn: www.liquidsoap.info env: GH_PAT: ${{ secrets.WEBSITE_TOKEN }} - name: Push odoc content if: success() && github.repository_owner == 'savonet' uses: crazy-max/ghaction-github-pages@df5cc2bfa78282ded844b354faee141f06b41865 # v4.2.0 with: repo: savonet/liquidsoap target_branch: gh_pages build_dir: ${{ github.run_number }}/odoc env: GH_PAT: ${{ secrets.WEBSITE_TOKEN }} run_tests: runs-on: depot-ubuntu-24.04-4 needs: build_details container: image: savonet/liquidsoap-ci:debian_bookworm@sha256:c335eb95e0af0f177b790cbe74d3d7eff68546c05c57efe4042b4b4d105dff1a options: --user root --privileged --ulimit core=-1 --security-opt seccomp=unconfined -v ${{ github.workspace }}/${{ github.run_number }}:/tmp/${{ github.run_number }} strategy: fail-fast: false matrix: target: ["@citest", "@doctest", "@mediatest"] env: HOME: /home/opam steps: - name: Get number of CPU cores uses: savonet/github-actions-cpu-cores-docker@f72bcfaa219a2f60deaf8b26d0707b1d9c67d274 # v1 id: cpu_cores - name: Enable core dump run: | ulimit -c unlimited mkdir -p /tmp/${{ github.run_number }}/core chown -R opam /tmp/${{ github.run_number }}/core echo /tmp/${{ github.run_number }}/core/core.%h.%e.%t > /proc/sys/kernel/core_pattern - name: Checkout code run: | cd /tmp/liquidsoap-full/liquidsoap rm -rf doc/content/build.md doc/content/install.md sudo -u opam -E git remote set-url origin https://github.com/savonet/liquidsoap.git sudo -u opam -E git fetch origin ${{ github.sha }} sudo -u opam -E git checkout ${{ github.sha }} - name: Build env: CPU_CORES: ${{ steps.cpu_cores.outputs.count }} run: | cd /tmp/liquidsoap-full/liquidsoap # TMP sudo -u opam -E ./.github/scripts/add-local-opam-packages.sh sudo -u opam -E ./.github/scripts/build-posix.sh "${{ steps.cpu_cores.outputs.count }}" cp /tmp/liquidsoap-full/liquidsoap/_build/default/src/bin/liquidsoap.exe /tmp/${{ github.run_number }}/core/liquidsoap - name: Compute stats run: | cd /tmp/liquidsoap-full/liquidsoap sudo -u opam -E ./.github/scripts/stats-posix.sh - name: Install additional packages if: matrix.target == '@doctest' run: | apt-get -y update apt-get -y install frei0r-plugins - name: Run tests env: CPU_CORES: ${{ steps.cpu_cores.outputs.count }} run: | cd /tmp/liquidsoap-full/liquidsoap sudo -u opam -E ./.github/scripts/test-posix.sh ${{ matrix.target }} - name: Finalize metrics run: | cd /tmp/liquidsoap-full/liquidsoap mkdir -p "/tmp/${{ github.run_number }}/metrics" chown -R opam "/tmp/${{ github.run_number }}/metrics" sudo -u opam -E ./.github/scripts/export-metrics.sh "${{ needs.build_details.outputs.branch }}" "/tmp/${{ github.run_number }}/metrics" - name: Upload metrics artifact uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: needs.build_details.outputs.is_fork != 'true' && matrix.target == '@citest' with: name: metrics path: ${{ github.workspace }}/${{ github.run_number }}/metrics - name: Upload metrics if: needs.build_details.outputs.is_fork != 'true' && matrix.target == '@citest' uses: peaceiris/actions-gh-pages@4f9cc6602d3f66b9c108549d475ec49e8ef4d45e # v4.0.0 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_branch: metrics publish_dir: ${{ github.workspace }}/${{ github.run_number }}/metrics keep_files: true - name: Save traces if: (success() || failure()) && ${{ needs.build_details.outputs.save_traces == 'true' }} run: | mkdir -p /tmp/${{ github.run_number }}/traces/${{ matrix.target }} cd /tmp/liquidsoap-full/liquidsoap find _build/default/tests | grep '\.trace$' | while read i; do mv "$i" /tmp/${{ github.run_number }}/traces/${{ matrix.target }}; done - name: Upload traces if: (success() || failure()) && ${{ needs.build_details.outputs.save_traces == 'true' }} uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: traces-${{ matrix.target }} path: ${{ github.workspace }}/${{ github.run_number }}/traces/${{ matrix.target }} - name: Export potential core dumps uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: failure() with: name: core-dump-${{ matrix.os }}-${{ matrix.platform }}-${{ matrix.target }} path: ${{ github.workspace }}/${{ github.run_number }}/core - name: Cleanup if: ${{ always() }} run: | rm -rf /tmp/${{ github.run_number }}/core build_opam: runs-on: depot-ubuntu-24.04-4 strategy: fail-fast: false matrix: ocaml-compiler: - 4.14.x - 5.3.x steps: - name: Checkout latest code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Update packages run: | sudo apt-get update - name: Setup OCaml uses: ocaml/setup-ocaml@e9f838657177763a90b9648afb8808cbd79aa45a # v3.2.16 with: ocaml-compiler: ${{ matrix.ocaml-compiler }} opam-pin: false opam-depext: false - name: Add local packages run: | ./.github/scripts/add-local-opam-packages.sh - name: Install liquidsoap run: | opam install --cli=2.1 --confirm-level=unsafe-yes . - name: Install ocamlformat if: matrix.ocaml-compiler == '5.3.x' run: | opam install ocamlformat=0.27.0 - name: Set PY env variable. if: matrix.ocaml-compiler == '5.3.x' run: echo "PY=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV - name: Restore pre-commit cache uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with: path: ~/.cache/pre-commit key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} - name: Run pre-commit if: matrix.ocaml-compiler == '5.3.x' uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 build_posix: runs-on: ${{ matrix.runs-on }} needs: build_details strategy: fail-fast: false matrix: os: ${{ fromJson(needs.build_details.outputs.build_os) }} platform: ${{ fromJson(needs.build_details.outputs.build_platform) }} include: ${{ fromJson(needs.build_details.outputs.build_include) }} container: image: savonet/liquidsoap-ci:${{ matrix.os }} options: --user root --privileged -v ${{ github.workspace }}/${{ github.run_number }}:/tmp/${{ github.run_number }} env: HOME: /home/opam IS_SNAPSHOT: ${{ needs.build_details.outputs.is_snapshot == 'true' }} steps: - name: Get number of CPU cores uses: savonet/github-actions-cpu-cores-docker@f72bcfaa219a2f60deaf8b26d0707b1d9c67d274 # v1 id: cpu_cores - name: Checkout code run: | cd /tmp/liquidsoap-full/liquidsoap rm -rf doc/content/build.md doc/content/install.md sudo -u opam -E git remote set-url origin https://github.com/savonet/liquidsoap.git sudo -u opam -E git fetch origin ${{ github.sha }} sudo -u opam -E git checkout ${{ github.sha }} - name: Build run: | cd /tmp/liquidsoap-full/liquidsoap export CPU_CORES=${{ steps.cpu_cores.outputs.count }} # TMP sudo -u opam -E ./.github/scripts/add-local-opam-packages.sh sudo -u opam -E ./.github/scripts/build-posix.sh "${{ steps.cpu_cores.outputs.count }}" - name: Build doc if: contains(matrix.os, 'debian') || contains(matrix.os, 'ubuntu') run: | cd /tmp/liquidsoap-full/liquidsoap sudo -u opam -E ./.github/scripts/build-doc.sh - name: Build debian package if: contains(matrix.os, 'debian') || contains(matrix.os, 'ubuntu') id: build_deb run: | mkdir -p /tmp/${{ github.run_number }}/${{ matrix.os }}_${{ matrix.platform }}/debian chown -R opam /tmp/${{ github.run_number }}/${{ matrix.os }}_${{ matrix.platform }}/debian chown -R opam "${GITHUB_OUTPUT}" cd /tmp/liquidsoap-full/liquidsoap sudo -u opam -E ./.github/scripts/build-deb.sh ${{ github.sha }} ${{ needs.build_details.outputs.branch }} ${{ matrix.os }} ${{ matrix.platform }} "${{ needs.build_details.outputs.is_rolling_release }}" "${{ needs.build_details.outputs.is_release }}" "${{ needs.build_details.outputs.minimal_exclude_deps }}" - name: Upload debian packages artifacts if: (contains(matrix.os, 'debian') || contains(matrix.os, 'ubuntu')) uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: ${{ steps.build_deb.outputs.basename }} path: ${{ github.workspace }}/${{ github.run_number }}/${{ matrix.os }}_${{ matrix.platform }}/debian if-no-files-found: error - name: Build alpine package if: matrix.os == 'alpine' id: build_apk run: | cd /tmp/liquidsoap-full/liquidsoap apk add alpine-sdk adduser opam abuild mkdir -p /tmp/${{ github.run_number }}/${{ matrix.os }}_${{ matrix.platform }}/alpine chown -R opam /tmp/${{ github.run_number }}/${{ matrix.os }}_${{ matrix.platform }}/alpine chown -R opam "${GITHUB_OUTPUT}" sudo -u opam -E ./.github/scripts/build-apk.sh ${{ needs.build_details.outputs.branch }} ${{ matrix.os }} ${{ matrix.platform }} ${{ matrix.alpine-arch }} "${{ needs.build_details.outputs.is_rolling_release }}" "${{ needs.build_details.outputs.is_release }}" "${{ needs.build_details.outputs.minimal_exclude_deps }}" - name: Upload alpine packages artifacts if: needs.build_details.outputs.is_fork != 'true' && matrix.os == 'alpine' uses: savonet/aws-s3-docker-action@master env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} SOURCE: ${{ github.workspace }}/${{ github.run_number }}/${{ matrix.os }}_${{ matrix.platform }}/alpine TARGET: ${{ needs.build_details.outputs.s3-artifact-basepath }} - name: Cleanup if: ${{ always() }} run: | rm -rf /tmp/${{ github.run_number }}/${{ matrix.os }}_${{ matrix.platform }} fetch_s3_artifacts: runs-on: depot-ubuntu-24.04-4 needs: [build_details, build_posix] steps: - name: Prepare directory run: | rm -rf ${{ github.workspace }}/${{ github.run_number }}/s3-artifacts mkdir -p ${{ github.workspace }}/${{ github.run_number }}/s3-artifacts - name: Fetch S3 artifacts if: needs.build_details.outputs.is_fork != 'true' uses: savonet/aws-s3-docker-action@master env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} SOURCE: ${{ needs.build_details.outputs.s3-artifact-basepath }} TARGET: ${{ github.workspace }}/${{ github.run_number }}/s3-artifacts - name: Upload S3 artifacts uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: needs.build_details.outputs.is_fork != 'true' with: name: alpine-packages path: ${{ github.workspace }}/${{ github.run_number }}/s3-artifacts build_win32: runs-on: depot-ubuntu-24.04-4 needs: build_details strategy: fail-fast: false matrix: system: [x64] container: image: savonet/liquidsoap-win32-${{ matrix.system }} options: --user root -v ${{ github.workspace }}/${{ github.run_number }}:/tmp/${{ github.run_number }} env: OPAM_DEPS: ao-windows,lastfm-windows,camomile-windows,cry-windows,dtools-windows,duppy-windows,ffmpeg-windows,ffmpeg-avutil-windows,mm-windows,re-windows,portaudio-windows,samplerate-windows,sedlex-windows,ssl-windows,srt-windows,winsvc-windows,mem_usage-windows,prometheus-app-windows,cohttp-lwt-unix-windows IS_SNAPSHOT: ${{ needs.build_details.outputs.is_snapshot == 'true' }} TOOLPREF64: /usr/src/mxe/usr/bin/x86_64-w64-mingw32.static- steps: - name: Get number of CPU cores uses: savonet/github-actions-cpu-cores-docker@f72bcfaa219a2f60deaf8b26d0707b1d9c67d274 # v1 id: cpu_cores - name: Checkout code run: | mkdir -p /tmp/${{ github.run_number }}/win32/liquidsoap cd /tmp/${{ github.run_number }}/win32/liquidsoap git init git remote add origin https://github.com/${{ github.repository }}.git git fetch origin ${{ github.sha }} git checkout ${{ github.sha }} chown -R opam /tmp/${{ github.run_number }}/win32 - name: Add local packages run: | cd /tmp/${{ github.run_number }}/win32/liquidsoap/ gosu opam:root ./.github/scripts/add-local-opam-packages.sh - name: Build windows binary run: | mkdir -p /tmp/${{ github.run_number }}/win32/dist chown -R opam /tmp/${{ github.run_number }}/win32/dist chown -R opam "${GITHUB_OUTPUT}" cd /tmp/${{ github.run_number }}/win32/liquidsoap gosu opam:root ./.github/scripts/build-win32.sh ${{ matrix.system }} ${{ needs.build_details.outputs.branch }} "${{ steps.cpu_cores.outputs.count }}" "${{ needs.build_details.outputs.is_rolling_release }}" "${{ needs.build_details.outputs.is_release }}" ${{ github.sha }} id: build - name: Upload artifact uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: ${{ steps.build.outputs.basename }} path: ${{ github.workspace }}/${{ github.run_number }}/win32/dist if-no-files-found: error - name: Cleanup if: ${{ always() }} run: | rm -rf /tmp/${{ github.run_number }}/win32 update_release: runs-on: depot-ubuntu-24.04-4 needs: [ build_details, build_no_depopts, build_js, run_tests, build_posix, build_win32, fetch_s3_artifacts, ] if: needs.build_details.outputs.is_release steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Download all artifact uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: path: artifacts/${{ needs.build_details.outputs.sha }} - name: List assets to upload id: release_assets run: | echo "release_assets<> $GITHUB_OUTPUT find artifacts/${{ needs.build_details.outputs.sha }} -type f | egrep '\.apk$|\.deb$|\.config|\.zip$' | sort -u >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - name: Generate release notes id: release_notes run: | echo "release_notes<> $GITHUB_OUTPUT if [ ${{ needs.build_details.outputs.is_rolling_release }} = "true" ]; then cat doc/content/rolling-release.md >> $GITHUB_OUTPUT echo "\n\n" >> $GITHUB_OUTPUT fi cat CHANGES.md | sed -e "/---/,\$d" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - name: Generate release assets notes id: release_assets_notes run: | echo "release_notes<> $GITHUB_OUTPUT cat doc/content/release-assets.md >> $GITHUB_OUTPUT echo "\n\n" >> $GITHUB_OUTPUT cat CHANGES.md | sed -e "/---/,\$d" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - name: Delete old release assets uses: mknejp/delete-release-assets@v1 with: token: ${{ secrets.GITHUB_TOKEN }} tag: ${{ needs.build_details.outputs.branch }} assets: "*" fail-if-no-release: false fail-if-no-assets: false - name: Upload assets to main repo release uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1 with: token: ${{ secrets.GITHUB_TOKEN }} tag_name: ${{ needs.build_details.outputs.branch }} files: ${{ steps.release_assets.outputs.release_assets }} prerelease: ${{ needs.build_details.outputs.is_rolling_release }} body: ${{ steps.release_notes.outputs.release_notes }} draft: ${{ !needs.build_details.outputs.is_rolling_release }} - name: Upload assets to release repo uses: softprops/action-gh-release@c95fe1489396fe8a9eb87c0abf8aa5b2ef267fda # v2.2.1 with: token: ${{ secrets.LIQUIDSOAP_RELEASE_ASSETS_TOKEN }} tag_name: ${{ needs.build_details.outputs.branch }} files: ${{ steps.release_assets.outputs.release_assets }} repository: savonet/liquidsoap-release-assets prerelease: ${{ needs.build_details.outputs.is_rolling_release }} body: ${{ steps.release_assets_notes.outputs.release_notes }} draft: ${{ !needs.build_details.outputs.is_rolling_release }} build_docker: runs-on: ${{ matrix.runs-on }} needs: [build_details, build_posix, fetch_s3_artifacts] strategy: fail-fast: false matrix: platform: ${{ fromJson(needs.build_details.outputs.build_platform) }} include: ${{ fromJson(needs.build_details.outputs.build_include) }} steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - name: Download all artifact uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: path: artifacts/${{ needs.build_details.outputs.sha }} - name: Get debian package run: | echo "deb-file=$(find artifacts/${{ needs.build_details.outputs.sha }} -type f | grep ${{ matrix.docker-debian-os }} | grep -v minimal | grep '${{ matrix.platform }}\.deb$' | grep -v dbgsym | grep deb)" >> "${GITHUB_OUTPUT}" id: debian_package - name: Get debian debug package run: | echo "deb-file=$(find artifacts/${{ needs.build_details.outputs.sha }} -type f | grep ${{ matrix.docker-debian-os }} | grep -v minimal | grep '${{ matrix.platform }}\.deb$' | grep dbgsym | grep deb)" >> "${GITHUB_OUTPUT}" id: debian_debug_package - name: Login to Docker Hub if: needs.build_details.outputs.publish_docker_image == 'true' uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Login to GitHub Container Registry if: needs.build_details.outputs.publish_docker_image == 'true' uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push docker image uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 env: DOCKER_BUILD_RECORD_UPLOAD: false with: build-args: | "DEB_FILE=${{ steps.debian_package.outputs.deb-file }}" "DEB_DEBUG_FILE=${{ steps.debian_debug_package.outputs.deb-file }}" context: . file: .github/docker/debian.dockerfile tags: | "savonet/liquidsoap-ci-build:${{ needs.build_details.outputs.branch }}_${{ matrix.platform }}" "ghcr.io/savonet/liquidsoap-ci-build:${{ needs.build_details.outputs.branch }}_${{ matrix.platform }}" push: ${{ needs.build_details.outputs.publish_docker_image }} provenance: false sbom: false build_docker_alpine: runs-on: ${{ matrix.runs-on }} needs: [build_details, build_posix, fetch_s3_artifacts] if: needs.build_details.outputs.is_fork != 'true' strategy: fail-fast: false matrix: platform: ${{ fromJson(needs.build_details.outputs.build_platform) }} include: ${{ fromJson(needs.build_details.outputs.build_include) }} steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Download all artifact uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: path: artifacts/${{ needs.build_details.outputs.sha }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - name: Get alpine package run: | echo "apk-file=$(find artifacts/${{ needs.build_details.outputs.sha }} -type f | grep -v minimal | grep 'apk$' | grep -v dbg | grep ${{ matrix.alpine-arch }})" >> "${GITHUB_OUTPUT}" id: alpine_package - name: Login to Docker Hub if: needs.build_details.outputs.publish_docker_image == 'true' uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Login to GitHub Container Registry if: needs.build_details.outputs.publish_docker_image == 'true' uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push docker image uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 env: DOCKER_BUILD_RECORD_UPLOAD: false with: build-args: | "APK_FILE=${{ steps.alpine_package.outputs.apk-file }}" context: . file: .github/docker/alpine.dockerfile tags: | "savonet/liquidsoap-ci-build:${{ needs.build_details.outputs.branch }}_alpine_${{ matrix.platform }}" "ghcr.io/savonet/liquidsoap-ci-build:${{ needs.build_details.outputs.branch }}_alpine_${{ matrix.platform }}" push: ${{ needs.build_details.outputs.publish_docker_image }} provenance: false sbom: false build_docker_minimal: runs-on: ${{ matrix.runs-on }} needs: [build_details, build_posix, fetch_s3_artifacts] strategy: fail-fast: false matrix: platform: ${{ fromJson(needs.build_details.outputs.build_platform) }} include: ${{ fromJson(needs.build_details.outputs.build_include) }} steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - name: Download all artifact uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: path: artifacts/${{ needs.build_details.outputs.sha }} - name: Get debian package run: | echo "deb-file=$(find artifacts/${{ needs.build_details.outputs.sha }} -type f | grep ${{ matrix.docker-debian-os }} | grep minimal | grep '${{ matrix.platform }}\.deb$' | grep -v dbgsym | grep deb)" >> "${GITHUB_OUTPUT}" id: debian_package - name: Get debian debug package run: | echo "deb-file=$(find artifacts/${{ needs.build_details.outputs.sha }} -type f | grep ${{ matrix.docker-debian-os }} | grep minimal | grep '${{ matrix.platform }}\.deb$' | grep dbgsym | grep deb)" >> "${GITHUB_OUTPUT}" id: debian_debug_package - name: Login to Docker Hub if: needs.build_details.outputs.publish_docker_image == 'true' uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Login to GitHub Container Registry if: needs.build_details.outputs.publish_docker_image == 'true' uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push docker image uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 env: DOCKER_BUILD_RECORD_UPLOAD: false with: build-args: | "DEB_FILE=${{ steps.debian_package.outputs.deb-file }}" "DEB_DEBUG_FILE=${{ steps.debian_debug_package.outputs.deb-file }}" context: . file: .github/docker/debian.dockerfile tags: | "savonet/liquidsoap-ci-build:${{ needs.build_details.outputs.branch }}-minimal_${{ matrix.platform }}" "ghcr.io/savonet/liquidsoap-ci-build:${{ needs.build_details.outputs.branch }}-minimal_${{ matrix.platform }}" push: ${{ needs.build_details.outputs.publish_docker_image }} provenance: false sbom: false build_docker_alpine_minimal: runs-on: ${{ matrix.runs-on }} needs: [build_details, build_posix, fetch_s3_artifacts] if: needs.build_details.outputs.is_fork != 'true' strategy: fail-fast: false matrix: platform: ${{ fromJson(needs.build_details.outputs.build_platform) }} include: ${{ fromJson(needs.build_details.outputs.build_include) }} steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Download all artifact uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: path: artifacts/${{ needs.build_details.outputs.sha }} - name: Set up Docker Buildx uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - name: Get alpine package run: | echo "apk-file=$(find artifacts/${{ needs.build_details.outputs.sha }} -type f | grep minimal | grep 'apk$' | grep -v dbg | grep ${{ matrix.alpine-arch }})" >> "${GITHUB_OUTPUT}" id: alpine_package - name: Login to Docker Hub if: needs.build_details.outputs.publish_docker_image == 'true' uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Login to GitHub Container Registry if: needs.build_details.outputs.publish_docker_image == 'true' uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push docker image uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 env: DOCKER_BUILD_RECORD_UPLOAD: false with: build-args: | "APK_FILE=${{ steps.alpine_package.outputs.apk-file }}" context: . file: .github/docker/alpine.dockerfile tags: | "savonet/liquidsoap-ci-build:${{ needs.build_details.outputs.branch }}-minimal_alpine_${{ matrix.platform }}" "ghcr.io/savonet/liquidsoap-ci-build:${{ needs.build_details.outputs.branch }}-minimal_alpine_${{ matrix.platform }}" push: ${{ needs.build_details.outputs.publish_docker_image }} provenance: false sbom: false build_docker_release: runs-on: depot-ubuntu-24.04-4 needs: [build_details, run_tests, build_docker, build_docker_alpine] if: needs.build_details.outputs.docker_release steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Push consolidated manifest run: .github/scripts/push-docker.sh ${{ needs.build_details.outputs.branch }} ${{ secrets.DOCKERHUB_USER }} ${{ secrets.DOCKERHUB_PASSWORD }} ${{ github.actor }} ${{ secrets.GITHUB_TOKEN }} ${{ github.sha }} build_docker_release-minimal: runs-on: depot-ubuntu-24.04-4 needs: [ build_details, run_tests, build_docker_minimal, build_docker_alpine_minimal, ] if: needs.build_details.outputs.docker_release steps: - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Push consolidated manifest run: .github/scripts/push-docker.sh ${{ needs.build_details.outputs.branch }}-minimal ${{ secrets.DOCKERHUB_USER }} ${{ secrets.DOCKERHUB_PASSWORD }} ${{ github.actor }} ${{ secrets.GITHUB_TOKEN }} ${{ github.sha }}-minimal liquidsoap-2.3.2/.gitignore000066400000000000000000000005401477303350200156630ustar00rootroot00000000000000*.sw* *~ _build/ liquidsoap.config *.install tests/streams/ssl.cert tests/streams/ssl.key .DS_Store package.json package-lock.json node_modules/ pnpm-lock.yaml package-lock.json src/tooling/prettier-plugin-liquidsoap/dist/liquidsoap.js src/tooling/prettier-plugin-liquidsoap/dist/web.mjs src/tooling/prettier-plugin-liquidsoap/dist/node.js *.conflicts liquidsoap-2.3.2/.ocamlformat000066400000000000000000000003371477303350200162040ustar00rootroot00000000000000version=0.27.0 profile = conventional break-separators = after space-around-lists = false doc-comments = before match-indent = 2 match-indent-nested = always parens-ite exp-grouping = preserve module-item-spacing = compact liquidsoap-2.3.2/.pre-commit-config.yaml000066400000000000000000000036161477303350200201630ustar00rootroot00000000000000--- # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks # # Run the following command to set up the pre-commit git hook scripts: # $ pre-commit install repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: check-added-large-files - id: check-case-conflict - id: check-symlinks - id: destroyed-symlinks - id: check-json - id: check-yaml - id: check-xml - id: check-merge-conflict - id: end-of-file-fixer exclude: dune.inc - id: mixed-line-ending exclude: dune.inc - id: trailing-whitespace exclude: dune.inc - repo: https://github.com/savonet/pre-commit-liquidsoap rev: 056cf2da9d985e1915a069679f126a461206504a hooks: - id: liquidsoap-prettier - repo: https://github.com/pre-commit/mirrors-prettier rev: v4.0.0-alpha.8 hooks: - id: prettier files: \.(md|yml|yaml|json)$ - repo: https://github.com/codespell-project/codespell rev: v2.3.0 hooks: - id: codespell args: [-w, --ignore-words=.codespellignore] exclude: ^(doc/orig/fosdem2020|tests/language/cue_test.liq) - repo: local hooks: - id: shfmt name: shfmt language: docker_image entry: mvdan/shfmt -i 2 -ci -sr -kp -w types: [shell] - id: shellcheck name: shellcheck language: docker_image entry: koalaman/shellcheck --color=always types: [shell] - id: dunefmt name: dunefmt language: system entry: opam exec dune -- build @fmt --auto-promote files: \.(ml|mli|dune|dune-project)$ pass_filenames: false - id: dunegen name: dunegen language: system entry: opam exec dune -- build @gendune --auto-promote pass_filenames: false always_run: true liquidsoap-2.3.2/CHANGES000077700000000000000000000000001477303350200162542CHANGES.mdustar00rootroot00000000000000liquidsoap-2.3.2/CHANGES.md000066400000000000000000003445661477303350200153100ustar00rootroot00000000000000# 2.3.2 (2025-04-01) 🃏 New: - Added support for multiple metadata fields in ogg and flac metadata - Added support for track-level REM ALBUM in cue file parsing (#4381) Changed: - Added `"pic"` to list of excluded metadata for automatic charset conversion. - Added `settings.charset.max_string_length` setting to prevent automatic charset conversions of strings over that length. Fixed: - Optimized CPU usage (#4369, #4370) - Fixed empty initial HLS segment (#4401) - Fixed support for `duration` metadata in image decoder (#4397) - Fixed cue-out bug in cue file parsing (#4381) - Bring back parse error location. (#4362) - Fixed SRT encoding when restarting a stream with reverse data flow (#4399) - Make sure that audioscrobbler `on_track`/`on_end` operations are sent to a asynchronous task queue. - Fixed resources accumulation leading to catchup when using `crossfade` (#4419, #4410) - Fixed source reselection logic issue that was causing crashes when using `switch` and `fallback` operators (#4420) - Fixed self-sync logic with pulse audio outputs (#4429) - Fixed script caching on windows. --- # 2.3.1 (2025-02-05) New: - Added support for address resolution preference in SRT (#4317) - Added global address resolution settings for SRT and Icecast (#4317) - Added support for parsing and rendering XML natively (#4252) - Added support for `WAVE_FORMAT_EXTENSIBLE` to the internal wav dexcoder. - Added optional `buffer_size` parameter to `input.alsa` and `output.alsa` (#4243) - Reimplemented audioscrobbler support natively using the more recent protocol (#4250) - Added boolean getter to disable/enable normalization (#4308) Changed: - Make alsa I/O work with buffer size different than liquidsoap internal frame (#4236) - Reimplemented CUE file parser in native liquidsoap script, added support for multiple files and EAC non-compliant extension (#1373, #4330) - Make `"song"` metadata mapping to `"title"` metadata in `input.harbor` disabled when either `"artist"` or `"title"` is also passed. Add a configuration key to disable this mechanism. (#4235, #2676) - `output.icecast` now re-sends the last metadata when connecting to the remote server unless explicitly disabled using the `send_last_metadata_on_connect` option (#3906) - Add full explicit support for `ipv4` vs. `ipv6` resolution in SRT inputs and outputs, add global `settings.srt.prefer_address` and `settings.icecast.prefer_address` (#4317) - Added generic SRT socket get/set API. Added new socket options, including `latency` and `ipv6only`. Fixed: - Fixed request resolution loop when enabling both `autocue` and `replaygain` metadata resolvers (#4245, fixed in #4246) - Fixed `flac` encoding segfault (#4286, #4274) - Fixed source `last_metadata` not being properly updated (#4262) - Convert all ICY (icecast) metadata from `input.http` to `utf8`. - Fixed `inotify` unwatching due to GC cleanup (#4275) - Fixed `delay` initial conditions (#4281) --- # 2.3.0 (2024-11-27) New: - Rewrote the streaming API to work with immutable frame content. This should greatly impact impredictable side-effect of the previous models w.r.t. track marks, content sharing and more. This also impacts multiple operators behavior. Mostly, things should be roughly the same with differences around behaviors related to track marks (`source.on_track` and etc). (#3577) - Added script caching layer for faster script startup time. See: https://www.liquidsoap.info/blog/2024-06-13-a-faster-liquidsoap/ for details (#3924, #3949, #3959 and #3977) - Rewrote the clock/streaming loop layer. This prepares our streaming system to support multicore when the OCaml compiler is mature enough to allow it. Clocks are now attached to sources via their `clock` methods. Returned value is a stripped down `clock` variable. Users can use the `clock` function to retrieve the full methods, e.g. `s = sine(); c = clock(s.clock)`. This value has advanced functions for clock control such as `start`/`stop`, `ticks` and `self_sync` to check for `self-sync`. (#3781) - Allow frames duration shorter than one video frames, typically values under `0.04s`. Smaller frames means less latency and memory consumption at the expense of a higher CPU usage (#3607) - Change default frame duration to `0.02s` (#4033) - Optimized runtime (#3927, #3928, #3919) - Added NDI output support (#4181) - Added `finally` to execute code regardless of whether or not an exception is raised (see: #3895 for more details). - Added support for Spinitron submission API (#4158) - Removed gstreamer support. Gstreamer's architecture was never a good fit for us and created a huge maintenance and debugging burden and it had been marked as deprecated for a while. Most, if not all of its features should be available using `ffmpeg`. (#4036) - Removed `taglib` support. It is superseded by the internal `ocaml-metadata` module and taglib, with its dependency on the C++ runtime library, has been causing issues with binary builds portability and crashes with the (not yet supported) OCaml 5 compiler. (#4087) - Added `video.canvas` to make it possible to position video elements independently of the rendered video size ([#3656](https://github.com/savonet/liquidsoap/pull/3656), [blog post](https://www.liquidsoap.info/blog/2024-02-10-video-canvas-and-ai/)) - Added cover manager from an original code by @vitoyucepi (#3651) - Added non-interleaved API to `%ffmpeg` encoder, enabled by default when only one stream is encoded. - Allow trailing commas in record definition (#3300). - Added `metadata.getter.source.float` (#3356). - BREAKING: Added `duration` and `ticks` to metadata available when computing HLS segment names (#4135) - Added optional `main_playlist_writer` to `output.file.hls` and derivated operator (#3484) - Added `is_nan`, `is_infinite`, `ceil`, `floor`, `sign` and `round` (#3407) - Added `%track.drop` to the `%ffmpeg` encoder to allow partial encoding of a source's available tracks (#3480) - Added `let { foo? } = ...` pattern matching (#3481) - Added `metadata.replaygain` method to extract unified replay gain value from metadata (#3438). - Added `metadata.parse.amplify` to manually parse amplify override metadata. - Added `compute` parameter to `file.replaygain` to control gain calculation (#3438). - Added `compute` parameter to `enable_replaygain_metadata` to control replay gain calculation (#3438). - Added `copy:` protocol (#3506) - Added `file.touch`. - Added support for sqlite databases (#3575). - Added `string.of_int` and `string.spaces`. - Added `list.assoc.nullable`. - Added `source.cue` (#3620). - Added `string.chars` (#4111) - Added atomic file write operations. - Added new `macos_say` speech synthesis protocol. Make it the default implementation for the `say:` protocol on `macos`. - Added `settings.request.timeout` to set the request timeout globally. Changed: - Reimplemented `request.once`, `single` and more using `source.dynamic`. Removed experiment flag on `source.dynamic`. The operator is considered stable enough to define advanced sources but the user should be careful when using it. - Mute SDL startup messages (#2913). - `int` can optionally raises an error when passing `nan` or `infinity`, `int(infinity)` now returns `max_int` and `int(-infinity)` returns `min_int`. (#3407) - Made default font a setting (#3507) - Changed internal metadata format to be immutable (#3297). - Removed `source.dump` and `source.drop` in favor of safer `request.dump` and `request.drop`. `source.{dump, drop}` can still be implemented manually when needed and with the proper knowledge of what's going on. - Allow a getter for the offset of `on_offset` and dropped the metadata mechanism for updating it (#3355). - `string.length` and `string.sub` now default to `utf8` encoding (#4109) - Disable output paging when `TERM` environment variable is not set. - Allow running as `root` user inside `docker` container by default (#3406). - Run `check_next` before playlist's requests resolutions (#3625) - Set `force` to `true` by default in `file.copy` to make operator behave as expected. - BREAKING: Float comparison now follows the expected specs, in particular: `nan == x` is always `false` and `nan != x` is always `true`. Use `float.is_nan` to test if a float is `nan`. - BREAKING: `replaygain` no longer takes `ebu_r128` parameter (#3438). - BREAKING: assume `replaygain_track_gain` always stores volume in _dB_ (#3438). - BREAKING: protocols can now check for nested static uri. Typically, this means that requests for an uri of the form: `annotate:key="value",...:/path/to/file.mp3` is now considered infallible if `/path/to/file.mp3` can be decoded. - Added `parents` option of `file.mkdir` (#3600, #3601). - Added `forced_major_collections` record field to the result of `runtime.gc.stat()` and `runtime.gc.quick_stat()` (#3783). - Changed the port for the built-in Prometheus exporter to `9599` (#3801). - Set `segments_overheader` in HLS outputs to disable segments cleanup altogether. - Added support for caching LV2 and LADSPA plugins (#3959). - Pulseaudio input and output now restart on pulseaudio errors (#4174). Fixed: - Fixed type generalization on values returned from function applications. Most notably, this should help with HTTP endpoint registration (#3303, fixed in #4030) --- # 2.2.5 (2024-05-01) (Mayday!) New: - Added `enable_autocue_metadata` and `autocue:` protocol to automatically compute cue points and crossfade parameters (#3753, #3811, @RM-FM and @Moonbase59) - Added various ffmpeg timestamps when exporting ffmpeg metadata from filters. - Added `db_levels` method to `blank.*` sources (#3790) - Added `excluded_metadata_resolvers` to `request.create` to make it possible to selectively disable specific metadata resolvers when resolving requests. - Normalized expected API from `autocue`, allow multiple implementation and adapt `cross`/`crossfade` to work with it out of the box with workaround for short tracks. - Added private and swapped memory reporting when compiled with `mem_usage`. - Added priorities to metadata deocoders, allows finer-control of metadata overriding. (#3887) Changed: - Allow to disable `http.*` url normalization. Add warning when url normalization changes the url (#3789) - Add `namespace` and optional source IDs to `mix`. Fixed: - Prevent request metadata from overriding root metadata (#3813) - Fixed `source.drop` and `source.dump` clock initialization. - Fixed bogus report of non-monotonous PTS content when using raw ffmpeg content. - Fixed streaming errors when disconnecting `input.harbor`. - Fixed issues with rendered id3v2 frame that contain binary data (#3817) - Fixed memory leaks with SRT listen socket polling callbacks. - Fixed `%ffmpeg` copy muxer logic with some audio/video streams (#3840) - Fixed `duration` metadata calculation in the presence of `cue_in`/`cue_out` metadata. --- # 2.2.4 (2024-02-04) New: - Added support for `id3v2` metadata in `output.*.hls` when using `%mp3`, `%shine` or `%fdkaac` encoders (#3604) - Added option to set preferred address class (`ipv4`, `ipv6` or system default) when resolving hostnames in http transports and `output.icecast` - Added `self_sync` option to `input.srt` to accommodate for streams in file mode (#3684) - Added `curve` parameter to fade functions and `liq_fade_{in,skip,out}_curve` metadata override (#3691) - Added `delay` parameter to fade functions to make it possible to add delay before fade happens. Add `liq_fade_{in,skip,out}_delay` metadata override. - Added `single_track` option to allow `sequence` to play each source until they are unavailable while keeping track marks. Changed: - `cue_cut` operator has been removed. Cueing mechanisms have been moved to underlying file-based sources. See migration notes for more details. Fixed: - Fix pop/click at the end of fade out/in (#3318) - Fix audio/video synchronization issues when decoding live streams using ffmpeg. - Fix issues with TLS connecting clients not being properly timed out (#3598) - Make sure reconnection errors are router through the regulat `on_error` callback in `output.icecast` (#3635) - Fixed discontinuity count after a restart in HLS outputs. - Fixed file header logic when reopening in `output.file` (#3675) - Fixed memory leaks when using dynamically created sources (`input.harbor`, `input.ffmpeg`, SRT sources and `request.dynamic`) - Fixed invalid array fill in `add` (#3678) - Fixed deadlock when connecting to a non-SSL icecast using the TLS transport (#3681) - Fixed crash when closing external process (#3685) --- # 2.2.2 (2023-11-02) New: - Added `string.escape.html` (#3418, @ghostnumber7) - Add support for getters in arguments of `blank.detect` (#3452). - Allow float in source content type annotation so that it possible to write: `source(audio=pcm(5.1))` Changed: - Trim urls in `input.ffmpeg` by default. Disable using `trim_url=false` (#3424) - Automatically add HLS-specific ffmpeg parameters to `%ffmpeg` encoder (#3483) - BREAKING: default `on_fail` removed on `playlist` (#3479) Fixed: - Allow `channel_layout` argument in ffmpeg encoder to set the number of channels. - Improved support for unitary minus, fix runtime call of optional methods (#3498) - Fixed `map.metadata` mutating existing metadata. - Fixed reloading loop in playlists with invalid files (#3479) - Fixed main HLS playlist codecs when using `mpegts` (#3483) - Fixed pop/clicks in crossfade and source with caching (#3318) - Fixed pop/clicks when resampling using `libsamplerate` (#3429) - Fixed gstreamer compilation. Remember that gstreamer features are DEPRECATED! (#3459) - Fixed html character escaping in `interactive.harbor` (#3418, @ghostnumber7) - Fixed icecast not reconnecting after erroring out while closing connection in some circumstances (#3427) - Fixed parse-only mode (#3423) - Fixed ffmpeg decoding failing on files with unknown codecs. - Fixed a crash due to `wait_until` timestamp being in the past when using `posix-time2` - Make sure that temporary files are always cleaned up in HLS outputs (#3493) --- # 2.2.1 (2023-09-05) Changed: - BREAKING: on HLS outputs, `on_file_change` events are now `"created"`, `"updated"` and `"deleted"`, to better reflect the new atomic file operations (#3284) - Added `compact` argument to the `http.response.json` function. `http.response.json` will produce minified JSON by default. Added a newline symbol to the end of the JSON data produced by `http.response.json`. (#3299) - Bumped internal ogg decoder to make sure that it is used over the ffmpeg decoder whenever possible. FFmpeg has issues with metadata in chained streams which needs to be fixed upstream. Unfortunately, `input.http` can only use the ffmpeg decoder at the moment. - Cleanup `output.file` encoding and file handling logic (#3328) - Added `ratio` to `source.{dump,drop}` to make it possible to control its CPU peaks. - Enhanced clock error reporting (#3317) Fixed: - Fixed slow memory leak in muxer operator (#3372, #3181, #3334) - Fixed discontinuity logic error in HLS outputs after a restart. - Fixed HTTP response status in `output.harbor` (#3255) - Make sure main HLS playlist is regenerated after being unlinked (#3275) - Fixed hard crash on icecast disconnection errors. - Fix `output.harbor` encoder header when encoding with `%ogg`, `%vorbis` and etc. (#3276) - Fixed quality argument parsing in ffmpeg encoders (#3267) - Make all HLS file write atomic (#3284) - Allow seek and cue operators to work with muxed sources using a single underlying source (#3252) - Fixed export of cover art metadata (#3279) - Remove use of `stereo:` protocol in `say:` protocol: this is now handled automatically by the decoder and generates latency via high CPU usage peak. - Fixed `output.file` reopening with flac encoding (#3328) --- # 2.2.0 (2023-07-21) New: - Added support for less memory hungry audio formats, namely `pcm_s16` and `pcm_f32` (#3008) - Added support for native osc library (#2426, #2480). - SRT: added support for passphrase, pbkeylen, streamid, added native type for srt sockets with methods, moved stats to socket methods, added `socket()` method on srt input/outputs (#2556) - HLS: Added support for ID3 in-stream metadata (#3154) and custom tags (#2898). - Added support for FLAC metadata (#2952) - Added support for YAML parsing and rendering (#2855) - Added support for the proprietary shared stereotool library (#2953) - Added TLS support via `ocaml-tls` (#3074) - Added `video.align`. - Added `string.index`. - Added support for ffmpeg decoder parameters to allow decoding of raw PCM stream and file (#3066) - Added support for unit interactive variables: those call a handler when their value is set. - Added support for id3v2 `v2.2.0` frames and pictures. - Added `track.audio.defer` to be used to buffer large amount of audio data (#3136) - Added `runtime.locale.force` to force the system's locale (#3231) - Added support for customizable, optimized `jemalloc` memory allocator (#3170) - Added `source.drop` to animate a source as fast as possible.. - Added in house replaygain computation: - `source.replaygain.compute` to compute replaygain of a source - `file.replaygain` to compute the replaygain of a file - Added support for ImageLib to decode images. - Added support for completion in emacs based on company (#2652). - Added syntactic sugar for record spread: `let {foo, gni, ..y} = x` and `y = { foo = 123, gni = "aabb", ...x}` (#2737) - Added `file.{copy, move}` (#2771) - Detect functions defining multiple arguments with the same label (#2823). - Added `null.map`. - References of type `'a` are now objects of type `(()->'a).{set : ('a) -> unit}`. This means that you should use `x()` instead of `!x` in order to get the value of a reference. Setting a reference can be done both by `x.set(v)` and `x := v`, which is still supported as a notation (#2881). - Added `ref.make` and `ref.map`. - Added `video.board`, `video.graph`, `video.info` (#2886). - Added the `pico2wave` protocol in order to perform speech synthesis using [Pico TTS](https://github.com/naggety/picotts) (#2934). - Added `settings.protocol.gtts.lang` to be able to select `gtts`' language, added `settings.protocol.gtts.options` to be able to add any other option (#3182) - Added `settings.protocol.pico2wave.lang` to be able to select `pico2wav` language (#3182) - Added `"metadata_url"` to the default list of exported metadata (#2946) - Added log colors! - Added `list.filter_map` and `list.flatten`. - Added `medialib` in order to store metadata of files in a folder and query them (#3115). - Added `--unsafe` option (#3113). This makes the startup much faster but disables some guarantees (and might even make the script crash...). - Added `string.split.first` (#3146). - Added `string.getter.single` (#3125). Changed: - Switched to `dune` for building the binary and libraries. - Changed `cry` to be a required dependency. - Changed default character encoding in `output.harbor`, `output.icecast` `output.shoutcast` to `UTF-8` (#2704) - BREAKING: all `timeout` settings and parameters are now `float` values and in seconds (#2809) - BREAKING: in `output.{shoutcast,icecast}`: - Old `icy_metadata` renamed to `send_icy_metadata` and changed to a nullable `bool`. `null` means guess. - New `icy_metadata` now returns a list of metadata to send with ICY updates. - Added `icy_song` argument to generate default `"song"` metadata for ICY updates. Defaults to ` - ` when available, otherwise `artist` or `title` if available, otherwise `null`, meaning don't add the metadata. - Cleanup, removed parameters that were irrelevant to each operator, i.e. `icy_id` in `output.icecast` and etc. - Make `mount` mandatory and `name` nullable. Use `mount` as `name` when `name` is `null`. - `reopen_on_error` and `reopen_on_metadata` in `output.file` and related operators are now callbacks to allow dynamic handling. - Added `reopen` method to `output.file`. - Added support for a Javascript build an interpreter. - Removed support for `%define` variables, superseded by support for actual variables in encoders. - Cancel pending append when skipping current track on `append` source. - Errors now report proper stack trace via their `trace` method, making it possible to programmatically point to file, line and character offsets of each step in the error call trace (#2712) - Reimplemented `harbor` http handler API to be more flexible. Added a new node/express-like registration and middleware API (#2599). - Switched default persistence for cross and fade-related overrides to follow documented behavior. By default, `"liq_fade_out"`, `"liq_fade_skip"`, `"liq_fade_in"`, `"liq_cross_duration"` and `"liq_fade_type"` now all reset on new tracks. Use `persist_overrides` to revert to previous behavior (`persist_override` for `cross`/`crossfade`) (#2488). - Allow running as root by default when docker container can be detected using the presence of a `/.dockerenv` file. - `id3v2` argument of `%mp3` encoder changed to `"none"` or version number to allow to choose the metadata version. `true` is still accepted and defaults to version `3`. Switched to our internal implementation so that it does not require `taglib` anymore. - Moved HLS outputs stream info as optional methods on their respective encoder. - Changed `self_sync` in `input.ffmpeg` to be a boolean getter, changed `self_sync` in `input.http` to be a nullable boolean getter. Set `self_sync` to `true` in `input.http` when an icecast or shoutcast server can be detected. - Add `sorted` option to `file.ls`. - Add `buffer_length` method to `input.external.rawaudio` and `input.external.wav` (#2612). - Added full `OCaml` backtrace as `trace` to runtime errors returned from OCaml code. - Removed confusing `let json.stringify` in favor of `json.stringify()`. - Font, font size and colors are now getters for text operators (`video.text`, `video.add_text`, etc.) (#2623). - Add `on_cycle` option to `video.add_text` to register a handler when cycling (#2621). - Renamed `{get,set}env` into `environment.{get,set}` - Renamed `add_decoder`, `add_oblivious_decoder` and `add_metadata_resolver` into, respectively, `decoder.add`, `decoder.oblivious.add`, `decoder.metadata.add` - Deprecated `get_mime`, added `file.mime.libmagic` and `file.mime.cli`, made `file.mime` try `file.mime.libmagic` if present and `file.mime.cli` otherwise, changed eturned value when no mime was found to `null()`. - Return a nullable float in `request.duration`. - Removed `--list-plugins-json` and `--list-plugins-xml` options. - Added `--list-functions-json` option. - Removed built-in use of `strftime` conversions in output filenames, replaced by an explicit call to `time.string` (#2593) - Added nullable default to `{int,float,bool}_of_string` conversion functions, raise an exception if conversion fails and no default is given. - Deprecated `string_of` in favor of `string` (#2700). - Deprecated `string_of_float` in favor of `string.float` (#2700). - Added `settings.protocol.youtube_dl.timeout` to specify timeout when using `youtube-dl` protocol (#2827). Use `yt-dlp` as default binary for the protocol. - The `sleeper` operator is now scripted (#2899). - Reworked remote request file extension resolution (#2947) - REMOVED `osx-secure-transport`. Doubt it was ever used, API deprecated upstream (#3067) - Renamed `rectangle` to `add_rectangle`, and similarly for `line`. Fixed: - The randomization function `list.shuffle` used in `playlist` was incorrect and could lead to incorrectly randomized playlists (#2507, #2500). - Fixed srt output in listener mode to allow more than one listener at a time and prevent listening socket from being re-created on listener disconnection (#2556) - Fixed race condition when switching `input.ffmpeg`-based urls (#2956) - Fixed deadlock in `%external` encoder (#3029) - Fixed crash in encoders due to concurrent access (#3064) - Fixed long-term connection issues with SSL (#3067) --- # 2.1.4 (2022-03-01) New: - Added `buffer_length` method to `buffer` operator. - Always display error backtrace when a fatal exception is raised in the streaming loop. - Added `umask()` to get the current `umask` and `umask.set(...)` to set the current `umask` (#2840) Changed: - Add break when restarting the external process in `input.external.{rawaudio,rawvideo}` (#2860, #2872) - Removed `disconnect` method on `input.harbor`. This method was doing the same as the `stop` method. Added `shutdown` method to properly shutdown the source even when not connected to an output. - Made process a string getter in `input.external.{rawaudio,rawvideo}` (#2877) Fixed: - Fixed parameter type for `stats_interval` in SRT I/O. - Fixed type generalization on variable and pattern bindings (#2782) - Fixed memory leak in http requests (#2935) - Make sure that exception raised in `request.dynamic` never crash the process (#2897) - Fixed `filename` getter being called multiple time in `output.file` (#2842) - Fixed default directory permissions in `output.*.hls` operators (#2930) - Space trim in interactive variables set on telnet (#2785) - Fixed internal streaming logic in `max_duration` and `crossfade`. - Make sure that there's at most one metadata at any given frame position (#2786) - Fixed `metadata.json.parse` always returns an empty list (#2816). - Fixed `icy_id` being ignored in `output.shoutcast` (#2819) - Fixed shutdown livelock with some ffmpeg inline encoder, decoder and filter operators. - Fixed input polling stop (#2769) - Fixed parsed error report in `%include` directives (#2775) - Fixed crash in external processes when received a `Unix.EINTR` event (#2861) - Fixed crash in `string.interpolate` (#2883) - Cleaned up srt support. --- # 2.1.3 (2022-11-04) New: - Added `time.string`. - Added `error.on_error` to report any error raised during the script's execution. Enhanced reported error positions (#2712) - Added `device_id` and `latency` options to `input.portaudio` and `output.portaudio` to be able to choose the requested device. Use `liquidsoap --list-portaudio-devices` to see the list of devices (#2733) - Added `disconnect` method to `input.harbor`, making it possible to disconnect a source client programmatically, including when a new client is trying to connect. Changed: - Send data in-memory in `http.{post,put}.file` when input data is already in memory. This allows to use plain `Content-Length` instead of `chunked` transfer encoding in these case, though `libcurl` seems to always prefer `chunked` encoding for `put` requests. - Better error message when an encoder is not available on windows (#2665) - Create output directory in HLS outputs when it does not exist using newly introduced `perms` permission argument (#2725) - Removed `restart_on_error` argument on `output.url` and added `restart_delay` which implements a delayed restart. Added `on_error` argument to be notified of errors (#2731) - Changed default `encoding` parameter in `string.{quote, escape}` to be `null`. Fallback to `"ascii"` encoding when no encoding is specified and `"utf8"` fails. This prevents unexpected script failures but might not be backward-compatible if you used a custom `escape_char` or `special_char` function (#2738) Fixed: - Enhanced methods typing support (#2659) - Add support for `song` metadata (mapped to `title`) and `url` (mapped to `metadata_url`) in `input.harbor` (#2676) - Fixed `blank.*` operator types. - Fixed request metadata escaping (#2732) - Fixed `input.external.rawadudio` mono input (#2742) - Fixed `http` response body on redirect (#2758) --- # 2.1.2 (2022-09-26) New: - Added `string.char`, `string.getter.flush` and `string.getter.concat`. - Added `http.multipart_form_data` and `http.{post,put}.file`. Changed: - Allow sub-second values in `sleep()` (#2610) - Allowed many new format for `taglib` (#2605) - Add `settings.ffmpeg.content.copy.relaxed_compatibility_check.set` settings to allow relaxed compatibility check for ffmpeg copy content, making it possible to encode streams with various audio samplerate or video size when the container supports it. Fixed: - Stop error loop when opening a listening ssl socket with non-existent certificate. (#2590) - Youtube HLS upload for live streams. - Fixed `data:...` uri scheme to conform to RFC 2397 (#2491) - Fixed multiple issues related to empty `ogg/opus` metadata (#2605) - Ensure that `video.add_text` fails when the source does (#2609) - Fixed metadata parsing in `server.insert_metadata` (#2619) - Fixed `extract_replaygain` path (#2624, @parnikkapore) - Fixed crash when terminating the process (#2585) - Fixed channels conversion when using `input.rawaudio` (#2602) Internal Change: - `ref()` implementation switched to OCaml's `Atomic` to prevent race conditions, `thread.mutexify` and `mutexify` functions removed. (#2603) --- # 2.1.1 (2022-08-28) New: - Added `process.quote.command` to generate complex quoted command strings suitable for use with `process.run` and os-independent. Changed: - Renamed `playlist.remaining` into `playlist.remaining_files` (#2524) - Added `id` argument to `replaygain` operator (#2537). - Made `ocurl` dependency required, added `uri` as required dependency (#2551) Fixed: - Fixed missing ffmpeg features on windows build. - Fixed sync issues with `ffmpeg.encode.*` inline encoders (#2584) - Fixed `http.get` issues when `user-agent` was not set (#2517) - Fixed order of `playlist.next` returned requests. - Fixed infinite loop when reloading a failed playlist (#2576) - Fixed http requests with urls containing spaces (#2551) - Fixed `on_connect` type for `srt` inputs and outputs. - Fixed parsing issues with functions/variables definitions beginning with `rec` or `replaces` (#2560) - Fixed infinite parse error loop (#2527) - Fixed empty initial `mp4` HLS segment. - Prevent initial start for autostart and fallible sources. --- # 2.1.0 (2022-07-15) New: - Added support for variables in encoders (#1858) - Added support for regular expressions (#1881) - Added generalized support for value extraction patterns (#1970) - Added support for string getter for `http.{post,put}` operations (#1984) - Added `output.youtube.live.hls` - Rewrote out internal JSON parser/renderer (#2011). **Breaking change** values that cannot be represented as `JSON` will now raise `error.json` when converted to `JSON`. `infinite` and `nan` floats can be exported using the `json5` export format. - Added socket API (#2014). - Added support for ffmpeg bitstream filters (#2387) - Added `liquidsoap.version.at_least`. - Added `video.rectangle`, `video.persistence`. - Added `video.vumeter`. - Added `video.slideshow`. - Added `video.add_text.camlimages` (#2202). - Added `video.text.*` and re-implemented `video.add_text.*` from those (#2226). - Added `irc.channel` operator to retrieve the contents of an IRC channel (#2210). - Added new in-house parsing of metadata for some image and video formats (#2236). - Added `file.download` - Added new options for `%ffmpeg` copy encoder: `ignore_keyframes` and `wait_for_keyframe` (#2382) Changed: - Removed support for partial application, which should avoid some type errors, improve performance and simplifies the code related to the reduction (#2204). - Video dimensions (width and height) can now be specified per stream in the type and are then used instead of the default ones. For instance, you can now write ``` s = (single("file.mp4") : source(video(width=300,height=200))) ``` in order to force the decoding of a file to be performed at the 300×200 resolution (#2212). - Video images are now _canvas_, which means that they do not directly contain the images, but are constituted of multiple images placed at various positions. This should make much more efficient operations such as making videos from multiple ones, adding a logo, etc. (#2207) - `output.youtube.live` renamed `output.youtube.live.rtmp`, remove `bitrate` and `quality` arguments and added a single encoder argument to allow stream copy and more. - `source.on_metadata` and `source.on_track` now return a source as this was the case in previous versions, and associated handlers are triggered only when the returned source is pulled (#2103). - Made `streams_info` parameter of `output.file.hls` a record (#2173). - Disable scrolling by default in `video.add_text`. You can re-enable it by using `video.add_text(speed=70, ...)`. - Added "example" sections to operators documentation, we now need to populate those (#2227). - Default implementation of `video.testsrc` is now builtin, previous implementation can be found under `video.testsrc.ffmpeg`. - Images can now generate blank audio if needed, no need to add `mux_audio(audio=blank(),image)` anymore (#2230). - Removed deprecated `timeout` argument in `http.*` operators. - Deprecated `request.ready` in favor of `request.resolved`. Fixed: - Fixed typo in `status` command of the `mix` operator. - Fixed performances issues with `input.ffmpeg` and `input.http` (#2475) - Fixed `list.shuffle` which was used to randomize playlists in `playlist` operator (#2507, #2500). --- # 2.0.7 (2022-07-15) Fixed: - Fixed memory leaks with opus bindings. - Make sure decoding buffer and samplerate converter are only created once. (#2475) - Make sure first metadata is always sent in icecast/shoutcast output (#2506) --- # 2.0.6 (2022-06-20) New: - Added `video/mp4` to list of recognized mime types for request resolutions. Changed: - Log errors when using `process.read` (##2420, @martinkirch) Fixed: - Memory leak when executing `process.run` (#2424) - Delay harbor server endpoint registration until application has started (#1589) - Print user-readable encoder parameter error report. - Fixed m3u metadata parsing when artist has a comma in their name (#2449) - Cleanup failed request in `playlist` operator. - Make sure requests are always cleaned up, making `request.destroy` calls optionals. # 2.0.5 (24-05-2022) New: - Extended m3u EXTINF parser to support empty duration and annotations. Changed: - Brought back `mix` operator (#2401) Fixed: - Allow crossfade duration override of `0.` - Buffer synchronization issues. - Drop methods from ffmpeg filter input source types to avoid unnecessary conflicts. - Fix evaluation of abstract values with methods. - Prevent some sources from being consumed when not active, namely ffmpeg inline encoders, `soundtouch`, `resample` and all the muxing operators. - Raise runtime exceptions in `string.replace` failures with useful message. (#2408) - Prevent `request.dynamic` from raising exceptions when checking if the source is ready (#2381) --- # 2.0.4 (23-04-2022) New: - Added `settings.video.add_text` to enforce consistent choice of `video.add_text` implementation (#2302) Changed: - Make sure source shutdown can only be called on sources that can actually be shutdown: - Remove generic `source.shutdown` - Keep `s.shutdown()` method only on sources that are active. Refs: #2259 - Optimized memory usage when accessing frame content (#2266) - Optimized memory usage when accessing ground terms. - Allow crossfade duration getter to override duration at the end of each track if duration isn't set via metadata. - Make sure crossfade metadata are not duplicated (#2153) - Renamed `map_metadata` into `metadata.map`, deprecated `map_metadata`. - Deprecatdd `list.mem_assoc` - Enhanced remaining time when using `add` (#2255) - Added `timeout_ms` to `http.*` to provide time in milliseconds, deprecated `timeout` argument. - Connect `output.icecast` when data is available instead of when operator starts to avoid useless connections when underlying source fails immediately. Fixed: - Prevent infinite loops when crossfade duration is negative (#2287) - Prevent mutex deadlock when recursively locking mutexes (#2274) - Mark method `add()` as internal in `request.queue`, fix method `length()` (#2274) - Fixed `retry_delay` being ignored in some cases in `request.dynamic`. - Prevent race condition in external process handler. - Fixed A/V sync when streaming encoded data via ffmpeg encoder (#2159) - Prevent stopped/iddle sources from being restarted when resetting `clock(s)` after too much latency (#2278) - Fixed registration of `video.add_text.ffmpeg` as possible implementation for `video.add_text` (#2302) - Fixed `http.*` calls preventing liquidsoap from shutting down. - Fixed `http` protocol not returning an error when timing out (#2242) - Reworked ffmpeg filters feeding mechanism. - Fixed inconsistencies in `playlist.parser` (#2257) - Fixed inconsistent reselect in `rotate` (#2300) - Fixed special characters escaping in `video.add_text.ffmpeg` (#2324) - Fixed `input.rawaudio` and `input.rawvideo` when handling non-stereo content. # 2.0.3 (11-02-2022) New: - Added support for memory debugging using `memtrace` - Added `time.{zone,zone.set,make}` (#2178) - Added `runtime.gc` module, rename `garbage_collect` as `runtime.gc.full_major` with deprecated compatibility wrapper, added `runtime.gc.stat`, `runtime.gc.quick_stat`, `runtime.gc.print_stat` and `runtime.gc.{get,set}`. - Added `runtime.sys.word_size` - Added optional support for `runtime.mem_usage` - Added `runtime.memory` wrapper to get info about the system and process' memory usage. - Added `configure.camomile_dir` to export expected location of camomile directory when packaging liquidsoap. - Added `liquidsoap.chroot.make` to copy all files required for a liquidsoap install. Changed: - Bumped `input.harbor` default buffer to `12.` to make it possible to use it with `crossfade` transitions without changing default values (#2156) - `year` method as returned in `time.local` and `time.utc` now returns the actual year instead of years since 1900 (#2178) - `mday`, `mon`, `wday` and `yday` methods as returned in `time.local` and `time.utc` have been renamed to, resp., `day`, `month`, `week_day` and `year_day` (#2178) - `month` method as returned in `time.local` and `time.utc` now returns the month as a number between `1` and `12` (#2178) - `week_day` method as returned in `time.local` and `time.utc` now returns the week day as a number between `1` and `7` (#2178) - `year_day` method as returned in `time.local` and `time.utc` now returns the week day as a number between `1` and `366` (#2178) - Added option to choose if `input.rtmp` should behave as a server or a client (#2197) - Allow dynamic text change in `video.add_text.ffmpeg` (#2189) - Removed `thread_name` argument from `thread.on_error` callbacks. Fixed: - Make sure metadata are replayed when switching to a source for the first time in switches/fallback (#2138) - Bring back `video.add_text.sdl` (#2187) - Fixed `thread.on_error` implementation (#2171) - Fixed `ffmpeg` video scaling to make sure it always is proportional (#2211) # 2.0.2 (28-12-2021) New: - Show code excerpts on errors (#2086) - Added `on_get_ready` callback to sources, to be executed after a source's has initialized. - Added `flush_and_skip` telnet command to `request.dynamic` to empty the request's queue before skipping the current track, forcing a full reload. - Added `last_metadata` method on sources to return the last metadata produced by the source. Fixed: - Fixed ffmpeg copy encoder crash when switching between streams. - Fixed unbound buffer in muxing operators (#2054) - Return correct positions when parsing strings (#2095) - Deadlock when shutting down with `input.rtmp` (#2089) - Add timeout to srt operations (#2082) - Fixed `request.queue` `queue` telnet command returning nothing (#2088) - Fixed single quotes being escaped in json stringify. (#2120) - Fixed frame caching issues when no initial break was present in the memoized frame. (#2109. AzuraCast/AzuraCast#4825) - Fixed `replay_metadata` not replaying metadata from active sources (#2109) # 2.0.1 (27-11-2021) New: - Added `time.predicate` to parse time predicates at runtime. - Added support for ffmpeg filter commands, unify `video.add_text.ffmpeg` with other operators, make it the default when available. (#2050) Changed: - Removed `encode_metadata` option in `input.file.hls` as it does nothing with the main encoder for HLS format, `%ffmpeg` (#2023) - Converted `output.icecast` optional parameters to `nullable`. Fixes: - Fixed switch-based sources not respecting track boundaries when using default transitions one track only per selected source. (#1999) - Fixed playlist annotation. (#2005) - Raise a proper runtime exception when `string.escape` fails. (#2010) - Account for internal caching in `request.dynamic.list`'s `queue` and `set_queue` methods. - Keep buffering for crossfade when new source has track mark but is still ready. - Added missing output `start`/`stop` commands. - Fixed `perms`, `dir_perms` and `append` not bring honored when delegating file output to the encoder. - Fixed base directory not being created when delegating file output to the encoder (#2069). - Use `process.quote` in process calls (#2031) # 2.0.0 (03-10-2021) New: - Add support for errors with `error.*` and `try ... catch` (#1242). - Add support for optional values with `null.*` (#1242). - Add support for `x ? y : z` syntax (#1266). - Added support for list spread and deconstruction syntax (#1269). - Add support for generic JSON objects, map `(string, 'a)` lists to regular lists, add support for json5 floats (`NaN`, `Infinity`), return `null` for those otherwise, rename `json_of` into `json.stringify` and `of_json` into `json.parse` with deprecation (#1824) - Added support for video encoding and decoding using `ffmpeg` (#1038). - Added support for hardware-accelerated video encoding using `ffmpeg` (#1380) - Added support for ffmpeg filters (#1038). - Added video support to `output.hls` (#1391). - Added mp4 support to `output.hls` (#1391). - Added `output.url` for encoders that support handling data output (currently only `%ffmpeg`) (#1038). - Added `output.file.dash.ffmpeg`. - Added LV2 support (#906). - Added `string.nth` (#970). - Added `string.binary.to_int` (#970). - Added `string.hex_of_int`. - Added `file.ls` (#1011). - Added native id3v2 tag parser, as well as associated function `file.mp3.metadata`, `file.mp3.parse_apic` and `file.cover` (#987). - Use a pager to display long help results (#1017). - Added new functions for lists: `lists.exists`, `list.for_all`, `list.init`, `list.ind`, `list.index`, `list.last`, `list.shuffle`. - Added `request.id`. - Added a profiler for the language. It can be enabled with `profiler.enable` and the results are obtained with `profiler.stats.string` (#1027). - Added `gtts` protocol to use Google TTS (#1034). - Added `liquidsoap.executable` to get the path of the currently running Liquidsoap. - Added `source.dump`. - Added `source.elapsed` and `source.duration` - Added `synth` protocol (#1014). - Added listener and caller mode for `input.srt` and `output.srt` (#1377) - Added support for `srt.enforced_encryption` setting. - Added support for prometheus reporting (#1000) - Add `validate` parameter to `register`, which allows to validate a value before setting it (#1046, @CyberDomovoy) - Add `string.null_terminated` (#960). - Removed `string.utf8.escape` in favor or a unifited, utf8-aware `string.escape`. - Add `string.unescape`. - Add `file.metadata` (#1058). - Add `predicate.activates`, `predicate.changes`, `predicate.first`, `predicate.once`, `predicate.signal` (#1075). - Add `playlist.list.reloadable` and `playlist.list` (#1133). - Make it possible to disable buffer overrun logs. - Add `accelerate` operator (#1144). - Add `video.resize`. - Add `getter.int_of_float` and `getter.float_of_int`. - Add `source.dump` (#1036). - Add `stereo` and `synth` protocols (#1036). - Add `video.add_text.ffmpeg`. - Added support for `file:///path/to/file` and `file:/path/to/file`protocols. - Added configure option to specify internal library install path (#1211). - Add support for records and methods (#1197). - Rename `unsafe.single.infallible` to `single.infallible`. - Add `list.indexed`. - Added optional support for high-resolution time and latency control on POSIX systems (#1050). - Added syntax for `for` and `while` loops (#1252). - Added a bunch of source-related methods (#1379). - Added `min` and `max` functions. - Added `lufs` to compute the LUFS loundness (#1497). - Added `interactive.harbor` in order to expose interactive variables over harbor (#1495). - Added `interactive.persistent` (as well as `interactive.save` and `interactive.load`) to make interactive variables persistent (#1495). - Added `server.harbor` (#1502). - Added `metronome`. - Added `playlist.files`. - Added `getter.is_constant`. - Added `assert`. - Added `source.available`. - Added `request.once`. - Added `file.getter`. - A better `normalize` function (with more reasonable parameters, more customisable, and written in Liquidsoap) is now provided. The old one is renamed `normalize.old`. - New and better `compress` function. The previous one was renamed `compress.old` (#868, #869). - Added `stereo.width`. - Added `file.mkdir`. - Added support for harbor's connected address in auth function and as a method (#1364). - Added `time.up`. - Added `video.cover`. - Added `video.still_frame`. - Added `request.status`. - Added `playlog` to record how long ago a song was last played (#333 and #1530). - Added `clock.log_delay` to configure how often clock catchup error messages should be printed. - Added `input.rtmp` (#1640). - Added `%ifversion` and `%else` preprocessing commands (#1682). - Added `dtmf` and `dtmf.detect` to generate and detect DTMF tones (#1796). - Added `sine.detect` to detect sines (#1796). - Added `on_air_timestamp` to request's metadata to get the request's `on_air` time as a Unix timestamp (#1871) Changed: - Implemented per-frame clock synchronization mechanism, should allow for more advanced flexibility when working with source synchronization while keeping the default safe behavior. (#1012) - Remove `active_source` type, make all output return `unit` type. (#1671) - Switch to YUV420 as internal image format, much more efficient (#848). - Use bigarrays for audio buffers (#950). - Re-implemented switch-derived operators (`fallback`, `rotate`, `random`) as scripted operators, removed `track_sensitive` argument from `rotate` and `random` as it does not have a sound meaning for them. - Added optional exit `code` to `shutdown`. - Renamed `verb` argument info `method` in `output.icecast`. - Simplified `add` behavior, also fixing an clock issue (#668). - Switch to more efficient callback API for decoders (#979). - Use system pagesize for buffer allocation (#915). - Use new Strings module in order to avoid concatenations (#984). - Native Liquidsoap implementation of list functions (#920). - Added `fallible` option to `single` operator. - Allow `input.ffmpeg` to control its own clock or delegate to CPU clock (#1628) - Reimplement `input.http` using `ffmpeg`, deprecate `input.https` in favor of unified `input.http` (#1628) - Changed `input.http` and `input.ffmpeg` `url` parameter into a string getter - Changed `request.queue` into a Liquidsoap implementation (#1013). - Removed `request.equeue`, such a feature could be re-implemented in Liquidsoap, see `request.queue`. - The `playlist` operator is now fully implemented in Liquidsoap (#1015). - Removed `playlist.once`, its behavior can be achieved by passing `"never"` to the `reload_mode` argument of `playlist` (#1015). - Removed `playlist.merged`: it is not that useful and can be achieved easily with `merge_tracks` on a `playlist` (#1015). - Deprecated `playlist.safe` (#1015). - Renamed `add_timeout` to `thread.run.recurrent`, added `thread.run` variant, renamed `exec_at` to `thread.when` and renamed `mutexify` to `thread.mutexify` (#1019). - Changed the weights of `add` to float (#1022). - Renamed `which` to `file.which`. - Change `blank()` duration semantics to mean forever only on negative values. - Get rid of numbering of universal variables (#1037). - Renamed `base64.decode`/`base64.encode` to `string.base64.decode`/`string.base64.encode`. - Vumeter is now implemented in Liquidsoap (#1103). - Change `input.http` and `input.https` `url` parameter into a string getter (#1084). - Added `path.home.unrelate`. - Use getters for arguments of `video.add_image` (#1176). - Add `x`, `y`, `width` and `height` argument to `image`, unify with `video.add_image`. - Generalize `audio_to_stereo` to video frames and those without audio. - Allow crossfading for video (#1132, #1135). - Use getters for parameters of synthesizer sources (#1036). - Renamed `empty` to `fail`. - Restored `request.dynamic` (#1213). - Requests are not typed anymore: their type is fixed at resolution time. - Deprecated `request.create.raw`, you should use `request.create` instead. - Reference setting and access are now handled as normal builtins instead of in the kernel. - Use records as return type of `http.*`, `https.*`, `rms`, `peak` and `request.queue` (#1234). - Indices of groups returned by `string.extract` are now integers instead of strings (#1240). - Generalize the `l[k]` notation so that the key `k` can be of any type (on which we know how to compare). - `ref` is not a keyword anymore: this means that `ref x` is not accepted anymore, you need to write `ref(x)` (#1254). - Renamed `file.unlink` to `file.remove`. - Deprecated `get_process_output`, `get_process_lines`, `test_process` and `system` in favor of `process.run`, `process.read`, `process.read.lines` and `process.test`. - Renamed `http_codes` to `http.codes` and put first member as integer. - Renamed `http.response` to `http.response` and `http.response.stream` to `http.response.stream`. - `localtime` and `gmtime` now return a record. - Deprecated `{eat,strip,skip,on}_blank` in favor of `blank.{eat,strip,skip,detect}`. - `http{,s}.{get,post,push}` now perform redirections if needed, which can be disabled with the `redirect` parameter (#1319). - Deprecated `gettimeofday` in favor or `time`, renamed `localtime` to `time.local` and `gmtime` to `time.utc`, and the argument of these two last functions is now optional (#1320). - Dropped optional `gavl` video converter in favor of `ffmpeg`. - Remove `persist` argument in `output.*.hls` and use nullable value for `persist_at`. - Deprecated source server commands in favor or direct call to source methods. Added wrappers for some of the old commands (#1379). - Deprecated catch-all `input` and `output` in favor or setting your desired input or output explicitly. - Implement `interactive.*` on script side (#1493). - `file.write` does not return a boolean anymore, exceptions are used for exceptional cases (#1500). - `source.dynamic` now takes a nullable argument. - Renamed `on_end` to `source.on_end`. - Changed the name of the arguments of `fallback.skip`. - Normalize ReplayGain handling: - we now use the standard `replaygain_track_gain` metadata - renamed the protocol from `replay_gain` to `replaygain` - added the `replaygain` operator to perform amplification - `normalize` now handles all channels uniformly. - First-order filter `filter.rc` now takes the cutoff frequency instead of the time constant as argument. - `file.watch` now returns unit with `unwatch` method. - Changed the interface for `bpm`: the bpm can now be retrieved using a method of the returned source instead of having a callback. - Removed `server.read*` and `server.write*`. Fixed: - Set `cloexec` on all relevant Unix calls (#1192). - Fix implementation of recursive functions (#934). - Make `blank()` source unavailable past is expected duration (#668). - Remove `video.add_text.gstreamer` shade in background (#1190). - Improve the quality of `video.add_text.gd` (#1188). - Exit with non-zero code on errors. - Fixed parsing of http URI arguments with `=` in them (#1340). - Fixed fade-out in crossfades when crossfade duration is the same as fade-out duration (#1351). - Fixed osc server not working when daemonized (#1365). - Fixed glitchy audio when using `input.harbor` (#1944) - Fixed `"tracknumber"` and `"year"` returning `0` in taglib (#1901) Removed: - LiGuidsoap, the old Liquidsoap GUI. 🪦 # 1.4.4 (27-02-2021) New: - Added `process.quote` to quote process' arguments (#1215) Changed: - Fetch mime type using curl first when available. - Make override metadata name case-sensitive in `amplify` (#1323) - Harnessed playlist file resolver to better support some combination of protocols and file resolution (#1362) Fixed: - Remote file resolution when passing URLs with spaces (#1410) - Fixed empty `{http,https}` body (#1417) - Fixed `input.harbor` shoutcast client connection (#1353) - Fixed exception reporting when output fails to start (#1372) - Fixed `random` track selection (#1468) - Fixed playlist request leak when using `reload="watch"` with `inotify` on a folder (#1451) - Deadlock when LO server thread crashes (#1409) # 1.4.3 (14-09-2020) Fixed: - Fixed exponential memory usage in clock unification algorithm (#1272). # 1.4.2 (03-05-2020) New: - Added `retry_delay` argument to `request.dynamic` (#1169). - Renamed `request.dynamic` to `request.dynamic.list` and updated its callback function type to return an array of requests, making possible to return multiple requests at once but, more importantly, to return `[]` when no next requests are available. (#1169) Changed: - Set `audio/flac` as mime for flac (#1143). - Deprecated `request.dynamic`. Fixed: - Fixed errors when installing bash-completion files (#1095) - Fixed failures in `extract-replaygain` script (#1125) - Do not crash when loading playlists using `~/path/to/..` paths. - Set `set_default_verify_paths` for SSL (#450) - Use 443 as default port for https (#1127) - Fix implementation of `rotate` (#1129). - Register audio/opus mime type for ogg decoding (#1089) - Re-encode name, genre and description in `output.icecast` using the given encoding (#1092) - Accept 24 bits per sample in %flac encoder (#1073). - Fix rare stack overflow during clock unification (#1108). - Prevent metadata inserted via `insert_metadata` from being visible to underlying sources (#1115) - Fix `cross()` fallability. - Fix decoder remaining time when decoding is done (#1159) - Fixed crash when cleaning up `output.hls` - Fix `get_process_lines` regexp logic (#1151) # 1.4.2 (03-05-2020) New: - Added `retry_delay` argument to `request.dynamic` (#1169). - Renamed `request.dynamic` to `request.dynamic.list` and updated its callback function type to return an array of requests, making possible to return multiple requests at once but, more importantly, to return `[]` when no next requests are available. (#1169) Changed: - Set `audio/flac` as mime for flac (#1143). - Deprecated `request.dynamic`. Fixed: - Fixed errors when installing bash-completion files (#1095) - Fixed failures in `extract-replaygain` script (#1125) - Do not crash when loading playlists using `~/path/to/..` paths. - Set `set_default_verify_paths` for SSL (#450) - Use 443 as default port for https (#1127) - Fix implementation of `rotate` (#1129). - Register audio/opus mime type for ogg decoding (#1089) - Re-encode name, genre and description in `output.icecast` using the given encoding (#1092) - Accept 24 bits per sample in %flac encoder (#1073). - Fix rare stack overflow during clock unification (#1108). - Prevent metadata inserted via `insert_metadata` from being visible to underlying sources (#1115) - Fix `cross()` fallability. - Fix decoder remaining time when decoding is done (#1159) - Fixed crash when cleaning up `output.hls` - Fix `get_process_lines` regexp logic (#1151) # 1.4.1 (18-02-2020) Fixed: - Fixed `fade.final` and `fade.initial` (#1009) # 1.4.0 (29-09-2019) New: - UTF8 parsing! - Added support for tuples: `x = (1,"aa",false)` (#838) - Added support for deconstructing tuples: `let (z,t,_) = x` (#838) - Added `input.{file,harbor}.hls` to read HLS stream (#59, #295, #296). - Added `output.hls` to natively stream in HLS (#758). - Added `%ffmpeg` native encoder, only for audio encoding for now (#952) - Added ffmpeg-based stream decoder, limited to mime type `application/ffmpeg` for now. - Added `(to_){string,float,int,bool}_getter` operators to handle getters in script side. - Made `p` parameter in `smooth_add` a `float` getter (#601) - Added `source.time` to get a source's clock time. - Added `max_duration` to limit a source's duration. - Added `file.temp_dir` to create temporary directories. - Added `file.{unlink,rmdir}` to remove, resp., file and directories. - Added `file.write` to write content to a file. - Added `file.read` to read contents of a file without loading all of it in memory. - Added `youtube-pl:<ID>` protocol to resolve and parse youtube playlists (or any playlist supported by `youtube-dl`) (#761) - Added `protocol.aws.endpoint` setting for the `s3://` protocol, thanks to @RecursiveGreen. (#778) - Added support for sandboxing `run_process` calls. (#785) - Added `harbor.{http,https}.static` to serve static path. - Added `log.{critical,severe,important,info,warning,debug}`. Use aliases in code as well (#800, #801, #802) - Added `sleep` function. - Added `mkavailable` function. - Added `fade.skip` function. (#804) - Added `video.external.testsrc` function. - Added `video.frame.*` and `audio.samplerate`. - Added `input.external.ffmpeg` and `output.external.ffmpeg`. - Added `output.youtube.live.ffmpeg`. - Added `output.file.hls.ffmpeg`. - Added `reopen` telnet command in `output.external`. - Added `on_frame` (#886). - Enabled external decoders in windows (#742) - Added support for bash completion. - Added `video.add_text.native`. - Added `configure.bindir` - Added `for` and `while` loop functions. - Added `list.case`. - Added `metadata.getter` and `metadata.getter.float`. - Added `string.contains`. - Added `request.uri`. - Added `{input,output}.srt` (#898) - Added `path.remove_extension`. - Added SSL read/write timeout options, use it for incoming socket connections (#932) - Added ffmpeg resampler (#947). - Added `lsl` and `lsr`. Changed: - Depends on OCaml >= 4.08.0 - Changed return type of `http.*` and `run_process` to use tuples (#838) - Better error reporting with coloring and uniform format. (#790) - Improved reporting of file, line and character during parsing errors. - Remove dynamic plugin build option. - Made `on_end` delay a float getter. - Reimplemented `fade.{in,initial,out,final}` as scripted operators. (#664) - Removed `cross`/`crossfade` operators, superseded by `smart_cross`/`smart_crossfade` - Rename `smart_cross`/`smart_crossfade` operators as `cross`/`crossfade` - Default behavior of `crossfade` is old (simple) crossfade. Use `smart=true` to enable old `smart_crossfade` behavior. - Rename `file.duration` as `request.duration` - Removed duplicate `is_directory` - Rename `{basename,dirname}` as `path.{is_directory,basename,dirname}` - Empty playlists return by scripted resolvers is now considered a failure to resolve. - Rewrite `smooth_add` to use new `mkcross` functions. - Reimplemented `open_process_full` to get a hand on `pid` and finer-grained closing workflow (#703) - Added `transition_length` to `switch`-based operators to limit transition lengths and allow garbage collection of transition sources. - SDL renders text in UTF-8. (#712) - Made `x` and `y` parameters in `video.add_text` `float` getters. (#730) - Reimplemented `extract-replaygain` using `ffmpeg`, added an optional replay gain option to the `ffmpeg2wav` protocol. Thanks to @Yamakaky for contributing on this. (#749) - The `ratio` parameter of `compress` and `limit` is a float getter. (#745) - Removed `rewrite_metadata` which had been deprecated for a while now. - Allow string getter for `harbor` HTTP responses. - Renamed `get_clock_status` to `clock.status` and `log_clocks` to `clock.log`. - Renamed `rms_window` parameter of `compress` to `window`. (#796) - Added `chop` operator. - Keep master tracks' boundaries in `mux_*` functions. (#795) - Added `new_track` optional argument to callback in `insert_metadata`. - Use getters for weights of `rotate`. (#808) - Added `conservative`, `length` and `default_duration` params to `playlist.{reloadable,once,merge}` (#818) - Renamed `input.external` into `input.external.rawaudio`, added `input.external.wav`. - Renamed `gstreamer.hls` to `output.file.hls.gstreamer`. - Raise an error when using a format (e.g. `%vorbis`, `%mp3`, ..) that is not enabled. (#857) - Set default encoders and ladspa plugins samplerate and channels to configured internal `"frame.audio.samplerate"` and `"frame.audio.channels"`. (#870) - Handle unary minus in the preprocessor instead of the parser in order to avoid duplicating the parser. (#860) - Add `filter` option to `playlist.once`. - Added a `replay_delay` option to the `pipe` operator to replay metadata and breaks after a delay instead of restart the piping process. (#885) - Add `buffer_length` telnet command to `input.harbor`. - Bumped default `length` parameter for request-based sources (`playlist`, `request.dynamic`, ..) to `40.` to assure that there always is at least one request ready to play when the current one ends. - Added support for cue in/out and fade in/out/type metadata support in `ffmpeg2wav` protocol. Rename protocol to `ffmpeg`. (#909) - `list.assoc` and `list.assoc.remove` require an ordered type as first component. - Renamed `quote` to `string.quote`, removed `process.quote` in favor or `string.quote` (#1635) - Added `phase_inversion={true/false}` to `%opus` encoder (#937) - Fixed encoders forcing frame rate and audio channels too early (#933) - Change filename to a string getter in file-based outputs. (#198) - Changed `audio.converter.samplerate.preferred` option to `audio.converter.samplerate.converters` to give a list of possible converters. Fixed: - Lack of documentation for `cross`/`crossfade` (#743) - Fixed before metadata being lost during crossfade not in conservative mode. - Correct types and default values for `random.int` (#767). - Allow changing pipeline in gstreamer functions. (#762) - Script deadlock after a long time, most likely related to old crossfade transitions (#755) - AVI export fixed. (#789) - `%external` does not stop processes anymore on each metadata. (#789) - Fixed exit getting stuck when using `input.jack` (#769) - Stop lo server on shutdown. (#820) - Fixed external process stop not detected on second and further calls (#833) - Add `seek` in operators where implementation is clear (#853) - Do not enter buffering mode between tracks in `buffer` (#836) - Fixed file descriptor leak in external processes (#865) - Fixed encoded output creating empty files from failing sources (#876) - Fixed `cue_cut` not working when used before `cross`/`crossfade` (#874) - Fixed audio glitches when seeking within a MP3 file. - Fixed `insert_metadata` logic when insert new track and metadata (#903) - Fixed `replay-gain` script default location. - Fixed audio glitches at the end of crossfade transitions. - Specify that `list.remove` removes only the first occurrence and avoid reversing the list (#922). - File descriptor leak when using openssl-based operators. - Fixed SSL read taking too long to timeout (#932) - Fixed output starting when underlying source is not available (#393) - Fixed `string.escape` also quoting its string. # 1.3.7 (09-04-2019) Changed: - Reimplemented `open_process_full` to get a hand on `pid` and finer-grained closing workflow (#703) - Better log message when request download times out (#708) - Drop `log.level` for `ffmpeg` messages to `5` Fixed: - Timeout when executing external processes (#691, #736, #726, #708) - Set buffering only when frame is partial in time_wrap.ml. Makes it work with crossfade transitions (#695) - Changed `Icy-MetaData:1` to `Icy-MetaData: 1` in HTTP source headers. Fixes some shoutcast implementations (#727) - Fixed deadlock in `input.http` source status command (#367) # 1.3.6 (23-01-2019) Fixed: - Fixed `smart_crossfade` transitions skipping data after track marks. (#683, #652) - Fixed `input.pulseaudio` parameters. - Fixed crash when copying frame content (#684) # 1.3.5 (25-12-2018) New: - Added a bunch of base mathematics primitive, `exp`, `log`, `cos`, `sine`, ... - Added `"extinf_duration"` to parsed `#EXTINF` metadata. Fixed: - Fixed inotify watch semantics (#677) - Enhanced `#EXTINF` parsing in ambiguous cases (#625) - Fixed `output.youtube.live` (#630) - Make sure server writes are synchronous (#643) - Fixed crash when loading some frei0r plugins (#435) - Fixed compilation with `osx-secure-transport` - Fixed invalid opus stream generated when no data was ever encoded (#180) # 1.3.4 (10-09-2018) New: - Added `FFMPEG` decoder using the new `ocaml-ffmpeg` API. Thanks for @gndl for the hard work there. - Added `"init.allow_root"` setting to allow running liquidsoap as root. - Added `on_track` callback for playlists. Can be used to force a reload. - Added `server.condition`, `server.wait`, `server.broadcast` and `server.signal`. Used to control server command execution. - Added `server.write`, `server.read{chars,line}` to write interactive server commands in conjunction with the above functions. (#544, #568) - Added `output.youtube.live` as a wrapper around `output.gstreamer.audio_video` to stream live to Youtube (#498) - Added metadata extraction to `ffmpeg2wav` protocol (#623). Changed: - Depends on OCaml >= 4.03.0 - Depends on camomile > 1.0.0 - Use `http{s}.head` when available to fetch remote file's mime type. (win32 port) - Better log messages for root exit and buffer override. - Switch default log to stdout. Set to file when `log.file.path` is set (#612) - Disabled Gstreamer stream decoder. - Removed asynchronous mode for `output.gstreamer.audio_video` - Reworked `smartcross` internal logic (#596) - Enabled `replaygain` on `m4a` files, thanks to @gilou (#604) - Added `encoding` parameter to `output.shoutcast` to allow alternative string encoding for metadata updates (#411) - Deprecated `rewrite_metadata` Fixed: - Decouple dyntools compilation. - Support for OCaml >= 4.06 - File descriptor leak in `output.icecast` (#548) - Fixed URL regexp for `input.https` (#593) - Multiple gstreamer fixes: - File decoder with video. - Memory leaks (#516, #511, #434, #318) - Process freeze (#608, 278) - Duppy crash on exit (#160) - Fixed audio glitches when using the `pipe` operator (#614) - Deadlock in external decoder. (#611) # 1.3.3 (14-10-2017) New: - Added `on_change` to `register` - Added IPv6 support for `input.harbor`. (#491) - Added `time`, `localtime` and `gmtime` to help with time-predicates (#481) - Added `on_start` to execute callback when liquidsoap starts. - Added `enable_external_ffmpeg_decoder` to enable ffmpeg-base external decoder. - Added `"decoder.external.{ffmpeg,ffprobe,flac,metaflac,faad,mpcdec}.path"` configuration settings. Changed: - Renamed secure transport harbor key paths to: `harbor.secure_transport.*` - Renamed secure transport I/O to: `{input,output}.harbor.secure_transport`. - Added `.wma` to `gstreamer` file decoder file extensions (#483) Fixed: - Fixed memory leak in `output.icecast` connection method. (#490) - Fixed `mutexify` - Make sure that metadata are always passed in increasing position order in `map_metadata` (#469) # 1.3.2 (02-09-2017) Changed: - Removed `kick` telnet/server command, duplicate of `stop`. - Support `replaygain` for mp3 files, thanks to @d4h3r0 (#460) - Implement `input.harbor.ssl` using SecureTransport for OSX. Fixed: - Fix scheduler loop causing high CPU usage when using Process_handler without some of the default callbacks. (#475) - Revert `wait_for` implementation to pre-`1.3.0`, using a custom `select` loop (#453) - Handle mime-type arguments in `input.harbor` streams. (#456) - Tell ocaml to use the same C compiler at build and link time. Fixes build on FreeBSD when using C++-based bindings such as taglib. (#465) - Accept any capitalization of HTTP(S) as regular HTTP URL (#464) - Fix compilation with osx-secure-transport enabled. - Fix deadlock calling logging functions from within `Gc.finalise` (#609) # 1.3.1 (28-05-2017) New: - Allow any tags allowed in `"encoder.encoder.export"` settings in vorbis streams (#418) - Allow `"audio/mp3"` mime-type for mp3 in file resolution protocol. (#451) Fixed: - Fixed `run_process`, `get_process_lines`, `get_process_output` when compiling with OCaml <= 4.03 (#437, #439) - Calls to `wait_for` while the scheduler isn't running (#442) - Revert default handling of environment in `run_process`, `get_process_lines`, `get_process_output` to passing calling process' environment by default. # 1.3.0 (27-04-2017) New: - Added support for recursive functions (#406) - Add peak and peak.stereo operators (#364) - Change `track_sensitive` parameter to a boolean getter (fixed value or anonymous function). - Add SSL support to the various harbor operators, either via openssl or OSX's SecureTransport. - Add optional "dj" and "next" metadata for Shoutcast v2, wrap "dj" value in a callback in output.shoutcast (#370, #388) - Allow partial parsing of JSON objects in `of_json`. - Generalize list.assoc to allow default values. Legacy code must be updated: `list.assoc(k,l)` -> `list.assoc(default="",k,l)` - Generalize list.hd to allow default values. Legacy code must be updated: `list.hd(l)` -> `list.hd(default="",l)` - Allow to pass a default to list.nth. Legacy code must be updated: `list.nth(l,pos)` -> `list.nth(default=<..>,l,pos)` - Added `on_offset` to execute a callback at a given offset within a source's tracks. - Added mutexify to protect a function from being called concurrently. - Added request.log to get log data associated with a request - Added `overlap_sources` to rotate between sources with overlapping tracks. - Added `replay_metadata` to `input.harbor()` - Added `\<char code>` syntax for strings (#368) - Added string.sub - Added `run_process` to run a process with optional environment and return (`stdout`,`stderr`,`exit_status`) - Added `add_playlist_parser` to register new playlist parsers - Added optional static parameter to `protocol.add` - Added file.temp to create fresh temporary filename - Added process: protocol - Reimplemented curl-based fetch process using process: - Added s3:// protocol to fetch files from AWS S3 using the AWS CLI. - Added polly: protocol to enable speech synthesis using AWS polly. Generated files are mono so make sure you use `audio_to_stereo()`. - Added youtube-dl: protocol to resolved requests using youtube-dl - Added `which()` to find an executable within the $PATH - Added `register()` to allow to register new configuration settings Changed: - Reverted default value for `--error_as_warnings` option, renamed to `--strict`. - Moved say: protocol registration to utils.liq. - Moved `get_process_lines` and `get_process_output` to utils.liq, added optional env parameter - Set `conservative=true` by default in `cross()` and `smartcross()` Deprecated (can be removed in any future version): - Dynamic plugins compilation, deprecated in favor of opam rebuild mechanism. Removed: - aac and aacplus encoders, removed in favor of fdk-aac. - dirac/schroedinger video encoder: obsolete, abandoned upstream. - `force_mpeg` option in taglib metadata decoder. Has not been used for years and allows to decouple taglib code from the mad decoder. Bugfixes: - Fix negative seek (#390) - Prevent flows metadata updata from stalling sources (#377) - Add revdns setting for telnet, set all revdns default to false (#372) - Fix icy metadata in output.harbor (#358) - Fix missing first line of headers from icy clients in `input.harbor` (#380) - Fix timestamp in some logged output (#395) - Fix crash in external (download) protocol. - Fix `fade.{in,out}` metadata handling for new fade duration and type. - Compute normalization regardless of child sources ready status in `add()` to avoid unexpected change of volume. # 1.2.1 (01-07-2016) New: - Support for https (SSL/TLS) icecast connections. - Added `http.{put,head,delete}`, `https.{get,post,head,put,delete}`. - Added `input.https`. - Added `list.mapi`. - Added `rotate.sequence`. - New `pipe()` operator to pipe audio data through an external program. - Switched to curl for request resolution/fetch. Bugfixes: - Fix metadata update for shoutcast v2 when sid <> 1 (#320). - Fix connection to `input.harbor` using the shoutcast v1 protocol (#337). # 1.2.0 (12-01-2016) New: - Websocket server (#90): this means that you can stream to harbor directly from your browser! - Add support for AIFF format (#112). - Add `url.split_args` to split the argument of an url (#123). - Add `buffer.adaptative` to cope with small network delays (#131). - Add sleeper operator to simulate network delays and test robustness (#131). - Add `stereo.left` and `stereo.right` to extract channels from a stereo stream. - Add restart command to restart liquidsoap (#135). - Add `file.contents` to read the contents of a file. - Add `filter.rc` for first-order RC filters. Enhancements: - Add support for sending OSC data (`osc.send_*`). - Native support for (some) AVI files (#256) which enables support for external video encoders (#233). - Improve rms operator (#105) to have per channel rms (#102), the ability to dynamically set window duration (#103) and multiple monitors (#104). - Icecast streaming can now use HTTP1.1 chunked encoding (#82, #107). - Add support for multiple shoutcast extensions (#216). - Fade type can be overridden by metadata in `fade.in` / `fade.out` (#64). - Allow LADSPA plugins with arbitrary number of channels (#191). - Rename shine encoder from `%mp3.fxp` to `%shine`. - fdkaac: dynamic plugin (#79), set afterburner parameter, use MPEG4 by default (#83). - Improved subtyping on lists (#125, #126). - Add native simple JSON decoder. - Better code: do not abusively use assertions (#137), issue more warnings and fix them (#162). Bugfixes: - Correctly close connection in http.get / http.post (#72). - Remove `input.lastfm` which has been broken for a while. - Lots of small bugfixes. # 1.1.1 (08-05-2013) New: - Add support for FDK-AAC, which seems to be the best AAC(+) encoder around for now. Replacement candidate for VO-AAC and AACPLUS - Add %ifencoder to check whether Liquidsoap was compiled with support for a particular encoding format. - There is now an emacs mode in scripts/liquidsoap-mode.el. - Liquidsoap can be used as a Windows service. Enhancements: - Handle more OSC types (`float`, `float_pair`, `bool`, `string`, `string_pair`) and added `osc.on_*.` - Better infrastructure for decoding images. `add_image` can now handle most image file types. - Add `random.int` as well as `min_int` and `max_int` to standard library. - Add `playlist.merge` to play a whole playlist as one track. - Add `gstreamer.hls` to play http live streams (HLS). - Add `say.program` to specify text-to-speech program in scripts. - Add "random" transition type to `video.fade.*` in order to select a random transition each time. - Add max parameter to drop data on buffer overrun with `input.gstreamer.*`. - Add `bytes_per_page` parameter to ogg encoders. - Add support for DTX in speex and opus, as well as VAD for speex. - Localize some more parsing errors in files. Bugfixes: - Avoid deadlocks in harbor. - Correctly flush lame encoder. - Correct sequence operator when there is only one source. - Handle relative URLs in http playlists. - portaudio is now an active source. - Avoid jack I/O lowering the volume. # 1.1.0 (04-03-2013) ** This version brings some new features as well as correcting bugs. ** New: - Add support for GStreamer decoding, processing and encoding (%gstreamer format, v4l webcam input is now implemented using GStreamer). - Add support for opus decoding and encoding. - Add support for the shine encoder, which can efficiently work on architectures without FPU. - Add support for automatically computing the duration of tracks in the "duration" metadata [LS-641]. It can be enabled with `set("request.metadata_decoders.duration",true)` - Add support for frei0r video effects. - Allow `%define`'d variables in encoding formats [LS-634], e.g. ``` %define BITRATE 24 %define STEREO true output.file(%mp3(bitrate = BITRATE, stereo = STEREO),"bla.mp3",s) ``` Enhancements: - Taglib now reads all metadatas (even non-standard ones). - Add a mode to automatically reload a playlist when the file was changed [LS-363,LS-523]. For instance, `s = playlist("~/Music",reload_mode="watch")`. Also, add `file.watch` to call a callback when a file is changed, with inotify support when present. - Add support for FFMpeg as video converter, which you can use with `set("video.converter.preferred", "ffmpeg")` - Add `back_time` argument to blank operators [LS-609]. - Add a metadata to override fade.final duration. - MIME is computed at most once when extracting replaygain. - Default samplerate converter is now "fast". - BPM detection (bpm) now uses a callback. - Add `clock.unify` to unify clocks of all sources from a list. - Add `source_url` metadata to `input.http` streams. - Improved error message when theora format is not supported. - Add list.filter function. - `video.add_image` can now take any image format as input. - Add `mux_stereo`. - Support for external decoders in streams. - Move bugtracker to https://github.com/savonet/liquidsoap/issues Bugfixes: - Configure is now compatible with OCaml >= 4.0 and removed support for OCaml < 3.11 [LS-625]. - Fix random memory access / memory leak when decoding AAC/MP4 files [LS-647]. - Correct resampling of wav files. - Use the length for data indicated in header for wav files. - `Argv.(0)` now returns the script name [LS-605]. - Liquidsoap should now operate fine when compiled with -noassert [LS-578]. - Better handling of inexistent MIDI channels. - Video decoder now correctly handles videos from Icecast. - Avoid visu.volume freezing Liquidsoap on shutdown. - Fix a memory leak when decoding both audio and video in ogg [LS-636]. - More efficient handling of video converters, also fixes some crashes [LS-623]. - Have the soundtouch operator preserve tags [LS-621]. - Fix remaining time estimation in cross and `smart_cross`. - Avoid deadlocks in harbor and `input.http`. - Remove leftover files in configure [LS-567]. - Handle wav files with padded fmt headers. - Handle end-of-stream when seeking mp3 with mad. # 1.0.1 (04-07-2012) ** This version brings bug fixes and minor enhancements over 1.0.0. ** Fixes: - correct type for the "flush" parameter in `output.external()` thanks to Romaric Petion for pointing it out - fix bug where MP3 encoder would discard initial ID3v2 rendering - fix bug where `smart_cross()` would stop before the end of tracks, thanks to Edward Kimber for raising the issue - load libraries in --interactive [LS-618] - update examples, notably the installed radio.liq thanks to Emery Hemingway for noticing the problem - generalize the types of `input.http()` and `input.harbor()` to allow variable content kinds, and also allow video for harbor [LS-601] - `request.equeue()` now allows to remove requests from the primary queue - fix compilation of lame dynamic plugin. New: - new values for metadata fields does not override old one anymore; use setting `request.metadata_decoders.override` to restore old behavior - `stereo_mode` and `internal_quality` parameters for %mp3 encoder - enable mad decoder for MP1 and MP2 in addition to MP3, create aliased configuration keys "decoder.file_extensions/mime_types.mad" - support for CUE sheet playlists and metadata in M3U - setting "decoder.taglib.force_mpeg" to force taglib to consider files as MPEG - scripting builtins `getenv()`, `setenv()` and `environment()` - scripting builtin `source.fallible()` - harbor is now verb-oriented, supporting GET, POST, PUT, HEAD, DELETE, OPTIONS - load DSSI plugins from environment variables and using `dssi.register()` - also display the type of the whole expression when -i is passed - generalized custom path support for facilitating standalone distributions - and as usual, various improvements in the code, log and error messages, etc. # 1.0.0 (08-10-2011) Finally, the 1.0.0 release! It brings several important fixes, but also some nice novelties. The most outstanding difference concerns `output.icecast()`: its restart and `restart_delay` parameters are gone, replaced by a new `on_stop` handler which is called on every error (failed connection or disconnection) and returns the new restart delay. The `on_error` handler receives a string describing the error which enables user-friendly reporting, adaptative delays, etc. Note that `on_error` defaults to `fun(_)->3`. which is equivalent to having restart=true, restart_delay=3. in previous versions, NOT the same as the former restart=false default. As a result, liquidsoap won't fail to startup if an initial connection attempt fails. Fixes: - LS-532,527: avoid freeze after errors in streaming threads or source initialization routines - LS-542: race condition in `playlist*()` breaking randomness - LS-489: double expiration lead to illegal queue length and freeze of request-based sources - Avoid multiple simultaneous reloading in `playlist*()`, thanks to Fabio Costa for his help on this one! - Pass charset information to icecast server to avoid encoding bugs - LS-555: timeout for icecast connection attempts - LS-559: permanent stop after disconnection on Ogg streams - LS-565: efficient and crash-free error handling in `input.http/harbor()` when the input stream has an invalid number of channels - LS-431: proper handling of duration in `blank()` avoids abusive empty tracks - LS-556: rework conversion operators, optimizations used to be unsafe & broken - LS-574: silent MIDI synthesis operators - LS-396: `drop*()`'s types reflect that they don't support variable arities - LS-442: allow comments not terminated by newline at end of file New: - `on_error` handler in `output.icecast()`, see above - New msg param in %mp3 for marking frame headers, defaults to version string - `output.file()`: new `on_close` parameter, may be used to write exact duration - %mp3.vbr/abr for variable bitrate MP3, %mp3 is now a synonym of %mp3.cbr - MP3 encoders now support ID3v2 tags - `input.http()`: new "status" command - LS-556: `mux_mono()` for adding a single audio channel into a stream - `video.add_text()` using libgd (gd4o) for environments without X Dependency on graphics can be disabled (to work around erreneous detection) - script language: add infix operator mod (patch by Fabio Costa) - `delay()` now has an "initial" parameter - LS-557: "server.timeout" setting can now be disabled by setting it to -1 - LS-532: `source.init()` for selective init with a way to handle errors, plus settings "clock.allow_streaming_errors" and "init.force_stat" (or --force-start on the command line) for easing dynamic uses of liquidsoap Enhancements: - Panic crash to avoid frozen liquidsoap after duppy crashes - Text-to-speech: festival and sox are now only runtime dependencies - LS-475,516: better support for dynamic URL change in `input.http()` - LS-484: display user-friendly error messages in interactive mode - LS-308: use seconds internally in request sources, avoid overflow and display more user-friendly debug messages - Cleanup `visu.volume()` and `video.vis_volume()` - LS-573: replace " " by "\_" in identifiers to make them valid in the server - Script syntax: unary minus now usable without parenthesis after semicolon - Two generic queues by default, to avoid deadlocks in advanced situations - Documentation, build & install system, etc. # 1.0.0 beta3 (05-08-2011) - Feature: Added `of_json` to parse json data. Depends on json-wheel. - Feature: Added file.exists and is_directory. - Feature: Added timeout options for: telnet, harbor (server), input.icecast - Enhancement: finer-grained timeout detection for `input.harbor` and `input.http` - Fix: deadlock when disconnecting harbor users through server/telnet command. - Fix: dynlink detection in native mode with old versions of ocaml - Fix: deadlock when an exception is raised during startup while the clock is owned by a source (e.g. `input.alsa`). See LS-527 for more details. # 1.0.0 beta2.1 (07-07-2011) - Fix: `playlist.safe()` was unusable in beta2, as a side effect of removing duplicate "timeout" parameter in `playlist()`. - Minor enhancements to documentation, settings and reference. # 1.0.0 beta2 (04-07-2011) This release introduces lots of fixes and cleanup, but also some new features. Major novelties: support for fast seeking and cue points, FLAC and improved AAC+ support, introduction of the liquidsoap yellowpages "flows", plugin support and improved messages for scripting errors Compatibility warning: `insert_metadata` has changed, and `clock.assign_new()` should be used instead of `clock()` to avoid some of the new static checks Decoders: - New support for seeking and fast computation of durations in most formats - New decoders: FLAC (native & Ogg) and images using Camlimages - Fixes in Ogg decoding: LS-515 (loss of data) and LS-537 (segfault). - Fix LS-337: periodical failures when decoding AAC(+) streams - AAC(+): use new ocaml-faad with builtin mp4ff, easier to build - New detection mechanism mixing extensions and MIME types (when available), with corresponding settings `decoder.file_extensions.<format>` and `decoder.mime_types.<format>`. - Decoder order can be user-defined thanks to new settings "decoder.file_decoders", "decoder.stream_decoders" and "decoder.metadata_decoders". - Indicate which decoder is used in the "decoder" metadata - More helpful log for various errors - Fix segfault with SdlImage image decoder Encoders: - New FLAC encoders %flac (native) and %ogg(%flac) - New AAC+ 2.0 and vo-aacenc - New settings to theora: keyframes make files much smaller! - New settings for WAV encoding: headerless, samplesize. - Fix segfaults with ocaml-aacplus - Enhancement LS-441: filter metadata before encoding, based on the "encoder.metadata.export" setting. - Rework infrastructure of encoded outputs to fit all formats, outputs and styles of metadata handling, file reopening (#386) Harbor: - New: `output.harbor()` which acts as a mini icecast server, accepting listeners directly. Encoding is shared among users, and is only performed when needed. - New: ability to register HTTP GET/POST handlers to create simpler web services, using `harbor.http.register/remove()`. - Make all settings local: port, user and password can be set independently for each `input.harbor()` source - New: "metadata_charset" and "icy_metadata_charset" in `input.harbor()` - Fix: race condition possibly leading to abusive "source taken" (LS-500) Icecast: - Add support for streaming native flac, only works when streaming to `input.harbor()`, not supported by actual Icecast servers - Fix bugs in ICY protocol support (header parsing, user name) - Use ICY metadata updates when streaming AAC(+) - New: "encoding" parameter for `output.icecast()`, used for recoding metadata Defaults to "latin1" with shoutcast servers - New: `icy.update_metadata()` function for manual updates - Enhance default "song" metadata, avoiding " - " when unnecessary (#467) Input/output: - New experimental `input.v4l/v4l2()` for webcams - New experimental `input/output.udp()` for unchecked UDP streaming, available with most formats (at your own risk) - Restore `output.pipe.external()`, now called `output.external()` - New parameters for most outputs and inputs (start, on_start, on_stop, fallible); cleanup and uniformize implementations (LS-365) - New ALSA settings alsa.alsa_buffer, alsa.buffer_length and alsa.periods Setting periods=0 allows to not attempt to set the number periods, which is impossible on some devices - New preference order in `input/output.preferred()`: pulseaudio, portaudio, oss, alsa, ao, dummy Operators: - New: support for cue points with `cue_cut()` - Change `insert.metadata()` which is now more script friendly, returning an insertion function rather than register a server command. The old functionality is available as `server.insert_metadata()`. - New: `rms()` operator for getting RMS of a stream, and `server.rms()` which makes this information available as a server command. - New: `track_sensitive` mode for blank detection operators - New: `playlist.reloadable()` for playing a list once, with a command for restarting it. - Remove `id.*()` which can be replaced by type annotations Scripting API: - New: OSC support through `osc.bool()`, `osc.float()` and `osc.float_pair()` - New: JSON export `json_of()` - New: `http.get()` and `http.post()` - New: `url.encode/decode()`, `base64.encode/decode()` - New: `string.recode()` for charset conversions using camomile - New: `notify_metadata()` and `osd_metadata()`, suitable for use with `on_track()` and `on_metadata()` - New: `request.metadata()` for getting a request's metadata - New: `string.length()` - Enhance `log_clocks()` with parameter for delaying startup - Enhance `get_clock_status()` with "uptime" reference time Server interface: - Print the playlist's URI when calling `<playlist>.uri` without an argument. - Enhance `<queue>.ignore` now works also in the primary queue - New command for changing the URL of an `input.http()`, ref #466. The command is `<id>.url` and it needs a restart (`<id>.stop`, then start) to take effect. - Fixed double registration of server commands which resulted in broken "help" command (LS-521) Script language: - Option "-i" doesn't show types for pervasives anymore - Pretty printing of types (LS-474) with indentation, also used in the documentation - Enhanced type errors (LS-459): no more traces, only the relevant part of types is displayed, plus a few special friendly messages. - Enhanced static checks (LS-123) thanks to the introduction of the `active_source` subtype, for unused variables and ignored values This should avoid common errors, help troubleshooting If needed, advanced users can work around errors using --check-lib and --errors-as-warnings General: - Do not attempt to install "daemon" files if user/group have not been set, unless forced using "make INSTALL_DAEMON= install" - Fix several core "source protocol" bugs, causing assert failures and other crashes: LS-460 (source becomes not ready without operator knowing) #403 (information about being ready is not precise enough) - Fix incorrect image accesses (LS-430) by introducing a safer VFrame API Applies to most video operators ( `video.fade()`, `video.text()`, effects...) - Cleanup resource (de)allocation, which is becoming critical with dynamic reconfigurations (e.g., dynamic output creation, `source.dynamic()`) Enforce that server commands are always deallocated (LS-495) Attempt to stop sources when initialization fails, so they cleanup as much as possible (LS-503) Avoid deadlocks upon crashes in IoRing-based operators Share code for stoppable feeding threads, use it in `input.harbor()` Avoid useless initialization of SDL systems - Dynamic loading of lame and aacplus libraries, making it possible to ship them as separate binary packages. This is particularly useless for non-free libraries and for those that depend on X (e.g., SDL) which is often undesirable on servers - Support for separate compilation of most optional features as plugins Use `--enable-<feature>-dynamic-plugin` in ./configure and --dynamic-plugins-dir in liquidsoap - Better Win32 support, more version checks, separate compilation of C files and compliance with Debian's OCaml standards - Externalize many audio and video functions in the new ocaml-mm library Factorize and optimize various conversions - Rewrite harbor and server code using the new Duppy monad, introducing camlp4 into the build system - New regression tests (make test) - More user-friendly exception printing - Avoid problems preventing backtrace printing Miscellaneous: - Update liguidsoap, make microphone input optional (LS-496) - Do not crash upon charset-recoding failures [LS-473] - Fix in `source.dynamic()`: missing source re-selection [LS-354] - Avoid deadlock on startup in daemon mode [LS-229] - Fixes in LADSPA and SDL causing early freezing of Frame parameters. - Fullscreen mode for `output.sdl()` - Fix: SIGPIPE used to cause crashes (LS-53,LS-287) - Fix: `video.volume()` could crash upon some end-of-track situationos - Fix: properly escape filenames in external file duration methods - Rework timeout management for various sockets (notably icecast & harbor) Set nodelay, remove `TCP_*TIMEOUT` [LS-508,LS-509] # 1.0.0 beta1 (06-09-2010) This beta version introduces two major new features: heterogeneous stream types and clocks. New: - Different sources can carry different types of content. - Encoding formats have been introduced to help infer stream content types. This brings static checking for bounds in encoding parameters. - Introduce conversions between stream contents (mono, stereo, drop audio, video, etc) and muxing. - Allow explicit type annotations in scripts. - Introduce clocks, cleanly allowing for the coexistence of different time flows, and avoiding inconsistencies that can result from it. Soundcard I/O and cross-based operators notably make use of it. - Remove root.sync, replaced by attaching a source s to clock(sync=false,s). - Enable dynamic source creation and `source.shutdown()`. - Extend and adapt MIDI and video operators. - Introduce purely metadata streams (audio=video=midi=0 channels) and metadata stream decoder. - Support WAV streams in `input.http/harbor()`. - Introduce static image decoder using SDL image. - Remove bound of request identifies (RID). - Experimental: `source.dynamic()` for advanced dangerous hacking. - Make path relative to script in `%include "PATH"`, and introduce `%include <...>` where path is relative to liquidsoap library directory. - Add `channels_matrix` parameter to `output.ao()`. - Add `on_(dis)connect` hooks in `input.harbor()`. Cleanup and fixes: - Lots of cleanup and fixes as with all major code rewriting. - Optimize video and stabilize it a little bit... still not perfect. - Rewrite stream and file decoding API, as well as file format detection. - Enhance shutdown and error message reporting, notably for icecast and request-based sources. - Avoid quasi-infinite loop in failed request resolving. # 0.9.3 (04-09-2010) This release is a bugfix of the latest snapshot (0.9.2). It will be the last bugfix before 1.0. Bugs fixed: - Add "audio/mpegurl" to the list of mime-type for basic playlist parsing. - Decode arguments passed to `input.harbor`. - Use Camomile framework to try to recode arguments and user/password passed to `input.harbor`. - Support Theora 1.1 API via ocaml-theora 0.2.0. - Fixed `input.lastfm`. - Fixed SDL output. - Allow magic file type detection to follow symlinks. # 0.9.2 (29-10-2009) This release is a SNAPSHOT of upcoming features. It also contains several important bugfixes. As a snapshot, it contains experimental or unpolished features, and also breaks compatibility with previous versions. You should in particular notice the two "New" items below: - `random(strict=true)` is now called `rotate()`; - request sources (`playlists`, `request.*`) have a new queuing behavior, check the doc (request-sources.html) or revert to conservative=true. Bugs fixed: - Ogg encoder now muxes pages according to their ending time. - Support "ogg/audio" and "ogg/video" mime types for HTTP ogg streams. - Changed external encoder's "restart_encoder" to the more relevant "restart_on_new_track". Added optional "restart_after_delay" to restart the encoder after some delay. Also completely rewrite stop mechanism. Stop operation now waits for the encoding process to finish, allowing proper restart_on_new_track like for ogg encoded data. - Factorized buffered I/O code. Also added a blocking API, which avoids "no available frame" and "reader not ready" messages and audio glitches. It enforces that `root.sync` be deactivated for these sources, such that synchronization is done by the source. (#203) - Factorized file decoding code. - Fixed reversed order when parsing playlists using `playlist.parse()`. - Avoid bad crashes when resources lack, e.g. no more memory. - Tighten and enforce the inter-source protocol. - All outputs: fix the autostart server command. With the former code, a modification of the autostart parameter was only taken into account one start/stop cycle later. - `on_blank()`: fix a bug that prevented the first call to on_noise. - Fixed estimated remaining samples on ogg files, fixes issues with operators relying on this value, in particular `crossfade()` and request-based sources when operating in non-conservative mode. - Fixed socket descriptor leak in `input.http`. (#318) - Fixed deadlock at init when an exception was raised at wake_up phase. (#319) - Fix `delay()` which could sometimes incorrectly declare itself ready, and thus try to get some data from its unavailable input source. - Bug in queue duration estimation led to infinite feeding of the queue, until all request IDs are taken. - Several fixes enforcing clean (non-deadlocking) sleep/shutdown. New: - The operator `rotate()` replaces random(strict=true), and `random()` does not have a strict parameter anymore. - Switch to new behaviour in request-based sources. Use conservative=true to get to the old behaviour. - `on_blank()`: provide an `on_noise` handler. - `playlist*()`: add server commands for reloading the playlist and changing its URI. - Fallible outputs: all outputs now have a fallible mode, in which they accept fallible sources, and automatically start/stop when the child fails to stream. - EXPERIMENTAL ogg/dirac encoding support. - `output.*.aacplus()`: native outputs encoding in AAC+ using ocaml-aacplus. - Switched to a custom implementation of the various shout protocols. It notably allows arbitrary content-type settings which enables AAC+ streaming. Enabled wrappers for external encoders for AAC+ format aacplusenc (#220 and #136). Also added custom IRC, AIM and ICQ headers to shoutcast wrappers (#192). - Harbor now supports Shoutcast/ICY headers properly. This allows in particular the use of any supported data format for the source client. - Harbor sources now also consider "song" field when updating metadata. - Added built-in support for m4a audio files and metadata. Made external support optional through enable_faad. - Added optional resampling for output.external operators (#273). - Added optional host and port parameters for audioscrobbling submissions. Allows to use alternate systems, such as libre.fm of jamendo's compatibility API. Added nowplaying submission, and various wrappers, including a full submission process (now playing at beginning, submit some times before the end). - New variables in the script language: `liquidsoap.version` and `configure.{libdir,pidfile,logdir}`. - Added built-in support for replay_gain, through the replay_gain protocol (enabled by default) and the replay gain metadata resolver (to be enabled using `enable_replaygain_metadata()`). (#103 & #317) - Reverse DNS operations can be disabled using settings keys `server.telnet.reverse_dns` and `harbor.reverse_dns`. Experimental: - MIDI: file decoding, synthesis, virtual keyboard. Removed: - RTP input and output. - Removed decoders using ocaml-natty. Slow, unmaintained and superseded by the mplayer decoder. # 0.9.1 (18-06-2009) Bugs fixed: - Fixed request task not ending properly for request-driven sources (playlist, request.queue, request.equeue). Fixes a problem reported when reloading an empty playlist multiple times. (#269) - Fixed math.h usage in rgb_c.c. - Fixed append operator. (#284) - Fixed OSS compilation for non-linux systems. - Disconnect idle harbor sources after timeout has expired. Thanks to Roman Savrulin for reporting and fixing ! - Taglib metadata resolver is only used on files decoded by the MP3 decoder. New: - Get a node's striping status when stripping blank with `strip_blank` (#260). - `on_connect` function for `input.harbor` now receives the list of headers given by the connected source (#266). - Added `on_end` operator, to execute a handler when a track ends. - Added estimated remaining time in the queue length for request-driven sources (`request.{equeue,queue}`, `playlist`). This allows these sources to prepare less files in advance. In particular, primary queue may only contain the file currently played. Default behaviour has been set to the old behaviour, and a conservative option has been added to switch to the new behaviour. New behavivour will be the default for the next release. fixes #169, references #146 # 0.9.0 (01-03-2009) Bugs fixed: - Fixed byte swapping function. - Fixed unix server socket failure (#160). - Fixed mp3 audio glitches when decoding files with picture id3v2 tags using ocaml-mad (#162). - Fixed liquidsoap crash on weird telnet and harbor input (#164). - Fixed `request.queue()` not considering initial queue on wake-up (#196). - Fixed source leak in `append()`. - Fixed `after_output` propagation in the transitions of switches (#208). - Fixed compilation for ocaml 3.11 (#216). - Fixed (again) Vorbis mono output (#211). - Avoid crashing on broken symlinks when parsing directories (#154). - Use random temporary file in liGuidsoap. - Fixed liGuidsoap to use the (not so) new metadata field initial_uri. - Fix bugs in the life cycle (sleep/wake_up) of queued request sources, which made `say_metadata` unfunctional. - Remove buggy unbuffered `{input,output}.jack`. (#231) - Fixed audio information sent to icecast. (#226) - Fixed global reset starting inactive sources. (#227) - Fixed shoutcast source initial answer in harbor. (#254) - Fixed frame and metadata duplication in cross operators. (#257) - Fixed several sources (outputs, external streams) that were not going to sleep correctly. Changes: - Warning: `interactive_float()` is now `interactive.float()`. New: - Compatible with OCaml 3.09. - Faster shutdown. - Rewrote ogg decoding, for files and streams. - Support for ogg skeletons, currently only used for theora. - Cleanup icecast class hierarchy and restart mechanism, especially with respect to the encoder. - Support for breaks and metadata in generators. As a result, `input.http()` and `input.harbor()` now fully support them. See new_track_on_metadata parameters there. - Switch operators (fallback,random and switch) can now replay the metadata of a source that has been left in the middle of a track. - New `force_mime` parameter for `input.http()`. - New `insert_missing` parameter for `append()`. - Multi-line strings in liq scripts, with a caml-like syntax. - Slight modification of the scripting syntax, handling unary minus. - Added `user_agent` parameter for `input.http` and `input.lastfm` - Added speex support for files and HTTP streams. - Added EXPERIMENTAL support for AU, AIFF and NSV mp3 audio files using ocaml-natty. - Added "prefix" parameter to the playlist operators. Allows to prefix each uri in order to force resolution through a specific protocol, like replaygain: for instance. (#166) - Support for external processes as audio encoder: - Added output.icecast.lame to output to icecast using the lame binary. - Added output.icecast.flac to output to icecast using the flac binary. - Full generic support awaits some changes in libshout. - Support for external processes as audio stream decoder: - Added `input.mplayer` to stream data coming from mplayer. - Support for external processes as audio file decoder: - Added support for flac audio files using the flac binary. - Added support for m4a files using the faad binary. - Added optional support for mplayer, enabled with `enable_mplayer()`. - Support for external metadata decoders. - Support for generic authentication function in harbor, also available for ICY (shoutcast) clients. - Added optional support for the samplerate library, and dynamic configuration of resampling routine. - Added `lag()` operator, which delays a stream by a constant time. - Initial support for PulseAudio. - Added experimental support for `{input,output}.marshal`, allowing raw communication between various liquidsoap instances. - Added optional alternatives to store buffered audio data: - raw: in memory, s16le format - disk: on disk, several big files - disk_manyfiles: on disk, a lot of small files See documentation for more details. - Added EXPERIMENTAL video support: - Support for ogg/theora file input. - Support for ogg/theora file and icecast output - Support for SDL output. - Optional support for ocaml-gavl as video converter. - Support for video in _some_ existing operators, including switches, `add()`, metadata/track manipulations. - Added operators: `video.fade.*`, `video.fill`, `video.greyscale`, `video.image`, `video.invert`, `video.lomo`, `video.noise`, `video.opacity`, `video.opacity.blur`, `video.rotate`, `video.scale`, `video.sepia`, `video.text`, `video.tile`, `video.transparent`, `video.volume`. # 0.3.8.1 (11-08-2008) - Fixed metadata propagation during default transition in smart_crossfade - Changed transition evaluation order in smart_crossfade - Fixed transition function in smart_crossfade # 0.3.8 (30-07-2008) Bugs fixed: - Vorbis mono output is now working - Fixed parameter parameter description in the documentation - Propagate new delay in add_timeout - Fixed inter-thread mutex lock/unlock in playlist.ml - Fixed "next" playlist command - Fixed race conditions in request_source.ml feeding task - cross/smartcross: raise the default for inhibition as setting it to exactly duration is not enough - Don't fail when $HOME is not set - Fixed metadata update in `input.harbor` with icecast2 source protocol - Fixed shutdown function. Fixes #153 - Fixed `input.oss`. Liquidsoap now works with OSS4 ! Fixes #158 New: - Added a hook to execute a function when playlist.once ends - Enhanced smart_crossfade - Added string.case and string.capitalize - New "exec_at" operator, to execute a function depending on a given predicate - Added script example in the documentation to listen to radio Nova and get the metadata from a web page. - Changed parameters name in fallback.skip to reflect who are the fallback and input source. - Added a dump file parameter to `input.harbor`, for debugging purpose. - Added an auth function parameter to `input.harbor` for custom authentifications. Fixes #157 - Added "primary_queue" and "secondary_queue" commands to request.queue and request.equeue sources. Also set the metadata "queue" to either "primary" or "secondary" for each request in the queue. Documented it too. - Insert latest metadata for a node of a switch source when switching back to it in a middle of a track. - Added a 'conservative' parameter to cross, smilar to the one in smartcross. Internal: - Enhanced liqi documentation parser to build the website. # 0.3.7 (03-06-2008) Bugs fixed: - Now works on FreeBSD and hopefully other unices that are stricter than Linux about Mutex usage. - `input.http()` now has a `bind_address` parameter. - Harbor socket now has a timeout for lost connections. - `smartcross()` is now more compliant with the inter-sources protocol, fixes several "get frame didn't change the buffer" bugs. - Ogg packeting bugs. - Buffering policy in `input.http/harbor()`. - No "." in IDs and labels. - Resources: FD leaks, useless threads (threads leaks?) in `input.http()`. - `fade.out()` used to run into infinite loops when the delay was 0. New: - New documentation system and website. - Self-documenting server with a more helpful "help" command. - Moved to duppy: less threads, lighter load, and an configurable scheduler. - Moved to Taglib for more reliable access to MP3 metadata. - MIME types, notably for playlists and MP3 files. - New Jack support. The old one has been renamed to `in/output.jack.legacy()`. - Harbor: per-mount passwords and the stop command to kick a source client. - Official Last.FM client. - Metadata is no more punctual but interval-based, which suppresses some surprising behaviours. - Perfected daemon behaviour. - All `output.file.*()` now have the features that used to be only in `output.file.vorbis()`, notable re-opening. Added %w to the strftime-like format codes allowed in their filename parameter. - Add `clear_metadata()` and `map_metadata()`. Now, `rewrite_metadata()` is a simple specialization of `map_metadata()`, written in utils.liq. - Dynamic amplification factor in `amplify()`, e.g. useful for replay gain. - Lots of new functions in the scripting API: for lists, requests, system interaction, shutdown, command-line parsing, scripted server commands, etc. As always: - code cleanup, style, etc. # 0.3.6 (17-12-2007) Bugfix release: - Close Http socket - Add http socket timeout - Close playlist file after reading its content - Fix http redirect support for lastfm files - Fix file open leak in camomile support - Fix playlist uri ending with "/" # 0.3.5 (08-11-2007) Bugfix release: - Fixed #57: scpls and mpegurl playlist parsing - Fixed #46: Late cross-scripts bindings # 0.3.4 (25-09-2007) Notation: "-" stands for a change, "+" for an addition. - Language - Support for polymorphism, subtyping and basic ad-hoc polymorphism, which allows a much simpler API, notably for maths and serialization. * Added `interactive_*()` for mutable values. - The right syntax for settings is now set("var", value) and can be used anywhere in the scripts. - The volume parameters of most operators are now in dB. - Many builtin functions added. - Nicer type error messages. - Sources - Added `input.lastfm()` to relay last.fm streams. - Added `input.harbor()` to received Icecast2 source streams. - Added `noise()` to generate white noise * Reimplemented playlist support, added various xml and text formats. * Added mpd protocol to find files using mpd. - Operators - New effects: `compress()`, `flanger()`, `pan()`. - New filters: `filter.fir.*()`, `filter.iir.*()`, `filter.biquad.*()`, `comb()`. - Added support for LADSPA effects. - Added `eat_blank()` to remove blanks. - Outputs - Added non-default restart option for `output.icecast.*()`. - Added the possibility to tweak some settings at runtime. - Split `output.icecast.vorbis()` into `output.icecast.vorbis.*()` to distinguish between encoding modes -- and similarly for output.file.vorbis and mp3. - Better handling of Icecast disconnections. - IO - Added portaudio support. * Jack support is now somewhat working. - As usual, lots of bug fixes, careful polishing & much more... # 0.3.3 (06-06-2007) - Major cleanup of the core stream representation; moved to float arrays, removing several back-and-forth conversions and enhancing the perfs a lot; reviewed all sources and operators, made many minor enhancements btw. - Lots of sound processing operators: compand, compress, normalize, pitch, bpm, soundtouch, saw, square, etc. Add more shapes to `fade.*()`. - New track processing operators: insert_metadata, on_track. - Smart cross: allows to select a transition based on the volumes around the end-of-track. - Support for AAC encoding/decoding. - Several fixes to output.icecast.mp3 in order to support shoutcast servers. - Automatic format recognition for `input.http()`, support for playlists. - OSS I/O. - Unbuffered ALSA I/O for low latency. - Server interface via UNIX domain sockets. - Better output.file.vorbis with support for re-opening the file, appending, interpolate strftime format codes, etc. - Add pre-processing and math primitives to the language, new `_[_]` notation for `assoc()`, ruby-style anti-quotation `("..#{..}..")`, `add_timeout()`, `execute()`, `log()`... - Ability to tweak the internal PCM stream format. - Classify sources and operators in categories for more structured doc. - Started a few visualization operators, text and graphics based. - Several bug fixes: request leaks, sine frequency, switch, etc. # 0.3.2 (16-03-2007) - New portable output to speakers using `libao()`. - Updated liGuidsoap to use it until ALSA gets enhanced. - Implemented a decent estimation of the remaining time in a track. - Added the `cross()` operator allowing cross-fading. - Generalized `say_metadata()` into `append()` and `prepend()`. - Per-track settings for `cross()`, `fade.*()`, `prepend()` and `append()` using requests' metadatas. - Implemented `input.http.mp3()`, including support for icy metadata. - New `pipe()` operator which allows one to filter the raw audio through an external program. However, sox and other common tools aren't suitable for that because they don't flush their output often enough. - New `on_blank()` operator for calling a callback on excessive blanks. - Restart outputs on insane latencies. - Type checkings for settings. - Setting for not starting the internal telnet server. - Now handles old and new versions of Camomile correctly. - Internal fixes and polishing (switches' cached selection, empty tracks..) # 0.3.1 (17-11-2006) - More standards-compliant tarball - Generate doc with locally built liquidsoap - Try to cope with ill-formed mp3 - Updated for newer versions of Camomile - So-called "strict" random-mode # 0.3.0 (27-08-2006) - Many minor and major fixes at every level! - Conversion of metadata to UTF8. - Got rid of too many threads by scheduling all download tasks in a single thread, and handling all of the server's clients in another single thread. - Simplified the time interval syntax and integrated it to the script language. - New protocol: Wget for FTP, HTTP and HTTPS. - Ability to define a new protocol from the script, typically using an external app/script for resolution, such as bubble. - Ability to use an external app/script for dynamically creating requests. - New `on_metadata` operator for performing arbitrary actions (typically a call to an external script) when metadata packets occur in a stream. - MP3 encoding, to file or shout. - API renamings and simplification. - Supports transition, as functions of type (source,source) -> source in all switching operators: schedule, random, fallback. - Restart icecast2 outputs on failures. - Major changes to the scripting language which is more uniform and flexible, has functions, a helpful (?) type inference, and a simple Ruby-like syntax. - Timing constraints and synchronization are managed by Root in a centralized way, no more by the many outputs which need it. - Audio decoding is no more based on file extensions, but on the existence of a valid decoder. - Added the equeue operators which allows interactive building of playlists, supporting insertion and moving of queued requests -- queue only allows you to push requests and cancel queued requests. - A Python-Gtk GUI for controlling operators, or more specifically as a console for creating your live show -- to be updated, still unstable. - Alsa input and output. - Blank detection, automatically skips on too long blanks, or strip them. - Http ogg/vorbis relay, the way to relay live shows. - Interactive mixer. - The request system was mostly rewritten to really fulfill its specification. - The server is no more associated to the queue operator but is now something external, in which all operators can plug commands. This much more logical design lead to many more interactive controls. The syntax of command outputs was also simplified for easier automated processing. - Dynamic loading of plugins. - Outputs are now operators. It makes it possible to output different streams in a single instance of Liquidsoap, which RadioPi needed. As a consequence we removed the restriction that one source must have at most one father, without any extra burden for the user. # 0.2.0 (20-04-2005) - Proper initial release. # 0.1.0 (2004) - Release for academic demonstration, not functional. ������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/CITATION.cff�����������������������������������������������������������������������0000664�0000000�0000000�00000001455�14773033502�0015573�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������cff-version: 1.2.0 message: "In order to reference this software, please cite it as below." abstract: "Liquidsoap is a powerful and flexible language for describing your streams. It offers a rich collection of operators that you can combine at will, giving you more power than you need for creating or transforming streams. But liquidsoap is still very light and easy to use, in the Unix tradition of simple strong components working together." authors: - family-names: "Beauxis" given-names: "Romain" - family-names: "Mimram" given-names: "Samuel" orcid: "https://orcid.org/0000-0002-0767-2569" title: "Liquidsoap" version: 2.2.2 doi: 10.5281/zenodo.1234 date-released: 2023-11-04 url: "https://www.liquidsoap.info/" repository-code: "https://github.com/savonet/liquidsoap" license: GPL-2.0 type: software �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/COPYING����������������������������������������������������������������������������0000664�0000000�0000000�00000043134�14773033502�0014734�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������ GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. <one line to give the program's name and a brief idea of what it does.> Copyright (C) <year> <name of author> This program 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. This program 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 this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. <signature of Ty Coon>, 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/Makefile���������������������������������������������������������������������������0000664�0000000�0000000�00000000060�14773033502�0015330�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������all: build build install clean test: @dune $@ ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/README�����������������������������������������������������������������������������0000777�0000000�0000000�00000000000�14773033502�0016026�2README.md�������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/README.md��������������������������������������������������������������������������0000664�0000000�0000000�00000020047�14773033502�0015156�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Liquidsoap Liquidsoap is a swiss-army knife for multimedia streaming, notably used for netradios and webtvs. It has tons of features, it's free and it's open-source! Liquidsoap is a powerful and flexible language for describing your streams. It offers a rich collection of operators that you can combine to create and transform streams. Liquidsoap is very light and easy to use, in the Unix tradition of simple strong components working together. Copyright 2003-2024 Savonet team [![GPL license](https://img.shields.io/github/license/savonet/liquidsoap)](https://github.com/savonet/liquidsoap/blob/master/COPYING) ![CI](https://github.com/savonet/liquidsoap/workflows/CI/badge.svg) [![GitHub release](https://img.shields.io/github/release/savonet/liquidsoap.svg)](https://GitHub.com/savonet/liquidsoap/releases/) [![Install with Opam!](https://img.shields.io/badge/Install%20with-Opam-1abc9c.svg)](http://opam.ocaml.org/packages/liquidsoap/) [![Chat on Discord!](https://img.shields.io/badge/Chat%20on-Discord-5865f2.svg)](http://chat.liquidsoap.info/) [![](https://img.shields.io/badge/Gurubase-Ask%20Liquidsoap%20Guru-006BFF)](https://gurubase.io/g/liquidsoap) [![Built with Depot](https://depot.dev/badges/built-with-depot.svg)](https://depot.dev/) | | | | ------------------------- | ----------------------------------------------------------------------- | | Homepage | http://liquidsoap.info | | Discord Chat | http://chat.liquidsoap.info | | Blog | https://www.liquidsoap.info/blog/ | | Bug reports | https://github.com/savonet/liquidsoap/issues | | User questions | https://github.com/savonet/liquidsoap/discussions | | IRC (deprecated) | #savonet on [irc.libera.chat](https://libera.chat/) (w/ discord bridge) | | Mailing list (deprecated) | savonet-users@lists.sourceforge.net | ## Installation See the instructions [here](https://www.liquidsoap.info/doc.html?path=install.html). ## Release Details Current release status by version: | Branch | Latest release | Supported | Rolling Release | | --------|----------------|-----------|-----------------| | `2.3.x` |[2.3.1](https://github.com/savonet/liquidsoap/releases/tag/v2.3.1) (docker: `savonet/liquidsoap:v2.3.1`) | ✅ | [2.3.x](https://github.com/savonet/liquidsoap/releases/tag/rolling-release-v2.3.x) (docker: `savonet/liquidsoap:rolling-release-v2.3.x` | | `2.2.x` | [2.2.5](https://github.com/savonet/liquidsoap/releases/tag/v2.2.5) (docker: `savonet/liquidsoap:v2.2.5`) | ❌ | [2.2.x](https://github.com/savonet/liquidsoap/releases/tag/rolling-release-v2.2.x) (docker: `savonet/liquidsoap:rolling-release-v2.2.x` | | `2.1.x` | [2.1.4](https://github.com/savonet/liquidsoap/releases/tag/v2.1.4) (docker: `savonet/liquidsoap:v2.1.4`) | ❌ | [2.1.x](https://github.com/savonet/liquidsoap/releases/tag/rolling-release-v2.1.x) (docker: `savonet/liquidsoap:rolling-release-v2.1.x` | ### Versions Liquidsoap releases follow a semantic versioning as follows: ``` <major_version>.<minor_version>.<bugfix_version> ``` Where: - `major_version` is bumped when there are major changes, i.e. changes in the paradigm, major implementation change etc. Versions with different major versions **are** incompatible - `minor_version` is bumped when there are minor changes, i.e. new operators, renaming, new modules etc. Version with different minor versions **may be** incompatible - `bugfix_version` is bumped when a new bugfix version is published. Versions with only bugfix version changes **should be** compatible Please note that liquidsoap is a complex framework with a lot of operators and advanced implementations. For this reason, it is possible that a bugfix actually fixes the behavior of an operator the way it was intended to be and may break scripts that previously relied on incorrect implementations. Therefore, we **strongly** recommend maintaining a `staging` environment that makes it possible to test new versions before using them in production. In this context, the semantic versioning above should guide you in knowing how much scrutiny you should put into a new release before validating it in your staging environment. ### Assets Release assets are provided at: https://github.com/savonet/liquidsoap/releases. Published, versioned releases are available using their published tag, i.e. `vx.y.z`. We also provide **rolling releases**. A rolling release is a snapshot of a current, unpublished release. It can be a future stable release or a future bugfix release for a given major/minor version. For both types of releases, we reserve the right to update, delete and add assets to the release at any time. If you are looking for permanent links to release assets, you should grab them from https://github.com/savonet/liquidsoap-release-assets/releases, which reflects all our releases but whose artifacts are never modified/deleted. ## Tooling | | | | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | | Formatting | [liquidsoap-prettier](https://github.com/savonet/liquidsoap-prettier) | | VSCode | [vscode-liquidsoap](https://marketplace.visualstudio.com/items?itemName=savonet.vscode-liquidsoap) | | Tree Sitter | [tree-sitter-liquidsoap](https://github.com/savonet/tree-sitter-liquidsoap), [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter) | | CodeMirror | [codemirror-lang-liquidsoap](https://github.com/savonet/codemirror-lang-liquidsoap) | | Playground | [https://www.liquidsoap.info/try/](https://www.liquidsoap.info/try/) | ## Documentation HTML documentation is available on our [website](http://liquidsoap.info) We also have written _the Liquidsoap book_ which is [available online](http://www.liquidsoap.info/book/book.pdf) and in [physical version](https://www.amazon.com/dp/B095PVTYR3). ## Contributing Contributions are more than welcome: you can submit [issues](https://github.com/savonet/liquidsoap/issues) if you find some, or contribute to the code through [pull requests](https://github.com/savonet/liquidsoap/pulls). You can checkout the code with ```sh git checkout git@github.com:savonet/liquidsoap.git ``` Please see [our documentation page](https://www.liquidsoap.info/doc-dev/build.html) about how to build the code. ## License This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ## Authors - Developers: - [Romain Beauxis](https://github.com/toots) - [Samuel Mimram](http://www.mimram.fr) - Former project leader and emeritus developer: - [David Baelde](http://www.lsv.fr/~baelde/) - Contributors: - Florent Bouchez - Julien Cristau - Stéphane Gimenez - Clément Renard - Vincent Tabard - Sattisvar Tandabany �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/RELEASING��������������������������������������������������������������������������0000664�0000000�0000000�00000001455�14773033502�0015135�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������----------------------------- - How to release liquidsoap - ----------------------------- [] Run the CI. This should run the tests and prepare all the assets. [] Update the versioned dependencies in `liquidsoap.opam`, release pending dependent bindings (see below on how to publish to `opam`). [] Update copyright years in headers and check that all files have license headers. [] Fill-in CHANGES, with the release date. Opam packages ------------- Packages can be published to opam using `opam publish`. Make sure to check upstream for improvement made on the package files before sending the updates. `opam` versioning for version suffix is: `x.y.z~suffix`. It is not compatible with liquidsoap's `configure` based version detection so make sure to avoid depending on version-suffixed packages. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/ROADMAP.md�������������������������������������������������������������������������0000664�0000000�0000000�00000005140�14773033502�0015301�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������## Backlog - Explore new compiled backends - Update the book - Romain to document new internals - Write article for ICFP - support for ffmpeg subtitles - use OCaml 5 (after it has matured) - use native (as in native.liq) implementation of switch (based on source.dynamic) - reimplement video.tile in native liq - rework buffer.adaptative - use source getters for switch in order to be able to play two tracks ever day (#2880) ### Maybe TODO: - remove requests and use sources instead everywhere (a request is a source with one track [or more]) (weak maybe) - Precise scheduling with queue.push, etc.: we could make the track available at some precise time if requests were sources... - this may allow stuff like `append` more easily - Add support for modules, load minimal API by default - Simple mechanism to tell source how much data will be expected in advance (e.g. 10s with cross) to allow automatic buffer management. - Redefine switch-based transitions. ### Nice to have - refine video support in order to have next liquidshop running on Liquidsoap (dogfooding) - use row variables for methods, using Garrigue's _Simple Type Inference for Structural Polymorphism_ - can we reimplement something like [melt](https://www.mltframework.org/)? - support for WebRTC using WHIP / WHEP - support decorations on a subtitle image track - make bindings to pipewire to support webcams and screensharing ## For 2.2 ### Done - ~~Separate language core (#2397)~~ - ~~Online version (#2397)~~ - ~~Available at: https://www.liquidsoap.info/try/~~ - ~~Needs some cleanup, definition of a minimal JS library.~~ - ~~Switch to `dune`~~wh - ~~Separate standard library (in pure liq)~~ - ~~support for multi-track audio~~ - ~~live switch with ffmpeg encoded content~~ - ~~deprecate "!" and ":=" in favor of x.get / x.set~~ - ~~switch to immutable content for metadata~~ - ~~Add script tooling, prettier etc.~~ - ~~switch to immutable content for frames (#2364)~~ - ~~frame should be changed to extensible arrays (a bit like `Strings`) instead of filling a buffer~~ - ~~take the opportunity to change the handling of track boundaries (currently boundary = we have a partial fill, which has quite messy corner cases)~~ ## For 2.3 ### Done: - ~~Rewrite streaming loop~~ - ~~rewrite the clock system~~ - ~~the code is unreadable and overengineered ⇒ simplify it~~ - we want to get rid of the assumption clock = thread (Feasible but problem with OCaml 5) - ~~Optimize runtime: start time, typing and memory usage~~ - ~~javascrtipt/browser support using [WebCodecs](https://developer.mozilla.org/en-US/docs/Web/API/WebCodecs_API)!~~ ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/SECURITY.md������������������������������������������������������������������������0000664�0000000�0000000�00000002446�14773033502�0015473�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Security Policy ## Supported Versions Currently, we provide active support for the following versions of liquidsoap: | Version | Supported | | ------- | ------------------ | | `2.2.x` | :white_check_mark: | | `2.1.x` | :x: | | `2.0.x` | :x: | | `1.4.x` | :x: | | `< 1.4` | :x: | If you find a vulnerability in an unsupported version, please tell us. We will assess the risk and guide you accordingly. ## Reporting Security Issues At Liquidsoap, we take security seriously. We appreciate your efforts to responsibly disclose your findings, and we are committed to working with you to resolve any security issues. To report a security issue, please use the ["Report a Vulnerability"](https://github.com/savonet/liquidsoap/security/advisories/new) in the GitHub Security Advisory tab. ## Responsible Disclosure We kindly ask you not to disclose any vulnerabilities publicly until we have addressed them. We aim to promptly respond to your report and provide an estimated timeline for a fix. ## Thank You We appreciate your contributions to enhancing the security of our project. Your assistance in identifying and resolving vulnerabilities is priceless, and we are committed to maintaining a safe and secure environment for all our users. ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/�������������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14773033502�0014441�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/.gitignore���������������������������������������������������������������������0000664�0000000�0000000�00000000045�14773033502�0016430�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������_build language.dtd liquidsoap.1 pdf �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/Makefile�����������������������������������������������������������������������0000664�0000000�0000000�00000000607�14773033502�0016104�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������MD = $(wildcard content/*.md) all: dune @dune build @doc test: dune @for i in $(MD); do \ echo -n "Compiling $$i... "; \ pandoc $$i -t json | pandoc-include --directory "content/liq" | pandoc -f json --metadata title="bla" --template=template.html -o /tmp/`basename $$i .md`.html; \ echo "done"; \ done @dune build @doctest dune: @dune build @gendune --auto-promote .PHONY: dune �������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/�����������������������������������������������������������������������0000775�0000000�0000000�00000000000�14773033502�0016113�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/beets.md���������������������������������������������������������������0000664�0000000�0000000�00000013101�14773033502�0017533�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Integrating a music library: an example with Beets Liquidsoap's native sources can read from files and folders, but if your radio uses an important music library (more than a thousand tracks) sorting by folders may not be enough. You will also need to adjust the playout gain per track (ReplayGain). In that case you would better have a music library queried by Liquidsoap. In this section we'll do this with [Beets](http://beets.io/). Beets holds your music catalog, cleans tracks' tags before importing, can compute each track's ReplayGain, and most importantly has a command-line interface we can leverage from Liquidsoap. The following examples may also inspire you to integrate another library or your own scripts. After installing Beets, enable the `random` plug-in (see [Beets documentation on plug-ins](https://beets.readthedocs.io/en/stable/plugins/index.html#using-plugins)). To enable gain normalization, install and configure the [`replaygain`](https://beets.readthedocs.io/en/stable/plugins/replaygain.html) plug-in. To easily add single tracks to you library, you might also be interested in the [drop2beets](https://github.com/martinkirch/drop2beets#drop2beets) plug-in. The following examples suppose you defined a `BEET` constant, which contains the complete path to your `beet` executable (on UNIX systems, find it with `which beet`). For example: ``` BEET = "/home/radio/.local/bin/beet" ``` Before creating a Liquidsoap source, let's see why Beets queries are interesting for a radio. ## Beets queries Queries are parameters that you usually provide to the `beet ls` command : Beets will find matching tracks. The `random` plug-in works the same, except that it returns only one track matching the query (see [the plug-in's documentation](https://beets.readthedocs.io/en/stable/plugins/random.html)). Once your library is imported, you can try the following queries on the command line by typing `beet ls [query]` or `beet random [query]`. To test quickly, add the `-t 60` option to `beet random` so it will select an hour worth of tracks matching your query. Without selectors, queries search in a track’s title, artist, album name, album artist, genre and comments. Typing an artist name or a complete title usually match the exact track, and you could do a lovely playlist just by querying `love`. But in a radio you'll usually query on other fields. You can select tracks by genre with the `genre:` selector. Be careful that `genre:Rock` also matches `Indie Rock`, `Punk Rock`, etc. To select songs having english lyrics, use `language:eng`. Or pick 80s songs with `year:1980..1990`. Beets also holds internal meta-data, like `added`: the date and time when you imported each song. You can use it to query tracks inserted over the past month with `added:-1m..`. Or you can query track imported more than a year ago with `added:..-1y`. Beets also lets you [set your own tags](https://beets.readthedocs.io/en/stable/guides/advanced.html#store-any-data-you-like). You can use the `info` plug-in to see everything Beets knows about title(s) matching a query by typing `beet info -l [query]`. See also [the Beets' documentation](https://beets.readthedocs.io/en/stable/reference/query.html) for more details on queries operators. All these options should allow you to create both general and specialized Liquidsoap sources. ## A source querying each next track from Beets As of Liquidsoap 2.x we can create a function that creates a dynamic source, given its `id` and a Beet query. We rely on `request.dynamic` to call `beet random` (with `-f '$path'` option so beets only returns the matching track's path) every time the source must prepare a new track: ```{.liquidsoap include="beets-source.liq" from="BEGIN" to="END"} ``` Note that - `query` can be empty, it will match all tracks in the library. - we set `retry_delay` to a second, to avoid looping on `beet` calls if something goes wrong. - The final type hint (`:source`) will avoid false typing errors when the source is integrated in complex operators. ## Applying ReplayGain When the [`replaygain` plug-in](https://beets.readthedocs.io/en/stable/plugins/replaygain.html) is enabled, all tracks will have an additional metadata field called `replaygain_track_gain`. Check that Beet is configured to [write ID3 tags](https://beets.readthedocs.io/en/stable/reference/config.html#importer-options) so Liquidsoap will be able to read this metadata - your Beet configuration should include something like: ``` import: write: yes ``` Then we only need to add `amplify` to our source creation function. In the example below we also add `blank.eat`, to automatically cut silence at the beginning or end of tracks. ```{.liquidsoap include="beets-amplify.liq" from="BEGIN"} ``` This is the recommended Beets integration ; such source will provide music continuously, at a regular volume. ## Beets as a requests protocol If you're queueing tracks with `request.queue`, you may prefer to integrate Beets as a protocol. In that case, the list of paths returned by `beet random -f '$path'` fits directly what's needed by protocol resolution: ```{.liquidsoap include="beets-protocol.liq" from="BEGIN"} ``` Once this is done, you can push a beets query from [the telnet server](server.html): if you created `request.queue(id="userrequested")`, the server command `userrequested.push beets:All along the watchtower` will push the Jimi Hendrix's song. With this method, you can benefit from replay gain metadata too, by wrapping the recipient queue in an `amplify` operator, like ```liquidsoap userrequested = amplify(override="replaygain_track_gain", 1.0, request.queue(id="userrequested") ) ``` ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/blank.md���������������������������������������������������������������0000664�0000000�0000000�00000002657�14773033502�0017536�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Blank detection [Liquidsoap](index.html) has three operators for dealing with blanks. On GeekRadio, we play many files, some of which include bonus tracks, which means that they end with a very long blank and then a little extra music. It's annoying to get that on air. The `blank.skip` operator skips the current track when a too long blank is detected, which avoids that. The typical usage is simple: ```liquidsoap # Wrap it with a blank skipper s = blank.skip(s) ``` At [RadioPi](http://www.radiopi.org/) they have another problem: sometimes they have technical problems, and while they think they are doing a live show, they're making noise only in the studio, while only blank is on air; sometimes, the staff has so much fun (or is it something else ?) doing live shows that they leave at the end of the show without thinking to turn off the live, and the listeners get some silence again. To avoid that problem we made the `blank.strip` operators which hides the stream when it's too blank (i.e. declare it as unavailable), which perfectly suits the typical setup used for live shows: ```{.liquidsoap include="blank-sorry.liq"} ``` If you don't get the difference between these two operators, you should learn more about liquidsoap's notion of [source](sources.html). Finally, if you need to do some custom action when there's too much blank, we have `blank.detect`: ```{.liquidsoap include="blank-detect.liq" from="BEGIN" to="END"} ``` ���������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/book.md����������������������������������������������������������������0000664�0000000�0000000�00000001014�14773033502�0017363�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# The Liquidsoap book Together with the release of Liquidsoap 2.0, we have written _the Liquidsoap book_ which covers in details the language and the process of building a radio. It complements the online documentation by providing a homogeneous and progressive presentation of Liquidsoap. [![The Liquidsoap book](/assets/img/book.svg){height=600px}](https://www.amazon.com/dp/B095PVTYR3) It can be [ordered from Amazon](https://www.amazon.com/dp/B095PVTYR3) (or [read online](http://www.liquidsoap.info/book/book.pdf)). ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/build.md���������������������������������������������������������������0000664�0000000�0000000�00000013176�14773033502�0017544�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Building Liquidsoap ## Forewords Installing liquidsoap can be a difficult task. The software relies on an up-to date OCaml compiler, as well as a bunch of OCaml modules and, for most of them, corresponding C library dependencies. Our recommended way of installing liquidsoap is via [opam](http://opam.ocaml.org/). `opam` can take care of installing the correct OCaml compiler, optional and required dependencies as well as system-specific package dependencies. The `opam` method is described in details in the [documentation](doc/content/install.md). We recommend that any interested user head over to this link to install the software via `opam`. The remainder of this document describes how to compile liquidsoap locally for developers. ## Overview Liquidsoap is compiled using [dune](https://dune.readthedocs.io/en/stable/), which is the most popular OCaml build system at the moment. `dune` is tightly integrated with `opam` so, even if you are installing from source using `dune`, `opam` remains an important tool. Generally speaking, compiling from source may require the latest version of the liquidsoap code as well as its dependencies. Some of its dependencies are optional and can be ignored at first and some are not. Keep in mind that, although `opam` is generally aware of required minimal version for dependencies, `dune` is not. If a dependency is outdated, `dune` compilation will simply fail, at which point your may have to figure out if you need to update a dependency. Each branch of liquidsoap is compiled using [github actions](https://github.com/savonet/liquidsoap/actions). When trying to build a specific branch, if the CI passes with it then, most likely, you are missing a dependency, or it is not the latest version. ## `opam` pinning `opam` pinning is a mechanism to update `opam` with the latest version of a package, even before it is published to the official opam repository. This is the easiest way to update a dependency to its latest version. You can pin directly from a local git repository checkout: ```shell git clone https://github.com/savonet/ocaml-metadata.git cd ocaml-metadata opam pin -ny . ``` You can also pin directly using a git url: ```shell opam pin -ny git+https://github.com/savonet/ocaml-cry ``` See `opam pin --help` for more details about the available options. ## Dependencies The best way to figure out what dependencies are required or optional and their versions is to use the latest `opam` package. Since `liquidsoap` development is using `dune` and `opam`, the dependencies are kept in sync via the local liquidsoap opam package(s) and this serves as the de-facto list of dependencies and their versions. First, you should pin the latest liquidsoap code: ```shell opam pin -ny git+https://github.com/savonet/liquidsoap ``` Then, ask `opam` to list all the dependencies for `liquidsoap`: ```shell opam info liquidsoap opam info liquidsoap-lang ``` This should give you a (long!) list of all dependencies. Then, you can query `opam` to see what each dependency does. This is particularly useful for optional dependencies on `liquidsoap-core` which provide opt-in features. For instance `opam info soundtouch` will let you know that this package provides functions for changing pitch and timestretching audio data. Lastly, there are two types of dependencies: - Dependencies maintained by us - Dependencies not maintained by us For dependencies not maintained by us, most of the time, we rely on the latest published version. Very rarely should you have to fetch/pin the latest version of these dependencies. For dependencies maintained by us, we may break their API during our development cycle, and you maybe have to fetch/pin the latest version when compiling the latest `liquidsoap` code. You may also have to check out a specific branch when compiling `liquidsoap` from a specific development branch when the changes in the liquidsoap code are paired with changes in one of our dependencies. Typically, this happens a lof with the `ffmpeg` binding. ## Environment variables When compiling Liquidsoap from source, certain environment variables can be set to control the build process and customize the build configuration. Here’s a brief overview of the relevant environment variables and their purposes: - `IS_SNAPSHOT`: Set this variable to indicate whether you are building a snapshot version of Liquidsoap. It affects the version suffix and whether the Git commit is displayed. - `LIQ_GIT_SHA`: Override Git commit hash (SHA) if the build system cannot automatically extract it from the repository. - `LIQ_VERSION`: Override the displayed version of Liquidsoap. - `LIQUIDSOAP_ENABLE_BUILD_CONFIG`: Determines whether the build configuration details are displayed during the build process. - `LIQUIDSOAP_BUILD_TARGET`: Controls the runtime lookup paths for Liquidsoap components. - Set to `default`: Uses paths detected in the OPAM switch directory. - Set to `standalone`: Uses paths relative to the binary location, ideal for self-contained deployments. - Set to `posix`: Configures paths to standard system directories. ## Compiling Once you have all dependencies installed, you should be able to compile via: ```shell dune build ``` If an error occurs, you may need to see if you need to update a dependency. Hopefully, with a short iteration of this cycle, you will end up with a successful build! Once you have a successful build, you can also use the top-level `liquidsoap` script. This script builds the latest code and executes it right away. It works as if you were calling the `liquidsoap` binary after installing it: ```shell ./liquidsoap -h output.ao ``` From here, you can start changing code, testing script etc. Happy hacking! ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/clocks.md��������������������������������������������������������������0000664�0000000�0000000�00000031057�14773033502�0017721�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Clocks In the [quickstart](quick_start.html) and in the introduction to liquidsoap [sources](sources.html), we have described a simple world in which sources communicate with each other, creating and transforming data that composes multimedia streams. In this simple view, all sources produce data at the same rate, animated by a single clock: at every cycle of the clock, a fixed amount of data is produced. While this simple picture is useful to get a fair idea of what's going on in liquidsoap, the full picture is more complex: in fact, a streaming system might involve _multiple clocks_, or in other words several time flows. It is only in very particular cases that liquidsoap scripts need to mention clocks explicitly. Otherwise, you won't even notice how many clocks are involved in your setup: indeed, liquidsoap can figure out the clocks by itself, much like it infers types. Nevertheless, there will sometimes be cases where your script cannot be assigned clocks in a correct way, in which case liquidsoap will complain. For that reason, every user should eventually get a minimum understanding of clocks. In the following, we first describe why we need clocks. Then we go through the possible errors that any user might encounter regarding clocks. Finally, we describe how to explicitly use clocks, and show a few striking examples of what can be achieved that way. ## Why multiple clocks The first reason is **external** to liquidsoap: there is simply not a unique notion of time in the real world. Your computer has an internal clock which indicates a slightly different time than your watch or another computer's clock. Moreover, when communicating with a remote computer, network latency causes extra time distortions. Even within a single computer there are several clocks: notably, each soundcard has its own clock, which will tick at a slightly different rate than the main clock of the computer. Since liquidsoap communicates with soundcards and remote computers, it has to take those mismatches into account. There are also some reasons that are purely **internal** to liquidsoap: in order to produce a stream at a given speed, a source might need to obtain data from another source at a different rate. This is obvious for an operator that speeds up or slows down audio (`stretch`). But it also holds more subtly for `cross`, `cross` as well as the derived operators: during the lapse of time where the operator combines data from an end of track with the beginning of the other other, the crossing operator needs twice as much stream data. After ten tracks, with a crossing duration of six seconds, one more minute will have passed for the source compared to the time of the crossing operator. In order to avoid inconsistencies caused by time differences, while maintaining a simple and efficient execution model for its sources, liquidsoap works under the restriction that one source belongs to a unique clock, fixed once for all when the source is created. The graph representation of streaming systems can be adapted into a good representation of what clocks mean. One simply needs to add boxes representing clocks: a source can belong to only one box, and all sources of a box produce streams at the same rate. For example, ```liquidsoap output.icecast(fallback([crossfade(playlist(...)),jingles])) ``` yields the following graph: ![Graph representation with clocks](/assets/img/graph_clocks.png) Here, clock_2 was created specifically for the crossfading operator; the rate of that clock is controlled by that operator, which can hence accelerate it around track changes without any risk of inconsistency. The other clock is simply a CPU-based clock, so that the main stream is produced following the ``real'' time rate. ## Error messages Most of the time you won't have to do anything special about clocks: operators that have special requirements regarding clocks will do what's necessary themselves, and liquidsoap will check that everything is fine. But if the check fails, you'll need to understand the error, which is what this section is for. ### Disjoint clocks On the following example, liquidsoap will issue the fatal error `a source cannot belong to two clocks`: ```liquidsoap s = playlist("~/media/audio") output.alsa(s) # perhaps for monitoring output.icecast(mount="radio.ogg",%vorbis,crossfade(s)) ``` Here, the source `s` is first assigned the ALSA clock, because it is tied to an ALSA output. Then, we attempt to build a `crossfade` over `s`. But this operator requires its source to belong to a dedicated internal clock (because crossfading requires control over the flow of the of the source, to accelerate it around track changes). The error expresses this conflict: `s` must belong at the same time to the ALSA clock and `crossfade`'s clock. ### Nested clocks On the following example, liquidsoap will issue the fatal error `cannot unify two nested clocks`: ```liquidsoap jingles = playlist("jingles.lst") music = rotate([1,10],[jingles,playlist("remote.lst")]) safe = rotate([1,10],[jingles,single("local.ogg")]) q = fallback([crossfade(music),safe]) ``` Let's see what happened. The `rotate` operator, like most operators, operates within a single clock, which means that `jingles` and our two `playlist` instances must belong to the same clock. Similarly, `music` and `safe` must belong to that same clock. When we applied crossfading to `music`, the `crossfade` operator created its own internal clock, call it `cross_clock`, to signify that it needs the ability to accelerate at will the streaming of `music`. So, `music` is attached to `cross_clock`, and all sources built above come along. Finally, we build the fallback, which requires that all of its sources belong to the same clock. In other words, `crossfade(music)` must belong to `cross_clock` just like `safe`. The error message simply says that this is forbidden: the internal clock of our crossfade cannot be its external clock -- otherwise it would not have exclusive control over its internal flow of time. The same error also occurs on `add([crossfade(s),s])`, the simplest example of conflicting time flows, described above. However, you won't find yourself writing this obviously problematic piece of code. On the other hand, one would sometimes like to write things like our first example. The key to the error with our first example is that the same `jingles` source is used in combination with `music` and `safe`. As a result, liquidsoap sees a potentially nasty situation, which indeed could be turned into a real mess by adding just a little more complexity. To obtain the desired effect without requiring illegal clock assignments, it suffices to create two jingle sources, one for each clock: ```liquidsoap music = rotate([1,10],[playlist("jingles.lst"), playlist("remote.lst")]) safe = rotate([1,10],[playlist("jingles.lst"), single("local.ogg")]) q = fallback([crossfade(music),safe]) ``` There is no problem anymore: `music` belongs to `crossfade`'s internal clock, and `crossfade(music)`, `safe` and the `fallback` belong to another clock. ## The clock API There are only a couple of operations dealing explicitly with clocks. The function `clock.assign_new(l)` creates a new clock and assigns it to all sources from the list `l`. For convenience, we also provide a wrapper, `clock(s)` which does the same with a single source instead of a list, and returns that source. With both functions, the new clock will follow (the computer's idea of) real time, unless `sync=false` is passed, in which case it will run as fast as possible. The old (pre-1.0.0) setting `root.sync` is superseded by `clock.assign_new()`. If you want to run an output as fast as your CPU allows, just attach it to a new clock without synchronization: ```liquidsoap clock.assign_new(sync="none",[source]) ``` This will automatically attach the appropriate sources to that clock. Another important use case of this operator is if your script involves multiple sources from the same external clock, typically multiple ALSA input or output from the same sound card or multiple jack input and output. By default (the so-called `clock_safe` mode), liquidsoap will assign a dedicated clock to each of those sources, leading either to an error or forcing the use of an unnecessary `buffer` (see below). Instead, you can allocate each source with `clock_safe=false` and assign them a single clock: ``` s1 = input.jack(clock_safe=false, ...) s2 = input.jack(clock_safe=false, ...) clock.assign_new([s1,s2]) ``` However, you may need to do it for other operators if they are totally unrelated to the first one. The `buffer()` operator can be used to communicate between any two clocks: it takes a source in one clock and builds a source in another. The trick is that it uses a buffer: if one clock happens to run too fast or too slow, the buffer may empty or overflow. Finally, `get_clock_status` provides information on existing clocks and track their respective times: it returns a list containing for each clock a pair `(name,time)` indicating the clock id its current time in _clock cycles_ -- a cycle corresponds to the duration of a frame, which is given in ticks, displayed on startup in the logs. The helper function `log_clocks` built around `get_clock_status` can be used to directly obtain a simple log file, suitable for graphing with gnuplot. Those functions are useful to debug latency issues. ## External clocks: decoupling latencies The first reason to explicitly assign clocks is to precisely handle the various latencies that might occur in your setup. Most input/output operators (ALSA, AO, Jack, OSS, etc) require their own clocks. Indeed, their processing rate is constrained by external sound APIs or by the hardware itself. Sometimes, it is too much of an inconvenience, in which case one can set `clock_safe=false` to allow another clock assignment -- use at your own risk, as this might create bad latency interferences. Currently, `output.icecast` does not require to belong to any particular clock. This allows to stream according to the soundcard's internal clock, like in most other tools: in `output.icecast(%vorbis,mount="live.ogg",input.alsa())`, the ALSA clock will drive the streaming of the soundcard input via icecast. Sometimes, the external factors tied to Icecast output cannot be disregarded: the network may lag. If you stream a soundcard input to Icecast and the network lags, there will be a glitch in the soundcard input -- a long enough lag will cause a disconnection. This might be undesirable, and is certainly disappointing if you are recording a backup of your precious soundcard input using `output.file`: by default it will suffer the same latencies and glitches, while in theory it could be perfect. To fix this you can explicitly separate Icecast (high latency, low quality acceptable) from the backup and soundcard input (low latency, high quality wanted): ```liquidsoap input = input.oss() # Icecast source, with its own clock: icecast_source = mksafe(buffer(input)) clock.assign_new(id="icecast", [icecast_source]) # Output to icecast: output.icecast(%mp3,mount="blah",icecast_source) # File output: output.file( %mp3,{time.string("record-%Y-%m-%d-%H-%M-%S.mp3")}, input) ``` Here, the soundcard input and file output end up in the OSS clock. The icecast output goes to the explicitly created `"icecast"` clock, and a buffer is used to connect it to the soundcard input. Small network lags will be absorbed by the buffer. Important lags and possible disconnections will result in an overflow of the buffer. In any case, the OSS input and file output won't be affected by those latencies, and the recording should be perfect. The Icecast quality is also better with that setup, since small lags are absorbed by the buffer and do not create a glitch in the OSS capture, so that Icecast listeners won't notice the lag at all. ## Internal clocks: exploiting multiple cores Clocks can also be useful even when external factors are not an issue. Indeed, several clocks run in several threads, which creates an opportunity to exploit multiple CPU cores. The story is a bit complex because OCaml has some limitations on exploiting multiple cores, but in many situations most of the computing is done in C code (typically decoding and encoding) so it parallelizes quite well. Typically, if you run several outputs that do not share much (any) code, you can put each of them in a separate clock. For example the following script takes one file and encodes it as MP3 twice. You should run it as `liquidsoap EXPR -- FILE` and observe that it fully exploits two cores: ```liquidsoap def one() s = single(argv(1)) clock.assign_new(sync="none",[s]) output.file(%mp3,"/dev/null",s) end one() one() ``` ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/complete_case.md�������������������������������������������������������0000664�0000000�0000000�00000003573�14773033502�0021250�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# A complete case analysis We will develop here a more complex example, according to the following specifications: - play different playlists during the day; - play user requests -- done via the telnet server; - insert about 1 jingle every 5 songs; - add one special jingle at the beginning of every hour, mixed on top of the normal stream; - relay live shows as soon as one is available; - and set up several outputs. Once you've managed to describe what you want in such a modular way, you're half the way. More precisely, you should think of a diagram such as the following, through which the audio streams flow, following the arrows. The nodes can modify the stream using some basic operators: switching and mixing in our case. The final nodes, the ends of the paths, are outputs: they are in charge of pulling the data out of the graph and send it to the world. In our case, we only have outputs to icecast, using two different formats. ![Graph for 'radio.liq'](/assets/img/liqgraph.png) Now here is how to write that in [Liquidsoap](index.html). ```{.liquidsoap include="complete-case.liq"} ``` To try this example you need to edit the file names. In order to witness the switch from one playlist to another you can change the time intervals. If it is 16:42, try the intervals `0h-16h45` and `16h45-24h` instead of `6h-22h` and `22h-6h`. To witness the clock jingle, you can ask for it to be played every minute by using the `0s` interval instead of `0m0s`. To try the transition to a live show you need to start a new stream on the `live.ogg` mount of your server. You can send a playlist to it using examples from the [quickstart](quick_start.html). To start a real live show from soundcard input you can use `darkice`, or simply liquidsoap if you have a working ALSA input, with: ```liquidsoap liquidsoap 'output.icecast(%vorbis, \ mount="live.ogg",host="...",password="...",input.alsa())' ``` �������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/cookbook.md������������������������������������������������������������0000664�0000000�0000000�00000034360�14773033502�0020251�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Cookbook The recipes show how to build a source with a particular feature. You can try short snippets by wrapping the code in an `output(..)` operator and passing it directly to liquidsoap: ```liquidsoap liquidsoap -v 'output(recipe)' ``` For longer recipes, you might want to create a short script: ```liquidsoap #!/usr/bin/liquidsoap -v log.file.path := "/tmp/<script>.log" log.stdout := true recipe = # <fill this> output(recipe) ``` See the [quickstart guide](quick_start.html) for more information on how to run [Liquidsoap](index.html), on what is this `output(..)` operator, etc. See also the [ffmpeg cookbook](ffmpeg_cookbook.html) for examples specific to the ffmpeg support. ## Files A source which infinitely repeats the same URI: ```{.liquidsoap include="single.liq"} ``` A source which plays a playlist of requests -- a playlist is a file with an URI per line. ```{.liquidsoap include="playlists.liq" to="END"} ``` When building your stream, you'll often need to make it unfallible. Usually, you achieve that using a fallback switch (see below) with a branch made of a safe `single`. Roughly, a single is safe when it is given a valid local audio file. ## Transcoding [Liquidsoap](index.html) can achieve basic streaming tasks like transcoding with ease. You input any number of "source" streams using `input.http`, and then transcode them to any number of formats / bitrates / etc. The only limitation is your hardware: encoding and decoding are both heavy on CPU. If you want to get the best use of CPUs (multicore, memory footprint etc.) when encoding media with Liquidsoap, we recommend using the `%ffmpeg` encoders. ```{.liquidsoap include="transcoding.liq"} ``` ## Re-encoding a file As a simple example using a fallible output, we shall consider re-encoding a file. We start by building a source that plays our file only once. That source is obviously fallible. We pass it to a file output, which has to be in fallible mode. We also disable the `sync` parameter on the source's clock, to encode the file as quickly as possible. Finally, we use the `on_stop` handler to shutdown liquidsoap when streaming is finished. ```{.liquidsoap include="re-encode.liq"} ``` ## Generating CUE files When making backups of streams in audio files, it can be useful to generate CUE files, which store the times where the various tracks occur along with their metadata (those could then be used later on to split the file for instance). This can be achieved using the `source.cue` operator: ```{.liquidsoap include="source-cue.liq" from="BEGIN" to="END"} ``` which will generate a CUE file of the following form ``` TITLE "My stream" PERFORMER "The performer" FILE "backup.mp3" MP3 TRACK 01 AUDIO TITLE "Title 1" PERFORMER "Artist 1" INDEX 01 00:00:00 TRACK 02 AUDIO TITLE "Title 2" PERFORMER "Artist 2" INDEX 01 01:12:67 ``` ## RTMP server With our [FFmpeg support](ffmpeg.html), it is possible to create a simple RTMP server with no re-encoding: ```{.liquidsoap include="rtmp.liq"} ``` ## Transmitting signal It is possible to send raw PCM signal between two instances using the [FFmpeg encoder](ffmpeg.html). Here's an example using the SRT transport protocol: Sender: ```{.liquidsoap include="srt-sender.liq" from="BEGIN"} ``` Receiver: ```{.liquidsoap include="srt-receiver.liq" to="END"} ``` ## Scheduling ```{.liquidsoap include="fallback.liq" to="END"} ``` ```{.liquidsoap include="scheduling.liq" from="BEGIN" to="END"} ``` ## Generating playlists from a media library In order to store all the metadata of the files in a given directory and use those to generate playlists, you can use the `medialib` operator which takes as argument the directory to index. On first run, it will index all the files of the given folder, which can take some time (you are advised to use the `persistency` parameter in order to specify a file where metadata will be stored to avoid reindexing at each run). The resulting object can then be queried with the `find` method in order to return all files matching the given conditions and thus generate a playlist: ```{.liquidsoap include="medialib.liq"} ``` The parameter of the `find` method follow the following convention: - `artist="XXX"` looks for files where the artist tag is exactly the given one - `artist_contains="XXX"` looks for files where the artist tag contains the given string as substring - `artist_matches="XXX"` looks for files where the artist tag matches the given regular expression (for instance `artist_matches="(a)+.*(b)+"` looks for files where the artist contains an `a` followed by a `b`). The tags for which such parameters are provided are: `artist`, `title`, `album` and `filename` (feel free to ask if you need more). Some numeric tags are also supported: - `year=1999` looks for files where the year is exactly the given one - `year_ge=1999` looks for files where the year at least the given one - `year_lt=1999` looks for files where the year at most the given one The following numeric tags are supported: `bpm`, `year`. If multiple arguments are passed, the function finds files with tags matching the conjunction of the corresponding condition. Finally, if you need more exotic search functions, the argument `predicate` can be used. It takes as argument a _predicate_ which is a function taking the metadata of a file and returning whether the file should be selected. For instance, the following looks for files where the name of the artist is of length 5: ```{.liquidsoap include="medialib-predicate.liq" from="BEGIN" to="END"} ``` The default implementation of `medialib` uses standard Liquidsoap functions and can be pretty expensive in terms of memory. A more efficient implementation is available if you compiled with support for sqlite3 databases. In this case, you can use the `medialib.sqlite` operator as follows: ```{.liquidsoap include="medialib.sqlite.liq"} ``` (we also support more advanced uses of [databases](database.html)). ## Force a file/playlist to be played at least every XX minutes It can be useful to have a special playlist that is played at least every 20 minutes for instance (3 times per hour). You may think of a promotional playlist for instance. Here is the recipe: ```{.liquidsoap include="regular.liq" from="BEGIN" to="END"} ``` Where promotions is a source selecting the file to be promoted. ## Play a jingle at a fixed time Suppose that we have a playlist `jingles` of jingles and we want to play one within the 5 first minutes of every hour, without interrupting the current song. We can think of doing something like ```{.liquidsoap include="fixed-time1.liq" from="BEGIN" to="END"} ``` but the problem is that it is likely to play many jingles. In order to play exactly one jingle, we can use the function `predicate.activates` which detects when a predicate (here `{ 0m-5m }`) becomes true: ```{.liquidsoap include="fixed-time2.liq" from="BEGIN" to="END"} ``` ## Handle special events: mix or switch Add a jingle to your normal source at the beginning of every hour: ```{.liquidsoap include="jingle-hour.liq" from="BEGIN" to="END"} ``` Switch to a live show as soon as one is available. Make the show unavailable when it is silent, and skip tracks from the normal source if they contain too much silence. ```{.liquidsoap include="switch-show.liq" from="BEGIN" to="END"} ``` Without the `track_sensitive=false` the fallback would wait the end of a track to switch to the live. When using the blank detection operators, make sure to fine-tune their `threshold` and `length` (float) parameters. ## Unix interface, dynamic requests Liquidsoap can create a source that uses files provided by the result of the execution of any arbitrary function of your own. This is explained in the documentation for [request-based sources](request_sources.html). For instance, the following snippet defines a source which repeatedly plays the first valid URI in the playlist: ```{.liquidsoap include="request.dynamic.liq" to="END"} ``` Of course a more interesting behaviour is obtained with a more interesting program than `cat`, see [Beets](beet.html) for example. Another way of using an external program is to define a new protocol which uses it to resolve URIs. `protocol.add` takes a protocol name, a function to be used for resolving URIs using that protocol. The function will be given the URI parameter part and the time left for resolving -- though nothing really bad happens if you don't respect it. It usually passes the parameter to an external program ; it is another way to integrate [Beets](beet.html), for example: ```{.liquidsoap include="beets-protocol-short.liq"} ``` When resolving the URI `beets:David Bowie`, liquidsoap will call the function, which will call `beet random -f '$path' David Bowie` which will output the path to a David Bowie song. ## Dynamic input with harbor The operator `input.harbor` allows you to receive a source stream directly inside a running liquidsoap. It starts a listening server on where any Icecast2-compatible source client can connect. When a source is connected, its input if fed to the corresponding source in the script, which becomes available. This can be very useful to relay a live stream without polling the Icecast server for it. An example can be: ```{.liquidsoap include="harbor-dynamic.liq"} ``` This script, when launched, will start a local server, here bound to "0.0.0.0". This means that it will listen on any IP address available on the machine for a connection coming from any IP address. The server will wait for any source stream on mount point "/live" to login. Then if you start a source client and tell it to stream to your server, on port 8080, with password "hackme", the live source will become available and the radio will stream it immediately. ## Play a short silence when transitioning out of `input.harbor` If the live connection is unstable, for instance when streaming through a roaming phone device, it can be interesting to add an extra `5s` of silence when transitioning out of a live `input.harbor` to give the input some chance to reconnect. This can be done with the `append` operator: ```{.liquidsoap include="append-silence.liq" to="END"} ``` ## Dump a stream into segmented files It is sometimes useful (or even legally necessary) to keep a backup of an audio stream. Storing all the stream in one file can be very impractical. In order to save a file per hour in wav format, the following script can be used: ```{.liquidsoap include="dump-hourly.liq" from="BEGIN"} ``` Here, the function `time.string` generates the file name by replacing `%H` by the hour, etc. The fact that it is between curly brackets, i.e. `{time.string(...)}`, ensures that it is re-evaluated each time a new file is created, the changing the file name each time according to the current time. In the following variant we write a new mp3 file each time new metadata is coming from `s`: ```{.liquidsoap include="dump-hourly2.liq" from="BEGIN"} ``` In the two examples we use [string interpolation](language.html) and time literals to generate the output file name. In order to limit the disk space used by this archive, on unix systems we can regularly call `find` to cleanup the folder; if we can to keep 31 days of recording: ```{.liquidsoap include="archive-cleaner.liq"} ``` ## Transitions There are two kinds of transitions. Transitions between two different children of a switch or fallback and transitions between tracks of the same source. ### Switch-based transitions The switch-based operators (`switch`, `fallback` and `random`) support transitions. For every child, you can specify a transition function computing the output stream when moving from one child to another. This function is given two `source` parameters: the child which is about to be left, and the new selected child. The default transition is `fun (a,b) -> b`, it simply relays the new selected child source. One limitation of these transitions, however, is that if the transition happen right at the end of a track, which is the default with `track_sensitive=true`, then there is no more data available for the old source, which makes it impossible to fade it out. If that is what you are expecting, you should look at crossfade-based transitions ### Crossfade-based transitions Crossfade-based transitions are more complex and involve buffering source data in advance to be able to compute a transition where ending and starting track potentially overlap. This does not work with all type of sources since some of them, such as `input.http` may only receive data at real-time rate and cannot be accelerated to buffer their data or else we risk running out of data. We provide a default operator named `cross.simple` transition which may be suitable for most usage. But you can also create your own customized crossfade transitions. This is in particular true if you are expecting crossfade transitions between tracks of your `music` source but not between a `music` track and e.g. some jingles. Here's how to do it in this case: ```{.liquidsoap include="cross.custom.liq" from="BEGIN" to="END"} ``` ## Alsa output delay You can use [Liquidsoap](index.html) to capture and play through alsa with a minimal delay. This particularly useful when you want to run a live show from your computer. You can then directly capture and play audio through external speakers without delay for the DJ ! This configuration is not trivial since it relies on your hardware. Some hardware will allow both recording and playing at the same time, some only one at once, and some none at all.. Those note to configure are what works for us, we don't know if they'll fit all hardware. First launch liquidsoap as a one line program ``` liquidsoap -v --debug 'input.alsa()' ``` Unless you're lucky, the logs are full of lines like the following: ``` Could not set buffer size to 'frame.size' (1920 samples), got 2048. ``` The solution is then to set liquidsoap's internal frame size to this value, which is most likely specific to your hardware. Let's try this script: ```{.liquidsoap include="frame-size.liq"} ``` The setting will be acknowledged in the log as follows: ``` Targeting 'frame.audio.size': 2048 audio samples = 2048 ticks. ``` If everything goes right, you may hear on your output the captured sound without any delay! If you experience problems it might be a good idea to double the value of the frame size. This increases stability, but also latency. ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/crossfade.md�����������������������������������������������������������0000664�0000000�0000000�00000002524�14773033502�0020411�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Crossfade ## Out of the box Liquidsoap provides a default `crossfade` operator out of the box. It is a simple operator that does the work and does it well! Over the years, we have realized that crossfading is a very sensitive topic and that people care a lot about specific details and how well it is done. Since release `2.2.5`, liquidsoap integrates an automated mechanism to compute crossfade transitions that was contributed by our users. If you have the `ffmpeg` bindings enabled, all you should need to do to enable this feature is adding the following to your script: ```liquidsoap enable_autocue_metadata() ``` This uses the default, internal implementation. If you want more control over the automated crossfade parameters, you can check out the external [autocue](https://github.com/Moonbase59/autocue) implementation and its associated documentation. ## Custom crossfades You can also define your own crossfade transitions if you want to be more specific about them! The base `cross` operator accepts a scripted transition function that, according to the average volume level (in dB) computed on the end of the ending track and the beginning of the new one, returns the transition that is desired. You can find its documentation in the [language reference](reference.html). Here's an example: ```{.liquidsoap include="crossfade.liq"} ``` ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/custom-path.md���������������������������������������������������������0000664�0000000�0000000�00000004700�14773033502�0020702�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Basics Starting with version `1.0.1`, it is possible to build a liquidsoap binary that can load all its dependencies from any arbitrary path. This is very useful to distribute a liquidsoap bundled binary, independent of the distribution used. You can enable custom path at configure time, by passing the `--enable-custom-path` configuration option. A custom loading path is a directory that contains the following file/directories: - `./camomile`: Camomile shared data. They are usually located in `/usr/(local/)share/camomile` - `./libs`: pervasive scripts. Their are located in `liquidsoap/scripts` in liquidsoap's sources - `./log`: default log directories - `./magic`: directory for magic files. See below for more details. - `./plugins`: default plugins directory (most likely empty) - `./run`: default runtime files directory # Adding liquidsoap binary In order to ship a liquidsoap binary which is independent of the distribution it will be run on, one need to also include its dynamic libraries, except for the most common. The following command may be used to list them: ``` ldd ./liquidsoap | grep usr | cut -d' ' -f 3 ``` Those libraries are usually copied into a `./ld` directory. Then, the `LD_LIBRARY_PATH` is used to point the dynamic loader to this directory. Finally, the `liquidsoap` library is usually added in `./bin/liquidsoap` # Configuration variables In the following, configuration variables may refer to either absolute or relative paths. If referring to a relative path, the path is resolved relatively to the directory where the `liquidsoap` binary is located at. In order to tell liquidsoap where its custom path is located, you need to set the `LIQUIDSOAP_BASE_DIR`. Another important variable is `MAGIC`. It tells liquidsoap where to load the libmagic's definitions and defaults to `../magic/magic.mgc`. Older versions of libmagic may require to use `magic/magic.mime` instead. # Full example For a fully-functional example, you can check our [heroku buildpack](https://github.com/savonet/heroku-buildpack-liquidsoap). Its layout is: ``` ./bin ./bin/liquidsoap ./camomile ./camomile/charmaps (...) ./ld ./ld/libao.so.2 (...) ./libs ./libs/externals.liq (...) ./log ./magic ./magic/magic.mime ./plugins ./run ``` Its configuration variables are set to: ``` LD_LIBRARY_PATH=/path/to/ld LIQUIDSOAP_BASE_DIR=.. MAGIC=../magic/magic.mime ``` As you can see, we use an old version of `libmagic` so we need to load `magic.mime` instead of `magic.mgc`. ����������������������������������������������������������������liquidsoap-2.3.2/doc/content/database.md������������������������������������������������������������0000664�0000000�0000000�00000010565�14773033502�0020210�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Database support Liquidsoap supports SQL databases through the sqlite library. If you build Liquidsoap by yourself, you should install the [SQLite3-OCaml](https://github.com/mmottl/sqlite3-ocaml) library, e.g. with `opam install sqlite3`{.bash}. In order to create or open a database, you should use the `sqlite` function, which takes as argument the file where the database is stored and returns an object whose methods can be used to modify or query the database: ```{.liquidsoap include="sqlite.liq" from="open-begin" to="open-end"} ``` table in the database can then be created by calling the `table.create` method on the object with as arguments the table name (labeled by `table`) and the list of columns specified by pairs consisting of the column name, and its type. Setting the `preserve` argument to `true`{.liquidsoap} allows not creating the table if one already exists under this name. In our example, we want to use our database to store metadata for files so that we create a table named `"metadata"`{.liquidsoap} with columns corresponding to the artist, title, etc.: ```{.liquidsoap include="sqlite.liq" from="create-begin" to="create-end"} ``` Inserting a row is then performed using the `insert` method, which takes as argument the table and a record containing the data for the row: ```{.liquidsoap include="sqlite.liq" from="insert-begin" to="insert-end"} ``` Since the field `filename` is a primary key, it has to be unique (two rows cannot have the same file name), so that inserting two files with the same filename in the database will result in an error. If we want that the second insertion replace the first one, we can pass the `replace=true`{.liquidsoap} argument to the `insert` function. We can query the database with the `select` method. For instance, to obtain all the files whose year is posterior to 2000, we can write ```{.liquidsoap include="sqlite.liq" from="select-begin" to="select-end"} ``` In the case where you want to use strings in your queries, you should always use `sqlite.escape` to properly escape it and avoid injections: ```{.liquidsoap include="sqlite.liq" from="select2-begin" to="select2-end"} ``` The `select` function, returns a list of rows. To each row will correspond a list of pairs strings consisting of - a string: the name of the column, - a nullable string: its value (this is nullable because the contents of a column can be NULL in databases). We could thus extract the filenames from the above queries and use those in order to build a playlist as follows: ```{.liquidsoap include="sqlite.liq" from="play-begin" to="play-end"} ``` This can be read as follows: for each row (by `list.map`{.liquidsoap}), we convert the row to a list of pairs of strings as described above (by calling the `to_list`{.liquidsoap} method), we replace take the field labeled `"filename"`{.liquidsoap} (by `list.assoc`{.liquidsoap}) and take its value, assuming that it is not null (by `null.get`{.liquidsoap}). Since manipulating rows as lists of pairs of strings is not convenient, Liquidsoap offers the possibility to represent them as records with constructions of the form ```liquidsoap let sqlite.row (r : {a : string; b : int}) = row ``` which instructs to parse the row `row`{.liquidsoap} as a record `r` with fields `a` and `b` of respective types `string`{.liquidsoap} and `int`{.liquidsoap}. The above filename extraction is thus more conveniently written as ```{.liquidsoap include="sqlite.liq" from="play2-begin" to="play2-end"} ``` Other useful methods include - `count` to count the number of rows satisfying a condition ```{.liquidsoap include="sqlite.liq" from="count-begin" to="count-end"} ``` - `delete` to delete rows from a table ```{.liquidsoap include="sqlite.liq" from="play-begin" to="play-end"} ``` - `table.drop` to delete tables from the database ```{.liquidsoap include="sqlite.liq" from="drop-begin" to="drop-end"} ``` - `exec` to execute an arbitrary SQL query which does not return anything: ```{.liquidsoap include="sqlite.liq" from="exec-begin" to="exec-end"} ``` - `query` to execute an arbitrary SQL query returning rows ```{.liquidsoap include="sqlite.liq" from="query-begin" to="query-end"} ``` Finally, if your aim is to index file metadata, you might be interested in the `medialib.sqlite`{.liquidsoap} operator which is implemented in the standard library as described above (see the [cookbook](cookbook.html)). �������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/documentation.md�������������������������������������������������������0000664�0000000�0000000�00000011044�14773033502�0021306�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Documentation index **How to use**: Start with the [quickstart](quick_start.html) and make sure you learn [how to find help](help.html). Then it's as you like: go for another [general tutorial](#general), or a [specific example](#specific), pick a [basic notion](#core), or some examples from the [cookbook](cookbook.html). If you've understood all you need, just browse the [reference](reference.html) and compose your dream stream. If you downloaded a source tarball of liquidsoap, you may first read the [build instructions](build.html). If you are migrating from a previous version, you might want to checkout [this page](migrating.html). ## General tutorials - [The book](bool.html): The Liquidsoap book - [Video presentations](presentations.html): some presentations we did about liquidsoap - [How to find help](help.html) about operators, settings, server commands, etc. - [Frequently Asked Questions, Troubleshooting](faq.html) - [Quickstart](quick_start.html): where anyone should start. - [Complete case analysis](complete_case.html): an example that is not a toy. - [Cookbook](cookbook.html): contains lots of idiomatic examples. ## Reference - [Script language](language.html): A more detailed presentation. - [Core API](reference.html): The core liquidsoap API - [Extra API](reference-extra.html): Extra functions and libraries. - [Protocols](protocols.html): List of protocols supported by liquidsoap. - [Settings](settings.html): The list of available settings for liquidsoap. - [FFmpeg](ffmpeg.html): FFmpeg support documentation. - [Encoding formats](encoding_formats.html): The available formats for encoding outputs. - [Videos streams](video.html): Use `liquidsoap` for video streams - [JSON import/export](json.html): Importing and exporting language values in JSON. - [Playlist parsers](playlist_parsers.html): Supported playlist formats. - [LADSPA plugins](ladspa.html): Using LADSPA plugins. - [Database](database.html): Support for SQL databases. ## Core - Basic concepts: [sources](sources.html), [clocks](clocks.html) and [requests](requests.html). - [Stream contents](stream_content.html): what kind of streams are supported, and how. - [Script loading](script_loading.html): load several scripts, learn about the script library. - [Execution phases](phases.html) ## Specific tutorials - [Blank detection](blank.html) - [Customize metadata](metadata.html) - [Dynamic source creation](dynamic_sources.html): dynamically create sources using server requests. - [External decoders](external_decoders.html): use an external program for decoding audio files. - [External encoders](external_encoders.html): use an external audio encoding program. - [External streams](external_streams.html): use an external program for streaming audio data. - [HLS output](hls_output.html): output your stream as HTTP Live Stream. - [HTTP input](http_input.html): relay external streams. - [Harbor input](harbor.html): receive streams from icecast and shoutcast source clients. - [ICY metadata update](icy_metadata.html): manipulate and configure metadata update in Icecast. - [Interaction with the Harbor](harbor_http.html): interact with a running Liquidsoap using the Harbor server. - [Interaction with the server](server.html) interact with a running Liquidsoap instance using the telnet server. - [Normalization and replay gain](replay_gain.html): normalize audio data. - [Profiling](profiling.html): profiling your scripts. - [Prometheus reporting](prometheus.html): metrics reporting via prometheus. - [Requests-based sources](request_sources.html): create advanced sources using requests. - [Seek and cue support](seek.html): seek and set cue-in and cue-out points in sources. - [Shoutcast output](shoutcast.html): output to shoutcast. - [Smart crossfading](smartcrossfade.html): define custom crossfade transitions. - [Using in production](in_production.html): integrate liquidsoap scripts in a production environment. ## User scripts - [Beets](beets.html): an example of a music database integration. - [Geekradio](geekradio.html) - [RadioPi](radiopi.html) - [Frequence3](frequence3.html) - [Video with a single static image](video-static.html) - [Split a CUE sheet](split-cue.html) ## Code snippets - [Code example index](scripts/index.html) ## Behind the curtains - [Some presentations and publications](../publications.html) explaining the theory underlying Liquidsoap - [OCaml libraries](../modules.html) used in Liquidsoap, that can be reused in other projects - [Documentation of some internals](../modules/liquidsoap/index.html) of Liquidsoap - [Documentation for previous versions](../previously.html) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/dynamic_sources.md�����������������������������������������������������0000664�0000000�0000000�00000001772�14773033502�0021633�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������Liquidsoap supports dynamic creation and destruction of sources during the execution of a script. The following gives an example of this. First some outlines: - This example is meant to create a new source and outputs. It is not easy currently to change a source being streamed - The idea is to create a new output using a telnet/server command. - In order for a Liquidsoap script to run without an active source at startup, it is necessary to include `settings.init.force_start := true` at the start of the script. In this example, we will register a command that dynamically create a new output based on an encoded stream and output it to an arbitrary url, as supported by the ffmpeg copy encoder. This script can be used to create a dynamic restreaming platform. Here's the code: ```{.liquidsoap include="dynamic-source.liq"} ``` After executing this script, you should see two telnet commands: - `restream.start <uri>` - `restream.stop <uri>` which you can use to create/destroy dynamically your sources. ������liquidsoap-2.3.2/doc/content/encoding_formats.md����������������������������������������������������0000664�0000000�0000000�00000021772�14773033502�0021767�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Encoding formats Encoders are used to define formats into which raw sources should be encoded by an output. Syntax for encoder is: `%encoder(parameters...)` or, if you use default parameters, `%encoder`. Please note that not all encoding formats are available at all time. Most of them require optional dependencies. If a format is not available, you should see an error like this: ```shell Error 12: Unsupported encoder: %sine(). You must be missing an optional dependency. ``` In particular, due to limitations with static linking on windows, only the `%ffmpeg` encoder is available with our windows build. However, this encoder provides a lot of codecs and formats, and it is quite likely that it can provide what you need. ## Formats determine the stream content In most liquidsoap scripts, the encoding format determines what kind of data is streamed. The type of an encoding format depends on its parameter. For example, `%mp3` has type `format(audio=pcm(stereo))`. The type of an output like `output.icecast` or `output.file` is something like `(...,format('a),...,source('a))->source('a)`. This means that your source will have to have the same type as your format. For example if you write ```liquidsoap output.file(%mp3,"/tmp/foo.mp3",playlist("~/audio")) ``` then the playlist source will have to stream stereo audio. In the case of audio format, liquidsoap tries its best to convert the format whenever possible. For instance, in the above, liquidsoap will convert mono files from the playlist to stereo files by duplicating the single audio channel in a mono file. Likewise, if the encoder requires mono audio, it will compute the mean of a stereo files. # List of formats and their syntax All parameters are optional, and the parenthesis are not needed when no parameter is passed. In the following default values are shown. As a special case, the keywords `mono` and `stereo` can be used to indicate the number of channels (whether is is passed as an integer or a boolean). ## MP3 Mp3 encoder comes in 3 flavors: - `%mp3` or `%mp3.cbr`: Constant bitrate encoding - `%mp3.vbr`: Variable bitrate, quality-based encoding. - `%mp3.abr`: Average bitrate based encoding. Parameters common to each flavor are: - `stereo=true/false`, `mono=true/false`: Encode stereo or mono data (default: `stereo`). - `stereo_mode`: One of: `"stereo"`, `"joint_stereo"` or `"default"` (default: `"default"`). Default means that the underlying library (`libmp3lame`) will pick the stereo mode based on compression ration and input channels. - `samplerate=44100`: Encoded data samplerate (default: `44100`) - `internal_quality=2`: Lame algorithms internal quality. A value between `0` and `9`, `0` being highest quality and `9` the worst (default: `2`). - `id3v2=true`: Add an `id3v2` tag to encoded data (default: `false`). Parameters for `%mp3` are: - `bitrate`: Encoded data fixed bitrate Parameters for `%mp3.vbr` are: - `quality`: Quality of encoded data; ranges from `0` (highest quality) to `9` (worst quality). Parameters for `%mp3.abr` are: - `bitrate`: Average bitrate - `min_bitrate`: Minimum bitrate - `max_bitrate`: Maximum bitrate - `hard_min`: Enforce minimal bitrate Examples: - Constant `128` kbps bitrate encoding: `%mp3(bitrate=128)` - Variable bitrate with quality `6` and samplerate of `22050` Hz: `%mp3.vbr(quality=7,samplerate=22050)` - Average bitrate with mean of `128` kbps, maximum bitrate `192` kbps and `id3v2` tags: `%mp3.abr(bitrate=128,max_bitrate=192,id3v2=true)` Optionally, liquidsoap can insert a message within mp3 data. You can set its value using the `msg` parameter. Setting it to `""` disables this feature. This is its default value. ## Shine Shine is the fixed-point mp3 encoder. It is useful on architectures without a FPU, such as ARM. It is named `%shine` or `%mp3.fxp` and its parameters are: ```liquidsoap %shine(channels=2,samplerate=44100,bitrate=128) ``` ## WAV ```liquidsoap %wav(stereo=true, channels=2, samplesize=16, header=true, duration=10.) ``` If `header` is `false`, the encoder outputs raw PCM. `duration` is optional and is used to set the WAV length header. Because Liquidsoap encodes a possibly infinite stream, there is no way to know in advance the duration of encoded data. Since WAV header has to be written first, by default its length is set to the maximum possible value. If you know the expected duration of the encoded data and you actually care about the WAV length header then you should use this parameter. ## FFmpeg See detailed [ffmpeg encoders](ffmpeg_encoder.html) article. ## Ogg The following formats can be put together in an Ogg container. The syntax for doing so is `%ogg(x,y,z)` but it is also possible to just write `%vorbis(...)`, for example, instead of `%ogg(%vorbis(...))`. All ogg encoders have a `bytes_per_page` parameter, which can be used to try to limit ogg logical pages size. For instance: ```liquidsoap # Try to limit vorbis pages size to 1024 bytes %vorbis(bytes_per_page=1024) ``` ### Vorbis ```liquidsoap # Variable bitrate %vorbis(samplerate=44100, channels=2, quality=0.3) % Average bitrate %vorbis.abr(samplerate=44100, channels=2, bitrate=128, max_bitrate=192, min_bitrate=64) # Constant bitrate %vorbis.cbr(samplerate=44100, channels=2, bitrate=128) ``` Quality ranges from -0.2 to 1, but quality -0.2 is only available with the aotuv implementation of libvorbis. ### Opus Opus is a lossy audio compression made especially suitable for interactive real-time applications over the Internet. Liquidsoap supports Opus data encapsulated into Ogg streams. The encoder is named `%opus` and its parameters are as follows. Please refer to the [Opus documentation](http://www.opus-codec.org/docs/) for information about their meanings and values. - `vbr`: one of `"none"`, `"constrained"` or `"unconstrained"` - `application`: One of `"audio"`, `"voip"` or `"restricted_lowdelay"` - `complexity`: Integer value between `0` and `10`. - `max_bandwidth`: One of `"narrow_band"`, `"medium_band"`, `"wide_band"`, `"super_wide_band"` or `"full_band"` - `samplerate`: input samplerate. Must be one of: `8000`, `12000`, `16000`, `24000` or `48000` - `frame_size`: encoding frame size, in milliseconds. Must be one of: `2.5`, `5.`, `10.`, `20.`, `40.` or `60.`. - `bitrate`: encoding bitrate, in `kbps`. Must be a value between `5` and `512`. You can also set it to `"auto"`. - `channels`: currently, only `1` or `2` channels are allowed. - `mono`, `stereo`: equivalent to `channels=1` and `channels=2`. - `signal`: one of `"voice"` or `"music"` ### Theora ```liquidsoap %theora(quality=40,width=640,height=480, picture_width=255,picture_height=255, picture_x=0, picture_y=0, aspect_numerator=1, aspect_denominator=1, keyframe_frequency=64, vp3_compatible=false, soft_target=false, buffer_delay=5, speed=0) ``` You can also pass `bitrate=x` explicitly instead of a quality. The default dimensions are liquidsoap's default, from the settings `frame.video.height/width`. ### Speex ```liquidsoap %speex(stereo=false, samplerate=44100, quality=7, mode=wideband, # One of: wideband|narrowband|ultra-wideband frames_per_packet=1, complexity=5) ``` You can also control quality using `abr=x` or `vbr=y`. ### Flac The flac encoding format comes in two flavors: - `%flac` is the native flac format, useful for file output but not for streaming purpose - `%ogg(%flac,...)` is the ogg/flac format, which can be used to broadcast data with icecast The parameters are: ```liquidsoap %flac(samplerate=44100, channels=2, compression=5, bits_per_sample=16) ``` `compression` ranges from 0 to 8 and `bits_per_sample` should be one of: `8`, `16`, `24` or `32`. Please note that `32` bits per sample is currently not supported by the underlying `libflac`. ## FDK-AAC This encoder can do both AAC and AAC+. Its syntax is: ```liquidsoap %fdkaac(channels=2, samplerate=44100, bandwidth="auto", bitrate=64, afterburner=false, aot="mpeg2_he_aac_v2", transmux="adts", sbr_mode=false) ``` Where `aot` is one of: `"mpeg4_aac_lc"`, `"mpeg4_he_aac"`, `"mpeg4_he_aac_v2"`, `"mpeg4_aac_ld"`, `"mpeg4_aac_eld"`, `"mpeg2_aac_lc"`, `"mpeg2_he_aac"` or `"mpeg2_he_aac_v2"` `bandwidth` is one of: `"auto"`, any supported integer value. `transmux` is one of: `"raw"`, `"adif"`, `"adts"`, `"latm"`, `"latm_out_of_band"` or `"loas"`. Bitrate can be either constant by passing: `bitrate=64` or variable: `vbr=<1-5>` You can consult the [Hydrogenaudio knowledge base](http://wiki.hydrogenaud.io/index.php?title=Fraunhofer_FDK_AAC) for more details on configuration values and meanings. ## External encoders For a detailed presentation of external encoders, see [this page](external_encoders.html). ```liquidsoap %external(channels=2,samplerate=44100,header=true, restart_on_crash=false, restart_on_metadata, restart_after_delay=30, process="progname") ``` Only one of `restart_on_metadata` and `restart_after_delay` should be passed. The delay is specified in seconds. The encoding process is mandatory, and can also be passed directly as a string, without `process=`. ������liquidsoap-2.3.2/doc/content/external_decoders.md���������������������������������������������������0000664�0000000�0000000�00000010217�14773033502�0022130�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������## Introduction You can use external programs in liquidsoap to decode audio files. The program must be able to output WAV data to its standard output (`stdout`) and, possibly, read encoded data from its standard input. Please note that this feature is not available under Windows. ## Basic operators External decoders are registered using the `decoder.add` and `decoder.oblivious.add` operators. They are invoked the following way: ### `decoder.add` ```liquidsoap decoder.add(name="my_decoder",description="My custom decoder", test,decoder) ``` `decoder.add` is used for external decoders that can read the encoded data from their standard input (stdin) and write the decoded data as WAV to their standard output (stdout). This operator is recommended because its estimation of the remaining time is better than the estimation done by the decoders registered using `decoder.oblivious.add`. The important parameters are: - `test` is a function used to determine if the file should be decoded by the decoder. Returned values are: - `0`: no decodable audio, - `-1`: decodable audio but number of audio channels unknown, - `x`: fixed number of decodable audio channels. - `decoder` is the string containing the shell command to run to execute the decoding process. ### `decoder.oblivious.add` `decoder.oblivious.add` is very similar to `decoder.add`. The main difference is that the decoding program reads encoded data directly from the local files and not its standard input. Decoders registered using this operator do not have a reliable estimation of the remaining time. You should use `decoder.oblivious.add` only if your decoding program is not able to read the encoded data from its standard input. ```liquidsoap decoder.oblivious.add(name="my_decoder",description="My custom decoder", buffer=5., test,decoder) ``` `decoder.add` is used for external decoders that can read the encoded data from their standard input (stdin) and write the decoded data as WAV to their standard output (stdout). This operator is recommended because its estimation of the remaining time is better than the estimation done by the decoders registered using `decoder.oblivious.add`. The important parameters are: - `test` is a function used to determine if the file should be decoded by the decoder. Returned values are: - `0`: no decodable audio, - `-1`: decodable audio but number of audio channels unknown, - `x`: fixed number of decodable audio channels. - `decoder` is a function that receives the name of the file that should be decoded and returns a string containing the shell command to run to execute the decoding process. ### `decoder.metadata.add` You may also register new metadata resolvers using the `decoder.metadata.add` operator. It is invoked the following way: `decoder.metadata.add(format,resolver)`, where: - `format` is the name of the resolved format. It is only informative. - `resolver` is a function `f` that returns a list of metadata of the form: `(label, value)`. It is invoked the following way: `f(format=name,file)`, where: - `format` contains the name of the format, as returned by the decoder that accepted to decode the file. `f` may return immediately if this is not an expected value. - `file` is the name of the file to decode. ## Wrappers On top of the basic operators, wrappers have been written for some common decoders. This includes the `flac` and `faad` decoders, by default. All the operators are defined in `externals.liq`. ### The FLAC decoder The flac decoder uses the `flac` command line. It is enabled if the binary can be found in the current `$PATH`. Its code is the following: ```{.liquidsoap include="decoder-flac.liq"} ``` Additionally, a metadata resolver is registered when the `metaflac` command can be found in the `$PATH`: ```{.liquidsoap include="decoder-metaflac.liq"} ``` ### The faad decoder The faad decoder uses the `faad` program, if found in the `$PATH`. It can decode AAC and AAC+ audio files. This program does not support reading encoded data from its standard input so the decoder is registered using `decoder.oblivious.add`. Its code is the following: ```{.liquidsoap include="decoder-faad.liq"} ``` ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/external_encoders.md���������������������������������������������������0000664�0000000�0000000�00000006547�14773033502�0022155�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������## Introduction You can use any external program that accepts wav or raw PCM data to encode audio data and use the resulting compressed stream as an output, either to a file, a pipe, or even icecast. When using an external encoding process, uncompressed PCM data will be sent to the process through its standard input (`stdin`), and encoded data will be read through its standard output (`stdout`). When using a process that does only file input or output, `/dev/stdin` and `/dev/stdout` can be used, though this may generate issues if the encoding process expects to be able to go backward/forward in the file. ## External encoders The main operators that can be used with external encoders are: - `output.file` - `output.icecast` In order to use external encoders with these operators, you have to use the `%external` [encoding format](encoding_formats.html). Its syntax is: ```liquidsoap %external(channels=2,samplerate=44100,header=true, restart_on_crash=false, restart_on_metadata, restart_after_delay=30, process="progname") ``` The available options are: - `process`: this parameter is a function that takes the current metadata and return the process to start. - `header`: if set to `false` then no WAV header will be added to the data fed to the encoding process, thus the encoding process shall operate on RAW data. - `restart_on_crash`: whether to restart the encoding process if it crashed. Useful when the external process fails to encode properly data after some time. - `restart_on_metadata`: restart encoding process on each new metadata. Useful in conjunction with the `process` parameter for audio formats that need a new header, possibly with metadatas, for each new track. This is the case for the ogg container. - `restart_encoder_delay`: Restart the encoder after some delay. This can be useful for encoders that cannot operate on infinite streams, or are buggy after some time, like the `lame` binary. The default for `lame` and `accplusenc`-based encoders is to restart the encoder every hour. Only one of `restart_encoder_delay` or `restart_on_new_track` should be used. The restart mechanism strongly relies on the good behaviour of the encoding process. The restart operation will close the standard input of the encoding process. The encoding process is then expected to finish its own operations and close its standard output. If it does not close its standard output, the encoding task will not finish. If your encoding process has this issue, you should turn the `restart_on_crash` option to `true` and kill the encoding process yourself. If you use an external encoder with the `output.icecast` operator, you should also use the following options of `output.icecast`: - `icy_metadata`: send new metadata as ICY update. This is the case for headerless formats, such as MP3 or AAC, and it appears to work also for ogg/vorbis streams. - `format`: Content-type (mime) of the data sent to icecast. For instance, for ogg data, it is one of `"application/ogg"`, `"audio/ogg"` or `"video/ogg"` and for mp3 data it is `"audio/mpeg"`. ## Video support Videos can also be encoded by programs able to read files in avi format from standard input. To use it, the flag `video=true` of `%external` should be used. For instance, a compressed avi file can be generated with `ffmpeg` using ```{.liquidsoap include="external-output.file.liq" from="BEGIN"} ``` ���������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/external_streams.md����������������������������������������������������0000664�0000000�0000000�00000002460�14773033502�0022017�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������## Introduction You can use an external program to create a source that will read data coming out of the standard output (`stdout`) of this program. Contrary to the external file decoders, data will be buffered and played when a sufficient amount was accumulated. The program should output data in signed 16 bits little endian PCM (s16le). Number of channels and samplerate can be specified. There is no need of any wav header in the data, though it should work too. ## Basic operator The basic operators for creating an external stream are `input.external.wav`, `input.external.rawaudio`, `input.external.avi` and `input.external.rawvideo` (depending on the format of the data produced by the external program). The parameters for the two first are - `buffer`: Duration of the pre-buffered data. - `max`: Maximum duration of the buffered data. - `channels`: Number of channels. - `samplerate`: Sample rate. - `restart`: Restart the process when it has exited normally. - `restart_on_error`: Restart the process when it has exited with error. The last parameter is unlabeled. It is a string containing the command that will be executed to run the external program. ## Wrappers A wrapper, `input.mplayer`, is defined to use mplayer as the external decoder. Its code is: ```{.liquidsoap include="input.mplayer.liq"} ``` ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/faq.md�����������������������������������������������������������������0000664�0000000�0000000�00000020755�14773033502�0017215�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Frequently Asked Questions ## What does this message means? ### Type error Liquidsoap might also reject a script with a series of errors of the form ` this value has type ... but it should be a subtype of ...` . Usually the last error tells you what the problem is, but the previous errors might provide a better information as to where the error comes from. For example, the error might indicate that a value of type `int` has been passed where a float was expected, in which case you should use a conversion, or more likely change an integer value such as `13` into a float `13.`. A type error can also show that you're trying to use a source of a certain content type (e.g., audio) in a place where another content type (e.g., pure video) is required. In that case the last error in the list is not the most useful one, but you will read something like this above: ``` At ...: Error 5: this value has type source(video=canvas(_),...) but it should be a subtype of source(audio=pcm(_),...) ``` Sometimes, the type error actually indicates a mistake in the order or labels of arguments. For example, given `output.icecast(mount="foo.ogg",source)` liquidsoap will complain that the second argument is a source (`source(?A)`) but should be a format (`format(?A)`): indeed, the first unlabelled argument is expected to be the encoding format, e.g., `%vorbis`, and the source comes only second. Finally, a type error can indicate that you have forgotten to pass a mandatory parameter to some function. For example, on the code `fallback([source.mux.audio(x),...])`, liquidsoap will complain as follows: ``` At line ...: Error 5: this value has type [(?id : _, audio : _) -> _] but it should be a subtype of the type of the value at ../libs/switches.liq, line 11, char 11-18 [source(_)] (inferred at ../libs/list.liq, line 102, char 29) ``` Indeed, `fallback` expects a source, but `source.mux.audio(x)` is still a function expecting the `audio` parameter. ### That source is fallible! See the [quickstart](quick_start.html), or read more about [sources](sources.html). ### Clock error Read about [clocks](clocks.html) for the errors `a source cannot belong to two clocks` and `cannot unify two nested clocks`. ### We must catchup x.xx! This error means that a clock is getting late in liquidsoap. This can be caused by an overloaded CPU, if your script is doing too much encoding or processing: in that case, you should reduce the load on your machine or simplify your liquidsoap script. The latency may also be caused by some lag, for example a network lag will cause the icecast output to hang, making the clock late. The first kind of latency is problematic because it tends to accumulate, eventually leading to the restarting of outputs: ``` Too much latency! Resetting active source... ``` The second kind of latency can often be ignored: if you are streaming to an icecast server, there are several buffers between you and your listeners which make this problem invisible to them. But in more realtime applications, even small lags will result in glitches. In some situations, it is possible to isolate some parts of a script from the latency caused by other parts. For example, it is possible to produce a clean script and back it up into a file, independently of its output to icecast (which again is sensitive to network lags). For more details on those techniques, read about [clocks](clocks.html). ### Unable to decode ``file'' as {audio=pcm}! This log message informs you that liquidsoap failed to decode a file, not necessarily because it cannot handle the file, but also possibly because the file does not contain the expected media type. For example, if audio and video is expected, an audio file with no video will be rejected. Liquidsoap is also able to convert audio channels in most situations. Typically, if stereo data is expected but the file contains mono audio, liquidsoap will use the single audio channel as both left and right channels. ### Runtime exceptions Liquidsoap scripts can raise runtime errors of the form: ``` At line 3, char 45: Error 14: Uncaught runtime error: type: not_found, message: "File not found!" ``` These are errors that the script programmer can catch and decide what to do when they occur. Such errors will typically occur when trying to read a file that does not exist and etc. The [language page](language.html) has more details about errors, how to raise them and how to catch them. You can head over there to get more information. ### Crashes Liquidsoap dies with messages such as these by the end of the log: ``` ... [threads:1] Thread "XXX" aborts with exception YYY! ... [stderr:3] Thread 2 killed on uncaught exception YYY. ... [stderr:3] Raised at file ..., line ..., etc. ``` Those internal errors can be of two sorts: - **Bug**: Normally, this means that you've found a bug, which you should report on the mailing list or bug tracker. - **User error**: In some cases, we let an exception go on user errors, instead of nicely reporting and handling it. By looking at the surrounding log messages, you might realize that liquidsoap crashed for a good reason, that you are responsible for fixing. You can still report a bug: you should not have seen an exception and its backtrace. In any case, once that kind of error happens, there is no way for the user to prevent liquidsoap from crashing. Those exceptions cannot be caught or handled in any way at the level of liquidsoap scripts. ## Troubleshooting ### Pulseaudio When using ALSA input or output or, more generally any audio input or output that is not using pulseaudio, you should disable pulseaudio, which is often installed by default. Pulseaudio emulates ALSA but this also generates bugs, in particular errors of this form: ``` Alsa.Unknown_error(1073697252)! ``` There are two things you may do: - Make sure your alsa input/output does not use pulseaudio - Disable pulseaudio on your system In the first case, you should first find out which sound card you want to use, with the command `aplay -l`. An example of its output is: ``` **** List of PLAYBACK Hardware Devices **** card 0: Intel [HDA Intel], device 0: STAC92xx Analog [STAC92xx Analog] Subdevices: 1/1 Subdevice #0: subdevice #0 ``` In this case, the card we want to use is: device `0`, subdevice `0`, thus: `hw:0,0`. We now create a file `/etc/asound.conf` (or `~/.asoundrc` for single-user configuration) that contains the following: ```liquidsoap pcm.liquidsoap { type plug slave { pcm "hw:0,0" } } ``` This creates a new alsa device that you can use with liquidsoap. The `plug` operator in ALSA is used to work-around any hardware limitations in your device (mixing multiple outputs, resampling etc.). In some cases you may need to read more about ALSA and define your own PCM device. Once you have created this device, you can use it in liquidsoap as follows: ```liquidsoap input.alsa(device="pcm.liquidsoap", ...) ``` In the second case -- disabling pulseaudio, you can edit the file `/etc/pulse/client.conf` and change or add this line: ``` autospawn = no ``` And kill any running pulseaudio process: ``` killall pulseaudio ``` Otherwise you may simply remove pulseaudio's packages, if you use Debian or Ubuntu: ``` apt-get remove pulseaudio libasound2-plugins ``` ### Listeners are disconnected at the end of every track Several media players, including renowned ones, do not properly support Ogg/Vorbis streams: they treat the end of a track as an end of file, resulting in the disconnection. Players that are affected by this problem include VLC. Players that are not affected include ogg123, liquidsoap. One way to work around this problem is to not use Ogg/Vorbis (which we do not recommend) or to not produce tracks within a Vorbis stream. This is done by dropping both metadata and track marks (for example using `source.drop.metadata_track_marks`). ### Encoding blank Encoding pure silence is often too effective for streaming: data is so compressed that there is nothing to send to listeners, whose clients eventually disconnect. Therefore, it is a good idea to use a non-silent jingle instead of `blank()` to fill in the blank. You can also achieve various effects using synthesis sources such as `noise()`, `sine()`, etc. ### Temporary files Liquidsoap relies on OCaml's `Filename.tmp_dir_name` variable to store temporary files. It is documented as follows: The name of the temporary directory: Under Unix, the value of the `TMPDIR` environment variable, or `"/tmp"` if the variable is not set. Under Windows, the value of the `TEMP` environment variable, or `"."` if the variable is not set. �������������������liquidsoap-2.3.2/doc/content/ffmpeg.md��������������������������������������������������������������0000664�0000000�0000000�00000026210�14773033502�0017702�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# FFmpeg Support Since the `2.0.x` release cycle, liquidsoap integrates a tight support of ffmpeg. This includes: - [Decoders](#decoders) - [Encoders](#encoders) - [Filters](#filters) - [Bitstream filters](#bitstream-filters) - [Encoded data tweaks](#encoded-data-tweaks) - [Examples](#examples) Ffmpeg support includes 3 types of content: - **Internal content**, that is content available to all liquidsoap operators: `PCM` audio and `YUV420p` video - **Raw content**, that is decoded content but stored as ffmpeg internal frame. This type of content is only available to ffmpeg filters and raw encoders. It can be used to avoid data copies back and forth between liquidsoap and ffmpeg. - **Copy content**, that is encoded content stored as ffmpeg internal packets. This type of content is only available to ffmpeg copy encoder and bitstream filters and requires a fairly good understanding of media codecs and containers. Copy contents can be used to avoid transcoding and pass encoded data end-to-end inside liquidsoap scripts. ## Enabling ffmpeg support FFmpeg support is available via the external [ocaml-ffmpeg](https://github.com/savonet/ocaml-ffmpeg) binding package. If you are using any binary asset from our release pages or via docker, this should already be included. If you are installing via [opam](https://opam.ocaml.org/), installing the `ffmpeg` package should do the trick: ```sh % opam install ffmpeg ``` ### fdk-aac support in ffmpeg One common question is how to install `ffmpeg` with `fdk-aac` support. This can get tricky because you need the _ffmpeg shared libraries_ compiled with `libfdk-aac`. This means that installing `libfdk-aac` alone will not be enough, you might also need to recompile `ffmpeg` to take advantage of it. When recompiling `ffmpeg`, make sure that the `--enable-shared` argument is passed to the `configure` script. Also, compiling the shared libraries is different than downloading the `ffmpeg` command line. Most `ffmpeg` downloads include a _static build_ of ffmpeg that is, one that does not use or provide shared libraries. On linux platforms, you can check what dynamic libraries liquidsoap is using using ```shell ldd /path/to/liquidsopap ``` On macos, you can use `otool -L`. In the list of libraries, you should see `libavcodec`. In turn, you should be able to use the same command to inspect the libraries required by the `libavcodec` used by the `liquidsoap` binary. If this includes `libfdk-aac`, you're good to go! On debian, you might be able to use [deb-multimedia.org](https://www.deb-multimedia.org/) to install a build of `ffmpeg` with `libfdk-aac` enabled. You are advised to follow the instructions on the website for the latest up-to date guide. You may also refer to [this conversation](https://github.com/savonet/liquidsoap/discussions/3027#discussioncomment-6072338). ## Decoders For the most part, you should never have to worry about the `ffmpeg` decoder. When enabled, it should be the preferred decoder for all supported media. When using raw or copied content, the decoder is able to produce the required content without the need of any intervention on the user part. Should you need to tweak it, here are a couple of pointers: The `settings.decoder.decoders` settings controls which decoders are to be used when trying to decode media files. You can use it to restrict which decoders are being used, for instance making sure only the ffmpeg decoder is used: ```liquidsoap settings.decoder.decoders := ["FFMPEG"] ``` Priority for the decoder is set via: ```liquidsoap settings.decoder.priorities.ffmpeg := 10 ``` You can use this setting to adjust whether or not the ffmpeg decoder should be tried first when decoding media files, in particular in conjunction with the other `settings.decoder.priorities.*` settings. For each type of media codec, the `settings.decoder.ffmpeg.codecs.*` settings can be used to tell `ffmpeg` which decoder to use to decode this type of content (there could more than one decoder for a given codec). For instance, for the `aac` codec: - `settings.decoder.ffmpeg.codecs.aac.available()` returns the list of available decoders, typically `["aac", "aac_fixed"]`. - `settings.decoder.ffmpeg.codecs.aac` can be used to choose which decoder should be used, typically: `settings.decoder.ffmpeg.codecs.aac := "aac"` When debugging issues with `ffmpeg`, it can be useful to increase the log verbosity. ```liquidsoap settings.ffmpeg.log.verbosity := "warning" ``` This settings sets the verbosity of `ffmpeg` logs. Possible values, from less verbose to more verbose are: `"quiet"`, `"panic"`, `"fatal"`, `"error"`, `"warning"`, `"info"`, `"verbose"` or `"debug"` Please note that, due to a technical limitation, we are not yet able to route `ffmpeg` logs through the liquidsoap logging facilities, which means that `ffmpeg` logs are currently only printed to the process's standard output and that the `settings.ffmpeg.log.level` is currently not used. ### Decoder arguments In some cases, for instance when sending raw PCM data, it might be required to pass some arguments to the ffmpeg decoder to let it know what kind of format, codec, etc. it should decode. There are two ways to do that: - For _streams_, the `content_type` argument can be used. The convention is to use `"application/ffmpeg;<arguments>"`. - For _files_, the `ffmpeg_options` metadata can be used, for instance using the `annotate` protocol: `annotate:ffmpeg_options="<arguments>":/path/to/file.raw` Here's an example of a SRT input and output that can be used to send raw PCM data between two instances: Sender: ```liquidsoap enc = %ffmpeg( format="s16le", %audio( codec="pcm_s16le", ac=2, ar=48000 ) ) output.srt(enc, s) ``` Receiver: ```liquidsoap s = input.srt( content_type="application/ffmpeg;format=s16le,ch_layout=stereo,sample_rate=48000" ) ``` If, instead of using `output.srt` above, we were using `output.file` and saving to a file named `bla.raw`, this file could be read with a `single` source this way: ```liquidsoap s = single("annotate:ffmpeg_options='format=s16le,ch_layout=stereo,sample_rate=44100':/tmp/bla.raw") ``` This could also be done in a `playlist` or `request.dynamic` and etc. ## Encoders See detailed [ffmpeg encoders](ffmpeg_encoder.html) article. ## Filters See detailed [ffmpeg filters](ffmpeg_filters.html) article. ## Bitstream filters FFmpeg bitstream filters are filters that modify the binary content of _encoded data_. They can be used to adjust certain aspects of media codecs and containers to make them fit some specific use, for instance a rtmp/flv output etc. They are particularly important when dealing with live switches of encoded content (see [Examples](#examples) section). The list of all bitstream filters is documented on [FFmpeg](https://www.ffmpeg.org/ffmpeg-bitstream-filters.html) online doc and our [extra API reference](reference-extras.html). Here's one such filter: ```liquidsoap % liquidsoap -h ffmpeg.filter.bitstream.h264_mp4toannexb FFmpeg h264_mp4toannexb bitstream filter. See ffmpeg documentation for more details. Type: (?id : string?, source(video=ffmpeg.copy('a), 'b)) -> source(video=ffmpeg.copy('a), 'b) Category: Source / FFmpeg filter Arguments: * id : string? Force the value of the source ID. * (unlabeled) : source(video=ffmpeg.copy('a), 'b) Methods: ... ``` Please consult the FFmpeg documentation for more details about that each filter do and why/how to use them. ## Encoded data tweaks Manipulating encoded content is powerful but can sometimes require some specific knowledge of internals aspects of media codecs and containers. This section lists some specific cases. ### Relaxed copy content compatibility check By default, liquidsoap keeps track of the content passed in a stream containing ffmpeg encoded content (`ffmpeg.copy`) and only allows file and stream decoders to return strictly compatible content, e.g. same video resolution or audio samplerate. Some containers such as `mp4`, however, do allow stream where video resolution or audio samplerate changes between tracks. In this case, you can relax those compatibility checks using the following setting: ```liquidsoap settings.ffmpeg.content.copy.relaxed_compatibility_check := true ``` This is a global setting for now and could be refined per-stream in the future if the needs arises. ### Shared encoders `liquisoap` provides operators to encode data using `%ffmpeg` and reuse it across output. This is called _inline encoding_. Here's an example: ```liquidsoap audio_source = single(audio_url) video_source = single(image) stream = source.mux.video(video=video_source, audio_source) stream = ffmpeg.encode.audio_video( %ffmpeg( %audio(codec="aac", b="128k"), %video(codec="libx264", b="4000k") ), stream ) flv = %ffmpeg( format="flv", %audio.copy, %video.copy, ) # Send to one youtube output: output.youtube.live.rtmp( encoder = flv, stream, ... ) mpegts = %ffmpeg( format="mpegts", %audio.copy, %video.copy, ) # And to a hls one: output.file.hls( ["mpegts", mpegts], stream, ... ) ``` Working with encoded data, however, requires a bit of knowledge of ffmpeg internal and media codecs and containers. Here, for instance, this stream will have issues because the `flv` format requires global data, something that in ffmpeg terms is called `extradata`. When working with a single encoder such as: ```liquidsoap %ffmpeg( format="flv", %audio(codec="aac", b="128k"), %video(codec="libx264", b="4000k") ) ``` We are aware when initializing the encoders that it is aimed for a `flv` container so the code implicitly enables the global header for each encoder. However, when encoding inline, we do not know at the time of encoding the container that will be used to encapsulate the stream, even worst, it can be used potentially with different containers with different requirements! In our case here, you have two ways to solve the issue: If you know that all the containers will be okay with global header, you can enable the corresponding flag in the encoder: ```liquidsoap stream = ffmpeg.encode.audio_video( %ffmpeg( %audio(codec="aac", b="128k", flags="+global_header"), %video(codec="libx264", b="4000k", flags="+global_header") ), stream ) ``` However, it is also possible that one stream needs global header but not the other one, which is the case here with `mpegts`. In this case, you can use the _bitstream filter_ `ffmpeg.filter.bitstream.extract_extradata` to extract global data to only one stream: ``` audio_source = single(audio_url) video_source = single(image) stream = source.mux.video(video=video_source, audio_source) stream = ffmpeg.encode.audio_video( %ffmpeg( %audio(codec="aac", b="128k"), %video(codec="libx264", b="4000k") ), stream ) flv = %ffmpeg( format="flv", %audio.copy, %video.copy, ) flv_stream = ffmpeg.filter.bitstream.extract_extradata(stream) # Send to one youtube output: output.youtube.live.rtmp( encoder = flv, flv_stream, ... ) mpegts = %ffmpeg( format="mpegts", %audio.copy, %video.copy, ) # And to a hls one: output.file.hls( ["mpegts", mpegts], stream, ... ) ``` ## Examples See detailed [ffmpeg cookbook](ffmpeg_cookbook.html) article. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/ffmpeg_cookbook.md�����������������������������������������������������0000664�0000000�0000000�00000006551�14773033502�0021576�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# FFmpeg cookbook Here are some examples of what is possible to do with the ffmpeg support in liquidsoap: ## Relaying without re-encoding With ffmpeg support, Liquidsoap can relay encoded streams without re-encoding them, making it possible to re-send a stream to multiple destinations. Here's an example: ```{.liquidsoap include="ffmpeg-relay.liq"} ``` We cannot use `mksafe` here because the content is not plain `pcm` samples, which this operator is designed to produce. There are several ways to make the source infallible, however, either by providing a `single(...)` source with the same encoded content as we expect from `encoded_source` or by creating an infallible source using `ffmpeg.encode.audio`. ## On-demand relaying without re-encoding Another refinement on the previous example is the capacity to relay a stream only when listeners are connected to it, all without re-encoding the content. To make it work, you will need a format that can be handled by `ffmpeg` for that purpose. `mp3` is a good example. In the script below, you need to match the encoded format of the stream with a blank file (or any other file). The `output.harbor` will then relay the data from the file if no one is connected and start/stop the underlying input when there are listeners: ```{.liquidsoap include="ffmpeg-relay-ondemand.liq"} ``` ## Shared encoding Liquidsoap can also encode in one place and share the encoded with data with multiple outputs, making it possible to minimize CPU resources. Here's an example adapted from the previous one: ```{.liquidsoap include="ffmpeg-shared-encoding.liq"} ``` Shared encoding is even more useful when dealing with video encoding, which is very costly. Here's a fun example sharing audio and video encoding and sending to different destinations, both via Icecast and to YouTube/Facebook via the rtmp protocol: ```{.liquidsoap include="ffmpeg-shared-encoding-rtmp.liq"} ``` ## Add transparent logo and video See: https://github.com/savonet/liquidsoap/discussions/1862 ## Live switch between encoded content _This is an ongoing development effort. Please refer to the online support channels if you are experiencing issues with this kind of feature._ Starting with liquidsoap `2.1.x`, it is gradually becoming possible to do proper live switches on encoded content and send the result to different outputs. Please note that this requires a solid knowledge of media codecs, containers and ffmpeg bitstream filters. Different input and output containers store codec binary data in different ways and those are not always compatible. This requires the use of bitstream filters to adapt the binary data and, it's possible some new filters will need to be written to support more combinations of input/output and codecs. Here's a use case that has been tested: live switch between a playlist of mp4 files and a rtmp flv input: ```{.liquidsoap include="live-switch.liq"} ``` - We need the `h264_mp4toannexb` on each stream to make sure that the mp4 data conforms to what the mpegts container expect - We need to disable ffmpeg's automatic insertion of bitstream filters via `-autobsf`. FFmpeg does not support this kind of live switch at the moment and its automatically inserted filters won't work, which is why we're doing it ourselves. That's it! In the future we want to extend this use-case to also be able to output to a `rtmp` output from the same data. And more! �������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/ffmpeg_encoder.md������������������������������������������������������0000664�0000000�0000000�00000020620�14773033502�0021400�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# FFmpeg encoder The `%ffmpeg` encoder should support all the options for `ffmpeg`'s [muxers](https://ffmpeg.org/ffmpeg-formats.html#Muxers) and [encoders](https://www.ffmpeg.org/ffmpeg-codecs.html), including private configuration options. Configuration value are passed as key/values, with values being of types: `string`, `int`, or `float`. If an option is not recognized (or: unused), it will raise an error during the instantiation of the encoder. Here are some configuration examples: ### Interleaved muxing FFmpeg provides two different APIs for muxing data, interleaved or not. The interleaved API buffers packets waiting to be outputted to make sure that all streams, e.g. audio and video, have their packets as close to each other as possible. This ensures that for instance, the stream does not start with a long chunk of audio data without any video content. However, this can come with some increased memory usage due to buffering. On the other hand, the non-interleaved API allows to send encoded packets directly to the output without intermediate buffering. This can sometimes result in better latency and lower memory usage. The `%ffmpeg` encoder can use either API. By default, it uses the interleaved API when encoding more than one stream. You can also specify the interleaving mode by passing the `interleaved` parameter: `%ffmpeg(interleaved=<true|false|"default">, ...)`. You might also want to take this into consideration when setting your encoder's parameters. Some video encoders can buffer frames for a while before outputting the first encoded frame, which can also create issues even with the interleaved API enabled (the interleaving buffer has a max size too!). Typically, with `libx264`, you can set `tune = "zerolatency"` to make sure that the encoder starts outputting data right away. ### Encoding examples - **AAC encoding at `22050kHz` using `fdk-aac` encoder and `mpegts` muxer** ```liquidsoap %ffmpeg(format="mpegts", %audio( codec="libfdk_aac", samplerate=22050, b="32k", afterburner=1, profile="aac_he_v2" ) ) ``` - **Mp3 encoding using `libshine` at `48000kHz`** ```liquidsoap %ffmpeg(format="mp3", %audio(codec="libshine", samplerate=48000)) ``` - **AC3 audio and H264 video encapsulated in an MPEG-TS stream** ```liquidsoap %ffmpeg( format="mpegts", %audio(codec="ac3", channel_coupling=0), %video( codec="libx264", b="2600k", "x264-params"="scenecut=0:open_gop=0:min-keyint=150:keyint=150", preset="ultrafast" ) ) ``` - **AC3 audio and H264 video encapsulated in an MPEG-TS stream using ffmpeg raw frames** ```liquidsoap %ffmpeg( format="mpegts", %audio.raw(codec="ac3", channel_coupling=0), %video.raw( codec="libx264", b="2600k", "x264-params"="scenecut=0:open_gop=0:min-keyint=150:keyint=150", preset="ultrafast" ) ) ``` - **Mp3 encoding using `libmp3lame` and video copy** ```liquidsoap %ffmpeg( format="mp3", %audio(codec="libmp3lame"), %video.copy ) ``` The full syntax is as follows: ```liquidsoap %ffmpeg( format=<format>, # Audio section %audio(codec=<codec>, <option_name>=<option_value>, ...), # Or: %audio.raw(codec=<codec>, <option_name>=<option_value>, ...), # Or: %audio.copy(<option>), # Video section %video(codec=<codec>, <option_name>=<option_value>, ...), # Or: %video.raw(codec=<codec>, <option_name>=<option_value>, ...), # Or: %video.copy(<option>), # Generic options <option_name>=<option_value>, ... ) ``` Where: - `<format>` is either a string value (e.g. `"mpegts"`), as returned by the `ffmpeg -formats` command or `none`. When set to `none` or simply no specified, the encoder will try to auto-detect it. - `<codec>` is a string value (e.g. `"libmp3lame"`), as returned by the `ffmpeg -codecs` command. - `<option_name>` can be any syntactically valid variable name or string. Strings are typically used when the option name is of the form: `foo-bar`. - `%audio(...)` is for options specific to the audio codec. Unused options will raise an exception. Any option supported by `ffmpeg` can be passed here. Streams encoded using `%audio` are using liquidsoap internal frame format and are fully handled on the liquidsoap side. - `%audio.raw(...)` behaves like `%audio` except that the audio data is kept as ffmpeg's internal format. This can avoid data copy and is also the format required to use [ffmpeg filters](ffmpeg_filters.html). - `%audio.copy` copies data without decoding or encoding it. This is great to avoid using the CPU, but in this case, the data cannot be processed with operators that modify it, such as `fade.{in,out}` or `smart_cross`. Also, all streams must agree on the same data format. - `%video(...)` is for options specific to the video codec. Unused options will raise an exception. Any option supported by `ffmpeg` can be passed here. - `%video.raw` and `%video.copy` have the same meaning as their `%audio` counterpart. - Generic options are passed to audio, video and format (container) setup. Unused options will raise an exception. Any option supported by `ffmpeg` can be passed here. ### HLS output The `%ffmpeg` encoder is the prime encoder for HLS output as it is the only one of our collection of encoder which can produce Mpeg-ts muxed data, which is required by most HLS clients. ### File output Some encoding formats, for instance `mp4`, require to rewind their stream and write a header after the fact, when encoding of the current track has finished. For historical reasons, such formats cannot be used with `output.file`. To remedy that, we have introduced the `output.url` operator. When using this operator, the encoder is fully in charge of the output file and can thus write headers after the fact. The `%ffmpeg` encoder is one such encoder that can be used with this operator. ### Copy options The `%audio.copy` and `%video.copy` encoders have two mutually exclusive options to handle keyframes: - `%audio.copy(wait_for_keyframe)` and `%video.copy(wait_for_keyframe)`: Wait until at least one keyframe has been passed to start passing encoded packets from a new stream. - `%audio.copy(ignore_keyframe)` and `%video.copy(ignore_keyframe)`: Ignore all keyframes. These options are useful when switching from one encoded stream to the next. With option `wait_for_keyframe`, the encoder discards any new packet at the beginning of a stream until a keyframe is passed. This means that playback will be paused until it can be resumed properly with no decoding glitches. This option is implemented globally when possible, i.e. in case of a video track with keyframes and an audio track with no keyframes, the audio track will discard packets until a video keyframe has been passed. This is the default option. With option `ignore_keyframe`, the encoder starts passing encoded data right away. Content is immediately added but playback might get stuck until a new keyframe is passed. It is worth noting that some audio encoders may also have keyframes. ### Hardware acceleration The `%ffmpeg` encoder supports multiple hardware acceleration provided by `ffmpeg`. If you are lucky and the encoder you are using provides support for hardware acceleration without any specific configuration, all you might have to do is select `codec="..."` (for instance on macOS, `codec="h264_videotoolbox"`) and it should work immediately. The type of hardware acceleration provided by ffmpeg are: 1. Internal hardware acceleration that works without any specific configuration. This is the happy path described above! 2. Device-based hardware acceleration that works with a specific device. 3. Frame-based hardware acceleration that work with a specific pixel format. The type of hardware acceleration to use for a given stream can be specified using the `hwaccel` option. Its value is one of: `"auto"`, `"none"`, `"internal"`, `"device"` or `"frame"`. For device-based hardware acceleration, the device to use can be specified using `hwaccel_device`. For frame-based hardware acceleration, the pixel format can be specified using `hwaccel_pixel_format`. In most cases, liquidsoap should be able to guess these values from the codec. Here's an example: ```liquidsoap enc = %ffmpeg( format="mpegts", %video( hwaccel="device", hwaccel_devic="/dev/...", ... ) ) ``` Hardware acceleration support is, of course, very hardware dependent, so we might not have tested all possible combinations. If you are having issues setting it up, do not hesitate to get in touch with us to see if your use-case is properly covered. ����������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/ffmpeg_filters.md������������������������������������������������������0000664�0000000�0000000�00000013020�14773033502�0021425�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# FFmpeg filters [FFmpeg filters](https://ffmpeg.org/ffmpeg-filters.html) provide audio and video filters that can be used to transform content using the ffmpeg library. They are enabled in liquidsoap when compiled with the optional [ffmpeg-avfilter](https://github.com/savonet/ocaml-ffmpeg). ## Filter as operators If enabled, the filters should appear as operators, prefixed with `ffmpeg.filter`. For instance: ``` Ffmpeg filter: Add echoing to the audio. Type: (?in_gain : float?, ?out_gain : float?, ?delays : string?, ?decays : string?, ffmpeg.filter.graph, ffmpeg.filter.audio) -> ffmpeg.filter.audio Category: Liquidsoap Parameters: * in_gain : float? (default: null) set signal input gain. (default: 0.6) * out_gain : float? (default: null) set signal output gain. (default: 0.3) * delays : string? (default: null) set list of signal delays. (default: "1000") * decays : string? (default: null) set list of signal decays. (default: "0.5") * (unlabeled) : ffmpeg.filter.graph (default: None) * (unlabeled) : ffmpeg.filter.audio (default: None) ``` Filters input and output are abstract values of type `ffmpeg.filter.audio` and `ffmpeg.filter.video`. They can be created using `ffmpeg.filter.audio.input`, `ffmpeg.filter.video.input`. These operators take [media tracks](multitrack.html) as input. Conversely, tracks can be created from them using `ffmpeg.filter.audio.output` and `ffmpeg.filter.video.output`. Filters are configured within the closure of a function. Here's an example: ```{.liquidsoap include="ffmpeg-filter-flanger-highpass.liq"} ``` This filter receives an audio input, creates a `ffmpeg.filter.audio.input` with it that can be passed to filters, applies a flanger effect and then a high pass effect, creates an audio output from it and returns it. Here's another example for video: ```{.liquidsoap include="ffmpeg-filter-hflip.liq"} ``` This filter receives a video input, creates a `ffmpeg.filter.video.input` with it that can be passed to filters, applies a `hflip` filter (flips the video vertically), creates a video output from it and returns it. ## Applying filters to a source When applying a filter, the input is placed in a clock that is driven by the output. This means that you cannot share other tracks from the input to the output. This can be an annoying source of confusion. Thus, when applying FFMpeg filters to sources with audio and video tracks, it is recommended to pass all the tracks through the filter, even if they are simply copied. Here's an example with the previous filter: ```{.liquidsoap include="ffmpeg-filter-hflip2.liq"} ``` FFmpeg filters are very powerful, they can also convert audio to video, for instance displaying information about the stream, and they can combined into powerful graph processing filters. ## Filter commands Some filters support [changing options at runtime](https://ffmpeg.org/ffmpeg-filters.html#Changing-options-at-runtime-with-a-command) with a command. These are also supported in liquidsoap. In order to do so, you have to use a slightly different API: ```{.liquidsoap include="ffmpeg-filter-dynamic-volume.liq" to="END"} ``` First, we instantiate a volume filter via `ffmpeg.filter.volume.create`. The filter instance has a `process_command`, which we use to create the `set_volume` function. Then, we apply the expected input to the filter and return the pair `(s, set_volume)` of source and function. The `ffmpeg.filter.<filter>.create` API is intended for advanced use if you want to use filter commands. Otherwise, `ffmpeg.filter.<filter>` provides a more straight forward API to filters. ## Filters with dynamic inputs or outputs Filters with dynamic inputs or outputs can have multiple inputs or outputs, decided at run-time. Typically, `ffmpeg.filter.split` splits a video stream into multiple streams and `ffmpeg.filter.merge` merges multiple video streams into a single one. For these filters, the operators' signature is a little different. Here's an example for dynamic outputs: ``` % liquidsoap -h ffmpeg.filter.asplit Ffmpeg filter: Pass on the audio input to N audio outputs. This filter has dynamic outputs: returned value is a tuple of audio and video outputs. Total number of outputs is determined at runtime. Type: (?outputs : int?, ffmpeg.filter.graph, ffmpeg.filter.audio) -> [ffmpeg.filter.audio] * [ffmpeg.filter.video] Category: Liquidsoap Flag: extra Parameters: * outputs : int? (default: null) set number of outputs. (default: 2) * (unlabeled) : ffmpeg.filter.graph (default: None) * (unlabeled) : ffmpeg.filter.audio (default: None) ``` This filter returns a tuple `(audio, video)` of possible dynamic outputs. Likewise, with dynamic inputs: ``` % liquidsoap -h ffmpeg.filter.amerge Ffmpeg filter: Merge two or more audio streams into a single multi-channel stream. This filter has dynamic inputs: last two arguments are lists of audio and video inputs. Total number of inputs is determined at runtime. Type: (?inputs : int?, ffmpeg.filter.graph, [ffmpeg.filter.audio], [ffmpeg.filter.video]) -> ffmpeg.filter.audio Category: Liquidsoap Flag: extra Parameters: * inputs : int? (default: null) specify the number of inputs. (default: 2) * (unlabeled) : ffmpeg.filter.graph (default: None) * (unlabeled) : [ffmpeg.filter.audio] (default: None) * (unlabeled) : [ffmpeg.filter.video] (default: None) ``` This filter receives an array of possible `audio` inputs as well as an array of possible `video` inputs. Put together, this can be used as such: ```{.liquidsoap include="ffmpeg-filter-parallel-flanger-highpass.liq"} ``` ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/flows_devel.md���������������������������������������������������������0000664�0000000�0000000�00000002554�14773033502�0020754�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Developing Flows [Flows](flows.html) is handled on the [Heroku](https://www.heroku.com/) platform. ## Getting started First steps to get started. - Create an account on [Heroku](https://www.heroku.com/). - Install the [Heroku utilities](https://toolbelt.heroku.com/). - Ask a Liquidsoap administrator to give you access to the repositories. The repositories of the main components are organized as follows. - `savonet-flows` is the python handler to submit metadata: - the associated [github repository](https://github.com/savonet/flows-submit) - the Heroku repository is `git@heroku.com:savonet-liquidsoap.git` - `savonet-flows-socket` is the node application to serve the webpage and client stuff. - the associated [github repository](https://github.com/savonet/flows-push) - the [test Heroku webpage](http://savonet-flows-socket.herokuapp.com/) is updated by pushing on `git@heroku.com:savonet-flows-socket-next.git` - the [prod Heroku webpage](http://savonet-flows-socket.herokuapp.com/) is updated by pushing on `git@heroku.com:savonet-flows-socket.git` Some more experimental repositories include: - a [command-line client](https://github.com/savonet/flows-client) ## Useful commands Getting the environment variables: ``` heroku config -s --app savonet-flows ``` Seeing the logs of the socket application: ``` heroku logs -t --app savonet-flows-socket ``` ����������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/frequence3.md����������������������������������������������������������0000664�0000000�0000000�00000000752�14773033502�0020501�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Fréquence 3 [Fréquence 3](http://www.frequence3.fr) uses Liquidsoap mainly on the backstage, for different purposes: - transcoding for different formats (OGG, weird MP3 relays...) - scheduling and playlist for audio backup streams, and test streams - blank detection They look forward to using Liquidsoap even more, and work with the Savonet team to make sure this tool can ease the work of webradios :) They provide an MP3 stream [here](http://streams.frequence3.net/mp3-128.m3u). ����������������������liquidsoap-2.3.2/doc/content/geekradio.md�����������������������������������������������������������0000664�0000000�0000000�00000004303�14773033502�0020367�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Geek Radio The historical webradio, founded by David Baelde and Samuel Mimram at the ENS Lyon. The very first version was, as many other radios, a Perl function called by Ices. It played files, one by one. On the campus, there was plenty of audio files available, so they soon wanted to index them and be able to ask easily for one file to be streamed. Samuel made a dirty campus indexer in OCaml, and David made an ugly Perl hack for adding user requests to the original system. It probably kind of worked for a while. Then they wanted something more, and realized it was all too ugly. So they made the binding of libshout for OCaml and built the first streamer in pure OCaml. It had a simple telnet interface so an IRC bot could send user requests easily to it, same for the website. There were two request queues, one for users, one for admins. But it was still not so nicely designed, and they felt it when they needed more. They wanted scheduling, especially techno music at night. Around that time students had to set up a project for one of their courses. David and Samuel proposed to build a complete flexible webradio system, that's Savonet. To give jobs to everybody, they had planned a complete rewriting of every part, with grand goals. A new website with so much features, a new intelligent multilingual bot, a new network libraries for glueing that, etc. Most died. But still, Liquidsoap was born, and they had plenty of new libraries for OCaml. Since then, Liquidsoap has been greatly enhanced, and is now spreading outside the ENS Lyon. ## Features The liquidsoap script schedules several static (but periodically reloaded) playlists played on different times, adds jingle to the usual stream every hour, adds short live interventions, or completely switches to live shows when available. It accepts user requests, which have priority over static playlists but not live shows, and adds speech-synthetized metadata information at the end of requests. Geek Radio used to have a Strider daemon running to fill our database. Since that project is now dead, a simple hack is now used instead: bubble. The usual way of sending a request is via an IRC bot, which queries the database and sends the chosen URI to liquidsoap. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/harbor.md��������������������������������������������������������������0000664�0000000�0000000�00000011103�14773033502�0017706�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Harbor input Liquidsoap is also able to receive a source using icecast or shoutcast source protocol with the `input.harbor` operator. Using this operator, the running liquidsoap will open a network socket and wait for an incoming connection. This operator is very useful to seamlessly add live streams into your final streams: you configure the live source client to connect directly to liquidsoap, and manage the switch to and from the live inside your script. Additionally, liquidsoap can handle many simultaneous harbor sources on different ports, with finer-grained authentication schemes that can be particularly useful when used with source clients designed for the shoutcast servers. SSL support in harbor can be enabled using of of the following `opam` packages: `ssl`, `osx-secure-transport`. If enabled using `ssl`, `input.harbor.ssl` will be available. If enabled with `osx-secure-transport`, it will be `input.harbor.secure_transport`. ## Parameters The global parameters for harbor can be retrieved using `liquidsoap --list-settings`. They are: - `harbor.bind_addr`: IP address on which the HTTP stream receiver should listen. The default is `"0.0.0.0"`. You can use this parameter to restrict connections only to your LAN. - `harbor.timeout`: Timeout for source connection, in seconds. Defaults to `30.`. - `harbor.verbose`: Print password used by source clients in logs, for debugging purposes. Defaults to: `false` - `harbor.reverse_dns`: Perform reverse DNS lookup to get the client's hostname from its IP. Defaults to: `true` - `harbor.icy_formats`: Content-type (mime) of formats which allow shout (ICY) metadata update. Defaults to: ` ["audio/mpeg"; "audio/aacp"; "audio/aac"; "audio/x-aac"; "audio/wav"; "audio/wave"]` If SSL support was enabled via `ssl`, you will have the following additional settings: - `harbor.ssl.certificate`: Path to the SSL certificate. - `harbor.ssl.private_key`: Path to the SSL private key (openssl only). - `harbor.ssl.password`: Optional password to unlock the private key. Obtaining a proper SSL certificate can be tricky. You may want to start with a self-signed certificate first. You can obtain a free, valid certificate at: [https://letsencrypt.org/](https://letsencrypt.org/) If SSL support is enable via `osx-secure-transport`, you will have the same settings but named: `harbor.secure_transport.*`. To create a self-signed certificate for local testing you can use the following one-liner: ``` openssl req -x509 -newkey rsa:4096 -sha256 -nodes -keyout server.key -out server.crt -subj "/CN=localhost" -days 3650 ``` You also have per-source parameters. You can retrieve them using the command `liquidsoap -h input.harbor`. The most important one are: - `user`, `password`: set a permanent login and password for this harbor source. - `auth`: Authenticate the user according to a specific function. - `port`: Use a custom port for this input. - `icy`: Enable ICY (shoutcast) source connections. - `id`: The mountpoint registered for the source is also the id of the source. When using different ports with different harbor inputs, mountpoints are attributed per-port. Hence, there can be a harbor input with mountpoint `"foo"` on port `1356` and a harbor input with mountpoint `"foo"` on port `3567`. Additionally, if an harbor source uses custom port `n` with shoutcast (ICY) source protocol enabled, shoutcast source clients should set their connection port to `n+1`. The `auth` function is a function, that takes a record `{user, password, address}` and returns a boolean representing whether the user should be granted access or not. Typical example can be: ```{.liquidsoap include="harbor-auth.liq"} ``` In the case of the `ICY` (shoutcast) source protocol, there is no `user` parameter for the source connection. Thus, the user used will be the `user` parameter passed to the `input.harbor` source. When using a custom authentication function, in case of a `ICY` (shoutcast) connection, the function will receive this value for the username. ## Usage When using harbor inputs, you first set the required settings, as described above. Then, you define each source using `input.harbor("mountpoint")`. This source is faillible and will become available when a source client is connected. The unlabeled parameter is the mount point that the source client may connect to. It should be `"/"` for shoutcast source clients. The source client may use any of the recognized audio input codec. Hence, when using shoucast source clients, you need to have compiled liquidsoap with mp3 decoding support (`ocaml-mad`). A sample code can be: ```{.liquidsoap include="harbor-usage.liq" to=-1} ``` �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/harbor_http.md���������������������������������������������������������0000664�0000000�0000000�00000012323�14773033502�0020752�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Harbor as HTTP server The harbor server can be used as a HTTP server. We provide two type of APIs for this: ## Simple API The `harbor.http.register.simple` function provides a simple, easy to use registration API for quick HTTP response implementation. This function receives a record describing the request and returns the HTTP response. The request passed to the function contains all expected information from the underlying HTTP query. The `data` method on a request is a _string getter_, that is a function of type: `() -> string` which returns the empty string `""` when all data has been consumed. You can use this function to e.g. write the request data to a file using `file.write.stream`. The `body` method can be used to read all of the request's data and store it in memory. Make sure to only use it if you know that the response should be small enough! For convenience, a HTTP response builder is provided via `http.response`. Here's an example: ```{.liquidsoap include="harbor.http.response.liq" from="BEGIN"} ``` where: - `port` is the port where to receive incoming connections - `method` is for the http method (or verb), one of: `"GET"`, `"PUT"`, `"POST"`, `"DELETE"`, `"OPTIONS"` and `"HEAD"` - `path` is the matched path. It can include named fragments, e.g. `"/users/:id/collabs/:cid"`. Named named fragments are passed via `request.query`, for instance: `req.query["cid"]`. ## Node/express API The `harbor.http.register` function offers a higher-level API for advanced HTTP response implementation. Its API is very similar to the node/express API. Here's an example: ```{.liquidsoap include="harbor.http.register.liq" from="BEGIN"} ``` where: - `port` is the port where to receive incoming connections - `method` is for the http method (or verb), one of: `"GET"`, `"PUT"`, `"POST"`, `"DELETE"`, `"OPTIONS"` and `"HEAD"` - `path` is the matched path. It can include named fragments, e.g. `"/users/:id/collabs/:cid"`. Matched named fragments are passed via `request.query`, for instance: `req.query["cid"]`. The handler function receives a record containing all the information about the request and fills up the details about the response, which is then used to write a proper HTTP response to the client. Named fragments from the request path are passed to the response `query` list. Middleware _a la_ node/express are also supported and registered via `http.harbor.middleware.register`. See `http.harbor.middleware.cors` for an example of how to implement one such middleware. Here's how you would enable the `cors` middleware: ``` harbor.http.middleware.register(harbor.http.middleware.cors(origin="example.com")) ``` ## Https support `https` is supported using either `libssl` or `ocaml-tls`. When compiled with either of them, a `http.transport.ssl` or `http.transport.tls` is available and can be passed to each `harbor` operator: ```liquidsoap transport = http.transport.ssl( # Server mode: required, # client mode: optional, add certificate to trusted pool certificate="/path/to/certificate/file", # Server mode: required, client mode: ignored key="/path/to/secret/key/file", # Required if key file requires one. # TLS does not support password encrypted keys! password="optional password" ) harbor.http.register(transport=transport, port=8000, ...) input.harbor(transport=..., port=8000, ...) output.harbor(transport=..., port=8000, ...) output.icecast(transport=..., port=8000, ...) ``` A given port can only support one type of transport at a time and registering handlers, sources or outputs on the same port with different transports will raise a `error.http` error. ## Advanced usage All registration functions have a `.regexp` counter part, e.g. `harbor.http.register.simple.regexp`. These function accept a full regular expression for their `path` argument. Named matches on the regular expression are also passed via the request's `query` parameter. It is also possible to directly interact with the underlying socket using the `simple` API: ```{.liquidsoap include="harbor-simple.liq"} ``` ## Examples These functions can be used to create your own HTTP interface. Some examples are: ## Redirect Icecast's pages Some source clients using the harbor may also request pages that are served by an icecast server, for instance listeners statistics. In this case, you can register the following handler: ```{.liquidsoap include="harbor-redirect.liq"} ``` ## Get metadata You can use harbor to register HTTP services to fecth/set the metadata of a source. ```{.liquidsoap include="harbor-metadata.liq" from="BEGIN"} ``` Once the script is running, a GET request for `/getmeta` at port `7000` returns the following: ``` HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 { "genre": "Soul", "album": "The Complete Stax-Volt Singles: 1959-1968 (Disc 8)", "artist": "Astors", "title": "Daddy Didn't Tell Me" } ``` ## Set metadata Using `insert_metadata`, you can register a GET handler that updates the metadata of a given source. For instance: ```{.liquidsoap include="harbor-insert-metadata.liq" from="BEGIN"} ``` Now, a request of the form `http://server:7000/setmeta?title=foo` will update the metadata of source `s` with `[("title","foo")]`. You can use this handler, for instance, in a custom HTML form. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/help.md����������������������������������������������������������������0000664�0000000�0000000�00000010255�14773033502�0017370�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Get help Liquidsoap is a self-documented application, which means that it can provide help about several of its aspects. You will learn here how to get help by yourself, by asking liquidsoap. If you do not succeed in asking the tool, you can of course get help from humans. We maintain the following communication channels: - Discord: [chat.liquidsoap.info](http://chat.liquidsoap.info/) - IRC: #savonet on [irc.libera.chat](https://libera.chat/) (through a discord bridge) - Mailing list: [savonet-users@lists.sourceforge.net](mailto:savonet-users@lists.sourceforge.net) ## Scripting API When scripting in liquidsoap, one uses functions that are either _builtin_ (_e.g._ `input.http` or `output.icecast`) or defined in the [script library](script_loading.html) (_e.g_ `output`). All these functions come with a documentation, that you can access by executing `liquidsoap -h FUNCTION` on the command-line. For example: ``` $ liquidsoap -h sine Generate a sine wave. Type: (?id : string?, ?amplitude : {float}, ?duration : float, ?{float}) -> source(audio=internal('a), video=internal('b), midi=internal('c)) Category: Source / Input Parameters: * id : string? (default: null) Force the value of the source ID. * amplitude : {float} (default: 1.) Maximal value of the waveform. * duration : float (default: -1.) Duration in seconds (negative means infinite). * (unlabeled) : {float} (default: 440.) Frequency of the sine. Methods: * fallible : bool Indicate if a source may fail, i.e. may not be ready to stream. * id : () -> string Identifier of the source. * is_active : () -> bool `true` if the source is active, i.e. it is continuously animated by its own clock whenever it is ready. Typically, `true` for outputs and sources such as `input.http`. * is_ready : () -> bool Indicate if a source is ready to stream. This does not mean that the source is currently streaming, just that its resources are all properly initialized. * is_up : () -> bool Indicate that the source can be asked to produce some data at any time. This is `true` when the source is currently being used or if it could be used at any time, typically inside a `switch` or `fallback`. * on_leave : ((() -> unit)) -> unit Register a function to be called when source is not used anymore by another source. * on_metadata : ((([string * string]) -> unit)) -> unit Call a given handler on metadata packets. * on_shutdown : ((() -> unit)) -> unit Register a function to be called when source shuts down. * on_track : ((([string * string]) -> unit)) -> unit Call a given handler on new tracks. * remaining : () -> float Estimation of remaining time in the current track. * seek : (float) -> float Seek forward, in seconds (returns the amount of time effectively seeked). * self_sync : () -> bool Is the source currently controlling its own real-time loop. * skip : () -> unit Skip to the next track. * time : () -> float Get a source's time, based on its assigned clock. ``` Of course if you do not know what function you need, you'd better go through the [API reference](reference.html). Please note that some functions in that list are optional and may not be available with your local `liquidsoap` install unless you install the optional dependency that enables it. The list of optional dependencies is listed via `opam info liquidsoap` if you have installed it this way or can in our [build page](build.html). ## Settings Liquidsoap scripts contain expression like `settings.log.stdout := true`. These are _settings_, global variables affecting the behaviour of the application. Some common settings have shortcut for convenience. These are all shortcuts to their respective `settings` values: ```{.liquidsoap include="settings.liq"} ``` You can have a list of available settings, with their documentation, by running `liquidsoap --list-settings`. The output of these commands is a valid liquidsoap script, which you can edit to set the values that you want, and load it ([implicitly](script_loading.html) or not) before you other scripts. You can browse online the [list of available settings](settings.html). ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/hls_output.md����������������������������������������������������������0000664�0000000�0000000�00000011021�14773033502�0020636�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# HLS Output Starting with liquidsoap `1.4.0`, it is possible to send your streams as [HLS output](https://en.wikipedia.org/wiki/HTTP_Live_Streaming). The main operator is `output.file.hls`. Here's an example using it, courtesy of [srt2hls](https://github.com/mbugeia/srt2hls): ```{.liquidsoap include="output.file.hls.liq"} ``` Let's see what's important here: - `streams` describes the encoded streams. It's a list of: `(stream_name, encoder)`. `stream_name` is used to generate the corresponding media playlists. Encoders can be any encoder supported by liquidsoap. However, the [HLS RFC](https://tools.ietf.org/html/rfc8216) limits the list of possible codecs to `mp3` and `aac`. Furthermore, for the best possible compatible, it is recommended to send data encapsulated in a `MPEG-TS` stream. Currently, the only encoder capable of doing this in liquidsoap is `%ffmpeg`. - `persist_at` is used to allow liquidsoap to restart while keeping the existing segments and playlists. When shutting down, liquidsoap stores the current configuration at `persist_at` and uses it to restart the HLS stream when restarting. - `segments` and `segments_overhead` are used to keep track of the generated segments. Each media playlist will contain a number of segments defined by `segments` and an extra set of segments, defined by `segments_overhead`, is kept past the playlist size for those listeners who are still listening on outdated segments. There are more useful options, in particular `on_file_change`, which can be used for instance to sync up your segments and playlists to a distant storage and hosting service such as S3. Liquidsoap also provides `output.harbor.hls` which allows to serve HLS streams directly from liquidsoap. Their options should be the same as `output.file.hls`, except for harbor-specifc options `port` and `path`. It is not recommended for listener-facing setup but can be useful to sync up with a caching system such as cloudfront. ## Keyframes and segment length In codec terminology, a `keyframe` can be understood as a piece of encoded data that contains enough information to start decoding the stream. Keyframes are very common in video codecs and can exist in audio codecs. In order to make sure that a HLS playlist can be decoded starting from any segment, liquidsoap tries to split segments on keyframe boundaries. When not possible, you will see a warning in the logs. Segment split is forced when reaching the value specified by `EXT-X-TARGETDURATION` to follow the HLS specifications. For metadata and extra tags, segment split will occur at the next keyframe. To make sure that all these requirements operate correctly, you should make sure to set a keyframe frequency in your encoder's settings that generates at lease one keyframe per segment. For instance, if your segments are at most `2s` long and encoded using `libx264`, you can use the `keyint` and `keyint-min` parameters, which are expressed in number of frames. If your video frame rate is `25fps` (liquidsoap's default), you should have at least one keyframe every `2 * 25 = 50` frames. ```liquidsoap %ffmpeg( ..., %video(codec="libx264", x264opts="keyint=50:min-keyint=50") ) ``` ## Metadata HLS outputs supports metadata in two ways: - Using the `%ffmpeg` encoder, through a `timed_id3` metadata logical stream with the `mpegts` format. - Through regular ID3 frames, as requested by the [HLS specifications](https://datatracker.ietf.org/doc/html/rfc8216#section-3.4) for `adts`, `mp3`, `ac3` and `eac3` formats with the `%ffmpeg` encoder and also natively using the `%mp3`, `%shine` or `%fdkaac` encoders. - There is currently no support for in-stream metadata for the `mp4` format. Metadata parameters are passed through the record methods of the streams' encoders. Here's an example ```{.liquidsoap include="hls-metadata.liq"} ``` Parameters are: - `id3`: Set to `false` to deactivate metadata on the streams. Defaults to `true`. - `id3_version`: Set the `id3v2` version used to export metadata - `replay_id3`: By default, the latest metadata is inserted at the beginning of each segment to make sure new listeners always get the latest metadata. Set to `false` to disable it. Metadata for these formats are activated by default. If you are experiencing any issues with them, you can disable them by setting `id3` to `false`. ## Mp4 format `mp4` container is supported by requires specific parameters. Here's an example that mixes `aac` and `flac` audio, The parameters required for `mp4` are `movflags` and `frag_duration`. ```{.liquidsoap include="hls-mp4.liq"} ``` ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/http_input.md����������������������������������������������������������0000664�0000000�0000000�00000001171�14773033502�0020633�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# HTTP input Liquidsoap can create a source that pulls its data from an HTTP location. This location can be a distant file or playlist, or an icecast or shoutcast stream. To use it in your script, simply create a source that way: ```{.liquidsoap include="http-input.liq" from="BEGIN" to="END"} ``` This operator will pull regularly the given location for its data, so it should be used for locations that are assumed to be available most of the time. If not, it might generate unnecessary traffic and pollute the logs. In this case, it is perhaps better to inverse the paradigm and use the [input.harbor](harbor.html) operator. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/icy_metadata.md��������������������������������������������������������0000664�0000000�0000000�00000004253�14773033502�0021065�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# ICY metadata _ICY metadata_ is the name for the mechanism used to update metadata in icecast's source streams. The techniques is primarily intended for data formats that do not support in-stream metadata, such as mp3 or AAC. However, it appears that icecast also supports ICY metadata update for ogg/vorbis streams. When using the ICY metadata update mechanism, new metadata are submitted separately from the stream's data, via a http GET request. The format of the request depends on the protocol you are using (ICY for shoutcast and icecast 1 or HTTP for icecast 2). Starting with 1.0, you can do several interesting things with icy metadata updates in liquidsoap. We list some of those here. ## Enable/disable ICY metadata updates You can enable or disable icy metadata update in `output.icecast` by setting the `send_icy_metadata` parameter to `null()`, `true` or `false`. The default value is `null()` and does the following: - Set `true` for: mp3, aac, aac+, wav - Set `false` for any format using the ogg container In some cases, `liquidsoap` might not be able to detect if ICY metadata need to be enabled, in which case it will ask you to set a `true` or `false` value for this parameter. ## `song` metadata Most Icecast listeners expect a `song` metadata to be generated. This metadata should combine both artist and title metadata and will be played preferably. We provide a default implementation that returns `artist` or `title` metadata when only one of these two is available and `$(artist) - $(title)` otherwise. You can use the `icy_song` parameter to use your own implementation. Returning `null()` from that function disables the metadata altogether. ## Update metadata manually The function `icy.update_metadata` implements a manual metadata update using the ICY mechanism. It can be used independently from the `icy_metadata` parameter described above, provided icecast supports ICY metadata for the intended stream. For instance the following script registers a telnet command name `metadata.update` that can be used to manually update metadata: ```{.liquidsoap include="icy-update.liq"} ``` As usual, `liquidsoap -h icy.update_metadata` lists all the arguments of the function. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/images�����������������������������������������������������������������0000777�0000000�0000000�00000000000�14773033502�0021641�2../orig/images��������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/in_production.md�������������������������������������������������������0000664�0000000�0000000�00000001660�14773033502�0021314�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Using in production The full installation of liquidsoap will typically install `/etc/liquidsoap`, `/etc/init.d/liquidsoap` and `/var/log/liquidsoap`. All these are meant for a particular usage of liquidsoap when running a stable radio. Your production `.liq` files should go in `/etc/liquidsoap`. You'll then start/stop them using the init script, _e.g._ `/etc/init.d/liquidsoap start`. Your scripts don't need to have the `#!` line, and liquidsoap will automatically be ran on daemon mode (`-d` option) for them. You should not override the `log.file.path` setting because a logrotate configuration is also installed so that log files in the standard directory are truncated and compressed if they grow too big. It is not very convenient to detect errors when using the init script. We advise users to check their scripts after modification (use `liquidsoap --check /etc/liquidsoap/script.liq`) before effectively restarting the daemon. ��������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/index.md���������������������������������������������������������������0000664�0000000�0000000�00000010160�14773033502�0017542�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Liquidsoap Liquidsoap is a powerful tool for building complex audio and video stream generators, typically targeting internet radios and webtvs. It consists of a simple script language, which has a first-class notion of source (basically a _stream_) and provides elementary source constructors and source compositions from which you can build the stream generator you want. This design makes liquidsoap flexible and easily extensible. We believe that liquidsoap is easy to use. For basic purposes, the scripts consist of the definition of a tree of sources. You will quickly [learn](quick_start.html) how natural it is to use liquidsoap in such cases. The good thing is that when you'll want to make your stream more complex, you'll be able to stay in the same framework and keep a maintainable configuration. Of course, using some complex features might require a deeper understanding of the concepts of [source](sources.html) and [request](requests.html) and of our [scripting language](language.html). We'll discuss below what liquidsoap is and what it isn't. If you're already familiar with it and want to get started, just jump to the [documentation index](documentation.html). It will provide guidance,starting with the [quickstart tour](quick_start.html). Liquidsoap is an open-source software from the [Savonet](http://liquidsoap.info) project. ## Features Here are a few things you can easily achieve using Liquidsoap: - Playing from files, playlists, directories or script playlists (plays the file chosen by an external program). - [Video streams](video.html) generation. - Decoding/encoding using any media format supported by FFmpeg. - Transcoding of media stream, relay of encoded media stream without re-encoding, sharing encoding to avoid encoding multiple times. - Transparent remote file access; easy addition of file resolution protocols. - Scheduling of many sources, depending on time, priorities, quotas, etc. - Mixing sources on top others. - Queuing of user requests; editable queues. - Sound processing: compression, normalization, echo, soundtouch, etc. - Speech and sound synthesis. - [Metadata](metadata.html) rewriting and insertion. - Arbitrary transitions: cross-fade, jingle insertion, custom, etc. The behaviour of the transition can be programmed to depend on metadata and average volume. - [Input of other streams](http_input.html): useful for switching to a live show. Liquidsoap can relay an HTTP stream but also host it. - [Blank detection](blank.html). - Definable event handlers on new tracks, new metadata and excessive blank. - Multiple outputs in the same instance: you can have several quality settings, use several media or even broadcast several contents from the same instance. - Output to HLS/Icecast/Shoutcast (MP3/Ogg) or a local file (WAV/MP3/Ogg/AAC). - Input/output via Jack, ALSA, OSS and PortAudio. Output via `libao`. - [Interactive control](advanced.html) of many operators via Telnet or UNIX domain socket, and indirectly using scripts, graphical/web/IRC interfaces. If you need something else, it's highly possible that you can have it -- at least by programming new sources/operators. Send us a request, we'll be happy to discuss these questions. ## Non-Features Liquidsoap is a flexible tool for processing audio and video streams, that's all. We've used it for several internet radio projects, and we know its flexibility is useful. However, internet radio usually requires more than just an audio stream, as such components cannot easily be built from basic primitives as we do in liquidsoap for streams. We don't have any magic solution for these, although we sometimes have some nice tools which could be adapted to various uses. Liquidsoap itself doesn't have a nice GUI or any graphical programming environment. You'll have to write the script by hand, and the only possible interaction with a running liquidsoap is the telnet server. Liquidsoap makes the interfacing with other tools easy, since it can call an external application (reading from the database) to get audio tracks, another one (updating last-played information) to notify that some file has been successfully played. An example of this is [Beets](beets.html). ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/install.md�������������������������������������������������������������0000664�0000000�0000000�00000012124�14773033502�0020103�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Installing Liquidsoap You can install liquidsoap using binary builds, with OPAM or from source. Binary builds are provided with our releases, either in the form of debian/ubuntu and alpine packages or as docker images (also for debian or alpine). Your favorite distribution may also have binary packages. The binary package and docker images we provide are compiled in two flavors: - The main `liquidsoap` packages are compiled with all available features and functions. This is a good starting point for general-purpose development - Binary packages and docker images labelled `-minimal` are compiled without the extra libraries and with a limited set of essential optional features Minimal builds are useful if you are concerned about size or memory usage. They also reduce the chances of running into issues that could be introduced by optional dependencies that you do not use. If your script works with them, they are recommended over the fully featured builds for production. Each binary build that we provide have a corresponding `*.config` file. This is a text file that lists all the features included in a specific build. You can consult it to know what features are available. You can also get the same information by calling `liquidsoap --build-config`, for instance when using a docker image. Binary packages and docker images are useful in that they provide a readily available liquidsoap installation. If you need more finer-grained build or if your distribution/OS does not have a binary build, you can install via OPAM, which is a very convenient package manager that can compile liquidsoap from sources and knows how to handle external dependencies for most OS/distributions. Lastly, compiling from source should be reserved to developers. - [Debian/Ubuntu](#debianubuntu) - [Alpine](#alpine) - [Docker](#docker) - [Windows](#windows) - [Using OPAM](#install-using-opam) - [From source](#installing-from-source) ## Debian/Ubuntu We generate debian and ubuntu packages automatically as part of our [release process](https://github.com/savonet/liquidsoap/releases). Otherwise, you can check out the official [debian](https://packages.debian.org/liquidsoap) and [ubuntu](https://packages.ubuntu.com/liquidsoap) packages. ## Alpine Alpine packages are also provided as part of our [release process](https://github.com/savonet/liquidsoap/releases). ## Docker We provide production-ready docker images via [Docker hub](https://hub.docker.com/r/savonet/liquidsoap). Docker images are tagged with a release tag (e.g. `v2.1.4`) and with the sha of their git commit (e.g. `a24bf49`). For instance, to fetch release `2.3.1`, you would do: ```shell docker pull savonet/liquidsoap:v2.3.1 ``` Please note that images tagged with a release tag may change while images tagged with a commit sha will not. ## Windows You can download a liquidsoap for windows from our [release page](https://github.com/savonet/liquidsoap/releases). ## Install using OPAM The recommended method to install liquidsoap from source is by using the [OCaml Package Manager](http://opam.ocaml.org/). OPAM is available in all major distributions and on windows. We actively support the liquidsoap packages there and its dependencies. You can read [here](https://opam.ocaml.org/doc/Usage.html) about how to use OPAM. In order to use it: - [you should have at least OPAM version 2.1](https://opam.ocaml.org/doc/Install.html), - not all version of the OCaml compiler are supported. You can run `opam info liquidsoap-lang` to find out. You can create a switch for a specific OCaml version as follows: ``` opam switch create <ocaml version> ``` A typical installation with most expected features is done by executing: ``` opam install ffmpeg liquidsoap ``` This will install `liquidsoap` along with the optional `ffmpeg` package, which provides most of the expected functionalities (encoding, decoding, metadata support etc) out of the box. The `opam` installer also handles external dependencies that is, dependencies from your operating system that are required for your install. Typically, this would be the `ffmpeg` shared libraries here, as well as `libcurl`, which is required for `liquidsoap` to install. In most cases, `opam` will simply ask for your permission to install these dependencies on your behalf. In some cases, however, you will have install them yourself. Most of liquidsoap's dependencies are only optional. For instance, if you want to enable opus encoding and decoding after you've already installed liquidsoap, you should execute the following: ``` opam install opus ``` This will install `opus` and its dependencies and recompile `liquidsoap` to take advantage of it. `opam info liquidsoap` should give you the list of all optional dependencies that you may enable in liquidsoap. **Note** `opam` handles external dependencies via your system's packaging. In order to build some of the associated OCaml modules, macos users using `homebrew` might need to add the following to their environment/shell configuration: ```shell export CPATH=/opt/homebrew/include export LIBRARY_PATH=/opt/homebrew/lib ``` ## Installing from source See the [build instructions](build.html) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/json.md����������������������������������������������������������������0000664�0000000�0000000�00000021257�14773033502�0017415�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������## Importing JSON values _Note:_ If you are reading this page for the first time, you might want to skip directly to the explicit type annotation below as this is the recommended way of parsing JSON data. The content before that is here to explain the inner workings of JSON parsing in `liquidsoap`. Liquidsoap supports importing JSON values through a special `let` syntax. Using this syntax makes it relatively natural to parse JSON data in your script while keeping type-safety at runtime. Here's an example: ```{.liquidsoap include="json1.liq"} ``` This prints: ``` We parsed a JSON object and got value abc for attribute foo! ``` What happened here is that liquidsoap kept track of the fact that `v` was called with `v.foo` and that the result of that was a string. Then, at runtime, it checks the parsed JSON value against this type and raises an issue if that did not match. For instance, the following script: ```liquidsoap let json.parse v = '{"foo": 123}' print("We parsed a JSON object and got value " ^ v.foo ^ " for attribute foo!") ``` raises the following exception: ``` Error 14: Uncaught runtime error: type: json, message: "Parsing error: json value cannot be parsed as type {foo: string, _}" ``` Of course, this all seems pretty trivial presented like that but, let's switch to reading a file instead: ```liquidsoap let json.parse v = file.contents("/path/to/file.json") print("We parsed a JSON object and got value " ^ v.foo ^ " for attribute foo!") ``` Now, this is getting somewhere! Let's push it further and parse a whole `package.json` from a typical `npm` package: ```liquidsoap # Content of package.json is: # { # "name": "my_package", # "version": "1.0.0", # "scripts": { # "test": "echo \"Error: no test specified\" && exit 1" # }, # ... let json.parse package = file.contents("/path/to/package.json") name = package.name version = package.version test = package.scripts.test print("This is package " ^ name ^ ", version " ^ version ^ " with test script: " ^ test) ``` And we get: ``` This is package my_package, version 1.0.0 with test script: echo "Error: no test specified" && exit 1 ``` This can even be combined with _patterns_: ```liquidsoap let json.parse { name, version, scripts = { test } } = file.contents("/path/to/package.json") print("This is package " ^ name ^ ", version " ^ version ^ " with test script: " ^ test) ``` Now, this is looking nice! ## Explicit type annotation Explicit type annotation are the recommended way to parse JSON data. Let's try a slight variation of the previous script now: ```liquidsoap let json.parse { name, version, scripts = { test } } = file.contents("/path/to/package.json") print("This is package #{name}, version #{version} with test script: #{test}") ``` This returns: ``` This is package null, version null with test script: null ``` What? 🤔 This is because, in this script, we only use `name`, `version`, etc.. through the interpolation syntax `#{...}`. However, interpolated variables can be anything so this does not leave enough information to the typing system to know what type those variables should be and, in this case, we default to `null`. In order to avoid bad surprises like this, it is usually recommended to add **type annotations** to your json parsing call to explicitly state what kind of data you are expecting. Let's add one here: ```liquidsoap let json.parse ({ name, version, scripts = { test } } : { name: string, version: string, scripts: { test: string } }) = file.contents("/path/to/package.json") print("This is package #{name}, version #{version} with test script: #{test}") ``` And we get: ``` This is package my_package, version 1.0.0 with test script: echo "Error: no test specified" && exit 1 ``` Back to normal! ### Type syntax The syntax for type annotation is as follows: #### Ground types `string`, `int`, `float` are parsed as, resp., a string, an integer or a floating point number. Note that if your json value contains an integer such as `123`, parsing it as a floating point number will succeed. Also, if an integer is too big to be represented as an `int` internally, it will be parsed as a floating point number. #### Nullable types All type annotation can be postfixed with a trailing `?` to denote a _nullable_ value. If a type is nullable, the json parser will return `null` when it cannot parse the value as the principal type. This is particularly useful when you are not sure of all the types that you are parsing. For instance, some `npm` packages do not have a `scripts` entry or a `test` entry, so you would parse them as: ```liquidsoap let json.parse ({ name, version, scripts, } : { name: string, version: string, scripts: { test: string? }? }) = file.contents("/path/to/package.json") ``` And, later, inspect the returned value to see if it is in fact present. You can do it in several ways: ```liquidsoap # Check if the value is defined: test = if null.defined(scripts) then null.get(scripts.test) else null () end # Use the ?? syntax: test = (scripts ?? { test = null() }).test ``` #### Tuple types The type `(int * float * string)` tells liquidsoap to parse a JSON array whose _first values_ are of type: `int`, `float` and `string`. If any further values are present in the array, they will be ignored. For arrays as well as any other structured types, the special notation `_` can be used to denote any type. For instance, `(_ * _ * float)` denotes an JSON array whose first 2 elements can be of any type and its third element is a floating point number. #### Lists The type `[int]` tells liquidsoap to parse a JSON array where _all its values_ are integers as a list of integers. If you are not sure if all elements in the array are integers, you can always use nullable integers: `[int?]` #### Objects The type `{foo: int}` tells liquidsoap to parse a JSON object as a record with an attribute labelled `foo` whose value is an integer. All other attributes are ignored. Arbitrary object keys can be parsed using the following syntax: `{"foo bar key" as foo_bar_key: int}`, which tells liquidsoap to parse a JSON object as a record with an attribute labelled `foo_bar_key` which maps to the attribute `"foo bar key"` from the JSON object. #### Associative lists as objects It can sometimes be useful to parse a JSON object as an associative list, for instance if you do not know in advance all the possible keys of an object. In this case, you can use the special type: `[(string * int)] as json.object`. This tells liquidsoap to parse the JSON object as a list of pairs `(string * int)` where `string` represents the attribute label and `int` represent the attribute value. If you are not sure if all the object values are integers you can always use nullable integers: `[(string * int?)] as json.object` ### Parsing errors When parsing fails, a `error.json` is raised which can be caught at runtime: ```liquidsoap try let json.parse ({ status, data = { track } } : { status: string, data: { track: string } }) = res # Do something on success here.. catch err: [error.json] do # Do something on parse errors here.. end ``` #### Example Here's a full example. Feel free to refer to `tests/language/json.liq` in the source code for more of them. ```{.liquidsoap include="json-ex.liq"} ``` It returns ``` - x : { foo = 34.24, gni_gno = true, nested = { tuple = (null, 3.14), list = [44., 55., 66.12], nullable_list = [null, 23, null], object_as_list = [("foo", 123.), ("gni", 456.0), ("gno", 3.14)], arbitrary_object_key = true, not_present = null } } ``` ### JSON5 extension Liquidsoap supports the [JSON5](https://json5.org/) extension. Parsing of `json5` values is enabled with the following argument: ```liquidsoap let json.parse[json5=true] x = ... ``` If a `json5` variable is in scope, you can also simply use `let json.parse[json5] x = ...` ## Exporting JSON values Exporting JSON values can be done using the `json.stringify` function: ```{.liquidsoap include="json-stringify.liq"} ``` Please note that not all values are exportable as JSON, for instance function. In such cases the function will raise an `error.json` exception. ## Generic JSON objects Generic `JSON` objects can be manipulated through the `json()` operator. This operator returns an opaque json variable with methods to `add` and `remove` attributes: ```liquidsoap j = json() j.add("foo", 1) j.add("bla", "bar") j.add("baz", 3.14) j.add("key_with_methods", "value".{method = 123}) j.add("record", { a = 1, b = "ert"}) j.remove("foo") s = json.stringify(j) - s: '{ "record": { "b": "ert", "a": 1 }, "key_with_methods": "value", "bla": "bar", "baz": 3.14 }' ``` �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/ladspa.md��������������������������������������������������������������0000664�0000000�0000000�00000003672�14773033502�0017711�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# LADSPA plugins in Liquidsoap [LADSPA](http://www.ladspa.org/) is a standard that allows software audio processors and effects to be plugged into a wide range of audio synthesis and recording packages. If enabled, Liquidsoap supports LADSPA plugins. In this case, installed plugins are detected at run-time and are all available in Liquidsoap under a name of the form: `ladspa.plugin`, for instance `ladspa.karaoke`, `ladspa.flanger` etc.. The full list of those operators can be found using `liquidsoap --list-plugins`. Also, as usual, `liquidsoap -h ladspa.plugin` returns a detailed description of each LADSPA's operators. For instance: ``` ./liquidsoap -h ladspa.flanger *** One entry in scripting values: Flanger by Steve Harris <steve@plugin.org.uk>. Category: Source / Sound Processing Type: (?id:string,?delay_base:'a,?feedback:'b, ?lfo_frequency:'c,?max_slowdown:'d, source(audio='#e,video='#f,midi='#g))-> source(audio='#e,video='#f,midi='#g) where 'a, 'b, 'c, 'd is either float or ()->float Flag: hidden Parameters: * id : string (default "") Force the value of the source ID. * delay_base : anything that is either float or ()->float (default 6.32499980927) Delay base (ms) (0.1 <= delay_base <= 25). * feedback : anything that is either float or ()->float (default 0.) Feedback (-1 <= feedback <= 1). * lfo_frequency : anything that is either float or ()->float (default 0.334370166063) LFO frequency (Hz) (0.05 <= lfo_frequency <= 100). * max_slowdown : anything that is either float or ()->float (default 2.5) Max slowdown (ms) (0 <= max_slowdown <= 10). * (unlabeled) : source(audio='#e,video='#f,midi='#g) (default None) ``` For advanced users, it is worth nothing that most of the parameters associated with LADSPA operators can take a function, for instance in the above: ` max_slowdown : anything that is either float or ()->float` . This means that those parameters may be dynamically changed while running a liquidsoap script. ����������������������������������������������������������������������liquidsoap-2.3.2/doc/content/language.md������������������������������������������������������������0000664�0000000�0000000�00000140067�14773033502�0020230�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Liquidsoap's scripting language _The following is adapted from the [Liquidsoap book](book.html). The reader is avised to check out the whole chapter in the book for more details about the liquidsoap language_ ## General features Liquidsoap is a novel language which was designed from scratch to handle media stream. It takes some inspiration from functional languages such as [OCaml](https://ocaml.org/) but features a syntax that is more intuitive to the general purpose programmer, similar to Ruby or Javascript. ### Typing One of the main features of the language is that it is _typed_. This means that every expression belongs to some type which indicates what it is. For instance, `"hello"` is a _string_ whereas `23` is an _integer_, and, when presenting a construction of the language, we will always indicate the associated type. Liquidsoap implements a _typechecking_ algorithm which ensures that whenever a string is expected a string will actually be given, and similarly for other types. This is done without running the program, so that it does not depend on some dynamic tests, but is rather enforced by theoretical considerations. Another distinguishing feature of this algorithm is that it also performs _type inference_: you never actually have to write a type, those are guessed automatically by Liquidsoap. This makes the language very safe, while remaining very easy to use. ### Functional programming The language is _functional_, which means that you can very easily define functions, and that functions can be passed as arguments of other functions. This might look like a crazy thing at first, but it is actually quite common in some language communities (such as OCaml). It also might look quite useless: why should we need such functions when describing webradios? You will soon discover that it happens to be quite convenient in many places: for handlers (we can specify the function which describes what to do when some event occurs such as when a DJ connects to the radio), for transitions (we pass a function which describes the shape we want for the transition) and so on. ### Streams The unique feature of Liquidsoap is that it allows the manipulation of _sources_ which are functions which will generate streams. These streams typically consist of stereo audio data, but we do restrict to this: they can contain audio with arbitrary number of channels, they can also contain an arbitrary number of video channels, and also MIDI channels (there is limited support for sound synthesis). ### Standard library Although the core of Liquidsoap is written in OCaml, many of the functions of Liquidsoap are written in the Liquidsoap language itself. Those are defined in the `stdlib.liq` script, which is loaded by default and includes all the libraries. You should not be frightened to have a look at the standard library, it is often useful to better grasp the language, learn design patterns and tricks, and add functionalities. Its location on your system is indicated in the variable `configure.libdir` and can be obtained by typing ## Basic values ### Integers and floats The _integers_, such as `3` or `42`, are of type `int`. Depending on the current architecture of the computer on which we are executing the script (32 or 64 bits, the latter being the most common nowadays) they are stored on 31 or 63 bits. The minimal (resp. maximal) representable integer can be obtained as the constant `min_int` (resp. `max_int`); typically, on a 64 bits architecture, they range from -4611686018427387904 to 4611686018427387903. The _floating point numbers_, such as `2.45`, are of type `float`, and are in double precision, meaning that they are always stored on 64 bits. We always write a decimal point in them, so that `3` and `3.` are not the same thing: the former is an integer and the latter is a float. This is a source of errors for beginners, but is necessary for typing to work well. ### Strings Strings are written between double or single quotes, e.g. `"hello!"` or `'hello!'`, and are of type `string`. In order to write the character "`"`" in a string, one cannot simply type "`"`" since this is already used to indicate the boundaries of a string: this character should be _escaped_, which means that the character "`\`" should be typed first so that ```liquidsoap print("My name is \"Sam\"!") ``` will actually display "`My name is "Sam"!`". Other commonly used escaped characters are "`\\`" for backslash and "`\n`" for new line. Alternatively, one can use the single quote notation, so that previous example can also be written as ```liquidsoap print('My name is "Sam"!') ``` This is most often used when testing JSON data which can contain many quotes or for command line arguments when calling external scripts. The character "`\`" can also be used at the end of the string to break long strings in scripts without actually inserting newlines in the strings. For instance, the script ```liquidsoap print("His name is \ Romain.") ``` will actually print ``` His name is Romain. ``` Note that there is no line change between "is" and "Romain", and the indentation before "Romain" is not shown either. The concatenation of two strings is achieved by the infix operator "`^`", as in ```liquidsoap user = "dj" print("Current user is " ^ user) ``` Instead of using concatenation, it is often rather convenient to use _string interpolation_: in a string, `#{e}` is replaced by the string representation of the result of the evaluation of the expression `e`: ```liquidsoap user = "admin" print("The user #{user} has just logged.") ``` will print `The user admin has just logged.` or ```liquidsoap print("The number #{random.float()} is random.") ``` will print `The number 0.663455738438 is random.` (at least it did last time I tried). #### Escaping strings Liquidsoap strings follow the most common lexical conventions from `C` and `javascript` and `JSON`, in particular, `string.unescape` recognizes the same escape sequences as `C` (except for `UTF-16` characters) and javascript. The following sequences are recognized: | Escape sequence | Hex value in ASCII | Character represented | | --------------- | ------------------ | ------------------------------------------------------------------------------------- | | `\a` | `\x07` | Alert (Beep, Bell) | | `\b` | `\x08` | Backspace | | `\e` | `\x1B` | Escape character | | `\f` | `\x0C` | Formfeed, Page Break | | `\n` | `\x0A` | Newline (Line Feed) | | `\r` | `\x0D` | Carriage Return | | `\t` | `\x09` | Horizontal Tab | | `\v` | `\x0B` | Vertical Tab | | `\\` | `\x5C` | Backslash | | `\/` | `\x2f` | Forward slash | | `\'` | `\x27` | Apostrophe or single quotation mark | | `\"` | `\x22` | Double quotation mark | | `\?` | `\x3F` | Question mark (used to avoid Digraphs and trigraphs) | | `\nnn` | any | The byte whose numerical value is given by _nnn_ interpreted as an _octal_ number | | `\xhh` | any | The byte whose numerical value is given by _hh_ interpreted as a _hexadecimal_ number | | `\uhhhh` | none | UTF8-8 code point given by _hhhh_ interpreted as an _hexadecimal_ number | This convention has been decided to follow the most common practices. In particular, `\nnn` is an _octal_ escape sequence in most languages including C, Ruby, Javascript, Python and more. This differs from OCaml where `\nnn` is considered a _digital_ escape sequence. These lexical conventions are used in the default `string.escape` and `string.unescape`. Here's an example of an escaped string: ``` # "\" \t \045 \x2f \u4f32";; - : string = "\" \t % / 2" ``` The function `string.quote` returns [JSON-compatible](https://www.json.org/json-en.html) strings. ### Regular expressions _This feature was introduced in liquidsoap version 2.1.0_ Regular expressions can be created using the `regexp` operator or the syntactic sugar: `r/.../<flags>`. For instance: ```liquidsoap # Using the regexp operator: r = regexp(flags=["g","i"], "foo([\\w])+bar") # Using the r/../ syntactic sugar: r = r/foo([\w])bar/gi ``` Using the `r/../` syntactic sugar makes it possible to write regular expressions without having to escape `\` characters, which makes them more easily readable. Regular expression flags are: - `i`: perform case-insensitive match - `g`: substitute all matched sub-strings, not just the first one - `s`: match all characters, including `\n` when using the `.` pattern - `m`: `^` and `$` match before/after newlines, not just at the beginning/end of a string Regular expressions have the following methods: - `replace(fn, s)`: replace matched substrings of `s` using function `fn`. If the `g` flag is not passed, only the first match is replaced otherwise, all matches are replaced - `split(s)`: split the given string on all substrings matching the regular expression. - `test(s)`: returns `true` if the given string matches the regular expression. - `exec(s)`: execute the regular expression and return a of list matches of the form: `[(<match index>, <match>), ..]`. Named matches are also supported and returned as property `groups` of type `[string * string]`: ```liquidsoap r/(foo)(?<gno>gni)?/g.exec("foogni") - : [int * string].{groups : [string * string]} = [ (2, "gni"), (1, "foo"), (0, "foogni") ].{ groups = [ ("gno", "gni") ] } ``` ### Booleans The _booleans_ are either `true` or `false` and are of type `bool`. They can be combined using the usual boolean operations - `and`: conjunction, - `or`: disjunction, - `not`: negation. Booleans typically originate from comparison operators, which take two values and return booleans: - `==`: compares for equality, - `!=`: compares for inequality, - `<=`: compares for inequality, and so on (`<`, `>=`, `>`). For instance, the following is a boolean expression: ```liquidsoap (n < 3) and not (s == "hello") ``` _Conditional branchings_ execute code depending on whether a condition is true or not. For instance, the code ```liquidsoap if (1 <= x and x <= 12) or (not 10h-15h) then print("The condition is satisfied") else print("The condition ain't satisfied") end ``` will print that the condition is satisfied when either `x` is between 1 and 12 or the current time is not between 10h and 15h. A conditional branching might return a value, which is the last computed value in the chosen branch. For instance, ```liquidsoap y = if x < 3 then "A" else "B" end ``` will assign `"A"` or `"B"` to `y` depending on whether `x` is below 3 or not. The two branches of a conditional should always have the same return type: ```liquidsoap x = if 1 == 2 then "A" else 5 end ``` will result in ``` At line 1, char 19-21: Error 5: this value has type string but it should be a subtype of int ``` meaning that `"A"` is a string but is expected to be an integer because the second branch returns an integer, and the two should be of same nature. The `else` branch is optional, in which case the `then` branch should be of type `unit`: ```liquidsoap if x == "admin" then print("Welcome admin") end ``` In the case where you want to perform a conditional branching in the `else` branch, the `elsif`{.liquidsoap} keyword should be used, as in the following example, which assigns 0, 1, 2 or 3 to `s` depending on whether `x` is `"a"`, `"b"`, `"c"` or something else: ```liquidsoap s = if x == "a" then 0 elsif x == "b" then 1 elsif x == "c" then 2 else 3 end ``` This is equivalent (but shorter to write) to the following sequence of imbricated conditional branchings: ```liquidsoap s = if x == "a" then 0 else if x == "b" then 1 else if x == "c" then 2 else 3 end end end ``` Finally, we should mention that the notation `c?a:b` is also available as a shorthand for `if c then a else b end`, so that the expression ```liquidsoap y = if x < 3 then "A" else "B" end ``` can be shortened to ```liquidsoap y = (x<3)?"A":"B" ``` (and people will think that you are a cool guy). #### Time predicates Time predicates are special boolean values such as `{0h-7h}`. These values are `true` or `false` depending on the current time. Some examples of time predicates are --- `{11h15-13h}` between 11h15 and 13h `{12h}` between 12h00 and 12h59 `{12h00}` at 12h00 `{00m}` on the first minute of every hour `{00m-09m}` on the first 10 minutes of every hour `{2w}` on Tuesday `{6w-7w}` on weekends --- Above, `w` stands for weekday: 1 is Monday, 2 is Tuesday, and so on. Sunday is both 0 and 7. Time predicate can also be parsed at runtime, for instance if you want to create them dynamically. The syntax is: ```liquidsoap # f = time.predicate("00m-30m");; f : () -> bool = <fun> ``` Be aware that, if parsing fails, it will raise `error.string`: ```liquidsoap # f = time.predicate("foo") Error 14: Uncaught runtime error: type: string, message: "Failed to parse foo as time predicate" ``` ### Unit Some functions, such as `print`, do not return a meaningful value: we are interested in what they are doing (e.g. printing on the standard output) and not in their result. However, since typing requires that everything returns something of some type, there is a particular type for the return of such functions: `unit`. Just as there are only two values in the booleans (`true` and `false`), there is only one value in the unit type, which is written `()`. This value can be thought of as the result of the expression saying "I'm done". ### Lists Some more elaborate values can be constructed by combining the previous ones. A first kind is _lists_ which are finite sequences of values, being all of the same type. They are constructed by square bracketing the sequence whose elements are separated by commas. For instance, the list ```liquidsoap [1, 4, 5] ``` is a list of three integers (1, 4 and 5), and its type is `[int]`, and the type of `["A", "B"]` would obviously be `[string]`. Note that a list can be empty: `[]`. You can extract list elements through _splats_ such as ```liquidsoap l = [1, 5, 7, 8, 9] let [x, _, z, ...t] = l ``` In this example, the value of `x` is `1`, the value of `z` is `7` and the value of `t` is [`8, 9]`. You can also combine lists in a similar way ```liquidsoap x = [1, ...[2, 3, 4], 5, ...[6, 7]] ``` In this example, the value of `x` is `[1, 2, 3, 4, 5, 6 ,7]` ### Tuples Another construction present in Liquidsoap is _tuples_ of values, which are finite sequences of values which, contrarily to lists, might have different types. For instance, ``` (3, 4.2, "hello") ``` is a triple (a tuple with three elements) of type ``` int * float * string ``` which indicate that the first element is an integer, the second a float and the third a string. Similarly to lists, there is a special syntax in order to access tuple elements. For instance, if `t` is the above tuple `(3, 4.2, "hello")`, we can write ```liquidsoap let (n, x, s) = t ``` which will assign the first element to the variable `n`, the second element to the variable `x` and the third element to the variable `s`. ## Programming primitives ### Variables We have already seen many examples of uses of _variables_: we use ```liquidsoap x = e ``` in order to assign the result of evaluating an expression `e` to a variable `x`, which can later on be referred to as `x`. Variables can be masked: we can define two variables with the same name, and at any point in the program the last defined value for the variable is used: ```liquidsoap n = 3 print(n) n = n + 2 print(n) ``` will print `3` and `5`. Contrarily to most languages, the value for a variable cannot be changed (unless we explicitly require this by using references, see below), so the above program does not modify the value of `n`, it is simply that a new `n` is defined. There is an alternative syntax for declaring variables which is ```liquidsoap def x = e end ``` It has the advantage that the expression `e` can spread over multiple lines and thus consist of multiple expressions, in which case the value of the last one will be assigned to `x`. This is particularly useful to use local variables when defining a value. ### References As indicated above, by default, the value of a variable cannot be changed. However, one can use a _reference_ in order to be able to do this. Those can be seen as memory cells, containing values of a given fixed type, which can be modified during the execution of the program. They are created with the `ref` keyword, with the initial value of the cell as argument. For instance, ```liquidsoap r = ref(5) ``` declares that `r` is a reference which contains `5` as initial value. Since `5` is an integer (of type `int`), the type of the reference `r` will be ``` ref(int) ``` meaning that its a memory cell containing integers. On such a reference, two operations are available. - One can obtain the value of the reference by applying the reference to `()`, so that `r()` denotes the value contained in the reference `r`, for instance ```liquidsoap x = r() + 4 ``` declares the variable `x` as being 9 (which is 5+4). - One can change the value of the reference by using the `:=` keyword, e.g. ```liquidsoap r := 2 ``` will assign the value 2 to `r`. Internally, this is done by calling the `set` method of the reference, so that the above is equivalent to writing ```liquidsoap r.set(2) ``` which used to be the syntax for some reference manipulations. ### Loops The usual looping constructions are available in Liquidsoap. The `for` loop repeatedly executes a portion of code with an integer variable varying between two bounds, being increased by one each time. For instance, the following code will print the integers `1`, `2`, `3`, `4` and `5`, which are the values successively taken by the variable `i`: ```liquidsoap for i = 1 to 5 do print(i) end ``` In practice, such loops could be used to add a bunch of numbered files (e.g. `music1.mp3`, `music2.mp3`, `music3.mp3`, etc.) in a request queue for instance. The `while` loop repeatedly executes a portion of code, as long a condition is satisfied. For instance, the following code doubles the contents of the reference `n` as long as its value is below `10`: ```liquidsoap n = ref(1) while n() < 10 do n := n() * 2 end print(n()) ``` The variable `n` will thus successively take the values `1`, `2`, `4`, `8` and `16`, at which point the looping condition `n() < 10` is not satisfied anymore and the loop is exited. The printed value is thus `16`. ## Functions Liquidsoap is built around the notion of function: most operations are performed by those. For some reason, we sometimes call _operators_ the functions acting on sources. Liquidsoap includes a standard library which consists of functions defined in the Liquidsoap language, including fairly complex operators such as `playlist` which plays a playlist or `crossfade` which takes care of fading between songs. ### Basics A function is a construction which takes a bunch of arguments and produces a result. For instance, we can define a function `f` taking two float arguments, prints the first and returns the result of adding twice the first to the second: ```liquidsoap def f(x, y) print(x) 2*x+y end ``` This function can also be written on one line if we use semicolons (`;`) to separate the instructions instead of changing line: ```liquidsoap def f(x, y) = print(x); 2*x+y end ``` The type of this function is ``` (int, int) -> int ``` The arrow `->` means that it is a function, on the left are the types of the arguments (here, two arguments of type `int`) and on the right is the type of the returned value of the function (here, `int`). In order to use this function, we have to apply it to arguments, as in ``` f(3, 4) ``` This will trigger the evaluation of the function, where the argument `x` (resp. `y`) is replaced by `3` (resp. `4`), i.e., it will print `3` and return the evaluation of `2*3+4`, which is `10`. ### Anonymous functions For concision in scripts, it is possible define a function without giving it a name, using the syntax ```liquidsoap fun (x) -> ... ``` This is called an _anonymous function_, and it is typically used in order to specify short handlers in arguments. #### Anonymous function with no arguments You will see that it is quite common to use anonymous functions with no arguments. For this reason, we have introduced a special convenient syntax for those and allow writing ```liquidsoap {...} ``` instead of ```liquidsoap fun () -> ... ``` ### Labeled arguments A function can have an arbitrary number of arguments, and when there are many of them it becomes difficult to keep track of their order and their order matter! For instance, the following function computes the sample rate given a number of samples in a given period of time: ```liquidsoap def samplerate(samples, duration) = samples / duration end ``` which is of type ``` (float, float) -> float ``` For instance, if you have 110250 samples over 2.5 seconds the samplerate will be `samplerate(110250., 2.5)` which is 44100. However, if you mix the order of the arguments and type `samplerate(2.5, 110250.)`, you will get quite a different result and this will not be detected by the typing system because both arguments have the same type. Fortunately, we can give _labels_ to arguments in order to prevent this, which forces explicitly naming the arguments. This is indicated by prefixing the arguments with a tilde "`~`": ```liquidsoap def samplerate(~samples, ~duration) = samples / duration end ``` The labels will be indicated as follows in the type: ``` (samples : float, duration : float) -> float ``` Namely, in the above type, we read that the argument labeled `samples` is a float and similarly for the one labeled `duration`. For those arguments, we have to give the name of the argument when calling the function: ```liquidsoap samplerate(samples=110250., duration=2.5) ``` The nice byproduct is that the order of the arguments does not matter anymore, the following will give the same result: ```liquidsoap samplerate(duration=2.5, samples=110250.) ``` Of course, a function can have both labeled and non-labeled arguments. ### Optional arguments Another useful feature is that we can give _default values_ to arguments, which thus become _optional_: if, when calling the function, a value is not specified for such arguments, the default value will be used. For instance, if for some reason we tend to generally measure samples over a period of 2.5 seconds, we can make this become the value for the `duration` parameter: ```{.liquidsoap include="samplerate3.liq" from="BEGIN" to="END"} ``` In this way, if we do not specify a value for the duration, its value will implicitly be assumed to be 2.5, so that the expression: ```liquidsoap samplerate(samples=110250.) ``` will still evaluate to 44100. Of course, if we want to use another value for the duration, we can still specify it, in which case the default value will be ignored: ```liquidsoap samplerate(samples=132300., duration=3.) ``` The presence of an optional argument is indicated in the type by prefixing the corresponding label with "`?`", so that the type of the above function is ``` (samples : float, ?duration : float) -> float ``` ### Getters We often want to be able to dynamically modify some parameters in a script. For instance, consider the operator `amplify`, which takes a float and an audio source and returns the audio amplified by the given volume factor: we can expect its type to be ``` (float, source('a)) -> source('a) ``` so that we can use it to have a radio consisting of a microphone input amplified by a factor 1.2 by ```liquidsoap mic = input.alsa() radio = amplify(1.2, mic) ``` In the above example, the volume 1.2 was supposedly chosen because the sound delivered by the microphone is not loud enough, but this loudness can vary from time to time, depending on the speaker for instance, and we would like to be able to dynamically update it. The problem with the current operator is that the volume is of type `float` and a float cannot change over time: it has a fixed value. In order for the volume to have the possibility to vary over time, instead of having a `float` argument for `amplify`, we have decided to have instead an argument of type ``` () -> float ``` This is a function which takes no argument and returns a float (remember that a function can take an arbitrary number of arguments, which includes zero arguments). It is very close to a float excepting that each time it is called the returned value can change: we now have the possibility of having something like a float which varies over time. We like to call such a function a _float getter_, since it can be seen as some kind of object on which the only operation we can perform is get the value. For instance, we can define a float getter by ```liquidsoap n = ref(0.) def f () n := n() + 1. n() end ``` Each time we call `f`, by writing `f()` in our script, the resulting float will be increased by one compared to the previous one: if we try it in an interactive session, we obtain ``` # f();; - : float = 1.0 # f();; - : float = 2.0 # f();; - : float = 3.0 ``` Since defining such arguments often involves expressions of the form ```liquidsoap fun () -> e ``` which is somewhat heavy, we have introduced the alternative syntax ```liquidsoap {e} ``` for it. Finally, in order to simplify things a bit, you will see that the type of amplify is actually ``` ({float}, source('a)) -> source('a) ``` where the type `{float}` means that both `float` and `() -> float` are accepted, so that you can still write constant floats where float getters are expected. What we actually call a _getter_ is generally an element of such a type, which is either a constant or a function with no argument. In order to work with such types, the standard library often uses the following functions: - `getter`, of type `({'a}) -> {'a}`, creates a getter, - `getter.get`, of type `({'a}) -> 'a`, retrieves the current value of a getter, - `getter.function`, of type `({'a}) -> () -> 'a`, creates a function from a getter. ### Recursive functions Liquidsoap supports functions which are _recursive_, i.e., that can call themselves. For instance, in mathematics, the factorial function on natural numbers is defined as fact(n)=1×2×3×...×n, but it can also be defined recursively as the function such that fact(0)=1 and fact(n)=n×fact(n-1) when n>0: you can easily check by hand that the two functions agree on small values of n (and prove that they agree on all values of n). This last formulation has the advantage of immediately translating to the following implementation of factorial: ```liquidsoap def rec fact(n) = if n == 0 then 1 else n * fact(n-1) end end ``` for which you can check that `fact(5)` gives 120, the expected result. As another example, the `list.length` function, which computes the length of a list, can be programmed in the following way in Liquidsoap: ```liquidsoap def rec length(l) if l == [] then 0 else 1 + length(list.tl(l)) end end ``` We do not detail much further this trait since it is unlikely to be used for radios, but you can see a few occurrences of it in the standard library. ## Records and modules ### Records Suppose that we want to store and manipulate structured data. For instance, a list of songs together with their duration and tempo. One way to store each song is as a tuple of type `string * float * float`, but there is a risk of confusion between the duration and the length which are both floats, and the situation would of course be worse if there were more fields. In order to overcome this, one can use a _record_ which is basically the same as a tuple, excepting that fields are named. In our case, we can store a song as ```liquidsoap song = { filename = "song.mp3", duration = 257., bpm = 132. } ``` which is a record with three fields respectively named `filename`, `duration` and `bpm`. The type of such a record is ``` {filename : string, duration : float, bpm : float} ``` which indicates the fields and their respective type. In order to access a field of a record, we can use the syntax `record.field`. For instance, we can print the duration with ```liquidsoap print("The duration of the song is #{song.duration} seconds") ``` Records can be re-used using _spreads_: ```liquidsoap song = { filename = "song.mp3", duration = 257., bpm = 132. } # This is a fresh value with all the fields from `song` and # a new `id` field: song_with_id = { id = 1234, ...song } ``` Alternatively, you can also extend a record using the explicit `v.{...}` syntax: ```liquidsoap song = { filename = "song.mp3", duration = 257., bpm = 132. } # This is a fresh value with all the fields from `song` and # a new `id` field: song_with_id = song.{id = 1234} ``` ### Modules Records are heavily used in Liquidsoap in order to structure the functions of the standard library. We tend to call _module_ a record with only functions, but this is really the same as a record. For instance, all the functions related to lists are in the `list` module and functions such as `list.hd` are fields of this record. For this reason, the `def` construction allows adding fields in record. For instance, the definition ```liquidsoap def list.last(l) list.nth(l, list.length(l)-1) end ``` adds, in the module `list`, a new field named `last`, which is a function which computes the last element of a list. Another shorter syntax to perform definitions consists in using the `let` keyword which allows assigning a value to a field, so that the previous example can be rewritten as ```liquidasoap let list.last = fun(l) -> list.nth(l, list.length(l)-1) ``` If you often use the functions of a specific module, the `open` keyword allows using its fields without having to prefix them by the module name. For instance, in the following example ```liquidsoap l = [1,2,3] open list x = nth(l, length(l)-1) ``` the `open list` directive allows directly using the functions in this module: we can simply write `nth` and `length` instead of `list.nth` and `list.length`. ### Values with fields A unique feature of the Liquidsoap language is that it allows adding fields to any value. We also call them _methods_ by analogy with object-oriented programming. For instance, we can write ```liquidsoap song = "test.mp3".{duration = 123., bpm = 120.} ``` which defines a string (`"test.mp3"`) with two methods (`duration` and `bpm`). This value has type ``` string.{duration : float, bpm : float} ``` and behaves like a string, e.g. we can concatenate it with other strings: ```liquidsoap print("the song is " ^ song) ``` but we can also invoke its methods like a record or a module: ```liquidsoap print("the duration is #{song.duration}") ``` The construction `def replaces` allows changing the main value while keeping the methods unchanged, so that ```liquidsoap def replaces song = "newfile.mp3" end print(song) ``` will print ``` "newfile.mp3".{duration = 123., bpm = 120.} ``` (note that the string is modified but not the fields `duration` and `bpm`). ### Optional fields During the execution of your script, it can be useful to allow functions to receive records that may or may not have a specific field. This can be used, for instance, to model optional arguments. This can be achieved in two ways: 1. Using the `x.foo ?? default` syntax Here's an example: ```liquidsoap # This functions adds 1 to x unless options has a # add field in which case it adds this value def f(x, options) = x + (options.add ?? 1) end ``` The type of this function is: ```liquidsoap f : (int, 'a.{add? : int}) -> int = <fun> ``` which denotes that the `options` argument can be any value that may or may not have a `add` field. However, if this field is present, it must be of type `int`. 2. Using the `x?.foo` syntax Given a variable `x`, `x?.foo` returns the field value `foo`, if present, or `null` otherwise. The `?.` syntax can be chained and works with functions, which make it a very convenient way to drill deep inside nested records: ```liquidsoap x?.fn(123, "aabb")?.field ``` ## Patterns As explained earlier, you can use several constructions to extract data from structured values such as `let [x, y] = l` and etc. These constructions are called _patterns_. Patterns allows to quickly access values nested deeply inside structured data in a way that remains pretty intuitive when reading the code. Patterns are constructed using _variable placeholders_, which are either a variable name such as: `x`, `foo`, etc. or the special symbol `_` for any ignored value. ### Tuple patterns Tuple patterns are pretty straight forward and consist of any sequence of variable captures: ```liquidsoap let (x, y, _, z) = (123, "aabbcc", true, 3.14) # x = 1, y = "aabbcc", z = 3.14 ``` ### List patterns List patterns are composed of variable placeholders, etc. and spreads of the form: `...<variable placeholder>` such as: `...z`. The spread `..._` can be simply written `...`. See below for an example. You can use any combination of: - Forward variable names: these capture the first elements of the list. - One spread: this captures any remaining element as a list. - Backward variable names: these capture the last elements of a the list. Here are some examples: ```liquidsoap # Forward capture: let [x, y, z] = [1, 2, 3] # x = 1, y = 2, z = 3 # Forward capture with spread: let [x, y, ...z] = [1, 2, 3, 4] # x = 1, y = 2, z = [3, 4] # Forward capture with ignored values: let [_, x, ...z] = [1, 2, 3, 4] # x = 2, z = [3, 4] # Full capture: let [x, y, ...z, t, u, v] = [1, 2, 3, 4, 5, 6, 7, 8, 9] # x = 1, y = 2, z = [3, 4, 5, 6, 7], t = 7, u = 8, v = 9 # Backward capture only. let [..., t, u, v] = [1, 2, 3, 4, 5] # t = 3, u = 4, v = 5 ``` ### Record and module patterns Record and module patterns consist of either variable names (not variable capture!), which capture method values or variable names with an associated pattern. Record patterns are of the form: `{<captured methods>}` while module patterns are of the form: `<variable capture>.{<captured methods>}` Here are some examples: ```liquidsoap # Record capture let {foo, bar} = {foo = 123, bar = "baz", gni = true} # foo = 123, bar = "baz" # Record capture with spread let {foo, bar, ...x} = {foo = 123, bar = "baz", gni = true} # foo = 123, bar = "baz", x = {gni = true} # Module capture let v.{foo, bar} = "aabbcc".{foo = 123, bar = "baz", gni = true} # v = "aabbcc", foo = 123, bar = "baz" # Module capture with ignored value let _.{foo, bar} = "aabbcc".{foo = 123, bar = "baz", gni = true} # foo = 123, bar = "baz" # Record capture with sub-patterns. Same works for module! let {foo = [x, y, z], gni} = {foo = [1, 2, 3], gni = "baz"} # foo = [1, 2, 3], x = 1, y = 2, z = 3, gni = "baz" # Record capture with optional methods: let { foo? } = () # foo = null() let { foo? } = { foo = 123 } # foo = 123 ``` ## Combining patterns As seen with record and modules, patterns can be combined at will, for instance, these are all valid patterns: ```liquidsoap let [{foo}, {gni}, ..., {baz}] = l let (_.{ bla = [..., z] }, t, _, u) = x ``` ## Advanced values In this section, we detail some more advanced values than the ones presented in. You are not expected to be understanding those in details for basic uses of Liquidsoap. ### Errors In the case where a function does not have a sensible result to return, it can raise an _error_. Typically, if we try to take the head of the empty list without specifying a default value (with the optional parameter `default`), an error will be raised. By default, this error will stop the script, which is usually not a desirable behavior. For instance, if you try to run a script containing ```liquidsoap list.hd([]) ``` the program will exit printing ``` Error 14: Uncaught runtime error: type: not_found, message: "no default value for list.hd" ``` This means that the error named "`not_found`" was raised, with a message explaining that the function did not have a reasonable default value of the head to provide. In order to avoid this, one can _catch_ exceptions with the syntax ```liquidsoap try code catch err do handler end ``` This will execute the instructions `code`: if an error is raised at some point during this, the code `handler` is executed, with `err` being the error. For instance, instead of writing ```liquidsaop l = [] x = list.hd(default=0, l) ``` we could equivalently write ```liquidsoap l = [] x = try list.hd(l) catch err do 0 end ``` The name and message associated to an error can respectively be retrieved using the error `kind` and `message` attributes, e.g. we can write ```liquidsoap try ... catch err do print("the error #{err.kind} was raised") print("the error message is #{err.message}") end ``` Typically, when reading from or writing to a file, errors will be raised when a problem occurs (such as reading from a non-existent file or writing a file in a non-existent directory) and one should always check for those and log the corresponding message: ```liquidsoap data = "bla" try file.write(data=data, "/non/existent/path") catch err do log.important("Could not write to file: #{error.message(err)}") end ``` Specific errors can be caught with the syntax ```liquidsoap try ... catch err : l do ... end ``` where `l` is a list of error names that we want to handle here. Errors can be raised from Liquidsoap with the function `error.raise`, which takes as arguments the error to raise and the error message. For instance: ```liquidsoap error.raise(error.not_found, "we could not find your result") ``` We should also mention that all the errors should be declared in advance with the function `error.register`, which takes as argument the name of the new error to register: ```liquidsoap myerr = error.register("my_error") error.raise(myerr, "testing my own error") ``` Lastly, if you need to make sure that a certain piece of code is executed whether or not there is an exception raised, you can use _finally_: ```liquidsoap # Without a catch block try ... finally ... end # With a catch block try ... catch ... do ... finally ... end ``` This is roughly equivalent to: ```liquidsoap finally_called = ref(false) def finally() = ... end try let ret = ... finally_called := true finally() ret # If specified: catch ... do let ret = ... if not finally_called() then finally() end ret end ``` The biggest different is that `finally` is called on all errors, including internal errors that cannot be caught by the runtime code. Errors raised in a `finally` block do override any previously raised errors. ### Nullable values It is sometimes useful to have a default value for a type. In Liquidsoap, there is a special value for this, which is called `null`. Given a type `t`, we write `t?` for the type of values which can be either of type `t` or be `null`: such a value is said to be _nullable_. For instance, we could redefine the `list.hd` function in order to return null (instead of raising an error) when the list is empty: ```liquidsoap def list.hd(l) if l == [] then null() else list.hd(l) end end ``` whose type would be ``` (['a]) -> 'a? ``` since it takes as argument a list whose elements are of type `'a` and returns a list whose elements are `'a` or `null`. As it can be observed above, the null value is created with `null()`. In order to use a nullable value, one typically uses the construction `x ?? d` which is the value `x` excepting when it is null, in which case it is the default value `d`. For instance, with the above head function: ```liquidsoap x = list.hd(l) print("the head is " ^ (x ?? "not defined")) ``` Some other useful functions include - `null.defined`: test whether a value is null or not, - `null.get`: obtain the value of a nullable value supposed to be distinct from `null`, - `null.case`: execute a function or another, depending on whether a value is null or not. ### Runtime evaluation of scripting values Similarly to how JSON is [parsed](json.html), you can evaluate string into values at runtime using the `eval` decorator. As with JSON, too, the recommended way to use it is by adding an explicit type annotation: ```liquidsoap let eval (x: {foo: int, bla: string}) = "{foo = 123, bla = \"gni\"}" print("x.foo = #{x.foo}, x.bla = #{x.bla}") ``` ### Including other files It is often useful to split your script over multiple files, either because it has become quite large, or because you want to be able to reuse common functions between different scripts. You can include a file `file.liq` in a script by writing ```liquidsoap %include "file.liq" ``` which will be evaluated as if you had pasted the contents of the file in place of the command. For instance, this is useful in order to store passwords out of the main file, in order to avoid risking leaking those when handing the script to some other people. Typically, one would have a file `passwords.liq` defining the passwords in variables, e.g. ```liquidsoap radio_pass = "secretpassword" ``` and would then use it by including it: ```liquidsoap %include "passwords.liq" radio = ... output.icecast(%mp3, host="localhost", port=8000, password=radio_pass, mount="my-radio.mp3", radio) ``` so that passwords are not shown in the main script. ### Code comments Comments can be added to your code in two ways: _Multi-line comments_ are comments that can span multiple lines. They are delimitated by the sequence of characters `#<` at the beginning and `>#` at the end. Anything in between those two sequences is considered code comment. Here are some examples: Simple multiline comments: ```liquidsoap #< This is a comment ># ``` Multiline comments can be nested: ```liquidsoap #< This is a top-level comment # This is also a comment #< This is a nested code comment ># ># ``` Fancy looking multiline comment ```liquidsoap #<------- BEGIN CODE COMMENT ----# Comments can also look like this #--------- END CODE COMMENT -----># ``` _Single-line comments_ are comments that are limited to the current line. Such comments are started with the character `#` without a following `<`. Anything after the initial `#` character and until the end of the line is considered code comment: ```liquidsoap def f(x) = # This is a single line comment. 123 end ``` ## Caching Type-checking scripts can take a lot of time and consume memory. To optimize things, this step can be cached. During the first execution, the script is parsed, type checked and evaluated. On second and any following execution, a cache of the script is used, reducing the typechecking phase, sometimes by a `100x` factor! Here's a log without caching on a M3 macbook pro: ``` 2024/07/03 14:31:41 [startup:3] main script hash computation: 0.03s 2024/07/03 14:31:41 [startup:3] main script cache retrieval: 0.03s 2024/07/03 14:31:41 [startup:3] stdlib hash computation: 0.03s 2024/07/03 14:31:41 [startup:3] stdlib cache retrieval: 0.03s 2024/07/03 14:31:41 [startup:3] Typechecking stdlib: 3.37s 2024/07/03 14:31:41 [startup:3] Typechecking main script: 0.00s ``` And the same log after caching: ``` 2024/07/03 14:32:59 [startup:3] main script hash computation: 0.02s 2024/07/03 14:32:59 [startup:3] Loading main script from cache! 2024/07/03 14:32:59 [startup:3] main script cache retrieval: 0.05s ``` Scripts can be cached ahead of time without executing them, for instance while compiling a docker image, using `--cache-only`. Caching can also be disabled using `--no-cache`. Caching happens at two different time: - First the standard library is cached - Then the script itself is cached Caching the standard library makes it possible to run the type-checker faster on new scripts. Here's an example of a log from running a new script with a cached standard library: ``` 2024/07/03 14:33:27 [startup:3] main script hash computation: 0.02s 2024/07/03 14:33:27 [startup:3] main script cache retrieval: 0.02s 2024/07/03 14:33:27 [startup:3] stdlib hash computation: 0.03s 2024/07/03 14:33:27 [startup:3] Loading stdlib from cache! 2024/07/03 14:33:27 [startup:3] stdlib cache retrieval: 0.10s 2024/07/03 14:33:27 [startup:3] Typechecking main script: 0.00s ``` Caching can be disabled by setting `LIQ_CACHE` to anything else than `"true"`. ### Cache locations Cache files can accumulate and also take up disk space so it is important to know where they are located! There are two type of cache locations: - System cache for cached files that should be shared with all liquidsoap scripts. This is where the standard library cache is located. This location is a system-wide path on unix system such as `/var/cache/liquidsoap`. - User cache for cached files that are specific to the user running liquidsoap scripts. On unix systems, this location is at `$HOME/.cache/liquidsoap`. On windows, the default cache directory for both type of cache locations is in the same directory as the binary. At runtime, `liquidsoap.cache(mode=<mode>)` returns the cache directory. `mode` should be one of: `"user"` or `"system"`. ### Cache maintenance There is a cache maintenance routine which deletes unused cache files after `10` days and keeps the cache to a maximum of `200` files. You can run the cache maintenance routing by calling `liquidsoap.cache.maintenance(mode=<mode>)` manually. Here, too, `mode` should be one of: `"user"` or `"system"`. ### Cache security Please be aware that the cache does _not_ encrypt its values. As such, user cache files should be considered sensitive as they may contain password and other runtime secrets that are available through your scripts. We recommend to: - Use environment variables as much as possible when passing secrets - Secure your user script and cache files. The default creation permissions for user cache files is: `0o600` so only the user creating them should be able to read them. You should make sure that your script permissions are also similarly restricted. ### Cache and memory usage One side-benefit from loading a script from cache is that the entire typechecking process is skipped. This can result is significant reduction in the initial memory consumption, typically down from about `375MB` to about `80MB`! If memory consumption is a concern but you are not sure you can cache your script, you can also set the environment variable `settings.init.compact_before_start` to `true`: ```liquidsoap settings.init.compact_before_start := true ``` This will run the OCaml memory compaction algorithm after typechecking your script but before running it. This will result in a similar memory footprint when running the script but will delay its initial startup time. ### Cache environment variables The following environment variables control the cache behavior: - `LIQ_CACHE`: disable the cache when set to anything else than `1` or `true` - `LIQ_CACHE_SYSTEM_DIR`: set the cache system directory - `LIQ_CACHE_SYSTEM_DIR_PERMS`: set the permission used when creating cache system directory (and its parents when needed). Default: `0o755` - `LIQ_CACHE_SYSTEM_FILE_PERMS`: set the permissions used when creating a system cache file. Default: `0o644` - `LIQ_CACHE_USER_DIR`: set the cache user directory - `LIQ_CACHE_USER_DIR_PERMS`: set the permission used when creating cache user directory (and its parents when needed). Default: `0o700`. - `LIQ_CACHE_USER_FILE_PERMS`: set the permissions used when creating a user cache file. Default: `0o600` - `LIQ_CACHE_MAX_DAYS`: set the maximum days a cache file can be stored before it is eligible to be deleted during the next cache maintenance pass. - `LIQ_CACHE_MAX_FILES`: set the maximum number of files in each cache directory. Older files are removed first. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/�������������������������������������������������������������������0000775�0000000�0000000�00000000000�14773033502�0016700�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/Makefile�����������������������������������������������������������0000664�0000000�0000000�00000000166�14773033502�0020343�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������all: check: @for i in *.liq; do \ echo -n "Testing $$i... "; \ ./liquidsoap --check $$i; \ echo " done"; \ done ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/append-silence.liq�������������������������������������������������0000664�0000000�0000000�00000002166�14773033502�0022303�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# The live source. We use a short buffer to switch more quickly to the source # when reconnecting. live_source = input.harbor("mount-point-name", buffer=3.) # A playlist source. playlist_source = playlist("/path/to/playlist") # Set to `true` when we should be adding silence. should_append = ref(false) # Append 5. of silence when needed. fallback_source = append( playlist_source, fun (_) -> if should_append() then should_append := false blank(duration=5.) else source.fail() end ) # Transition to live def to_live(playlist, live) = sequence([playlist,live]) end # Transition back to playlist def to_playlist(live, playlist) = # Ask to insert a silent track. should_append := true # Cancel current track. This will also set the playlist to play a new # track. If needed, `cancel_pending` can be used to for a new silent track # without skipping the playlist current track. fallback_source.skip() sequence([live, playlist]) end radio = fallback( track_sensitive=false, transitions=[to_live, to_playlist], [live_source, fallback_source] ) # END output.dummy(fallible=true, radio) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/archive-cleaner.liq������������������������������������������������0000664�0000000�0000000�00000000502�14773033502�0022434�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������thread.run( every=3600., { list.iter( fun (msg) -> log(msg, label="archive_cleaner"), list.append( process.read.lines( "find /archive/* -type f -mtime +31 -delete" ), process.read.lines( "find /archive/* -type d -empty -delete" ) ) ) } ) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/basic-radio.liq����������������������������������������������������0000664�0000000�0000000�00000001164�14773033502�0021566�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/liquidsoap # Log dir log.file.path.set("/tmp/basic-radio.log") # Music myplaylist = playlist("~/radio/music.m3u") # Some jingles jingles = playlist("~/radio/jingles.m3u") # If something goes wrong, we'll play this security = single("~/radio/sounds/default.ogg") # Start building the feed with music radio = myplaylist # Now add some jingles radio = random(weights=[1, 4], [jingles, radio]) # And finally the security radio = fallback(track_sensitive=false, [radio, security]) # Stream it out output.icecast( %vorbis, host="localhost", port=8000, password="hackme", mount="basic-radio.ogg", radio ) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/beets-amplify.liq��������������������������������������������������0000664�0000000�0000000�00000001074�14773033502�0022152�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������BEET = "/usr/bin/beet" # BEGIN def beets(id, query) = beets_src = blank.eat( id="#{id}_", start_blank=true, max_blank=1.0, threshold=-45.0, amplify( override="replaygain_track_gain", 1.0, request.dynamic( id=id, retry_delay=1., { request.create( string.trim( process.read( "#{BEET} random -f '$path' #{query}" ) ) ) } ) ) ) (beets_src : source) end ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/beets-protocol-short.liq�������������������������������������������0000664�0000000�0000000�00000000255�14773033502�0023507�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������protocol.add( "beets", fun (~rlog=_, ~maxtime=_, arg) -> list.hd( process.read.lines( "/home/me/path/to/beet random -f '$path' #{arg}" ) ) ) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/beets-protocol.liq�������������������������������������������������0000664�0000000�0000000�00000001105�14773033502�0022345�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������BEET = "/usr/bin/beets" # BEGIN def beets_protocol(~rlog, ~maxtime, arg) = timeout = maxtime - time() command = "#{BEET} random -f '$path' #{arg}" p = process.run(timeout=timeout, command) if p.status == "exit" and p.status.code == 0 then string.trim(p.stdout) else rlog( "Failed to execute #{command}: #{p.status} (#{p.status.code}) #{p.stderr}" ) null() end end protocol.add( "beets", beets_protocol, syntax= "same arguments as beet's random module, see \ https://beets.readthedocs.io/en/stable/reference/query.html" ) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/beets-source.liq���������������������������������������������������0000664�0000000�0000000�00000001016�14773033502�0022005�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������BEET = "/usr/bin/beet" # BEGIN def beets(id, query) = beets_src = request.dynamic(id=id, retry_delay=1., { request.create( string.trim( process.read("#{BEET} random -f '$path' #{query}") ) ) }) (beets_src:source) end all_music = beets("all_music", "") recent_music = beets("recent_music", "added:-1m..") rock_music = beets("rock_music", "genre:Rock") # END output.dummy(fallible=true,all_music) output.dummy(fallible=true,recent_music) output.dummy(fallible=true,rock_music) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/blank-detect.liq���������������������������������������������������0000664�0000000�0000000�00000000302�14773033502�0021737�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������s = playlist("my-playlist") # BEGIN def handler() = process.run( "/path/to/your/script to do whatever you want" ) end s = blank.detect(handler, s) # END output.dummy(fallible=true, s) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/blank-sorry.liq����������������������������������������������������0000664�0000000�0000000�00000000734�14773033502�0021656�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������live = input.pulseaudio() interlude = single("/path/to/sorryfortheblank.ogg") # After 5 sec of blank the microphone stream is ignored, which causes the stream # to fallback to interlude. As soon as noise comes back to the microphone the # stream comes back to the live -- thanks to track_sensitive=false. stream = fallback(track_sensitive=false, [blank.strip(max_blank=5., live), interlude]) # Put that stream to a local file output.file(%vorbis, "/tmp/hop.ogg", stream) ������������������������������������liquidsoap-2.3.2/doc/content/liq/check��������������������������������������������������������������0000775�0000000�0000000�00000000106�14773033502�0017700�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/sh # shellcheck disable=SC2068 ../../../liquidsoap --check $@ ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/complete-case.liq��������������������������������������������������0000664�0000000�0000000�00000003125�14773033502�0022131�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/liquidsoap # Lines starting with # are comments, they are ignored. # Put the log file in some directory where you have permission to write. log.file.path := "/tmp/<script>.log" # Print log messages to the console, can also be done by passing the -v option # to Liquidsoap. log.stdout := true # Use the telnet server for requests settings.server.telnet := true # A bunch of files and playlists, supposedly all located in the same base dir. default = single("~/radio/default.ogg") day = playlist("~/radio/day.pls") night = playlist("~/radio/night.pls") jingles = playlist("~/radio/jingles.pls") clock = single("~/radio/clock.ogg") # Play user requests if there are any, otherwise one of our playlists, and the # default file if anything goes wrong. radio = fallback( [ request.queue(id="request"), switch([({6h-22h}, day), ({22h-6h}, night)]), default ] ) # Add the normal jingles radio = random(weights=[1, 5], [jingles, radio]) # And the clock jingle radio = add([radio, switch([({true}, clock)])]) radio = mksafe(radio) # Add the ability to relay live shows full = fallback( track_sensitive=false, [input.http("http://localhost:8000/live.ogg"), radio] ) # Output the full stream in OGG and MP3 output.icecast( %mp3, host="localhost", port=8000, password="hackme", mount="radio", full ) output.icecast( %vorbis, host="localhost", port=8000, password="hackme", mount="radio.ogg", full ) # Output the stream without live in OGG output.icecast( %vorbis, host="localhost", port=8000, password="hackme", mount="radio_nolive.ogg", radio ) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/cross.custom.liq���������������������������������������������������0000664�0000000�0000000�00000001533�14773033502�0022053�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������music = sine() jingles = sine() # BEGIN # A function to add a source_tag metadata to a source: def source_tag(s, tag) = def f(_) = [("source_tag", (tag : string))] end metadata.map(id=tag, insert_missing=true, f, s) end # Tag our sources music = source_tag(music, "music") jingles = source_tag(jingles, "jingles") # Combine them with one jingle every 3 music tracks radio = rotate(weights=[1, 3], [jingles, music]) # Now a custom crossfade transition: def transition(a, b) = # If old or new source is not music, no fade if a.metadata["source_tag"] != "music" or a.metadata["source_tag"] != "music" then sequence([a.source, b.source]) else # Else, apply the standard transition cross.simple(a.source, b.source) end end # Apply it! radio = cross(duration=5., transition, radio) # END output.dummy(fallible=true, radio) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/crossfade.liq������������������������������������������������������0000664�0000000�0000000�00000006151�14773033502�0021363�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Smart transition for crossfade # @category Source / Fade # @param ~fade_in Fade-in duration, if any. # @param ~fade_out Fade-out duration, if any. # @param ~high Value, in dB, for loud sound level. # @param ~medium Value, in dB, for medium sound level. # @param ~margin Margin to detect sources that have too different sound level for crossing. # @param ~default Smart crossfade: transition used when no rule applies (default: sequence). # @param a Ending track # @param b Starting track def cross.smart( ~id=null(), ~fade_in=3., ~fade_out=3., ~default=(fun (a, b) -> (sequence([a, b]) : source)), ~high=-15., ~medium=-32., ~margin=4., a, b ) = id = string.id.default(default="crossfade", id) def log(~level=3, x) = log(label=id, level=level, x) end let fade.out = fun (s) -> fade.out(type="sin", duration=fade_out, s) let fade.in = fun (s) -> fade.in(type="sin", duration=fade_in, s) add = fun (a, b) -> add(normalize=false, [b, a]) # This is for the type system.. ignore(a.metadata["foo"]) ignore(b.metadata["foo"]) if # If A and B are not too loud and close, fully cross-fade them. a.db_level <= medium and b.db_level <= medium and abs(a.db_level - b.db_level) <= margin then log( "Old <= medium, new <= medium and |old-new| <= margin." ) log( "Old and new source are not too loud and close." ) log( "Transition: crossed, fade-in, fade-out." ) add(fade.out(a.source), fade.in(b.source)) elsif # If B is significantly louder than A, only fade-out A. # We don't want to fade almost silent things, ask for >medium. b.db_level >= a.db_level + margin and a.db_level >= medium and b.db_level <= high then log( "new >= old + margin, old >= medium and new <= high." ) log( "New source is significantly louder than old one." ) log( "Transition: crossed, fade-out." ) add(fade.out(a.source), b.source) elsif # Opposite as the previous one. a.db_level >= b.db_level + margin and b.db_level >= medium and a.db_level <= high then log( "old >= new + margin, new >= medium and old <= high" ) log( "Old source is significantly louder than new one." ) log( "Transition: crossed, fade-in." ) add(a.source, fade.in(b.source)) elsif # Do not fade if it's already very low. b.db_level >= a.db_level + margin and a.db_level <= medium and b.db_level <= high then log( "new >= old + margin, old <= medium and new <= high." ) log( "Do not fade if it's already very low." ) log( "Transition: crossed, no fade." ) add(a.source, b.source) # What to do with a loud end and a quiet beginning ? # A good idea is to use a jingle to separate the two tracks, # but that's another story. else # Otherwise, A and B are just too loud to overlap nicely, or the # difference between them is too large and overlapping would completely # mask one of them. log( "No transition: using default." ) default(a.source, b.source) end end �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/decoder-faad.liq���������������������������������������������������0000664�0000000�0000000�00000004444�14773033502�0021713�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������aac_mimes = [ "audio/aac", "audio/aacp", "audio/3gpp", "audio/3gpp2", "audio/mp4", "audio/MP4A-LATM", "audio/mpeg4-generic", "audio/x-hx-aac-adts" ] aac_filexts = ["m4a", "m4b", "m4p", "m4v", "m4r", "3gp", "mp4", "aac"] # Faad is not very selective so we are checking only file that end with a known # extension or mime type def faad_test(fname) = # Get the file's mime mime = file.mime(fname) ?? "" # Test mime if list.mem(mime, aac_mimes) then true else # Otherwise test file extension ret = string.extract(pattern="\\.(.+)$", fname) if list.length(ret) != 0 then ext = ret[1] list.mem(ext, aac_filexts) else false end end end if process.test( "which faad" ) then log( level=3, "Found faad binary: enabling external faad decoder and metadata resolver." ) faad_p = ( fun (f) -> "faad -w #{process.quote(f)} 2>/dev/null" ) def test_faad(fname) = if faad_test(fname) then channels = list.hd( default="", process.read.lines( "faad -i #{process.quote(fname)} 2>&1 | grep 'ch,'" ) ) ret = string.extract( pattern= ", (\\d) ch,", channels ) ret = if list.length(ret) == 0 then # If we pass the faad_test, chances are high that the file will # contain aac audio data.. "-1" else ret[1] end int_of_string(default=(-1), ret) else 0 end end decoder.oblivious.add( name="FAAD", description= "Decode files using the faad binary.", test=test_faad, faad_p ) def faad_meta(~metadata=_, file) = if faad_test(file) then ret = process.read.lines( "faad -i #{process.quote(file)} 2>&1" ) # Yea, this is tuff programming (again)! def get_meta(l, s) = ret = string.extract(pattern="^(\\w+):\\s(.+)$", s) if list.length(ret) > 0 then list.append([(ret[1], ret[2])], l) else l end end list.fold(get_meta, [], ret) else [] end end decoder.metadata.add("FAAD", faad_meta) end ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/decoder-flac.liq���������������������������������������������������0000664�0000000�0000000�00000001162�14773033502�0021717�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������flac_p = "/usr/bin/flac" def test_flac(f) = if process.test("which metaflac") then channels = list.hd(default="",process.read.lines("metaflac --show-channels #{process.quote(f)} 2>/dev/null")) #If the value is not an int, this returns 0 and we are ok :) int_of_string(channels) else if string.match(pattern="flac", f) then # We do not know the number of audio channels so setting to -1 (-1) else # All tests failed: no audio decodable using flac.. 0 end end end decoder.add(name="FLAC", description="Decode files using the flac decoder binary.", test=test_flac, flac_p) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/decoder-metaflac.liq�����������������������������������������������0000664�0000000�0000000�00000001442�14773033502�0022567�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������if process.test( "which metaflac" ) then log( level=3, "Found metaflac binary: enabling flac external metadata resolver." ) def flac_meta(~metadata=_, fname) = ret = process.read.lines( "metaflac --export-tags-to=- #{process.quote(fname)} 2>/dev/null" ) ret = list.map(fun (s) -> string.split(separator="=", s), ret) # Could be made better. def f(l', l) = if list.length(l) >= 2 then list.append([(list.hd(default="", l), list.nth(default="", l, 1))], l') else if list.length(l) >= 1 then list.append([(list.hd(default="", l), "")], l') else l' end end end list.fold(f, [], ret) end decoder.metadata.add("FLAC", flac_meta) end ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/dump-hourly.liq����������������������������������������������������0000664�0000000�0000000�00000000257�14773033502�0021700�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������s = sine() # BEGIN # A source to dump # s = ... # Dump the stream output.file( %wav, {time.string("/archive/%Y-%m-%d/%Y-%m-%d-%H_%M_%S.mp3")}, s, reopen_when={0m} ) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/dump-hourly2.liq���������������������������������������������������0000664�0000000�0000000�00000000346�14773033502�0021761�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������s = sine() # BEGIN filename = { time.string( '/archive/$(if $(title),"$(title)","Unknown \ archive")-%Y-%m-%d/%Y-%m-%d-%H_%M_%S.mp3' ) } output.file(%mp3, filename, s, reopen_on_metadata=fun (_) -> true) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/dynamic-source.liq�������������������������������������������������0000664�0000000�0000000�00000002115�14773033502�0022330�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������settings.init.force_start := true settings.server.telnet := true # Replace the path here with a path to some video files: s = playlist("/path/to/files") streams = ref([]) count = ref(0) enc = %ffmpeg(format = "flv", %audio.copy, %video.copy) def create_stream(url) = if list.assoc.mem(url, streams()) then "Stream for url #{url} already exists!" else out = output.url(id="restream-#{count()}", fallible=true, url=url, enc, s) count := count() + 1 streams := [...streams(), (url, out.shutdown)] "OK!" end end def delete_stream(url) = if not list.assoc.mem(url, streams()) then "Stream for url #{url} does not exists!" else shutdown = list.assoc(url, streams()) shutdown() streams := list.filter((fun (el) -> fst(el) != url), streams()) "OK!" end end server.register( namespace="restream", description= "Redirect a stream.", usage= "start <url>", "start", create_stream ) server.register( namespace="restream", description= "Stop a dynamic playlist.", usage= "stop <url>", "stop", delete_stream ) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/external-output.file.liq�������������������������������������������0000664�0000000�0000000�00000000207�14773033502�0023504�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������s = playlist("bla") # BEGIN output.file( %external(process="ffmpeg -i pipe:0 -f avi pipe:1", video = true), "/tmp/test.avi", s ) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/fallback.liq�������������������������������������������������������0000664�0000000�0000000�00000000202�14773033502�0021140�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# A fallback switch s = fallback([playlist("http://my/playlist"), single("/my/jingle.ogg")]) # END output.dummy(fallible=true, s) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/ffmpeg-filter-dynamic-volume.liq�����������������������������������0000664�0000000�0000000�00000001442�14773033502�0025066�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������def dynamic_volume(s) = def mkfilter(graph) = filter = ffmpeg.filter.volume.create(graph) def set_volume(v) = ignore(filter.process_command("volume", "#{v}")) end let {audio = audio_track} = source.tracks(s) audio_track = ffmpeg.filter.audio.input(graph, audio_track) filter.set_input(audio_track) audio_track = filter.output audio_track = ffmpeg.filter.audio.output(graph, audio_track) s = source( { audio=audio_track, metadata=track.metadata(audio_track), track_marks=track.track_marks(audio_track) } ) (s, set_volume) end ffmpeg.filter.create(mkfilter) end s = playlist("my_playlist") let (s, set_volume) = dynamic_volume(s) # END output.dummy(fallible=true, s) ignore(set_volume) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/ffmpeg-filter-flanger-highpass.liq���������������������������������0000664�0000000�0000000�00000000560�14773033502�0025357�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������def flanger_highpass(audio_track) = def mkfilter(graph) = audio_track = ffmpeg.filter.audio.input(graph, audio_track) audio_track = ffmpeg.filter.flanger(graph, audio_track, delay=10.) audio_track = ffmpeg.filter.highpass(graph, audio_track, frequency=4000.) ffmpeg.filter.audio.output(graph, audio_track) end ffmpeg.filter.create(mkfilter) end ������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/ffmpeg-filter-hflip.liq��������������������������������������������0000664�0000000�0000000�00000000412�14773033502�0023233�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������def hflip(video_track) = def mkfilter(graph) = video_track = ffmpeg.filter.video.input(graph, video_track) video_track = ffmpeg.filter.hflip(graph, video_track) ffmpeg.filter.video.output(graph, video_track) end ffmpeg.filter.create(mkfilter) end ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/ffmpeg-filter-hflip2.liq�������������������������������������������0000664�0000000�0000000�00000001276�14773033502�0023326�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������def hflip(s) = def mkfilter(graph) = let { audio = audio_track, video = video_track} = source.tracks(s) video_track = ffmpeg.filter.video.input(graph, video_track) video_track = ffmpeg.filter.hflip(graph, video_track) audio_track = ffmpeg.filter.audio.input(graph, audio_track) audio_track = ffmpeg.filter.acopy(graph, audio_track) video_track = ffmpeg.filter.video.output(graph, video_track) audio_track = ffmpeg.filter.audio.output(graph, audio_track) source({ audio = audio_track, video = video_track, metadata = track.metadata(audio_track), track_marks = track.track_marks(audio_track) }) end ffmpeg.filter.create(mkfilter) end ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/ffmpeg-filter-parallel-flanger-highpass.liq������������������������0000664�0000000�0000000�00000001575�14773033502�0027160�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������def parallel_flanger_highpass(s) = def mkfilter(graph) = audio_track = ffmpeg.filter.audio.input(graph, s.tracks.audio) let (audio, _) = ffmpeg.filter.asplit(outputs=2, graph, audio_track) let [a1, a2] = audio a1 = ffmpeg.filter.flanger(graph, a1, delay=10.) a2 = ffmpeg.filter.highpass(graph, a2, frequency=4000.) # For some reason, we need to enforce the format here. a1 = ffmpeg.filter.aformat( sample_fmts="s16", sample_rates="44100", channel_layouts="stereo", graph, a1 ) a2 = ffmpeg.filter.aformat( sample_fmts="s16", sample_rates="44100", channel_layouts="stereo", graph, a2 ) audio_track = ffmpeg.filter.amerge(inputs=2, graph, [a1, a2], []) ffmpeg.filter.audio.output(graph, audio_track) end ffmpeg.filter.create(mkfilter) end �����������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/ffmpeg-live-switch.liq���������������������������������������������0000664�0000000�0000000�00000000744�14773033502�0023114�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������s1 = input.rtmp(listen=false, "rtmp://....") s1 = ffmpeg.filter.bitstream.h264_mp4toannexb(s1) s2 = playlist("/path/to/playlist") s2 = ffmpeg.filter.bitstream.h264_mp4toannexb(s2) s = fallback(track_sensitive=false, [s1, s2]) mpegts = %ffmpeg(format = "mpegts", fflags = "-autobsf", %audio.copy, %video.copy) streams = [("mpegts", mpegts)] output_dir = "/tmp/hls" output.file.hls( playlist="live.m3u8", fallible=true, segment_duration=5., output_dir, streams, s ) ����������������������������liquidsoap-2.3.2/doc/content/liq/ffmpeg-relay-ondemand.liq������������������������������������������0000664�0000000�0000000�00000001357�14773033502�0023556�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������stream = input.http(start = false, "https://wwoz-sc.streamguys1.com/wwoz-hi.mp3") listeners_count = ref(0) def on_connect(~headers=_, ~uri=_, ~protocol=_, _) = listeners_count := listeners_count() + 1 if listeners_count() > 0 and not stream.is_started() then log("Starting input") stream.start() end end def on_disconnect(_) = listeners_count := listeners_count() - 1 if listeners_count() == 0 and stream.is_started() then log("Stopping input") stream.stop() end end blank = single("/tmp/blank.mp3") stream = fallback(track_sensitive=false, [stream, blank]) output.harbor( %ffmpeg(format="mp3", %audio.copy), format="audio/mpeg", mount="relay", on_connect=on_connect, on_disconnect=on_disconnect, stream) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/ffmpeg-relay.liq���������������������������������������������������0000664�0000000�0000000�00000001025�14773033502�0021763�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Input the stream, # from an Icecast server or any other source encoded_source = input.http("https://icecast.radiofrance.fr/fip-hifi.aac") # Send to one server here: output.icecast( %ffmpeg(format="adts", %audio.copy), fallible=true, mount="/restream", host="streaming.example.com", port=8000, password="xxx", encoded_source) # An another one here: output.icecast( %ffmpeg(format="adts", %audio.copy), fallible=true, mount="/restream", host="streaming2.example.com", port=8000, password="xxx", encoded_source) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/ffmpeg-shared-encoding-rtmp.liq������������������������������������0000664�0000000�0000000�00000001526�14773033502�0024667�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# An audio source... audio = sine() # Encode it in mp3 audio = ffmpeg.encode.audio(%ffmpeg(%audio(codec = "libmp3lame")), audio) # Send it to icecast output.icecast( %ffmpeg(format = "mp3", %audio.copy), host="...", password="...", mount="/stream", audio ) # A video source, for instance a static image video = single("image.png") # Encode it in h264 format video = ffmpeg.encode.video(%ffmpeg(%video(codec = "libx264")), video) # Mux it with the audio stream = source.mux.video(video=video, audio) # Copy encoder for the rtmp stream enc = %ffmpeg(format = "flv", %audio.copy, %video.copy) # Send to YouTube key = "..." url = "rtmp://a.rtmp.youtube.com/live2/#{key}" output.url(url=url, enc, stream) # Send to Facebook key = "..." url = "rtmps://live-api-s.facebook.com:443/rtmp/#{key}" output.url(self_sync=true, url=url, enc, stream) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/ffmpeg-shared-encoding.liq�����������������������������������������0000664�0000000�0000000�00000001165�14773033502�0023706�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Input the stream, from an Icecast server or any other source source = input.http("https://icecast.radiofrance.fr/fip-hifi.aac") # Make it infallible: source = mksafe(source) # Encode it in mp3: source = ffmpeg.encode.audio(%ffmpeg(%audio(codec = "libmp3lame")), source) # Send to one server here: output.icecast( %ffmpeg(format = "mp3", %audio.copy), mount="/restream", host="streaming.example.com", port=8000, password="xxx", source ) # An another one here: output.icecast( %ffmpeg(format = "mp3", %audio.copy), mount="/restream", host="streaming2.example.com", port=8000, password="xxx", source ) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/fixed-time1.liq����������������������������������������������������0000664�0000000�0000000�00000000217�14773033502�0021523�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������jingles = sine() playlist = sine() # BEGIN radio = switch([({0m-5m}, jingles), ({true}, playlist)]) # END output.dummy(fallible=true, radio) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/fixed-time2.liq����������������������������������������������������0000664�0000000�0000000�00000000243�14773033502�0021523�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������jingles = sine() playlist = sine() # BEGIN radio = switch([(predicate.activates({0m-5m}), jingles), ({true}, playlist)]) # END output.dummy(fallible=true, radio) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/frame-size.liq�����������������������������������������������������0000664�0000000�0000000�00000000625�14773033502�0021454�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������%ifndef input.alsa let input.alsa = blank %endif %ifndef output.alsa let output.alsa = output.dummy %endif # BEGIN # Set correct frame size: # This makes it possible to set any audio frame size. # Make sure that you do NOT use video in this case! video.frame.rate := 0 # Now set the audio frame size exactly as required: settings.frame.audio.size := 2048 input = input.alsa() output.alsa(input) # END �����������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/harbor-auth.liq����������������������������������������������������0000664�0000000�0000000�00000001103�14773033502�0021616�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������def auth(args) = # Call an external process to check the credentials: The script will return # the string "true" of "false". # # First call the script. Make sure to apply proper escaping of the arguments # to prevent command injection! ret = process.read.lines( "/path/to/script --user=#{args.user} --password=#{args.password}" ) # Then get the first line of its output. ret = list.hd(default="", ret) # Finally returns the boolean represented by the output (bool_of_string can # also be used). if ret == "true" then true else false end end �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/harbor-dynamic.liq�������������������������������������������������0000664�0000000�0000000�00000000647�14773033502�0022315�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Serveur settings settings.harbor.bind_addrs := ["0.0.0.0"] # An emergency file emergency = single("/path/to/emergency/single.ogg") # A playlist playlist = playlist("/path/to/playlist") # A live source live = input.harbor("live",port=8080,password="hackme") # fallback radio = fallback(track_sensitive=false, [live, playlist, emergency]) # output it output.icecast( %vorbis, mount="test", host="host", radio) �����������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/harbor-insert-metadata.liq�����������������������������������������0000664�0000000�0000000�00000001065�14773033502�0023746�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������s = sine() # BEGIN # s = some source # Create a source equipped with a `insert_metadata` method: s = insert_metadata(s) # The handler def set_meta(request, response) = # Filter out unusual metadata meta = metadata.export(request.query) # Grab the returned message ret = if meta != [] then s.insert_metadata(meta) "OK!" else "No metadata to add!" end response.html("<html><body><b>#{ret}</b></body></html>") end # Register handler on port 700 harbor.http.register(port=7000, method="GET", "/setmeta", set_meta) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/harbor-metadata.liq������������������������������������������������0000664�0000000�0000000�00000000427�14773033502�0022445�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������s=sine() # BEGIN meta = ref([]) # s = some source s.on_metadata(fun (m) -> meta := m) # Return the json content of meta def get_meta(_, response) = response.json(meta()) end # Register get_meta at port 700 harbor.http.register(port=7000, method="GET", "/getmeta", get_meta) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/harbor-redirect.liq������������������������������������������������0000664�0000000�0000000�00000000564�14773033502�0022470�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Redirect all files other than /admin.* to icecast, located at localhost:8000. def redirect_icecast(request, response) = response.redirect("http://localhost:8000#{request.path}") end # Register this handler at port 8005 (provided harbor sources are also served # from this port). harbor.http.register.regexp( port=8005, method="GET", r/^\/admin/, redirect_icecast ) ��������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/harbor-simple.liq��������������������������������������������������0000664�0000000�0000000�00000000400�14773033502�0022145�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Custom response def handler(req) = req.socket.write( "HTTP/1.0 201 YYR\r\nFoo: bar\r\n\r\n" ) req.socket.close() # Null indicates that we're using the socket directly. null() end harbor.http.register.simple("/custom", port=3456, handler) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/harbor-usage.liq���������������������������������������������������0000664�0000000�0000000�00000001016�14773033502�0021764�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������settings.harbor.bind_addrs := ["0.0.0.0"] # Some code... # This defines a source waiting on mount point /test-harbor live = input.harbor("test-harbor", port=8080, password="xxx") files = playlist("the-playlist") # This is the final stream. Uses the live source as soon as available, and # don't wait for an end of track, since we don't want to cut the beginning of # the live stream. # # You may insert a jingle transition here... radio = fallback(track_sensitive=false, [live, files]) output.dummy(fallible=true, radio) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/harbor.http.register.liq�������������������������������������������0000664�0000000�0000000�00000002521�14773033502�0023465�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������path = "/bla" # BEGIN def handler(request, response) = log( "Got a request on path #{request.path}, protocol version: #{ request.http_version }, method: #{request.method}, headers: #{request.headers}, query: #{ request.query }, body: #{request.body()}" ) # Set response code. Defaults to 200 response.status_code(201) # Set response status message. Uses `status_code` if not specified response.status_message("Created") # Replaces response headers response.headers([("X-Foo", "bar")]) # Set a single header response.header("X-Foo", "bar") # Set http protocol version response.http_version("1.1") # Same as setting the "Content-Type" header response.content_type("application/liquidsoap") # Set response data. Can be a `string` or a function of type `()->string` returning an empty string # when done such as `file.read` response.data("foo") # Advanced wrappers: # Sets content-type to json and data to `json.stringify({foo = "bla"})` response.json({foo="bla"}) # Sets `status_code` and `Location:` header for a HTTP redirect response. Takes an optional `status_code` argument. response.redirect("http://...") # Sets content-type to html and data to `"<p>It works!</p>"` response.html( "<p>It works!</p>" ) end harbor.http.register(port=8080, method="GET", path, handler) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/harbor.http.response.liq�������������������������������������������0000664�0000000�0000000�00000000670�14773033502�0023502�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������path = "/bla" # BEGIN def handler(request) = log( "Got a request on path #{request.path}, protocol version: #{ request.http_version }, method: #{request.method}, headers: #{request.headers}, query: #{ request.query }, body: #{request.body()}" ) http.response( content_type="text/html", data= "<p>ok, this works!</p>" ) end harbor.http.register.simple(port=8080, method="GET", path, handler) ������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/hls-metadata.liq���������������������������������������������������0000664�0000000�0000000�00000000712�14773033502�0021753�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������s = mksafe(playlist("playlist")) output.file.hls( "/tmp/path/to/directory", [ ("aac", %ffmpeg(format = "adts", %audio(codec = "aac")).{id3_version=3}), ( "ts-with-meta", %ffmpeg(format = "mpegts", %audio(codec = "aac")).{id3_version=4} ), ("ts", %ffmpeg(format = "mpegts", %audio(codec = "aac")).{id3=false}), ( "mp3", %ffmpeg(format = "mp3", %audio(codec = "libmp3lame")).{replay_id3=false} ) ], s ) ������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/hls-mp4.liq��������������������������������������������������������0000664�0000000�0000000�00000001651�14773033502�0020676�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������radio = mksafe(playlist("playlist")) aac_lofi = %ffmpeg(format="mp4", %audio( codec="aac", channels=2, ar=44100, b="192k" )) flac_hifi = %ffmpeg(format="mp4", strict="-2", %audio( codec="flac", channels=2, ar=44100 )) flac_hires = %ffmpeg(format="mp4", strict="-2", %audio( codec="flac", channels=2, ar=48000 )) streams = [("aac_lofi", aac_lofi), ("flac_hifi", flac_hifi), ("flac_hires", flac_hires)] output.file.hls(playlist="live.m3u8", "/tmp/path/to/directory", streams, radio) ���������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/http-input.liq�����������������������������������������������������0000664�0000000�0000000�00000000260�14773033502�0021521�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������url = "http://radiopi.org:8080/reggae" # BEGIN # url is a HTTP location, like http://radiopi.org:8080/reggae source = input.http(url) # END output.dummy(fallible=true, source) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/icy-update.liq�����������������������������������������������������0000664�0000000�0000000�00000001142�14773033502�0021451�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������def icy_update(v) = # Parse the argument l = string.split(separator=",", v) def split(l, v) = v = string.split(separator="=", v) if list.length(v) >= 2 then list.append(l, [(list.nth(v, 0, default=""), list.nth(v, 1, default=""))]) else l end end meta = list.fold(split, [], l) # Update metadata icy.update_metadata( mount="/mystream", password="hackme", host="myserver.net", meta ) "Done !" end server.register( "update", namespace="metadata", description= "Update metadata", usage= "update title=foo,album=bar,..", icy_update ) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/input.mplayer.liq��������������������������������������������������0000664�0000000�0000000�00000001204�14773033502�0022213�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Stream data from mplayer # @category Source / Input # @param s data URI. # @param ~restart restart on exit. # @param ~restart_on_error restart on exit with error. # @param ~buffer Duration of the pre-buffered data. # @param ~max Maximum duration of the buffered data. def input.mplayer( ~id="input.mplayer", ~restart=true, ~restart_on_error=false, ~buffer=0.2, ~max=10., s ) = input.external.rawaudio( id=id, restart=restart, restart_on_error=restart_on_error, buffer=buffer, max=max, "mplayer -really-quiet -ao pcm:file=/dev/stdout -vc null -vo null #{ process.quote(s) } 2>/dev/null" ) end ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/jingle-hour.liq����������������������������������������������������0000664�0000000�0000000�00000000173�14773033502�0021633�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������normal = sine() jingle = sine() # BEGIN s = add([normal, switch([({0m}, jingle)])]) # END output.dummy(fallible=true, s) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/json-ex.liq��������������������������������������������������������0000664�0000000�0000000�00000001171�14773033502�0020772�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������data = '{ "foo": 34.24, "gni gno": true, "nested": { "tuple": [123, 3.14, false], "list": [44.0, 55, 66.12], "nullable_list": [12.33, 23, "aabb"], "object_as_list": { "foo": 123, "gni": 456.0, "gno": 3.14 }, "arbitrary object key ✨": true }, "extra": "ignored" }' let json.parse ( x : { foo: float, "gni gno" as gni_gno: bool, nested: { tuple: (_ * float), list: [float], nullable_list: [int?], object_as_list: [(string * float)] as json.object, "arbitrary object key ✨" as arbitrary_object_key: bool, not_present: bool? } }) = data �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/json-stringify.liq�������������������������������������������������0000664�0000000�0000000�00000000071�14773033502�0022372�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������r = {artist="Bla", title="Blo"} print(json.stringify(r)) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/json1.liq����������������������������������������������������������0000664�0000000�0000000�00000000165�14773033502�0020443�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������let json.parse v = '{"foo": "abc"}' print("We parsed a JSON object and got value " ^ v.foo ^ " for attribute foo!") �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/liquidsoap���������������������������������������������������������0000775�0000000�0000000�00000000076�14773033502�0021003�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/sh # shellcheck disable=SC2068 ../../../liquidsoap $@ ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/live-switch.liq����������������������������������������������������0000664�0000000�0000000�00000000744�14773033502�0021652�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������s1 = input.rtmp(listen=false, "rtmp://....") s1 = ffmpeg.filter.bitstream.h264_mp4toannexb(s1) s2 = playlist("/path/to/playlist") s2 = ffmpeg.filter.bitstream.h264_mp4toannexb(s2) s = fallback(track_sensitive=false, [s1, s2]) mpegts = %ffmpeg(format = "mpegts", fflags = "-autobsf", %audio.copy, %video.copy) streams = [("mpegts", mpegts)] output_dir = "/tmp/hls" output.file.hls( playlist="live.m3u8", fallible=true, segment_duration=5., output_dir, streams, s ) ����������������������������liquidsoap-2.3.2/doc/content/liq/medialib-predicate.liq���������������������������������������������0000664�0000000�0000000�00000000160�14773033502�0023110�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������m = medialib("") # BEGIN def p(m) string.length(m["artist"]) == 5 end l = m.find(predicate=p) # END ignore(l) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/medialib.liq�������������������������������������������������������0000664�0000000�0000000�00000000217�14773033502�0021155�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������m = medialib(persistency="/tmp/medialib.json", "~/music/") l = m.find(artist_contains="Brassens") l = list.shuffle(l) output(playlist.list(l)) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/medialib.sqlite.liq������������������������������������������������0000664�0000000�0000000�00000000222�14773033502�0022451�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������m = medialib.sqlite(database="/tmp/medialib.sql", "~/music/") l = m.find(artist_contains="Brassens") l = list.shuffle(l) output(playlist.list(l)) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/multitrack-add-video-track.liq�������������������������������������0000664�0000000�0000000�00000001045�14773033502�0024522�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# A playlist of audio files s = playlist("a_playlist") # A static image image = single("/path/to/image.png") # Get the playlist's audio track, metadata and track marks let {audio = playlist_audio, metadata, track_marks} = source.tracks(s) # Get the video track from our static image let {video = image_video} = source.tracks(image) # Mux the audio tracks with the image s = source( { audio=playlist_audio, video=image_video, metadata=metadata, track_marks=track_marks } ) # END output.dummy(fallible=true, s) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/multitrack-add-video-track2.liq������������������������������������0000664�0000000�0000000�00000000372�14773033502�0024606�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# A playlist of audio files s = playlist("a_playlist") # A static image image = single("/path/to/image.png") # Mux the audio tracks with the image s = source(source.tracks(s).{video=source.tracks(image).video}) # END output.dummy(fallible=true, s) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/multitrack-default-video-track.liq���������������������������������0000664�0000000�0000000�00000000555�14773033502�0025423�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������s = playlist("a_playlist") # A default video source: image = single("/path/to/image.png") # Pick `s` video track if it has one, otherwise use the default one: video = source.tracks(s).video ?? source.tracks(image).video # Return a source that always has video: s = source(source.tracks(s).{video=video}) # END output.dummy(fallible=true, s) output.dummy(image) ���������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/multitrack.liq�����������������������������������������������������0000664�0000000�0000000�00000000241�14773033502�0021563�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������s = single("/path/to/movie.mkv") # Copy first audio track and video: output.file( %ffmpeg( %audio.copy, %video.copy ), "/path/to/copy.mkv", s ) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/multitrack2.liq����������������������������������������������������0000664�0000000�0000000�00000000403�14773033502�0021645�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������s = single("/path/to/movie.mkv") # Copy first audio track and video track # and re-encode second audio track: output.file( %ffmpeg( %audio.copy, %audio_2( channels=2, codec="aac" ), %video.copy ), "/path/to/copy.mkv", s ) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/multitrack3.liq����������������������������������������������������0000664�0000000�0000000�00000000367�14773033502�0021657�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������s = playlist("/path/to/playlist") # Copy first audio track and video track # and re-encode second audio track: output.file( fallible=true, %ffmpeg(%audio.copy, %audio_2(channels = 2, codec = "aac"), %video.copy), "/path/to/copy.mkv", s ) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/output.file.hls.liq������������������������������������������������0000664�0000000�0000000�00000001532�14773033502�0022453�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������s = mksafe(playlist("playlist")) aac_lofi = %ffmpeg(format = "mpegts", %audio(codec = "aac", channels = 2, ar = 44100)) aac_midfi = %ffmpeg( format = "mpegts", %audio(codec = "aac", channels = 2, ar = 44100, b = "96k") ) aac_hifi = %ffmpeg( format = "mpegts", %audio(codec = "aac", channels = 2, ar = 44100, b = "192k") ) streams = [("aac_lofi", aac_lofi), ("aac_midfi", aac_midfi), ("aac_hifi", aac_hifi)] def segment_name(metadata) = timestamp = int_of_float(time()) let {stream_name, duration, position, extname} = metadata "#{stream_name}_#{duration}_#{timestamp}_#{position}.#{extname}" end output.file.hls( playlist="live.m3u8", segment_duration=2.0, segments=5, segments_overhead=5, segment_name=segment_name, persist_at="/tmp/path/to/state.config", "/tmp/path/to/hls/directory", streams, s ) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/playlists.liq������������������������������������������������������0000664�0000000�0000000�00000000554�14773033502�0021437�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Shuffle, play every URI, start over. s1 = playlist("/my/playlist.txt") # Do not randomize s2 = playlist(mode="normal", "/my/pl.m3u") # The playlist can come from any URI, can be reloaded every 10 minutes. s3 = playlist(reload=600, "http://my/playlist.txt") # END output.dummy(fallible=true, s1) output.dummy(fallible=true, s2) output.dummy(fallible=true, s3) ����������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/prometheus-callback.liq��������������������������������������������0000664�0000000�0000000�00000000750�14773033502�0023336�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# A0 is_playing_metric = prometheus.gauge(labels=["source"], help="Whether source is playing.", "liquidsoap_is_playing") # A1 # B0 playlist = playlist(id="playlist", "my-playlist") set_playlist_is_playing = is_playing_metric(label_values=["radio"]) # B1 # C0 def check_if_ready(set_is_ready, s) = def callback() = if source.is_ready(s) then set_is_ready(1.) else set_is_ready(0.) end end callback end thread.run( every=1., check_if_ready(set_playlist_is_playing, playlist) ) ������������������������liquidsoap-2.3.2/doc/content/liq/prometheus-settings.liq��������������������������������������������0000664�0000000�0000000�00000000141�14773033502�0023434�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Prometheus settings settings.prometheus.server := true settings.prometheus.server.port := 9599 �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/radiopi.liq��������������������������������������������������������0000664�0000000�0000000�00000024103�14773033502�0021036�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/usr/bin/liquidsoap # Standard settings log.file := true log.file.path := "/var/log/liquidsoap/pi.log" log.stdout := false init.daemon := true init.daemon.pidfile.path := "/var/run/liquidsoap/pi.pid" # Enable telnet server settings.server.telnet.set(true) # Enable harbor for any external connection settings.harbor.bind_addrs.set(["0.0.0.0"]) # Verbose logs log.level.set(4) # We use the scheduler intensively, # therefore we create many queues. settings.scheduler.generic_queues.set(5) settings.scheduler.fast_queues.set(3) settings.scheduler.non_blocking_queues.set(3) # === Settings === # The host to request files stream = "XXXxXXXx" # The command to request files scripts = "ssh XXxxxXXX@#{stream} '/path/to/scripts/" # A substitution on the returned path sed = " | sed -e s#/path/to/files/#ftp://user:password@#{stream}/#'" # Enable replay gain enable_replaygain_metadata() pass = "XXxXXXXx" ice_host = "localhost" descr = "RadioPi" url = "http://radiopi.org" # === Live === # A live source, on which we strip blank (make the source unavailable when # streaming blank). live = blank.strip( input.harbor( id="live", port=8000, password=pass, buffer=8., max=20., "live.ogg" ), max_blank=10., threshold=-50. ) # This source relays the live data, when available, to the other streamer, in # uncompressed format (WAV). output.icecast( %wav, host=stream, port=8005, password=pass, mount="live.ogg", fallible=true, live ) # This source relays the live source to "live.ogg". This is used for debugging # purposes, to see what is sent to the harbor source. output.icecast( %vorbis, host="127.0.0.1", port=8080, password=pass, mount="live.ogg", fallible=true, live ) # This source starts an archive of the live stream when available title = '$(if $(title),"$(title)","Emission inconnue")$(if $(artist), " par \ $(artist)") - %m-%d-%Y, %H:%M:%S' output.file( %vorbis, reopen_on_metadata=fun (_) -> true, fallible=true, "/data/archives/brutes/" ^ title ^ ".ogg", live ) # === Channels === # Specialize the output functions def out(fmt, ~id, ~mount, ~name, ~genre, s) = output.icecast( fmt, id=id, description=descr, url=url, host=ice_host, port=8080, password=pass, fallible=true, mount=mount, name=name, genre=genre, s ) end def out_aac32(~id, ~mount, ~name, ~genre, s) = out(%fdkaac(bitrate = 32), id=id, mount=mount, name=name, genre=genre, s) end def out_aac(~id, ~mount, ~name, ~genre, s) = out(%fdkaac(bitrate = 64), id=id, mount=mount, name=name, genre=genre, s) end def out(~id, ~mount, ~name, ~genre, s) = out(%mp3, id=id, mount=mount, name=name, genre=genre, s) end # A file for playing during failures interlude = single("/home/radiopi/fallback.mp3") # Lastfm submission def lastfm(m) = if m["type"] == "chansons" and ( m["canal"] == "reggae" or m["canal"] == "Jazz" or m["canal"] == "That70Sound" ) then canal = if (m["canal"] == "That70Sound") then "70sound" else m["canal"] end username = "radiopi-" ^ canal audioscrobbler.api.track.scrobble.metadata( username=username, password="xXXxx", m ) end end # === Basic sources === # Custom crossfade to deal with jingles. def crossfade( ~start_next=5., ~fade_in=3., ~fade_out=3., ~default=(fun (a, b) -> sequence([a, b])), ~high=-15., ~medium=-32., ~margin=4., ~width=2., s ) = fade_out = fun (s) -> fade.out(type="sin", duration=fade_out, s) fade_in = fun (s) -> fade.in(type="sin", duration=fade_in, s) add = fun (a, b) -> add(normalize=false, [b, a]) log = fun (~level=3, x) -> log(label="crossfade", level=level, x) def transition(a, b) = list.iter( fun (x) -> log( level=4, "Before: #{x}" ), a.metadata ) list.iter( fun (x) -> log( level=4, "After : #{x}" ), b.metadata ) if a.metadata["type"] == "jingles" or b.metadata["type"] == "jingles" then log( "Old or new file is a jingle: sequenced transition." ) sequence([a.source, b.source]) elsif # If A and B are not too loud and close, fully cross-fade them. a.db_level <= medium and b.db_level <= medium and abs(a.db_level - b.db_level) <= margin then log( "Old <= medium, new <= medium and |old-new| <= margin." ) log( "Old and new source are not too loud and close." ) log( "Transition: crossed, fade-in, fade-out." ) add(fade_out(a.source), fade_in(b.source)) elsif # If B is significantly louder than A, only fade-out A. # We don't want to fade almost silent things, ask for >medium. b.db_level >= a.db_level + margin and a.db_level >= medium and b.db_level <= high then log( "new >= old + margin, old >= medium and new <= high." ) log( "New source is significantly louder than old one." ) log( "Transition: crossed, fade-out." ) add(fade_out(a.source), b.source) elsif # Opposite as the previous one. a.db_level >= b.db_level + margin and b.db_level >= medium and a.db_level <= high then log( "old >= new + margin, new >= medium and old <= high" ) log( "Old source is significantly louder than new one." ) log( "Transition: crossed, fade-in." ) add(a.source, fade_in(b.source)) elsif # Do not fade if it's already very low. b.db_level >= a.db_level + margin and a.db_level <= medium and b.db_level <= high then log( "new >= old + margin, old <= medium and new <= high." ) log( "Do not fade if it's already very low." ) log( "Transition: crossed, no fade." ) add(a.source, b.source) # What to do with a loud end and a quiet beginning? # A good idea is to use a jingle to separate the two tracks, # but that's another story. else # Otherwise, A and B are just too loud to overlap nicely, or the # difference between them is too large and overlapping would completely # mask one of them. log( "No transition: using default." ) default(a.source, b.source) end end cross(width=width, duration=start_next, transition, s) end # Create a radiopilote-driven source def channel_radiopilote(~skip=true, name) = log( "Creating canal #{name}" ) # Request function def req() = log( "Request for #{name}" ) ret = list.hd( process.read.lines( scripts ^ "radiopilote-getnext " ^ process.quote(name) ^ sed ) ) log( "Got answer: #{ret} for #{name}" ) request.create(ret) end # Create the dynamic source. s = request.dynamic(id="dyn_" ^ name, req, timeout=60.) # Apply normalization using replaygain information. s = amplify(1., override="replay_gain", s) # Skip blank when asked to s = if skip then blank.skip(s, max_blank=10., threshold=-40.) else s end # Submit new tracks on lastfm s = source.on_metadata(s, lastfm) # Tell the system when a new track is played s = source.on_metadata( s, fun (meta) -> process.run( scripts ^ "radiopilote-feedback " ^ process.quote(meta["canal"]) ^ " " ^ process.quote(meta["file_id"]) ^ "'" ) ) # Finally apply a smart crossfading crossfade(s) end # Basic source jazz = channel_radiopilote("jazz") discoqueen = channel_radiopilote("discoqueen") # Avoid skipping blank with classic music !! classique = channel_radiopilote(skip=false, "classique") That70Sound = channel_radiopilote("That70Sound") metal = channel_radiopilote("metal") reggae = channel_radiopilote("reggae") Rock = channel_radiopilote("Rock") # Group those sources in a separate # clock (good for multithreading/multicore) clock.assign_new([jazz, That70Sound, metal, reggae]) # === Mixing live === # To create a channel from a basic source, add: # - a new-track notification for radiopilote # - metadata rewriting # - the live shows # - the failsafe 'interlude' source to channels # - blank detection def mklive(s) = # Transition function: if transitioning to the live, fade out the old source # if transitioning from live, fade.in the new source. NOTE: We cannot skip the # current song because reloading new songs for all the sources when live # starts costs too much CPU. def trans(old, new) = if source.id(new) == source.id(live) then log( "Transition to live!" ) add([new, fade.out(old)]) elsif source.id(old) == source.id(live) then log( "Transitioning from live!" ) add([fade.in(new), old]) else log( "Dummy transition" ) new end end fallback( track_sensitive=false, transitions=[trans, trans, trans], [live, (s : source), interlude] ) end # Create a channel using mklive(), encode and output it to icecast. def mkoutput(~out=out, mount, source, name, genre) = out(id=(mount : string), mount=mount, name=name, genre=genre, mklive(source)) end # === Outputs === mkoutput( "jazz", jazz, "RadioPi - Canal Jazz", "jazz" ) mkoutput( "discoqueen", discoqueen, "RadioPi - Canal DiscoQueen", "discoqueen" ) mkoutput( "classique", classique, "RadioPi - Canal Classique", "classique" ) mkoutput( "That70Sound", That70Sound, "RadioPi - Canal That70Sound", "That70Sound" ) mkoutput( "metal", metal, "RadioPi - Canal Metal", "metal" ) mkoutput( "reggae", reggae, "RadioPi - Canal Reggae", "reggae" ) mkoutput( "Rock", Rock, "RadioPi - Canal Rock", "Rock" ) # Test outouts mkoutput( out=out_aac, "reggae.aacp", reggae, "RadioPi - Canal Reggae (64 kbits AAC+ test stream)", "reggae" ) mkoutput( out=out_aac32, "reggae.aacp32", reggae, "RadioPi - Canal Reggae (32 kbits AAC+ test stream)", "reggae" ) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/re-encode.liq������������������������������������������������������0000664�0000000�0000000�00000000623�14773033502�0021251�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# The input file, any format supported by liquidsoap input = "/tmp/input.mp3" # The output file target = "/tmp/output.ogg" # A source that plays the file once source = once(single(input)) # We use a clock with disabled synchronization clock.assign_new(sync="none", [source]) # Finally, we output the source to an ogg/vorbis file output.file(%vorbis, target, fallible=true, on_stop=shutdown, source) �������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/regular.liq��������������������������������������������������������0000664�0000000�0000000�00000000317�14773033502�0021051�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������promotions = sine() other_source = sine() # BEGIN # (1200 sec = 20 min) timed_promotions = delay(1200., promotions) main_source = fallback([timed_promotions, other_source]) # END output.dummy(main_source) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/replaygain-metadata.liq��������������������������������������������0000664�0000000�0000000�00000000223�14773033502�0023315�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������enable_replaygain_metadata() s = playlist("~/playlist") s = amplify(1., override="replaygain_track_gain", s) # END output.dummy(fallible=true, s) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/replaygain-playlist.liq��������������������������������������������0000664�0000000�0000000�00000000153�14773033502�0023400�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������enable_replaygain_metadata() s = replaygain(playlist("~/playlist")) # END output.dummy(fallible=true, s) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/request.dynamic.liq������������������������������������������������0000664�0000000�0000000�00000000526�14773033502�0022525�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������files = process.read.lines( "cat " ^ process.quote("playlist.pls") ) pos = ref(0) def get_next() = if files == [] then null() else file = list.nth(files, pos()) pos := pos() + 1 mod list.length(files) request.create(file) end end s = request.dynamic(get_next) # END output.dummy(fallible=true, s) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/rtmp.liq�����������������������������������������������������������0000664�0000000�0000000�00000000233�14773033502�0020367�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������s = playlist("my_playlist") enc = %ffmpeg( format="flv", listen=1, %audio.copy, %video.copy ) output.url(url="rtmp://host/app/instance", enc, s) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/samplerate3.liq����������������������������������������������������0000664�0000000�0000000�00000000245�14773033502�0021630�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# BEGIN def samplerate(~samples, ~duration=2.5) = samples / duration end # END print(samplerate(samples=110250.)) print(samplerate(samples=110250., duration=1.)) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/scheduling.liq�����������������������������������������������������0000664�0000000�0000000�00000000276�14773033502�0021541�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������night = sine() day = sine() # BEGIN # A scheduler, assuming you have defined the night and day sources s = switch([({0h-7h}, night), ({7h-24h}, day)]) # END output.dummy(fallible=true, s) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/seek-telnet.liq����������������������������������������������������0000664�0000000�0000000�00000000643�14773033502�0021632�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# A playlist source s = playlist("/path/to/music") # The server seeking function def seek(t) = t = float_of_string(default=0., t) log( "Seeking #{t} sec" ) ret = source.seek(s, t) "Seeked #{ret} seconds." end # Register the function server.register( namespace=source.id(s), description= "Seek to a relative position in source #{source.id(s)}", usage= "seek <duration>", "seek", seek ) ���������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/settings.liq�������������������������������������������������������0000664�0000000�0000000�00000000251�14773033502�0021245�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������log.level := 4 log.file := true log.stdout := true init.daemon := true audio.samplerate := 48000 audio.channels := 2 video.frame.width := 720 video.frame.height := 1280 �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/shoutcast.liq������������������������������������������������������0000664�0000000�0000000�00000000203�14773033502�0021417�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������source = single("audiofile.ogg") output.shoutcast( %mp3, host="shoutcast.example.org", port=8000, password="changeme", source ) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/single.liq���������������������������������������������������������0000664�0000000�0000000�00000000032�14773033502�0020663�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������single("/my/default.ogg") ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/source-cue.liq�����������������������������������������������������0000775�0000000�0000000�00000000733�14773033502�0021467�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!./liquidsoap radio = insert_metadata(sine()) f = radio.insert_metadata # BEGIN radio = source.cue( title= "My stream", file="backup.mp3", "/tmp/backup.cue", radio ) output.file(%mp3, "/tmp/backup.mp3", radio) # END thread.run( delay=1., {f([("artist", "artist1"), ("album", "album1"), ("title", "title1")])} ) thread.run( delay=2., {f([("artist", "artist2"), ("album", "album1"), ("title", "title2")])} ) thread.run(delay=3., shutdown) �������������������������������������liquidsoap-2.3.2/doc/content/liq/space_overhead.liq�������������������������������������������������0000775�0000000�0000000�00000000570�14773033502�0022364�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!./liquidsoap # This code was contributed by AzuraCast. Possible settings: # - less memory: space_overhead = 20 # - less cpu: space_overhead = 140 # - balanced: space_overhead = 80 # Optimize for memory usage over CPU: this results in a slightly increased CPU # usage and reduced memory usage. runtime.gc.set(runtime.gc.get().{space_overhead=20, allocation_policy=2}) ����������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/split-cue.liq������������������������������������������������������0000664�0000000�0000000�00000000666�14773033502�0021324�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Log to stdout log.file := false log.stdout := true log.level := 4 # Initial playlist cue = "/path/to/sheet.cue" # Create a playlist with this CUE sheet and tell Liquidsoap to shutdown when we # are done. s = playlist(cue, on_done=shutdown) # Shove all that to a output.file operator. output.file( %mp3(id3v2 = true, bitrate = 320), fallible=true, reopen_on_metadata=fun (_) -> true, "/path/to/$(track) - $(title).mp3", s ) ��������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/sqlite.liq���������������������������������������������������������0000775�0000000�0000000�00000003176�14773033502�0020722�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!../../../liquidsoap %ifdef sqlite # open-begin db = sqlite("/tmp/database.sql") # open-end # drop-begin db.table.drop("metadata") # drop-end # create-begin db.table.create( "metadata", preserve=true, [ ( "filename", "STRING PRIMARY KEY" ), ("artist", "STRING"), ("title", "STRING"), ("year", "INT") ] ) # create-end # insert-begin db.insert( table="metadata", { artist="Naps", title= "Best life", year=2021, filename="naps.mp3" } ) db.insert( table="metadata", { artist="Orelsan", title= "L'odeur de l'essence", year=2021, filename="orelsan.mp3" } ) # insert-end # count-begin n = db.count(table="metadata", where="year=2023") # count-end ignore(n) # select-begin l = db.select( table="metadata", where= "year >= 2000" ) # select-end # select2-begin find_artist = "Brassens" l' = db.select( table="metadata", where= "artist = #{sqlite.escape(find_artist)}" ) # select2-end ignore(l') # query-begin l'' = db.query( "SELECT * FROM metadata WHERE artist = 'bla'" ) # query-end ignore(l'') # play-begin files = list.map(fun (row) -> null.get(list.assoc("filename", row.to_list())), l) s = playlist.list(files) output(s) # play-end # play2-begin def f(row) = let sqlite.row (r : {filename: string, artist: string, title: string, year: int} ) = row r.filename end files = list.map(f, l) s = playlist.list(files) output(s) # play2-end # delete-begin db.delete( table="metadata", where= "year < 1900" ) # delete-end # exec-begin db.exec( "DROP TABLE IF EXISTS metadata" ) # exec-end %endif ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/srt-receiver.liq���������������������������������������������������0000664�0000000�0000000�00000000230�14773033502�0022014�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������s = input.srt( content_type= "application/ffmpeg;format=s16le,ch_layout=stereo,sample_rate=48000" ) # END output.dummy(fallible=true, s) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/srt-sender.liq�����������������������������������������������������0000664�0000000�0000000�00000000170�14773033502�0021473�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������s = sine() # BEGIN enc = %ffmpeg(format = "s16le", %audio(codec = "pcm_s16le", ac = 2, ar = 48000)) output.srt(enc, s) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/switch-show.liq����������������������������������������������������0000664�0000000�0000000�00000000330�14773033502�0021662�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������normal = sine() # BEGIN stripped_stream = blank.strip(input.http("http://myicecast:8080/live.ogg")) s = fallback(track_sensitive=false, [stripped_stream, blank.strip(normal)]) # END output.dummy(fallible=true, s) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/transcoding.liq����������������������������������������������������0000664�0000000�0000000�00000001272�14773033502�0021724�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Input the stream from an Icecast server or any other source url = "https://icecast.radiofrance.fr/fip-hifi.aac" input = mksafe(input.http(url)) # First transcoder: mp3 32 kbps. We also degrade the samplerate, and encode in # mono Accordingly, a mono conversion is performed on the input stream output.icecast( %mp3(bitrate=32, samplerate=22050, stereo=false), mount="/your-stream-32.mp3", host="streaming.example.com", port=8000, password="xxx", mean(input)) # Second transcoder: mp3 128 kbps using %ffmpeg output.icecast( %ffmpeg(format="mp3", %audio(codec="libmp3lame", b="128k")), mount="/your-stream-128.mp3", host="streaming.example.com", port=8000, password="xxx", input) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/video-anonymizer.liq�����������������������������������������������0000664�0000000�0000000�00000001743�14773033502�0022713�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Input from webcam cam = input.v4l2() # Detect faces (this generates a white disk over faces) mask = video.frei0r.opencvfacedetect(cam) # Pixellize the video censored = video.frei0r.pixeliz0r(block_width=0.1, block_height=0.1, cam) # Generate a mask for video without the face unmask = video.frei0r.invert0r(mask) # Put the pixellized face over the video s = video.frei0r.addition( video.frei0r.multiply(mask, censored), video.frei0r.multiply(unmask, cam)) # We have to bufferize the source s = buffer(buffer=0.1,mksafe(s)) # Input audio from microphone mic = input.pulseaudio() # Transpose sound to generate a funny voice mic = soundtouch(pitch=1.5, buffer(mic)) # Add sound to video s = source.mux.audio(audio=mic, s) # Let's hear the sound output.pulseaudio(fallible=true, s) # Let's see the video output.sdl(fallible=true, s) s = mksafe(s) # Output the video/sound into a file in theora/vorbis format output.file(%ogg(%theora(quality=63),%vorbis), "anonymous.ogv", s) �����������������������������liquidsoap-2.3.2/doc/content/liq/video-bluescreen.liq�����������������������������������������������0000664�0000000�0000000�00000001003�14773033502�0022634�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# The video of the bunny s = single("big_buck_bunny_720p_stereo.ogg") # Input from the webcam cam = input.v4l2() # Flip the video around a vertical axis so that it is easier to position # yourself cam = video.frei0r.flippo(x_axis=true, cam) # Make the white background transparent I had to tweak the precision parameter # so that I will be seen but not the wall cam = video.alpha.of_color(color=0xffffff, precision=0.64, cam) # Superpose the two videos s = add([s,cam]) # Output to SDL output.sdl(fallible=true, s) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/video-canvas-example.liq�������������������������������������������0000664�0000000�0000000�00000001205�14773033502�0023415�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������background = blank() # BEGIN # Change to video.canvas.virtual_10k.actual_720p etc.. to render # in different sizes without changing the values below! let {px, rem, vh, vw, width, height} = video.canvas.virtual_10k.actual_1080p video.frame.width := width video.frame.height := height background = video.add_image( x=0.3 @ vw, y=0.01 @ vh, width=1562 @ px, height=1562 @ px, file="/path/to/cover.jpg", background ) background = video.add_text( color=0xFCB900, speed=0, x=234 @ px, y=4437 @ px, size=1.5 @ rem, "Some text", background ) # END output.dummy(fallible=true, background) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/video-default-canvas.liq�������������������������������������������0000664�0000000�0000000�00000001020�14773033502�0023401�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Standard video canvas based off a `10k` virtual canvas. # @category Source / Video processing def video.canvas.virtual_10k = def make(width, height) = video.canvas.make( virtual_width=10000, actual_size={width=width, height=height}, font_size=160 ) end { actual_360p=make(640, 360), actual_480p=make(640, 480), actual_720p=make(1280, 720), actual_1080p=make(1920, 1080), actual_1440p=make(2560, 1440), actual_4k=make(3840, 2160), actual_8k=make(7680, 4320) } end ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/video-in-video.liq�������������������������������������������������0000664�0000000�0000000�00000000205�14773033502�0022222�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������s = blank() s2 = blank() # BEGIN s2 = video.scale(scale=0.2, x=10, y=10, s2) s = add([s, s2]) # END output.dummy(fallible=true, s) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/video-logo.liq�����������������������������������������������������0000664�0000000�0000000�00000000235�14773033502�0021453�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������s = blank() # BEGIN s = video.add_image( width=30,height=30, x=10,y=10, file="logo.jpg", s) # END output.dummy(fallible=true, s) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/video-osc.liq������������������������������������������������������0000664�0000000�0000000�00000001541�14773033502�0021300�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Set the OSC port to match TouchOSC's default port settings.osc.port := 8000 # Input from the webcam with sound cam = input.v4l2() mic = input.pulseaudio() s = source.mux.audio(audio=mic, cam) s = mksafe(s) # We get the angle from fader 3 angle = osc.float("/1/fader3", 0.) # we rescale the position of fader 3 so that it corresponds to a 2π rotation angle = fun() -> angle() * 2. * 3.1416 # ...and we rotate the video according to the angle s = video.rotate(angle=angle, s) # Change brightness according to fader 1 s = video.frei0r.brightness(brightness=osc.float("/1/fader1",0.5), s) # Change contrast according to fader 2 s = video.frei0r.contrast0r(contrast=osc.float("/1/fader2",0.5), s) # We have to buffer here otherwise we get clocks problems s = buffer(s) # Output sound and video output.pulseaudio(fallible=true, s) output.sdl(fallible=true, s) ���������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/video-simple.liq���������������������������������������������������0000664�0000000�0000000�00000000414�14773033502�0022003�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������s = single("video.mp4") output.icecast( %ffmpeg(format="ogg", %audio(codec="libvorbis"), %video(codec="libtheora") ), host="localhost", port=8000, password="hackme", mount="/videostream", s) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/video-static.liq���������������������������������������������������0000664�0000000�0000000�00000001055�14773033502�0022003�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������log.level := 4 audio = once(single("/tmp/bla.mp3")) video = single("/tmp/bla.jpg") # Mux audio and video source = source.mux.video(video=video,audio) # Disable real-time processing, to process with the maximum speed clock.assign_new(sync='none',[source]) # Encode video and copy audio: encoder = %ffmpeg(format="mp4", %audio.copy, %video(codec="libx264")) # Output to a theora file, shutdown on stop output.file(fallible=true,on_stop=shutdown, encoder, "/tmp/encoded-video.mp4", source) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/video-text.liq�����������������������������������������������������0000664�0000000�0000000�00000000240�14773033502�0021473�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������s = blank() s = video.add_text.sdl( font="/usr/share/fonts/truetype/ttf-dejavu/DejaVuSans.ttf", "Hello world!", s) output.dummy(fallible=true, s) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/video-transition.liq�����������������������������������������������0000664�0000000�0000000�00000000137�14773033502�0022706�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������s = blank() # BEGIN s = video.fade.in(transition="fade", duration=3., s) # END output.dummy(s) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/video-weather.liq��������������������������������������������������0000664�0000000�0000000�00000000250�14773033502�0022147�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������img = single("weather.jpg") cam = input.v4l2() cam = video.alpha.of_color(color=0x0000ff, precision=0.2, cam) s = add([img, cam]) # END output.dummy(fallible=true, s) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/liq/video-webcam.liq���������������������������������������������������0000664�0000000�0000000�00000000031�14773033502�0021743�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������output.sdl(input.v4l2()) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/memory.md��������������������������������������������������������������0000664�0000000�0000000�00000017741�14773033502�0017757�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������## Controlling memory usage When using liquidsoap in production, it can be important to understand how to control the memory footprint of the application. This is not an easy topic as there are several layers of memory management inside the application and also some trade-off considerations between memory footprint and CPU usage. As of writing (version `2.2.0`), some of the trade-off that we are making with the OCaml garbage collector do not seem satisfactory in some memory-intensive conditions. Hopefully, this will improve in future major release (`2.3.x` and later). But first, let's look at what's going on. ### The OCaml memory model The OCaml compiler provides a garbage collector. This module is able to track memory blocks used by the OCaml program and free them when they are not used without the programmer's intervention. This is done by scanning the memory currently allocated by the OCaml program to identify the memory blocks that are not in use anymore. While this is transparent to the user (you!), this also means that there will be extra CPU cycles dedicated to this operation. How often these cycle occur help controlling the growth of unused memory but with the understanding that _to minimize unused memory, more CPU cycles have to be dedicated to tracking it_. You can find more information about the OCaml garbage collection on [this page](https://ocaml.org/docs/garbage-collection). Inside liquidsoap scripts, the operations that the OCaml compiler provides to control the garbage collector are available within the `runtime.gc` module. The documentation for these operations can be found in the [OCaml Gc module documentation](https://v2.ocaml.org/api/Gc.html). Typically, to change the garbage collector parameters, one can do: ```{.liquidsoap include="space_overhead.liq" from=1} ``` These parameters and functions make it possible to experiment and see if you can find better parameters for your application. ### C memory allocations Not all the memory in the application is allocated by the OCaml garbage collector. External libraries such as `ffmpeg`, `libmp3lame` and etc. need to allocate their own memory. This is usually referred to as _C memory allocations_ though it does not have to be allocated by a program written in `C`.. Another, more technically appropriate is _heap memory_ though, dynamically memory allocated by the OCaml garbage collector also lives in the program's heap.. 😅 This type of memory is also cleaned up by the OCaml garbage collector. To do so, a _custom block_ is passed to the OCaml program with a reference to a C memory pointer and how to clean it up. When the OCaml program detects that this custom block is no longer in use, it triggers the required operations to clean its corresponding C memory. However, things get complicated when considering how to fine-tune the garbage collector to account for memory allocated on the C side.. Remember that, as we discussed in the previous section, the garbage collector has to consume CPU cycles to free up memory. And, in the case of memory allocated on the C side, a single OCaml value (usually a small amount of memory) can actually refer to a much larger amount of C memory. This is typically the case when the corresponding C memory represents decoded video frames, which is usually a fairly large amount of memory. In general, the trade-off is: if the garbage collector does not run often enough, a lot of these rather larger C memory blocks are lingering longer, which leads to potentially huge amount of memory needlessly consumed by the application. Conversely, if the garbage collector runs too often, memory usage is controlled but CPU usage is increased. As of now, the strategy implemented by the OCaml compiler consists in tracking the ratio of OCaml held memory vs. its corresponding C memory and running the garbage collector more often when this ratio increases. However, this is not optimal in cases where the application purposefully holds large amount of C memory such as when doing video processing. In the future, we would like to explore tightening up our control of this mechanism. It should be possible trick the garbage collector by not declaring the full anmount of allocated C memory to make it possible to run the memory cleaning operations on purpose and at specific times, typically after a streaming cycle has ended. Most of the tools for that are already exported in the scripting language so, we will make sure to report our progress on the [blog](https://liquidsoap.info/blog) for anyone to test it. ### Audio data format Another source of memory usage is the audio data format. By default, we store audio data using OCaml's native floating point numbers in order to be able to run the application, including audio processing (crossfade, filters, fades etc) at the best possible speed and CPU usage. However, OCaml's native float are stored using 64 bits (8 bytes), which is a large amount of memory per number. If you are concerned with reducing your audio memory footprint, for instance if your applications has a lot of audio sources with buffers, you can do a couple of things: 1. Use the [ffmpeg raw content](ffmpeg.html). This means storing all the audio content as ffmpeg audio frames. This is an opaque format that works very well if your script can use ffmpeg end-to-end, for instance processing audio using [ffmpeg filters](ffmpeg_filters.html).. 2. Use one of the `pcm_f32` or `pcm_s16` audio format. These formats are less opaque. Their data is stored in a C memory array and can be accessed by the OCaml program. Some, but not all, of our operators do support them transparently. When using `pcm_s16`, audio samples are stored as 16 bit signed integers (2 bytes, the audio CD format). When using `pcm_f32`, audio samples are stored as 32 bit float (4 bytes). 16 bit signed integers is probably enough for most applications and consumes 4 times less memory than OCaml's native floating point numbers. The `pcm_*` formats can be required by the encoders by adding `pcm_s16` or `pcm_f32` to their list of parameters. This will, in turn, inform all operators and decoders to operate with this format, if they support it: ```liquidsoap # Mp3 encoder, pcm_s16 encoder = %mp3(pcm_s16, channels=2) # Ogg/opus encoder, pcm_f32 encoder = %ogg(%vorbis(pcm_f32)) # FFmpeg AAC encoder, pcm_s16 encoder = %ffmpeg(format="mp4",%audio(pcm_s16, codec="aac")) ``` For both `pcm_*` and ffmpeg raw formats, you can use also conversion functions (`ffmpeg.raw.decode.*`, `ffmpeg.raw.encode.*`, `audio.decode.pcm_*`, `audio.encode.pcm_*`) to convert content back and forth. In general, working with the `pcm_*` formats is easier. If you know what you are doing, though, working with raw FFmpeg frames can also have some advantages. In both cases, there might be an increase in CPU usage if your script needs to process audio (for instance via a `crossfade`) when converting these formats back and forth. Finally, if you need to store large amount of audio data, for instance to create a one hour delay, you should consider using the `track.audio.defer` operator which was designed for this purpose. ### `jemalloc` Lastly, the user-land memory allocator [jemalloc](https://github.com/jemalloc/jemalloc) can be used to control all memory allocations (C and OCaml). This allocator is particularly good at preventing memory fragmentation, which is an important topic for an application like liquidsoap running short streaming cycle involving small amount of memory (FFmpeg frames etc). The allocator is enabled by installing the `jemalloc` opam package and is included in all our production builds (except windows). It also comes with a lot of customization options that are exported via the [runtime.jemalloc.\*](https://www.liquidsoap.info/doc-dev/reference.html#runtime.jemalloc.epoch) functions. If you want to explore more, we recommend [reading about it](https://engineering.fb.com/2011/01/03/core-data/scalable-memory-allocation-using-jemalloc/) and then exploring the [manual page](http://jemalloc.net/jemalloc.3.html) which contains details about all the available settings. �������������������������������liquidsoap-2.3.2/doc/content/metadata.md������������������������������������������������������������0000664�0000000�0000000�00000007172�14773033502�0020224�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Customize metadata using Liquidsoap Liquidsoap has several mechanism for manipulating the metadata attached to your stream. In this page we quickly detail and compare the different operators, see the [language reference](reference.html) for full details about them. **Warning**. The protocol used by Shoutcast and Icecast before version 2 does not support many fields. It mainly support one: `song`. So, if you need to customize the metadata displayed by these servers, you should customize only the `song` metadata. ## The annotate protocol The metadata are read from files, so the most simple way is to properly tag the files. However, if it not possible to modify the files for some reason, the `annotate` protocol can be used in playlists to insert and modify some metadata. For instance, in the playlist ``` annotate:title="Title 1",artist="Artist 1":music1.mp3 annotate:title="Title 2",artist="Artist 2":music2.mp3 ``` the title metadata for file music1.mp3 will be overridden and changed to ``Title 1'' (and similarly for the artist). ## Map metadata The `metadata.map` operator applies a specified function to transform each metadata chunk of a stream. It can be used to add or decorate metadata, but is also useful in more complex cases. A simple example using it: ```liquidsoap # A function applied to each metadata chunk def append_title(m) = # Grab the current title title = m["title"] # Return a new title metadata [("title","#{title} - www.station.com")] end # Apply metadata.map to s using append_title s = metadata.map(append_title, s) ``` The effect of `metadata.map` by default is to update the metadata with the returned values. Hence in the function `append_title` defined in the code above returns a new metadata for the label `title` and the other metadata remain untouched. You can change this by using the `update` option, and you can also remove any metadata (even empty one) using the `strip` option. See the documentation on `metadata.map` for more details. ## Insert metadata ### Using the telnet server This operator is used for inserting metadata using a server command. If you have an `server.insert_metadata` node named `ID` in your configuration, as in ``` server.insert_metadata(id="ID", source) ``` you can connect to the server (either telnet or socket) and execute commands like ``` ID.insert key1="val1",key2="val2",... ``` ### In Liquidsoap Sometimes it is desirable to change the metadata dynamically when an event occurs. In this case, the function `insert_metadata` (not to be confused with `server.insert_metadata` above) can be used: when applied to a source it returns a source with an added `insert_metadata` method. For instance, suppose that you want to insert metadata on the stream using the OSC protocol. When a pair of strings `title'' `The new title'' is received on `/metadata`, we want to change the title of the stream accordingly. This can be achieved as follows. ```liquidsoap # Our main music source s = playlist("...") s = mksafe(s) # Create a source with a `insert_metadata` method s = insert_metadata(s) # Handler for OSC events (gets pairs of strings) def on_meta(m) = # Extract the label label = fst(m) # Extract the value value = snd(m) # A debug message print("Insert metadata #{label} = #{value}") # Insert the metadata s.insert_metadata([(label,value)]) end # Call the above handler when we have a pair of strings on /metadata osc.on_string_pair("/metadata",on_meta) # Output on icecast output.icecast(%mp3,mount="test.mp3",s) ``` We can then change the title of the stream by sending OSC messages, for instance ``` oscsend localhost 7777 "/metadata" ss "title" "The new title" ``` ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/migrating.md�����������������������������������������������������������0000664�0000000�0000000�00000063410�14773033502�0020422�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Migrating to a new Liquidsoap version In this page, we list the most common catches when migrating to a new version of Liquidsoap. ### Generalities If you are installing via `opam`, it can be useful to create a [new switch](https://opam.ocaml.org/doc/Usage.html) to install the new version of `liquidsoap`. This will allow to test the new version while keeping the old version around in case you to revert to it. More generally, we recommend to always keep a version of your script around and also to make sure that you test your new script with a staging environment that is close to production. Streaming issues can build up over time. We do our best to release the most stable possible code but problems can arise from many reasons so, always best to first to a trial run before putting things to production! ## From 2.2.x to 2.3.x ### Script caching A mechanism for caching script was added. There are two caches, one for the standard library that is shared by all scripts, and one for individual scripts. Scripts should run the same way with or without caching. However, caching your script has two advantage: - The script starts much faster. - Much less memory is used when starting. This memory is used the first time running the script to typecheck it and more. This is what we're caching. You can pre-cache a script using the `--cache-only` command: ```liquidsoap $ liquidsoap --cache-only /path/to/script.liq ``` The location of the two caches can be found by running `liquidsoap --build-config`. You can also set them using the `$LIQ_CACHE_USER_DIR` and `$LIQ_CACHE_SYSTEM_DIR` environment variables. Typically, inside a docker container, to pre-cache a script you would set `$LIQ_CACHE_SYSTEM_DIR` to the appropriate location and then run `liquidsoap --cache-only`: ```dockerfile ENV LIQ_CACHE_USER_DIR=/path/to/liquidsoap/cache RUN mkdir -p $LIQ_CACHE_USER_DIR && \ liquidsoap --cache-only /path/to/script.liq ``` See [the language page](language.html#caching) for more details! ### Default frame size Default frame size has been set to `0.02s`, down from `0.04s` in previous releases. This should lower the latency of your liquidsoap script. See [this PR](https://github.com/savonet/liquidsoap/pull/4033) for more details. ### Crossfade transitions and track marks Track marks can now be properly passed through crossfade transitions. This means that you also have to make sure that your transition function is fallible! For instance, this silly transition function: ```liquidsoap def transition(_, _) = blank(duration=2.) end ``` Will never terminate! Typically, to insert a jingle you would do: ```liquidsoap def transition(old, new) = sequence([old.source, single("/path/to/jingle.mp3"), new.source]) end ``` ### Replaygain - There is a new `metadata.replaygain` function that extracts the replay gain value in _dB_ from the metadata. It handles both `r128_track_gain` and `replaygain_track_gain` internally and returns a single unified gain value. - The `file.replaygain` function now takes a new compute parameter: `file.replaygain(~id=null(), ~compute=true, ~ratio=50., file_name)`. The compute parameter determines if gain should be calculated when the metadata does not already contain replaygain tags. - The `enable_replaygain_metadata` function now accepts a compute parameter to control replaygain calculation. - The `replaygain` function no longer takes an `ebu_r128` parameter. The signature is now simply: `replaygain(~id=null(), s)`. Previously, `ebu_r128` allowed controlling whether EBU R128 or standard replaygain was used. However, EBU R128 data is now extracted directly from metadata when available. So `replaygain` cannot control the gain type via this parameter anymore. ### Regular expressions The library providing regular expressions has been switched with `2.3.0`. This means that subtle differences can arise with the evaluation of some regular expressions. Here's an example that was recently reported: In `2.2.x`, this was true: ``` # When using a regular expression with a capture pattern to split, the value matched for splitting is returned: % string.split(separator="(:|,)", "foo:bar") ["foo", ":", "bar"] # But not when using a regular expression without matching: % string.split(separator=":|,", "foo:bar") ["foo", "bar"] ``` In `2.3.x`, the matched pattern is not returned: ``` % string.split(separator="(:|,)", "foo:bar") ["foo", "bar"] % string.split(separator=":|,", "foo:bar") ["foo", "bar"] ``` ### Static requests Static requests detection can now work with nested requests. Typically, a request for this URI: `annotate:key="value",...:/path/to/file.mp3` will be considered static if `/path/to/file.mp3` can be decoded. Practically, this means that more source will now be considered infallible, for instance a `single` using the above uri. In most cases, this should improve the user experience when building new scripts and streaming systems. In rare cases where you actually wanted a fallible source, you can still pass `fallible=true` to e.g. the `single` operator or use the `fallible:` protocol. ### String functions Some string functions have been updated to account for string encoding. In particular, `string.length` and `string.sub` now assume that their given string is in `utf8` by default. While this is what most user expect, this can lead to backward incompatibilities and new exceptions. You can change back to the old default by passing `encoding="ascii"` to these functions or using the `settings.string.default_encoding` settings. ### `check_next` `check_next` in playlist operators is now called _before_ the request is resolved, to make it possible to cut out unwanted requests before consuming process time. If you need to see the request's metadata or if the request resolves into a valid tile, however, you might need to call `request.resolve` inside your `check_next` script. ### Regular expressions The backend to interpret regular expressions has been changed. For the most part, all existing regular expressions should be supported but you might experience some incompatibilities with advanced/complex ones. Known incompatibilities include: - `(?P<name>pattern)` for named captures is not supported. `(?<name>pattern)` should be used instead. ### `segment_name` in HLS outputs To make segment name more flexible, `duration` (segment duration in seconds) and `ticks` (segment exact duration in liquidsoap's main ticks) have been added to the data available when calling `segment_name`. To prevent any further breakage of this function, its arguments have been changed to a single record containing all the available attributes: ```liquidsoap def segment_name(metadata) = "#{metadata.stream_name}_#{metadata.position}.#{metadata.extname}" end ``` ### `on_air` metadata Request `on_air` and `on_air_timestamp` metadata are deprecated. These values were never reliable. They are set at the request level when `request.dynamic` and all its derived sources start playing a request. However, a request can be used in multiple sources and the source using it can be used in multiple outputs or even not be actually being on the air if, for instance, it not selected by a `switch` or `fallback`. Instead, it is recommended to get this data directly from the outputs. Starting with `2.3.0`, all output now add `on_air` and `on_air_timestamp` to the metadata returned by `on_track`, `on_metadata` and `last_metadata` and the telnet `metadata` command. For the telnet `metadata` command, these metadata need to be added to the `settings.encoder.metadata.export` setting first. If you are looking for an event-based API, you can use the output's `on_track` methods to track the metadata currently being played and the time at which it started being played. For backward compatibility and easier migration, `on_air` and `on_air_timestamp` metadata can be enabled using the `settings.request.deprecated_on_air_metadata` setting: ```liquidsoap settings.request.deprecated_on_air_metadata := true ``` However, it is highly recommended to migrate your script to use one of the new method. ### `last_metadata` The implementation of `last_metadata` was updated to clear the last metadata when a new track begins. This is more in line with most user's expectation: last metadata is intended to reflect the metadata of the current track. If you need to, you can revert to the previous behavior using the source's `reset_last_metadata_on_track` method: ```liquidsoap s.reset_last_metadata_on_track := false ``` ### Gstreamer `gstreamer` was removed. It had been deprecated for a while. We expect `ffmpeg` to carry most, if not all of gstreamer's features. See [this PR](https://github.com/savonet/liquidsoap/pull/4036) for more details. ### Prometheus The default port for the Prometheus metrics exporter has changed from `9090` to `9599`. As before, you can change it with `settings.prometheus.server.port := <your port value>`. ### `source.dynamic` Many operators such as `single` and `request.once` have been reworked to use `source.dynamic` as their underlying implementation. The operator is now considered usable in production although we urge caution when using it: it is very powerful but can also break things! If you were (boldly!) using this operator before, the most important change is that its `set` method has been removed in favor of a unique callback API. ## From 2.1.x to 2.2.x ### References The `!x` notation for getting the value of a reference is now deprecated. You should write `x()` instead. And `x := v` is now an alias for `x.set(v)` (both can be used interchangeably). ### Icecast and Shoutcast outputs `output.icecast` and `output.shoutcast` are some of our oldest operators and were in dire need of some cleanup so we did it! We applied the following changes: - You should now use `output.icecast` only for sending to icecast servers and `output.shoutcast` only for sending to shoutcast servers. All shared options have been moved to their respective specialized operator. - Old `icy_metadata` argument was renamed to `send_icy_metadata` and changed to a nullable `bool`. `null` means guess. - New `icy_metadata` argument now returns a list of metadata to send with ICY updates. - Added a `icy_song` argument to generate default `"song"` metadata for ICY updates. Defaults to `<artist> - <title>` when available, otherwise `artist` or `title` if available, otherwise `null`, meaning don't add the metadata. - Cleaned up and removed parameters that were irrelevant to each operator, i.e. `icy_id` in `output.icecast` and etc. - Made `mount` mandatory and `name` nullable. Use `mount` as `name` when `name` is `null`. ### HLS events Starting with version `2.2.1`, on HLS outputs, `on_file_change` events are now `"created"`, `"updated"` and `"deleted"`. This breaking was required to reflect the fact that file changes are now atomic. See [this issue](https://github.com/savonet/liquidsoap/issues/3284) for more details. ### `cue_cut` Starting with version `2.2.4`, the `cue_cut` operator has been removed. Requests cue-in and cue-out processing has been integrated directly into requests resolution. In most cases, you simply can remove the operator from your script. In some cases, you might need to disable `cue_in_metadata` and `cue_out_metadat` either when creating new requests or when creating `playlist` sources. ### Harbor HTTP server and SSL support The API for registering HTTP server endpoint and using SSL was completely rewritten. It should be more flexible and provide node/express like API for registering endpoints and middleware. You can checkout [the harbor HTTP documentation](harbor_http.html) for more details. The [Https support](harbor_http.html#https-support) section also explains the new SSL/TLS API. ### Timeout We used to have timeout values labelled `timeout` or `timeout_ms`, some of these would be integer and in milliseconds, other floating point and in seconds etc. This was pretty confusing so, now all `timeout` settings and arguments have been unified to be named `timeout` and hold a floating point value representing a number of seconds. In most cases, your script will not execute until you have updated your custom `timeout` values but you should also review all of them to make sure that they follow the new convention. ### Metadata overrides Some metadata overrides have been made to reset on track boundaries. Previously, those were permanent even though they were documented as only applying to the current track. If you need to keep the previous behavior, you can used the `persist_overrides` parameters (`persis_override` for `cross`/`crossfade`). The list of concerned metadata is: - `"liq_fade_out"` - `"liq_fade_skip"` - `"liq_fade_in"` - `"liq_cross_duration"` - `"liq_fade_type"` ### JSON rendering The confusing `let json.stringify` syntax has been removed as it did not provide any feature not already covered by either the `json.stringify()` function or the generic `json()` object mapper. Please use either of those now. ### Default character encoding in `output.{harbor,icecast,shoutcast}` Default encoding for `output.harbor`, `output.icecast` and `output.shoutcast` metadata has been changed to `UTF-8` in all cases. Legacy systems used to expect `ISO-8859-1` (also known as `latin1`) for metadata inserted into `mp3` streams via the `icy` mechanism. It seems that, nowadays, most software expect `UTF-8` out of the box, including for legacy systems that previously assumed other encodings. Therefore, by changing this default value, we try to match expectations of the largest number of users of our software. If you are using one of these outputs, make sure to test this assumptions with your listners' clients. If needed, the characters encoding can be set to a different value using the operator's parameters. ### Decoder names Decoder names have been converted to lowercase. If you were relying on specific settings for decoders priority/ordering, you will need to convert them to lowercase, for instance: ``` settings.decoder.decoders.set(["FFMPEG"]) ``` becomes: ``` settings.decoder.decoders.set(["ffmpeg"]) ``` Actually, because of the above change in references, this even becomes: ``` settings.decoder.decoders := ["ffmpeg"] ``` ### `strftime` Add file-based operators do not support `strftime` type conversions out of the box anymore. Instead, you should use explicit conversions using `time.string`. This means that this script: ```liquidsoap output.file("/path/to/file%H%M%S.wav", ...) ``` becomes: ```liquidsoap output.file({time.string("/path/to/file%H%M%S.wav")}, ...) ``` ### Other breaking changes - `reopen_on_error` and `reopen_on_metadata` in `output.file` an related outputs are now callbacks. - `request.duration` now returns a `nullable` float, `null` being value returned when the request duration could not be computed. - `getenv` (resp. `setenv`) has been renamed to `environment.get` (resp. `environment.set`). ## From 2.0.x to 2.1.x ### Regular expressions First-class [regular expression](language.html#regular-expressions) are introduced and are used to replace the following operators: - `string.match(pattern=<regexp>, <string>` is replaced by: `r/<regexp>/.test(<string>)` - `string.extract(pattern=<regexp>, <string>)` is replaced by: `r/<regexp>/.exec(<string>)` - `string.replace(pattern=<regexp>, <string>)` is replaced by: `r/<regexp>/g.replace(<string>)` - `string.split(separator=<regexp>, <string>)` is replaced by: `r/<regexp>/.split(<string>)` ### Partial application In order to improve performance, avoid some programming errors and simplify the code, the support for partial application of functions was removed (from our experience it was not used much anyway). This means that you should now provide all required arguments for functions. The behavior corresponding to partial application can of course still be achieved by explicitly abstracting (with `fun(x) -> ...`) over some arguments. For instance, suppose that we defined the addition function with two arguments with ```liquidsoap def add(x,y) = x + y end ``` and defined the successor function by partially applying it to the first argument ```liquidsoap suc = add(1) ``` We now need to explicitly provide the second argument, and the `suc` function should now be defined as ```liquidsoap suc = fun(x) -> add(1, x) ``` or ```liquidsoap def suc(x) = add(1, x) end ``` ### JSON parsing JSON parsing was greatly improved and is now much more user-friendly. You can check out our detailed presentation [here](json.html). ### Runtime evaluation Runtime evaluation of strings has been re-implemented as a type-safe eval `let` decoration. You can now do: ```liquidsoap let eval x = "[1,2,3]" ``` And, just like with JSON parsing, the recommended use is with a _type annotation_: ```liquidsoap let eval (x: [int]) = "[1,2,3]" ``` ### Deprecations and breaking changes - The argument `streams_info` of `output.file.hls` is now a record. - Deprecated argument `timeout` of `http.*` operators. - `source.on_metadata` and `source.on_track` now return a source as this was the case in previous versions, and associated handlers are triggered only when the returned source is pulled - `output.youtube.live` renamed `output.youtube.live.rtmp`, remove `bitrate` and `quality` arguments and added a single encoder argument to allow stream copy and more. - `list.mem_assoc` is replaced by `list.assoc.mem` - `timeout` argument in `http.*` operators is replaced by `timeout_ms`. - `request.ready` is replaced by `request.resolved` ## From 1.4.x to 2.0.0 ### `audio_to_stereo` `audio_to_stereo` should not be required in most situations anymore. `liquidsoap` can handle channels conversions transparently now! ### `auth` function in `input.harbor` The type of the `auth` function in `input.harbor` has changed. Where before, you would do: ```liquidsoap def auth(user, password) = ... end ``` You would now do: ```liquidsoap def auth(params) user = params.user password = params.password ... end ``` ### Type errors with lists of sources Now that sources have their own methods, the actual list of methods attached to each source can vary from one to the next. For instance, `playlist` has a `reload` method but `input.http` does not. This currently confuses the type checker and leads to errors that look like this: ```liquidsoap At script.liq, line xxx, char yyy-zzz: Error 5: this value has type _ * source(audio=?A, video=?B, midi=?C) .{ time : () -> float, shutdown : () -> unit, fallible : bool, skip : () -> unit, seek : (float) -> float, is_active : () -> bool, is_up : () -> bool, log : {level : (() -> int?).{set : ((int) -> unit)} }, self_sync : () -> bool, duration : () -> float, elapsed : () -> float, remaining : () -> float, on_track : ((([string * string]) -> unit)) -> unit, on_leave : ((() -> unit)) -> unit, on_shutdown : ((() -> unit)) -> unit, on_metadata : ((([string * string]) -> unit)) -> unit, is_ready : () -> bool, id : () -> string, selected : (() -> source(audio=?D, video=?E, midi=?F)?) } but it should be a subtype of the type of the value at radio.liq, line 122, char 2-21 _ * _.{reload : _} ``` In such cases, we recommend to give a little nudge to the typechecker by using the `(s:source)` type annotation where a list of source is causing the issue. For instance: ```liquidsoap s = fallback([ (s1:source), (s2:source), (s3:source) ]) ``` This tells the type checker not to worry about the source methods and just focus on what matters, that they are actually sources.. 🙂 ### Http input and operators In order to provide as much compatibility as possible with the different HTTP protocols and implementation, we have decided to delegate HTTP support to external libraries which have large scale support and implementation. This means that, if you have installed `liquidsoap` using `opam`: - You need to install the `ocurl` package to enable all HTTP request operators, `http.get`, `http.post`, `http.put`, `http.delete` and `http.head` - You need to install the `ffmpeg` package (version `1.0.0` or above) to enable `input.http` - You do not need to install the `ssl` package anymore to enable their `https` counter-part. These operators have been deprecated. ### Crossfade The parameters for `cross` transitions was changed to take advantage of the new module system. Instead of passing multiple arguments related to the ending and starting track, those are regrouped into a single record. So, if you had a transition like this: ```liquidsoap def transition( ending_dB_level, starting_dB_level, ending_metadata, starting_metadata, ending_source, starting_source) = ... end ``` You would now do: ```liquidsoap def transition(ending, starting) = # Now you can use: # - ending.db_level, ending.metadata, ending.source # - starting.db_level, starting.metadata, starting.source ... end ``` ### Settings Settings are now exported as records. Where you would before write: ```liquidsoap set("decoder.decoders", ["MAD", "FFMPEG"]) ``` You can now write: ```liquidsoap settings.decoder.decoders.set(["MAD", "FFMPEG"]) ``` Likewise, to get a setting's value you can now do: ```liquidsoap current_decoders = settings.decoder.decoders() ``` This provides many good features, in particular type-safety. For convenience, we have added shorter versions of the most used settings. These are all shortcuts to their respective `settings` values: ```liquidsoap log.level.set(4) log.file.set(true) log.stdout.set(true) init.daemon.set(true) audio.samplerate.set(48000) audio.channels.set(2) video.frame.width.set(720) video.frame.height.set(1280) ``` The `register` operator could not be adapted to this new API and had to be removed, however, backward-compatible `set` and `get` operators are provided. Make sure to replace them as they should be removed in a future version. ### Metadata insertion The function `insert_metadata` does not return a pair anymore, but a source with a method named `insert_metadata`. This means that you should change the code ```liquidsoap fs = insert_metadata(s) # The function to insert metadata f = fst(ms) # The source with inserted metadata s = snd(ms) ... # Using the function f([("artist", "Bob")]) ... # Using the source output.pulseaudio(s) ``` to ```liquidsoap s = insert_metadata(s) ... # Using the function s.insert_metadata([("artist", "Bob")]) ... # Using the source output.pulseaudio(s) ``` ### Request-based queueing Queueing for request-based sources has been simplified. The `default_duration` and `length` have been removed in favor of a simpler implementation. You can now pass a `prefetch` parameter which tells the source how many requests should be queued in advance. Should you need more advanced queueing strategy, `request.dynamic.list` and `request.dynamic` now export functions to retrieve and set their own queue of requests. ### JSON import/export `json_of` has been renamed `json.stringify` and `of_json` has been renamed `json.parse`. JSON export has been enhanced with a new generic json object export. Associative lists of type `(string, 'a)` are now exported as lists. See our [JSON documentation page](json.html) for more details. Convenience functions have been added to convert metadata to and from JSON object format: `metadata.json.stringify` and `metadata.json.parse`. ### Returned types from output operators Starting with liquidsoap `2.0.0`, output operators return the empty value `()` while they previously returned a source. This helps enforce the fact that outputs should be end-points of your scripting graphs. However, in some cases, this can cause issues while migrating old scripts, in particular if the returned value of an output was used in the script. The way to fix this is to apply your operator to the source directly underneath the output. For instance, the following clock assignment: ```liquidsoap s = ... clock.assign_new([output.icecast(..., s)]) ``` Should now be written: ```liquidsoap s = ... clock.assign_new([s], ...) output.icecast(..., s) ``` ### Deprecated operators Some operators have been deprecated. For most of them, we provide a backward-compatible support but it is good practice to update your script. You should see logs in your script when running deprecated operatords. Here's a list of the most important ones: - `playlist.safe` is replaced by: `playlist(mksafe(..))` - `playlist.once` is replaced by: `playlist`, setting `reload_mode` argument to `"never"` and `loop` to `false` - `rewrite_metadata` should be rewritten using `metadata.map` - `fade.initial` and `fade.final` are not needed anymore - `get_process_output` is replaced by: `process.read` - `get_process_lines` is replaced by: `process.read.lines` - `test_process` is replaced by: `process.test` - `system` is replaced by: `process.run` - `add_timeout` is replaced by: `thread.run.recurrent` - `on_blank` is replaced by: `blank.detect` - `skip_blank` is replaced by: `blank.skip` - `eat_blank` is replaced by: `blank.eat` - `strip_blank` is replaced by: `blank.strip` - `which` is replaced by: `file.which` - `register_flow`: flow is no longer maintained - `empty` is replaced by: `source.fail` - `file.unlink` is replaced by: `file.remove` - `string.utf8.escape` is replaced by: `string.escape` - `metadata.map` is replaced by: `metadata.map` ### Windows build The windows binary is statically built and, for this reason, we cannot enable both the `%ffmpeg` encoder and any encoder that uses the same underlying libraries, for instance `libmp3lame` for `mp3` encoding. The technical reason is that both libraries import the same C symbols, which makes compilation fail. The `%ffmpeg` encoder provides all the functionalities of the internal encoders that conflict with it along with many more format we do not support otherwise. For this reason, it was decided to enable the `%ffmpeg` encoder and disable all other encoders. This means that, if you were previously using a different encoder than `%ffmpeg`, you will need to adapt your script to use it. For instance, for mp3 encoding with variable bitrate: ```liquidsoap %ffmpeg(format="mp3", %audio(codec="libmp3lame", q=7)) ``` ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/multitrack.md����������������������������������������������������������0000664�0000000�0000000�00000030257�14773033502�0020623�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Multitrack support Starting with version `2.2.0`, liquidsoap now supports track operations, making it possible to manipulate data at the track level, including demuxing, remuxing, encoding, decoding, applying filters and more! **Only the FFmpeg decoder and encoder supports multitrack**. This means that you need to have `liquidsoap` compiled with the FFmpeg support to be able to decode or encode sources with multiple audio/video tracks. Support for track muxing and demuxing and track-level operators, however, does not require the FFmpeg support but, without it, all decoders and outputs are limited to at most one `audio` and one `video` track. ## Multitrack sources Liquidsoap sources can have multiple tracks, such as an english language audio track and a french language audio track. The number of tracks in a source is determined by how you use it. For instance, if you have a file `movie.mkv` with two audio tracks and one video track, you can create a source `s` with it using the `single` operator (or `playlist`, `request.dynamic` etc): ```liquidsoap s = single("/path/to/movie.mkv") ``` By default, `liquidsoap` decodes _only_ the track that you tell it to pick. So, if you output this source as an output with only one audio track, it will happily do so: ```{.liquidsoap include="multitrack.liq"} ``` Resulting in the following logs: ``` [output_file:3] Content type is {audio=ffmpeg.copy,video=ffmpeg.copy}. [decoder.ffmpeg:3] Requested content-type for "/path/to/movie.mkv": {audio=ffmpeg.copy,video=ffmpeg.copy} [decoder.ffmpeg:3] FFmpeg recognizes "/path/to/movie.mkv" as video: {codec: h264, 1920x1038, yuv420p}, audio: {codec: aac, 48000Hz, 6 channel(s)}, audio_2: {codec: aac, 48000Hz, 6 channel(s)} [decoder.ffmpeg:3] Decoded content-type for "/path/to/movie.mkv": {audio=ffmpeg.copy(codec="aac",channel_layout="5.1",sample_format=fltp,sample_rate=48000),video=ffmpeg.copy(codec="h264",width=1920,height=1038,aspect_ratio=1/1,pixel_format=yuv420p)} ``` This shows that the `output.file` source was initialized with expected content-type `{audio=ffmpeg.copy,video=ffmpeg.copy}`, i.e. one audio and one video track, both copied from the original file. This comes from the `%ffmpeg` encoder definition in our script. Then, the `single` file was decoded with the same requested content-type and FFmpeg reported all the details about the file's content, including a second audio track named `audio_2`. Eventually, we picked up only first `audio` and first `video` track and reported a more detailed content-type now that we know the actual content of each track. Now, let's say that we want to also keep the second audio track but convert it to stereo and re-encode it into `aac`. We can then do: ```{.liquidsoap include="multitrack2.liq"} ``` And now we see the following logs: ``` [output_file:3] Content type is {audio=ffmpeg.copy,audio_2=pcm(stereo),video=ffmpeg.copy}. [decoder.ffmpeg:3] Requested content-type for "/path/to/movie.mkv": {audio=ffmpeg.copy,audio_2=pcm(stereo),video=ffmpeg.copy} [decoder.ffmpeg:3] FFmpeg recognizes "/path/to/movie.mkv" as video: {codec: h264, 1920x1038, yuv420p}, audio: {codec: aac, 48000Hz, 6 channel(s)}, audio_2: {codec: aac, 48000Hz, 6 channel(s)} [decoder.ffmpeg:3] Decoded content-type for "/path/to/movie.mkv": {audio=ffmpeg.copy(codec="aac",channel_layout="5.1",sample_format=fltp,sample_rate=48000),audio_2=pcm(5.1),video=ffmpeg.copy(codec="h264",width=1920,height=1038,aspect_ratio=1/1,pixel_format=yuv420p)} ``` Now, we are actually using both audio tracks from `movie.mkv` and one of them is being converted to stereo audio! One thing to keep in mind, however, is that **expected content-type drives the input decoder**. Typically, if, instead of a `single`, you use a `playlist`: ```{.liquidsoap include="multitrack3.liq"} ``` Then all the files in the playlist who do not have at least two `audio` tracks and one `video` track will be rejected by the decoder! Lastly, it is important to keep in mind that **decoders always assume a specific nomenclature for tracks**. The convention when decoding is to name the first audio track `audio`, then `audio_2`, `audio_3` and etc. Likewise for video: `video`, `video_2`, `video_3`. Typically, the following will not work: ```liquidsoap s = playlist("/path/to/playlist") # Copy first audio track and video track # and re-encode second audio track: output.file( fallible=true, %ffmpeg( %audio_fr.copy, %audio_en( channels=2, codec="aac" ), %video.copy ), "/path/to/copy.mkv", s ) ``` This is because the decoder has no way of knowing which of the audio track present in the files in `s` should be matched to `audio_en` and `audio_fr.copy`. One might think that the order in which they are declared in the encoder could be used but this would also be tricky as the decoder could report tracks in different order when decoding different files from the playlist. Also, and perhaps more importantly, tracks can be demuxed and remuxed at will, which also makes us loose the notion of track order. Actually, let's talk about demuxing and remuxing next! ## Tracks demuxing and muxing For any given source, you can extract its tracks using the `source.tracks` operator: ```liquidsoap s = playlist(...) let {audio, video, metadata, track_marks} = source.tracks(s) ``` In the above, `audio` and `video` represent, resp., the `audio` and `video` track from the source `s`. The `metadata` and `track_marks` tracks are special track type that are available in any source and hold, as the name suggests, the source's metadata and track marks. We will see later how this can be used to e.g. drop all tracks from a source (something that used to be done with the `drop_tracks` operator), or select metadata only from a specific source or track. Internally, **a track is a source restricted to a single content-type**. This means that: - When pulling data for a given track, the underlying source is used, potentially also pulling data for its other tracks - Tracks are subject to the same limitations as sources w.r.t. clocks - Tracks, like sources, always have a `metadata` and `track_marks` tracks. The `track.metadata` and `track.track_marks` operators can be used to retrieve them. Tracks can be muxed using the `source` operator. The operator takes a record of tracks and creates a source with them. Tracks can have any name and type except `metadata` and `track_marks` that are reserved for their corresponding track types. ### Add a video track Here's how to add a video track to a source ```{.liquidsoap include="multitrack-add-video-track.liq" to="END"} ``` The above example was purposely written in a longer form to make it more explicit. However, if you wish to just add/replace a track, you can also overload the existing tracks from the first source as follows: ```{.liquidsoap include="multitrack-add-video-track2.liq" to="END"} ``` ### Add a default video track You can also check if a source has a certain track and do something accordingly: ```{.liquidsoap include="multitrack-default-video-track.liq" to="END"} ``` Please note, however, that **tracks available in the playlist sources are determined based on the first decoded file**. If the first file in the playlist is audio-only then the playlist content-type is assumed to be audio-only for the whole playlist and the default video is added to _all decoded files_. To decide on a case-by-case basis, you might need some more advanced coding! ### Merge all tracks As mentioned before, you can also remove the track marks from a source as follows: ```liquidsoap s = playlist(...) # Extract all tracks except track_marks: let {track_marks=_, ...tracks} = source.tracks(s) s = source(tracks) ``` ## Track-level operators Some, but not all, operators have been updated to operate at the track level. They are documented under the `Track` section in [the API documentation](reference.html). More operators might be converted in the future (feel free to file a feature request for those!). For instance, to convert an audio track to mono PCM audio, one can do: ```liquidsoap mono_track = track.audio.mean(audio_track) ``` Likewise, inline encoders are now available at the track level, for instance: ```liquidsoap encoded_audio_track = track.ffmpeg.encode.audio(%ffmpeg(%audio(codec="aac")), audio_track) ``` However, remember that tracks have the same limitations w.r.t. clocks that sources have. Here, in particular, `encoded_audio_track` is in a new clock (due to the fact that ffmpeg inline encoding is not synchronous). Therefore, the following will fail: ```liquidsoap s = playlist(...) let {audio, metadata, track_marks} = source.tracks(s) encoded = source({ audio = track.ffmpeg.encode.audio(%ffmpeg(%audio(codec="aac")), audio), metadata = metadata, track_marks = track_marks }) ``` This is because `metadata` and `track_marks` are tracks from the underlying `s` source, which belongs to a different clock. In this case, you should use the track marks and metadata from the encoded track: ```liquidsoap s = playlist(...) let {audio, metadata, track_marks} = source.tracks(s) encoded_audio = track.ffmpeg.encode.audio(%ffmpeg(%audio(codec="aac")), audio) encoded = source({ audio = encoded_audio, metadata = track.metadata(encoded_audio), track_marks = track_marks(encoded_audio) }) ``` ## Conventions Now that we have seen how we can create any collection of tracks with any possible name, in order to make things work, we need to assume a couple of conventions. **For decoders**, the convention, as explained above, is, when decoding files, to name the first audio track `audio`, then `audio_2`, `audio_3` and etc. Likewise for video: `video`, `video_2`, `video_3`. This is the convention that you should use when demuxing tracks from request-based source: ```liquidsoap s = playlist(...) let {audio, audio_2, video, video_2, video_3} = source.tracks(s) ``` **For encoders**, to drive content-type at runtime, since tracks can be remuxed with any arbitrary name, we need to a way to decide what type of content a track contains, being `audio`, `video` or, potentially `midi` and, planned for later, `subtitles`. This is achieved using the following convention, by order of priority: 1. A `copy` track is any track named `%<track_name>.copy`. We do not need to know the track's content in this case. 2. If a track has `audio_content` or `video_content` as parameter (for instance `%foo(audio_content, ...)`) then it is considered, resp., `audio` or `video`. 3. If the track name has `audio` or `video` in it (for instance `%dolby_audio_fr`) then it is considered, resp., `audio` or `video` 4. If the track codec is hardcoded (for instance (`%foo(codec="aac", ...)`) then the codec is used to detect the content. For instance, imagine that you want to encode a source with a `fr` and `en` audio track and a `director_cut` video track, you can do the following: ```liquidsoap output.file( %ffmpeg( %en(codec="aac"), %fr(codec="aac"), %director_cut(codec="libx264") ), "/path/to/copy.mkv", s ) ``` This works because each of the codec used in these tracks can be mapped to a specific content-type. Now, imagine that you actually want to use a variable for the codec of the `en` and `director_cut` track. In this case, you can do: ```liquidsoap output.file( %ffmpeg( %en(audio_content, codec=audio_codec) %fr(codec="aac"), %director_cut(video_content, codec=video_codec) ), "/path/to/copy.mkv", s ) ``` This informs `liquidsoap` what type of content these tracks contain. However, you might also opt for a more explicit track naming scheme. Something like: ```liquidsoap output.file( %ffmpeg( %audio_en(codec=audio_codec), %audio_fr(codec="aac"), %director_cut_video(codec=video_codec) ), "/path/to/copy.mkv", s ) ``` In this case, `liquidsoap` assumes that the track with `audio` in their name are indeed audio track and the same goes for video tracks. Lastly, these naming conventions have no bearing for the `FFmpeg` encoder. At the FFmpeg encoder level, tracks are identified by an integer and stored in the order they are declared in the `%ffmpeg` encoder. This means that, once encoded and saved to a file, track names internal to liquidsoap are not saved by the FFmpeg encoder and, instead, when decoding the file, you will get `audio`, `audio_2`, `video` and etc. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/on2.md�����������������������������������������������������������������0000664�0000000�0000000�00000000776�14773033502�0017145�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������Savonet was at the [ON2: Test Signals](http://testsignals.org/) conference in Berlin, on October 22-23 2010. We presented Liquidsoap, but also held the first Liquidsoap workshop. <iframe src="https://player.vimeo.com/video/16528307" width="640" height="352" frameborder="0" allow="autoplay; fullscreen" allowfullscreen></iframe> <p><a href="https://vimeo.com/16528307">Liquidsoap presentation at ON2</a> from <a href="https://vimeo.com/smimram">Samuel Mimram</a> on <a href="https://vimeo.com">Vimeo</a>.</p> ��liquidsoap-2.3.2/doc/content/phases.md��������������������������������������������������������������0000664�0000000�0000000�00000003400�14773033502�0017715�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Liquidsoap execution phases There are various stages of running liquidsoap: - **Parsing**: read scripts and scripting expressions, can fail with syntax errors. - **Static analysis**: infer the type of all expressions, leaves some type unknown and may fail with type errors. - **Instantiation**: when script is executed, sources get created. Remaining unknown [stream types](stream_contents.html) are forced according to `frame.*.channels` settings, [clocks](clock.html) are assigned (but unknown clocks may remain) and some sources are checked to be [infallible](source.htmls). Each of these steps may raise an error. - **Collection**: Unknown clocks become the default clock so that all sources are assigned to one clock. Active sources newly attached to clocks are initialized for streaming, shutdown sources are detached from their clocks, and clocks are started or destroyed as needed. Streaming has started. Usually, liquidsoap is ran by passing one or several scripts and expressions to execute. Those expressions set up some sources, and outputs typically don't change anymore. If those initially provided active sources fail to be initialized (invalid parameter, fail to connect, etc.) liquidsoap will terminate with an error. It is however possible to **dynamically** create active sources, through registered server commands, event handlers, etc. They will be initialized and run as statically created ones. In **interactive** mode (passing the `--interactive` option) it is also possible to input expressions in a liquidsoap prompt, and their execution can trigger the creation of new outputs. Outputs can be deactivated using `source.shutdown()`: they will stop streaming and will be destroyed. The full liquidsoap instance can be shutdown using the `shutdown()` command. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/playlist_parsers.md����������������������������������������������������0000664�0000000�0000000�00000007432�14773033502�0022043�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Playlist parsers Liquidsoap supports various playlists formats. Those formats can be used for `playlist` sources, `input.http` streams and manually using `request.create`. ## Supported formats Most supported playlists format are _strict_, which means that the decoder can be sure that is has found a correct playlist for that format. Some other format, such as `m3u`, however, may cause _false positive_ detections. All formats are identified by their _mime-type_ or _content-type_. Supported formats are the following: - Text formats: - `audio/x-scpls`: [PLS format](http://en.wikipedia.org/wiki/PLS_%28file_format%29), **strict** - `application/x-cue`: [CUE format](http://en.wikipedia.org/wiki/.cue), **strict**. This format's usage is described below. - `audio/x-mpegurl`, `audio/mpegurl`: [M3U](http://en.wikipedia.org/wiki/M3u), **non strict** - Xml formats: - `video/x-ms-asf`, `audio/x-ms-asx`: [ASX](http://en.wikipedia.org/wiki/Advanced_Stream_Redirector), **strict** - `application/smil+xml`, `application/smil+xml`, [SMIL](http://en.wikipedia.org/wiki/Synchronized_Multimedia_Integration_Language), **strict** - `application/xspf+xml`, [XSPF](http://en.wikipedia.org/wiki/Xspf), **strict** - `application/rss+xml`, [Podcast](http://en.wikipedia.org/wiki/Podcast), **strict** Playlist format is driven by the **Content-Type** and **Content-Disposition** HTTP headers _(see m3u example below)_. You should make sure that your HTTP endpoint returns appropriate values for those. As last resort, you should be able to use the `settings.http.mime.extnames` settings to add or adjust support for your endpoint's mime-type if liquidsoap supports its corresponding playlist format. See for instance issue [#3451](https://github.com/savonet/liquidsoap/issues/3451). ## Usage Playlist files are parsed automatically when used in a `playlist` or `input.http` operator. Each of these two operators has specific options to specify how to pick up a track from the playlist, _e.g._ pick a random track, the first one etc. Additionally, you can also manually parse and process a playlist using `request.create` and `request.resolve` and some programming magic. You can check the code source for `playlist.reloadable` in our standard library for a detailed example. ### Remote M3U playlist example Here is an example of a m3u playlist being read from nodejs/express . liquidsoap script: ```liquidsoap #!/usr/local/bin/liquidsoap p = playlist(reload=10, "http://localhost:8080/radio/playlists/0/playlist.m3u") ``` nodejs/express app: ```js import express from "express"; const app = express(); app.get("/radio/playlists/:id/playlist.m3u", async (req, res) => { const playlist = ["/media/foo.mp3", "/media/bar.mp3"]; // Liquidsoap will use the file extension from the `Content-Disposition` header to guess // the playlist format res.set( "Content-Disposition", `attachment; filename="playlist-${req.params.id}.m3u"`, ); // Otherwise, it will try to guess the file extension from the playlist mime-type. res.set("Content-Type", "audio/x-mpegurl"); res .send(playlist.join("\r\n") + "\r\n") .status(200) .end(); }); const server = app.listen(8080); ``` ## Special case: CUE format The CUE format originates from CD burning programs. They describe the set of tracks of a whole CD and are accompanied by a single file containing audio data for the whole CD. By default, the CUE playlist parser will add metadata from cue-in and cue-out points for each track described in the playlist, which are automatically handled with source-base operators such as `playlist`. The metadata added for cue-in and cue-out positions can be customized using the following configuration keys: ```liquidsoap settings.playlists.cue_in_metadata := "liq_cue_in" settings.playlists.cue_out_metadata := "liq_cue_out" ``` ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/presentations.md�������������������������������������������������������0000664�0000000�0000000�00000003122�14773033502�0021331�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Presentations about Liquidsoap ## Liquidshop In January 2021, we organized [the _Liquidshop_, a workshop around Liquidsoap](http://www.liquidsoap.info/liquidshop/), where you can find lots of presentations around Liquidsoap. In particular, Romain presented the main features of the upcoming Liquidsoap 2.0: <iframe width="560" height="315" src="https://www.youtube.com/embed/VT6TEjJzWoY" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> and Gilou did a tutorial about setting up a webradio with Liquidsoap <iframe width="560" height="315" src="https://www.youtube.com/embed/B8l8uqBS6-c" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> ## FOSDEM 2020 We presented liquidsoap during the FOSDEM 2020 conference. You can watch the video here: <iframe src="https://player.vimeo.com/video/388951779" width="640" height="360" frameborder="0" allow="autoplay; fullscreen" allowfullscreen></iframe> <p><a href="https://vimeo.com/388951779">Functional audio and video stream generation with Liquidsoap</a> from <a href="https://vimeo.com/user27259977">Romain Beauxis</a> on <a href="https://vimeo.com">Vimeo</a>.</p> The slides for the presentation are available <a href="/fosdem2020/index.html" target="_blank">here</a> ## ON2 We were part of the ON2 conference, during which we presented liquidsoap and held a complete workshop about how to install and use it. You can see the details about this event <a href="on2.html">here</a>. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/profiling.md�����������������������������������������������������������0000664�0000000�0000000�00000002201�14773033502�0020421�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Profiling scripts Sometimes, some functions of your script are taking up time and you would like to optimize those. We are not speaking here about the encoding of streams, which usually takes the vast majority of the spent computing power, but of functions written directly in Liquidsoap. In order to understand those better, Liquidsoap has a _profiler_ which records all the function calls. It can be enabled with ```liquidsoap profiler.enable() ``` (or by passing the `--profile` commandline flag of Liquidsoap) and the statistics can be obtained with ```liquidsoap print(profiler.stats.string()) ``` It will output something like ``` function self total calls + 0.359139919281 0.359139919281 302000 list.add 0.324638843536 442.74707818 202000 if 0.242718935013 442.951756954 102002 list.cons 0.230906486511 442.277146816 101000 ``` where each lines consists of a function, the time spent in the functions, the time spent in the function and the functions it has called and the number of calls to the function. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/prometheus.md����������������������������������������������������������0000664�0000000�0000000�00000010455�14773033502�0020635�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Prometheus reporting When compiled with optional support for [mirage/prometheus](https://github.com/mirage/prometheus), `liquidsoap` can export [prometheus](https://prometheus.io/) metrics. The basic settings to enable exports are: ```{.liquidsoap include="prometheus-settings.liq"} ``` Common metrics, namely `gauge`, `counter` and `summary` are provided via the script language, as well as a specialized operator to track source's latencies. A fully-featured implementation can be found at [mbugeia/srt2hls](https://github.com/mbugeia/srt2hls) ## Basic operators The 3 basic operators are: - `prometheus.counter` - `prometheus.gauge` - `prometheus.summary` They share a similar type and API, which is as follows: ```liquidsoap (help : string, ?namespace : string, ?subsystem : string, labels : [string], string) -> (label_values : [string]) -> (float) -> unit ``` This type can be a little confusing. Here's how it works: 1. First, one has to create a metric factory of a given type. For instance: ```{.liquidsoap include="prometheus-callback.liq" from="A0" to="A1"} ``` 2. Then, the metric factory can be used to instantiate speific metrics by passing the label's values: ```{.liquidsoap include="prometheus-callback.liq" from="B0" to="B1"} ``` The returned function is a setter for this metric, i.e. - For `gauge` metrics, it sets the gauge value - For `counter` metrics, it increases the counter value - For `summary` metrics, it registers an observation Finally, the programmer can now use that callback to set the metric as desired. For instance here: ```{.liquidsoap include="prometheus-callback.liq" from="C0"} ``` ## `prometheus.latency` The `prometheus.latency` operator provides prometheus metrics describing the internal latency of a given source. It is fairly easy to use: ```liquidsoap s = (...) prometheus.latency(s) ``` The metrics are computed over a sliding window that can be defined as a parameter of the operator. Exported metrics are: ``` # Input metrics: liquidsoap_input_latency{...} <value> liquidsoap_input_max_latency{...} <value> liquidsoap_input_peak_latency{...} <value> # Output metrics: liquidsoap_outputput_latency{...} <value> liquidsoap_output_max_latency{...} <value> liquidsoap_output_peak_latency{...} <value> # Overall metrics: liquidsoap_overall_latency{...} <value> liquidsoap_overall_max_latency{...} <value> liquidsoap_overall_peak_latency{...} <value> ``` The 3 different groups of values are: - **input**: metrics related to the time it takes to generate audio data - **output**: metrics related to the time it takes to output (encode and send) audio data - **overall**: the sum of all previous two groups Each group of metrics is divided into 3 subsets: - Mean latency value over the sliding window - Max latency value over the sliding window - Peak latency since start Latencies are reported over a frame's duration, which is typically around `0.04` seconds. Thus, in a situation where liquidsoap does not observe latency catch-ups, the overall mean latency `liquidsoap_overall_latency` should always be near that value. These metrics can be used to report and track the source of latencies and catch-ups while streaming. Typically, if a source starts taking too much time to generate its audio data, this should be reflects in the `input` latencies. Likewise for encoding and network output. Keep in mind, however, that enabling these metrics can have a CPU cost. It is rather small with a couple of sources but can increase with the number of sources being tracked. The user of these metrics is advised to keep track of CPU usage while ramping up on using them. ## OCaml specific metrics The prometheus binding used by `liquidsoap` also exports default OCaml-related metrics. They are as follows: ``` ocaml_gc_allocated_bytes <value> ocaml_gc_compactions <value> ocaml_gc_heap_words <value> ocaml_gc_major_collections <value> ocaml_gc_major_words <value> ocaml_gc_minor_collections <value> ocaml_gc_top_heap_words <value> process_cpu_seconds_total <value> ``` These metrics can be useful when debugging issues with `liquidsoap`, in particular to track is an observed increase in memory usage is related to OCaml memory allocation or not. More than often, if the increase is not related to OCaml, it can be safely assumed that the issue might come from an external library used by `liquisoap`. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/protocols-presentation.md����������������������������������������������0000664�0000000�0000000�00000004503�14773033502�0023174�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Protocols Protocols in liquidsoap are used to resolve requests URIs. The syntax is: `protocol:arguments`, for instance: `http://www.example.com`, `say:Something to say` etc. Most protocols are written using the script language. You can look at the file `protocols.liq` for a list of them. In particular, the `process:` protocol can use an external command to prepare resolve a request. Here's an example using the AWS command-line to download a file from S3: ```liquidsoap def s3_protocol(~rlog,~maxtime,arg) = extname = file.extension(leading_dot=false,dir_sep="/",arg) [process_uri(extname=extname,"aws s3 cp s3:#{arg} $(output)")] end protocol.add("s3",s3_protocol,doc="Fetch files from s3 using the AWS CLI", syntax="s3://uri") ``` Each protocol needs to register a handler, here the `s3_protocol` function. This function takes the protocol arguments and returns a list of new requests or files. Liquidsoap will then call this function, collect the returned list and keep resolving requests from the list until it finds a suitable file. This makes it possible to create your own custom resolution chain, including for instance cue-points. Here's an example: ```liquidsoap def cue_protocol(~rlog,~maxtime,arg) = [process_uri(extname="wav",uri=uri,"ffmpeg -y -i $(input) -af -ss 10 -t 30 $(output)")] end protocol.add("cue_cut",cue_protocol) ``` This protocol returns 30s of data from the input file, stating at the 10s mark. Likewise, you can apply a normalization program: ```liquidsoap def normalization_protocol(~rlog,~maxtime,arg) = # "normalize" command here is just an example.. [process_uri(extname="wav",uri=arg,"normalize $(inpuit)")] end protocol.add("normalize",normalization_protoco) ``` Now, you can push requests of the form: ``` normalize:cue_cut:http://www.server.com/file.mp3 ``` and the file will be cut and normalized before being played by liquidsoap. When defining custom protocols, you should pay attention to two variables: - `rlog` is the logging function. Messages passed to this function will be registered with the request and can be used to debug any issue - `maxtime` is the maximum time (in UNIX epoch) that the requests should run. After that time, it should return and be considered timed out. You may want to read from `protocols.liq` to see how to enforce this when calling external processes. ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/publications.md��������������������������������������������������������0000664�0000000�0000000�00000002343�14773033502�0021133�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- header-includes: | \DeclareUnicodeCharacter{03BB}{$\lambda$} ... # The theory behind Liquidsoap ## Publications ### Liquidsoap: a High-Level Programming Language for Multimedia Streaming Many of the advanced features of the Liquidsoap language are described in [Liquidsoap: a High-Level Programming Language for Multimedia Streaming](/assets/docs/bbm10.pdf). The article details in particular Liquidsoap's handling of heterogeneous stream contents (e.g. audio and video), as well as the model for clocks in the language. ### De la webradio lambda à la λ-webradio The first published presentation of Liquidsoap was made in [De la webradio lambda à la λ-webradio](/assets/docs/bm08.pdf) (_Baelde D. and Mimram S. in proceedings of Journées Francophnes des Languages Applicatifs (JFLA), pages 47-61, 2008_) -- yes, it's in French, sorry. It gives a broad description of the Liquidsoap tool and explains the theory behind the language, which is formalized as a variant of the typed λ-calculus with labels and optional arguments. The article describes the typing inference algorithm as well as some properties of the language (confluence) and of typing (subject reduction, admissible rules, termination of typed terms). ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/quick_start.md���������������������������������������������������������0000664�0000000�0000000�00000024501�14773033502�0020770�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Quickstart ## The Internet radio toolchain [Liquidsoap](index.html) is a general audio stream generator, but is mainly intended for Internet radios. Before starting with the proper Liquidsoap tutorial let's describe quickly the components of the internet radio toolchain, in case the reader is not familiar with it. The chain is made of: - the stream generator (Liquidsoap, [ices](https://www.icecast.org/ices/), or for example a DJ-software running on your local PC) which creates an audio stream (Ogg Vorbis or MP3); - the streaming media server ([Icecast](http://www.icecast.org), [HLS](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) (via a HTTP server), ...) which relays several streams from their sources to their listeners; - the media player (iTunes, VLC, a web browser, ...) which gets the audio stream from the streaming media server and plays it to the listener's speakers. ![Internet radio toolchain](/assets/img/schema-webradio-inkscape.png) The stream is always passed from the stream generator to the server, whether or not there are listeners. It is then sent by the server to every listener. The more listeners you have, the more bandwidth you need. If you use Icecast, you can broadcast more than one audio feed using the same server. Each audio feed or stream is identified by its "mount point" on the server. If you connect to the `foo.ogg` mount point, the URL of your stream will be [http://localhost:8000/foo.ogg](http://localhost:8000/foo.ogg) -- assuming that your Icecast is on localhost on port 8000. If you need further information on this you might want to read Icecast's [documentation](http://www.icecast.org). A proper setup of a streaming server is required for running Liquidsoap. Now, let's create an audio stream. ## Starting to use Liquidsoap We assume that you have a fully installed Liquidsoap. In particular the library `stdlib.liq` and its accompanying scripts should have been installed, otherwise Liquidsoap won't know the operators which have been defined there. ### Sources A stream is built with Liquidsoap by using or creating sources. A source is a media stream containing audio and/or video, track marks and metadata. In the following picture we represent a stream which has at least three tracks (one of which starts before the snapshot), and a few metadata packets (notice that they do not necessarily coincide with new tracks). ![A stream](/assets/img/stream.png) Liquidsoap provides many functions for creating sources from scratch (e.g. `playlist`), and also for creating complex sources by putting together simpler ones (e.g. `switch` in the following example). Eventually, sources are plugged into outputs (typically named `output.*`) which continuously pull the source's content and output it to speakers, to a file, to a streaming server, etc. These outputs what brings life into your sources. ### That source is fallible! A couple of things can go wrong in your streaming system. In Liquidsoap, we say that a source is _infallible_ if it is always available. Otherwise, it is _fallible_, meaning that something could go wrong and the source would not be available. By default, an output requires that its input source is infallible, otherwise it complains that "That source is fallible!" For example, a normal `playlist` is fallible. Firstly, because it could contain only invalid files, or at least spend too much time on invalid files to be able to prepare a valid one on time. Moreover, a playlist could contain remote files, which may not be accessible quickly at all times. A queue of user requests is another example of fallible source. Also, if `file.ogg` is a valid local file, then `single("file.ogg")` is an infallible source. When an output complains about its source being fallible, you have to turn it into an infallible one. Many solutions are available. The function `mksafe` takes a source and returns an infallible source, streaming silence when the input stream becomes unavailable. In a radio-like stream, silence is not the preferred solution, and you will probably prefer to `fallback` on an infallible "security" source: ```liquidsoap fallback([your_fallible_source_here, single("failure.ogg")]) ``` Finally, if you do not care about failures, you can pass the parameter `fallible=true` to most outputs (or pass the option `--no-fallible-check` to Liquidsoap). In that case, the output will accept a fallible source, and stop whenever the source fails and restart when it is ready to produce data again. ## One-line expressions Liquidsoap is a scripting language. Many simple setups can be achieved by evaluating one-line expressions. ### Playlists In the first example we'll play a playlist. Let's put a list of audio files in `playlist.pls`: one filename per line, lines starting with a `#` are ignored. You can also put remote files' URLs, if your liquidsoap has [support](help.html#plugins) for the corresponding protocols. Then just run: ```liquidsoap liquidsoap 'output(playlist("playlist.pls"))' ``` Other playlist formats are supported, such as M3U and, depending on your configuration, XSPF. Instead of giving the filename of a playlist, you can also use a directory name, and liquidsoap will recursively look for audio files in it. Depending on your configuration, the output `output` will use AO, Alsa or OSS, or won't do anything if you do not have support for these libs. In that case, the next example is for you. ### Streaming out to a server **Note:** in the following, we assume that you have installed the following optional dependencies: - `cry` for icecast output - `vorbis` for ogg/vorbis encoding - `ffmpeg` for ffmpeg encoding Liquidsoap is capable of playing audio on your speakers, but it can also send audio to a streaming server such as Icecast or Shoutcast. One instance of liquidsoap can stream one audio feed in many formats (and even many audio feeds in many formats!). You may already have an Icecast server. Otherwise you can install and configure your own Icecast server. The configuration typically consists in setting the admin and source passwords, in `/etc/icecast2/icecast.xml`. These passwords should really be changed if your server is visible from the hostile internet, unless you want people to kick your source as admins, or add their own source and steal your bandwidth. We are now going to send an audio stream, encoded as Ogg Vorbis, to an Icecast server: ```liquidsoap liquidsoap \ 'output.icecast(%vorbis, host = "localhost", port = 8000, password = "hackme", mount = "liq.ogg", mksafe(playlist("playlist.m3u")))' ``` The main difference with the previous is that we used `output.icecast` instead of `output`. The second difference is the use of the `mksafe` which turns your fallible playlist source into an infallible source. If you want to use HLS instead for streaming, you can do: ```liquidsoap liquidsoap \ 'output.file.hls( "/path/to/hls/directory", [("aac", %ffmpeg( format="mpegts", %audio(codec="aac", b="128k") ))], mksafe(playlist("playlist.m3u")))' ``` Once started, this will place all the files required for HLS stream into the local path `"/path/to/hls/directory"` which you can then server over HTTP. The HLS output has many interesting options, including callbacks to upload its files and more. See the [HLS Output](hls_output.html) page for more details. ### Input from another streaming server Liquidsoap can use another stream as an audio source. This may be useful if you do some live shows. ```liquidsoap liquidsoap \ 'output(input.http("https://icecast.radiofrance.fr/fip-hifi.aac"))' ``` ### Input from the soundcard If you're lucky and have a working ALSA support, try one of these... but beware that ALSA may not work out of the box. ```liquidsoap liquidsoap 'output.alsa(input.alsa())' ``` ```liquidsoap liquidsoap 'output.alsa(input.alsa())' ``` ### Other examples You can play with many more examples. Here are a few more. To build your own, lookup the [API documentation](reference.html) to check what functions are available, and what parameters they accept. ```liquidsoap # Listen to your playlist, but normalize the volume liquidsoap 'output(normalize(playlist("playlist_file")))' ``` ```liquidsoap # ... same, but also add smart cross-fading liquidsoap 'output(crossfade( normalize(playlist("playlist_file"))))' ``` ## Script files We have seen how to create a very basic stream using one-line expressions. If you need something a little bit more complicated, they will prove uneasy to manage. In order to make your code more readable, you can write it down to a file, named with the extension `.liq` (eg: `myscript.liq`). To run the script: ```liquidsoap liquidsoap myscript.liq ``` On UNIX, you can also put `#!/path/to/your/liquidsoap` as the first line of your script ("shebang"). Don't forget to make the file executable: ``` chmod u+x myscript.liq ``` Then you'll be able to run it like this: ``` ./myscript.liq ``` Usually, the path of the liquidsoap executable is `/usr/bin/liquidsoap`, and we'll use this in the following. ## A simple radio In this section, we build a basic radio station that plays songs randomly chosen from a playlist, adds a few jingles (more or less one every four songs), and output an Ogg Vorbis stream to an Icecast server. Before reading the code of the corresponding liquidsoap script, it might be useful to visualize the streaming process with the following tree-like diagram. The idea is that the audio streams flows through this diagram, following the arrows. In this case the nodes (`fallback` and `random`) select one of the incoming streams and relay it. The final node `output.icecast` is an output: it actively pulls the data out of the graph and sends it to the world. ![Graph for 'basic-radio.liq'](/assets/img/basic-radio-graph.png) ```{.liquidsoap include="basic-radio.liq"} ``` ## What's next? You can first have a look at a [more complex example](complete_case.html). There is also a second tutorial about [advanced techniques](advanced.html). You should definitely learn [how to get help](help.html). If you know enough liquidsoap for your use, you'll only need to refer to the [scripting reference](reference.html), or see the [cookbook](cookbook.html). At some point, you might read more about Liquidsoap's [scripting language](language.html). For a better understanding of liquidsoap, it is also useful to read a bit about the notions of [sources](sources.html) and [requests](requests.html). �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/radiopi.md�������������������������������������������������������������0000664�0000000�0000000�00000004163�14773033502�0020070�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# RadioPi [RadioPi](http://www.radiopi.org) is the web radio of the ECP (Ecole Centrale de Paris). RadioPi runs many channels. There are topical channels (Reggae, Hip-Hop, Jazz, ...). On top of that, they periodically broadcast live shows, which are relayed on all channels. We met a RadioPi manager right after having released Liquidsoap 0.2.0, and he was seduced by the system. They needed quite complex features, which they were at that time fulfilling using dirty tricks, loads of obfuscated scripts. Using Liquidsoap now allow them to do all they want in an integrated way, but also provided new features. ### The migration process Quite easy actually. They used to have many instances Ices2, each of these calling a Perl script to get the next song. Other scripts were used for switching channels to live shows. Now they have this single Liquidsoap script, no more. It calls external scripts to interact with their web-based song scheduling system. And they won new features: blank detection and distributed encoding. The first machine gets its files from a ftp server opened on the second machine. Liquidsoap handles download automatically. Each file is given by an external script, `radiopilote-getnext`, whose answer looks as follows (except that it's on a single line): ``` annotate:file_id="3541",length="400.613877551",\ type="chansons",title="John Holt - Holigan",\ artist="RadioPi - Canal reggae",\ album="Studio One SeleKta! - Album Studio 1 12",\ canal="reggae":ftp://***:***@host/files/3541.mp3 ``` Note that we use annotate to pass some variables to liquidsoap... ```{.liquidsoap include="radiopi.liq"} ``` The other machine has a similar configuration except that files are local, but this is exactly the same for liquidsoap ! Using harbor, the live connects directly to liquidsoap, using port `8000` (icecast runs on port `8080`). Then, liquidsoap starts a relay to the other encoder, and both switch their channels to the new live. Additionally, a file output is started upon live connection, in order to backup the stream. You could also add a relay to icecast in order to manually check what's received by the harbor. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/reference-header.md����������������������������������������������������0000664�0000000�0000000�00000001552�14773033502�0021624�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Liquidsoap scripting language reference The **Source / ...** categories contain all functions that return sources. The **Input** functions are those which build elementary sources (playing files, synthesizing sound, etc.). The **Output** functions are those which take a source and register it for being streamed to the outside (file, soundcard, audio server, etc.). The **Visualization** functions are experimental ones that let you visualize in real-time some aspects of the audio stream. The **Sound Processing** functions are those which basically work on the source as a continuous audio stream. They would typically be mixers of streams, audio effects or analysis. Finally, **Track Processing** functions are basically all others, often having a behaviour that depends on or affects the extra information that liquidsoap puts in streams: track limits and metadata. ������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/release-assets.md������������������������������������������������������0000664�0000000�0000000�00000001101�14773033502�0021346�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������This release provides liquidsoap assets before they are published as a new versioned release. You can use it to install the latest stable code before it is published and test/prepare your production environment for it. Rolling releases can also be useful for us to quickly detect and report bugs before the final published release! Assets listed in this release will never be modified or deleted. Feel free to use them for packaging or distribution purposes. For more details about our release process, please checkout https://github.com/savonet/liquidsoap#release-details ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/replay_gain.md���������������������������������������������������������0000664�0000000�0000000�00000010530�14773033502�0020726�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Normalization and ReplayGain ## Normalization If you want to have a constant average volume on an audio stream, you can use the `normalize` operator. However, this operator cannot guess the volume of the whole stream, and can be "surprised" by rapid changes of the volume. This can lead to a volume that is too low, too high, oscillates. In some cases, dynamic normalization also creates saturation. To tweak the normalization, several parameters are available. These are listed and explained in the [reference](reference.html) and also visible by executing `liquidsoap -h normalize`. However, if the stream you want to normalize consist of audio files, using the replay gain technology might be a better choice. ## Replay gain [ReplayGain](https://en.wikipedia.org/wiki/ReplayGain) is a proposed standard that is (more or less) respected by many open-source tools. It provides a way to obtain an overall uniform perceived loudness over a track or a set of tracks. The computation of the loudness is based on how the human ear actually perceives each range of frequency. Having computed the average perceived loudness on a track or an album, it is easy to renormalize the tracks when playing, ensuring a comfortable, consistent listening experience. Because it is track-based, replay gain does not suffer from the typical problems of stream-based, dynamic approaches. Namely, these distort the initial audio, since they constantly adapt the amplification factor. Sometimes it oscillates too quickly in a weird audible way. Sometimes it does not adapt quickly enough, leading to under or over-amplified sections. ### Computing or retrieving replay gain information The first step in order to use replay gain is to fetch or compute the appropriate normalization level for a given file. Replay gain information can be found in various metadata fields depending on the audio format and the replay gain computation tool. Liquidsoap provides a script for extracting the replay gain value which requires the `ffmpeg` binary. There are two ways to use our replain gain script, one that works for _all_ files and one that can be enabled on a per-file basis, if you need finer grained control over replay gain. #### Using the replay gain metadata resolver The metadata solution is uniform: without changing anything, _all_ your files will have a new `replaygain_track_gain` metadata when the computation succeeded. However, keep in mind that this computation can be costly and will be done each time a remote file is downloaded to be prepared for streaming unless it already has the information pre-computed. For this reason, it is recommended to pre-compute replay gain information as much as possible, specially if you intent to stream large audio files. The replay gain metadata resolver is not enabled by default. You can do it by adding the following code to your script: ```liquidsoap enable_replaygain_metadata() ``` #### Using the `replaygain:` protocol The `replaygain:` protocol triggers replay gain retrieval or computation on a per-file bases. To use it, you prefix your request URIs with it. For instance, replacing `/path/to/file.mp3` with `replaygain:/path/to/file.mp3`. When resolving such a request, a call to our script will be issued, resulting in your file having the extra `replaygain_track_gain` metadata. Prepending `replaygain:` is easy if you are using a script behind some `request.dynamic.list` operator. If you are using the `playlist` operator, you can use its `prefix` parameter. Protocols can be chained, for instance: ``` annotate:foo="bar":replaygain:/path/to/file.mp3 ``` ### Applying replay gain information After fetching or computing the replay gain information, the next step is to use it to correct the source's volume. The `amplify()` operator is used for that. This operator can be made to behave according to a given metadata, here the `replaygain` metadata. This is done using the `override` parameter. For replay gain implementation, the `amplify` operator would typically be added immediately on top of the basic tracks source, before transitions or other audio processing operators. Typically: ```{.liquidsoap include="replaygain-metadata.liq" to="END"} ``` For convenience, we added the `replaygain` operator which performs the amplification on the right metadata so that this can further be simplified to ```{.liquidsoap include="replaygain-playlist.liq" to="END"} ``` ������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/request_sources.md�����������������������������������������������������0000664�0000000�0000000�00000007775�14773033502�0021710�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������Playing files is the most common way to build an audio stream. In liquidsoap, files are accessed through [requests](requests.html), which combine the retrieval of a possibly remote file, and its decoding. Liquidsoap provides several operators for playing requests: `single`, `playlist` and `playlist.safe`, `request.dynamic.list`, `request.queue` and `request.equeue`. In a few cases (`single` with a local file, or `playlist.safe`) a request operator will know that it can always get a ready request instantaneously. It will then be [infallible](sources.html). Otherwise, it will have a queue of requests ready to be played (local files with a valid content), and will feed this queue in the background. This process is described here. ## Common parameters Queued request sources maintain an _estimated remaining time_, and trigger a new request resolution when this remaining time goes below their `length` parameter. The estimation is based on the duration of files prepared in the queue, and the estimated remaining time in the currently playing file. Precise file durations being expensive to compute, they are not forced: if a duration is provided in the metadata it shall be used, otherwise the `default_length` is assumed. For example, with the default 10 seconds of wanted queue length, the operator will only prepare a new file 10 seconds before the end of the current one. Up to liquidsoap 0.9.1, the estimated remaining time in the current track was not taken into account. With this behavior, each request-based source would keep at least one song in queue, which was sometimes inconvenient. This behavior can be restored by passing `conservative=true`, which is useful in some cases: it helps to ensure that a song will be ready in case of skip; generally, it prepares things more in advance, which is good when resolution is long (_e.g._, heavily loaded server, remote files). ## Request.dynamic This source takes a custom function for creating its new requests. This function, of type `()->request`, can for example call an external program. To create the request, the function will have to use the `request.create` function which has type `(string,?indicators:[string])`. The first string is the initial URI of the request, which is resolved to get an audio file. The second argument can be used to directly specify the first row of URIs (see the page about [requests](requests.html) for more details), in which case the initial URI is just here for naming, and the resolving process will try your list of indicators one by one until a valid audio file is obtained. An example that takes the output of an external script as an URI to create a new request can be: ```liquidsoap def my_request_function() = # Get the first line of my external process result = list.hd(default="", process.read.lines("my_script my_params")) # Create and return a request using this result [request.create(result)] end # Create the source s = request.dynamic.list(my_request_function) ``` ## Queues Liquidsoap features two sources which provide request queues that can be directly manipulated by the user, via the server interface: `request.queue` and `request.equeue`. The former is a queued source where you can only push new requests, while the later can be edited. Both operators actually deal with two queues: _primary_ and _secondary_ queues. The secondary queue is user-controlled. The primary queue is the one that all queued request sources have, its behavior is the same as described above, and it cannot be changed in any way by the user. Requests added to the secondary queue sit there until the feeding process gets them and attempts to prepare them and put them in the primary queue. You can set how many requests will be in that primary queue by tweaking the common parameters of all queued request sources. The two sources are controlled via the [command server](advanced.html). They both feature commands for looking up the queues, queuing new requests, and the `equeue` operator also allows removal and exchange of requests in the secondary queue. ���liquidsoap-2.3.2/doc/content/requests.md������������������������������������������������������������0000664�0000000�0000000�00000006033�14773033502�0020312�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# An abstract notion of files: requests The request is an abstract notion of file which can be conveniently used for defining powerful sources. A request can denote a local file, a remote file, or even a dynamically generated file. They are resolved to a local file thanks to a set of _protocols_. Then, audio requests are transparently decoded thanks to a set of audio and metadata _formats_. The systematic use of requests to access files allows you to use remote URIs instead of local paths everywhere. It is perfectly OK to create a playlist for a remote list containing remote URIs: ` playlist("http://my/friends/playlist.pls")` . ## The resolution process The nice thing about resolution is that it is recursive and supports backtracking. An URI can be changed into a list of new ones, which are in turn resolved. The process succeeds if some valid local file appears at some point. If it doesn't succeed on one branch then it goes back to another branch. A typical complex resolution would be: - `bubble:artist="bodom"` _ `ftp://no/where` _ `Error` - `ftp://some/valid.ogg` \* `/tmp/success.ogg` On top of that, metadata is extracted at every step in the branch. Usually, only the final local file yields interesting metadata (artist,album,...). But metadata can also be the nickname of the user who requested the song, set using the `annotate` protocol. At the end of the resolution process, in case of a media request, liquidsoap checks that the file is decodable, _i.e._, there should be a valid decoder for it. Each request gets assigned a request identifier (RID) which is used by various sources to identify which request(s) they are using. Knowing this number, you can monitor a request, even after it's been destroyed (see setting `request.grace_time`). Two [server](server.html) commands are available: `request.trace` shows a log of the resolution process and `request.metadata` shows the current request metadata. In addition, server commands are available to obtain the list of all requests, alive requests, currently resolving requests and currently playing requests (respectively `request.all`, `request.alive`, `request.resolving`, `request.on_air`). ## Currently supported protocols - HTTP, HTTPS, FTP thanks to curl - SAY for speech synthesis (requires festival): `say:I am a robot` resolves to the WAV file resulting from the synthesis. - TIME for speech synthesis of the current time: ` time: It is exactly $(time), and you're still listening.` - ANNOTATE for manually setting metadata, typically used in ` annotate:nick="alice",message="for bob":/some/track/uri` The extra metadata can then be synthesized in the audio stream, or merged into the standard metadata fields, or used on a rich web interface... It is also possible to add a new protocol from the script, as it is done with [Beets](beets.html) for getting songs from a database query. ## Currently supported formats - MPEG-1 Layer II (MP2) and Layer III (MP3) through libmad and `ocaml-mad` - Ogg Vorbis through libvorbis and `ocaml-vorbis` - WAV - AAC - and much more through external decoders! �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/rolling-release.md�����������������������������������������������������0000664�0000000�0000000�00000001220�14773033502�0021514�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������This release provides liquidsoap assets before they are published as a new versioned release. You can use it to install the latest stable code before it is published and test/prepare your production environment for it. Rolling releases can also be useful for us to quickly detect and report bugs before the final published release! ⚠️ **Warning** ⚠️ Assets in this release will be deleted. If you are looking for permanent links to release assets, please head over to https://github.com/savonet/liquidsoap-release-assets/releases For more details about our release process, please checkout https://github.com/savonet/liquidsoap#release-details ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/script_loading.md������������������������������������������������������0000664�0000000�0000000�00000002263�14773033502�0021441�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Script loading When you run liquidsoap for streaming, the command line has the following form: ``` $ liquidsoap script_or_expr_1 ... script_or_expr_N ``` This allows you to ask liquidsoap to load definition and settings from some scripts so that the become available when processing the next ones. For example you can store your passwords by defined the variable `xxx` in `secret.liq`, and then refer to that variable in your main script `main.liq`. You would then run `liquidsoap secret.liq main.liq`. If you ever need to communicate `main.liq` there won't be any risk of divulgating your password. ## The pervasive script library In fact, liquidsoap also implicitly loads scripts before those that you specify on the command-line. These scripts are meant to contain standard utilities. Liquidsoap finds them in `LIBDIR/liquidsoap/VERSION` where `LIBDIR` depends on your configuration (it is typically `/usr/local/lib` or `/usr/lib`) and `VERSION` is the version of liquidsoap (_e.g._ `0.3.8` or `svn`). Currently, liquidsoap loads `stdlib.liq` from the library directory, and this file includes some others. You can add your personal standard library in that directory if you find it useful. ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/seek.md����������������������������������������������������������������0000664�0000000�0000000�00000003606�14773033502�0017371�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Seeking in liquidsoap Starting with Liquidsoap `1.0.0-beta2`, it is now possible to seek within sources! Not all sources support seeking though: currently, they are mostly file-based sources such as `request.queue`, `playlist`, `request.dynamic.list` etc.. The basic function to seek within a source is `source.seek`. It has the following type: ``` (source('a),float)->float ``` The parameters are: - The source to seek. - The duration in seconds to seek from current position. The function returns the duration actually seeked. Please note that seeking is done to a position relative to the _current_ position. For instance, `source.seek(s,3.)` will seek 3 seconds forward in source `s` and `source.seek(s,(-4.))` will seek 4 seconds backward. Since seeking is currently only supported by request-based sources, it is recommended to hook the function as close as possible to the original source. Here is an example that implements a server/telnet seek function: ```{.liquidsoap include="seek-telnet.liq"} ``` ## Cue points File-based sources support cue-points to cut the beginning and end of tracks The values of cue-in and cue-out points are given in absolute position through the source's metadata. For instance, the following source will cue-in at 10 seconds and cue-out at 45 seconds on all its tracks: ```liquidsoap s = playlist(prefix="annotate:liq_cue_in=\"10.\",liq_cue_out=\"45\":", "/path/to/music") ``` As in the above example, you may use the `annotate` protocol to pass custom cue points along with the files passed to Liquidsoap. This is particularly useful in combination with `request.dynamic` as an external script can build-up the appropriate URI, including cue-points, based on information from your own scheduling back-end. Alternatively, you may use `metadata.map` to add those metadata. The operator `metadata.map` supports seeking and passes it to its underlying source. ��������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/server.md��������������������������������������������������������������0000664�0000000�0000000�00000020751�14773033502�0017750�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Interaction with the server Liquidsoap starts with one or several scripts as its configuration, and then streams forever if everything goes well. Once started, you can still interact with it by means of the _server_. The server allows you to run commands. Some are general and always available, some belong to a specific operator. For example the `request.queue()` instances register commands to enqueue new requests, the outputs register commands to start or stop the outputting, display the last ten metadata chunks, etc. The protocol of the server is a simple human-readable one. Currently it does not have any kind of authentication and permissions. It is currently available via two media: TCP and Unix sockets. The TCP socket provides a simple telnet-like interface, available only on the local host by default. The Unix socket interface (_cf._ the `server.socket` setting) is through some sort of virtual file. This is more constraining, which allows one to restrict the use of the socket to some privileged users. You can find more details on how to configure the server in the [documentation](help.html#settings) of the settings key `server`, in particular `server.telnet` for the TCP interface and `server.socket` for the Unix interface. Liquidsoap also embeds some [documentation](help.html#server) about the available server commands. ### Using telnet Now, we shall simply enable the Telnet interface to the server, by setting `settings.server.telnet := true` or simply passing the `-t` option on the command-line. In a [complete case analysis](complete_case.html) we set up a `request.queue()` instance to play user requests. It had the identifier `"queue"`. We are now going to interact via the server to push requests into that queue: ``` dbaelde@selassie:~$ telnet localhost 1234 Trying 127.0.0.1... Connected to localhost.localdomain. Escape character is '^]'. queue.push /path/to/some/file.ogg 5 END request.metadata 5 [...] END queue.push http://remote/audio.ogg 6 END request.trace 6 [...see if the download started/succeeded...] END exit ``` Of course, the server isn't very user-friendly. But it is easy to write scripts to interact with Liquidsoap in that way, to implement a website or an IRC interface to your radio. However, this sort of tool is often bound to a specific usage, so we have not released any of ours. ### Web interface Another simple way to test the telnet server consists in using the ```liquidsoap server.harbor() ``` server.harbor api: https://www.liquidsoap.info/doc-2.0.0/reference-extras.html#server.harbor command which will start a web interface accessible at <http://localhost:8000/telnet> providing an emulation of a telnet. ## Interactive variables Sometimes it is useful to control a variable using telnet. A simple way to achieve this is to use the `interactive.float` function. For instance, in order to dynamically the volume of a source: ```liquidsoap # Register a telnet variable named volume with 1 as initial value v = interactive.float("volume", 1.) # Change the volume accordingly source = amplify(v, source) ``` The first line registers the variable volume on the telnet. Its value can be changed using the telnet command ```liquidsoap var.set volume = 0.5 ``` and it can be retrieved using ```liquidsoap var.get volume ``` Similarly, we can switch between two tracks using `interactive.bool` and `switch` as follows: ```liquidsoap # Activate the telnet server settings.server.telnet := true # The two sources s1 = playlist("~/Music") s2 = sine() # Create an interactive boolean b = interactive.bool("button", true) # Switch between the tracks depending on the boolean s = switch(track_sensitive=false,[(b,s1), ({true},s2)]) # Output the result output.pulseaudio(s) ``` By default the source s1 is played. To switch to s2, you can connect on the telnet server and type `var.set button = false`. ### Web interface A nice web interface can be obtained by running ```liquidsoap interactive.harbor() ``` interactive.harbor api: https://www.liquidsoap.info/doc-2.0.0/reference.html#interactive.harbor after all interactive variables have been defined. This will start a web server accessible at <http://localhost:8000/interactive> on which you can easily change the values for the interactive variables. ### Persistency By default, interactive variables are not _persistent_, which means that their values are lost if you restart the script. This can be changed by running the command ```liquidsoap interactive.persistent("vars.json") ``` after all the interactive variables have been defined. This will store the values of all the interactive variables in the file `vars.json` (in JSON format) whenever you modify them, and reload them next time your run your script. This can be very handy for setting parameters for sound effects for instance. ## Securing the server The command server provided by liquidsoap is very convenient for manipulating a running instance of Liquidsoap. However, no authentication mechanism is provided. The telnet server has no authentication and listens by default on the localhost (`127.0.0.1`) network interface, which means that it is accessible to any logged user on the machine. Many users have expressed interest into setting up a secured access to the command server, using for instance user and password information. While we understand and share this need, we do not believe this is a task that lies into Liquidsoap's scope. An authentication mechanism is not something that should be implemented naively. Being SSH, HTTP login or any other mechanism, all these methods have been, at some point, exposed to security issues. Thus, implementing our own secure access would require a constant care about possible security issues. Rather than doing our own home-made secure access, we believe that our users should be able to define their own secure access to the command server, taking advantage of a mainstream authentication mechanism, for instance HTTP or SSH login. In order to give an example of this approach, we show here how to create a SSH access to the command server: we create a SSH user that, when logging through SSH, has only access to the command server. First, we enable the unix socket for the command server in Liquidsoap: ```liquidsoap settings.server.socket := true settings.server.socket.path := "/path/to/socket" ``` When started, liquidsoap will create a socket file `/path/to/socket` that can be used to interact with the command server. For instance, if your user has read and write rights on the socket file, you can do ```liquidsoap socat /path/to/socket - ``` The interface is then exactly the same has for the telnet server. We define now a new ``shell''. This shell is in fact the invocation of the socat command. Thus, we create a `/usr/local/bin/liq_shell` file with the following content: ```bash #!/bin/sh # We test if the file is a socket, readable and writable. if [ -S /path/to/socket ] && [ -w /path/to/socket ] && \ [ -r /path/to/socket ]; then socat /path/to/socket - else # If not, we exit.. exit 1 fi ``` We set this file as executable, and we add it in the list of shells in `/etc/shells`. Now, we create a user with the `liq_shell` as its shell: ``` adduser --shell /usr/local/bin/liq_shell liq-user ``` You also need to make sure that `liq-user` has read and write rights on the socket file. Finally, when logging through ssh with `liq-user`, we get: ``` 11:27 toots@leonard % ssh liq-user@localhost liq-user@localhost's password: Linux leonard 2.6.32-4-amd64 #1 SMP Mon Apr 5 21:14:10 UTC 2010 x86_64 The programs included with the Debian GNU/Linux system are free software; the exact distribution terms for each program are described in the individual files in /usr/share/doc/*/copyright. Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent permitted by applicable law. Last login: Tue Oct 5 11:26:52 2010 from localhost help Available commands: (...) | exit | help [<command>] | list | quit | request.alive | request.all | request.metadata <rid> | request.on_air | request.resolving | request.trace <rid> | uptime | var.get <variable> | var.list | var.set <variable> = <value> | version Type "help <command>" for more information. END exit Bye! END Connection to localhost closed. ``` This is an example of how you can use an existing secure access to secure the access to liquidsoap's command server. This way, you make sure that you are using a mainstream secure application, here SSH. This example may be adapted similarly to use an online HTTP login mechanism, which is probably the most comment type of mechanism intended for the command line server. �����������������������liquidsoap-2.3.2/doc/content/shoutcast.md�����������������������������������������������������������0000664�0000000�0000000�00000002565�14773033502�0020462�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Streaming to Shoutcast Although Liquidsoap is primarily aimed at streaming to Icecast servers (that provide much more features than Shoutcast), it is also able to stream to Shoutcast. ## Shoutcast output Shoutcast server accept streams encoded with the MP3 or AAC/AAC+ codec. You to compile Liquidsoap with `lame` support, so it can encode in MP3. Liquidsoap also has support for AAC+ encoding using FDK-AAC or using an [external encoder](external_encoders.html). The recommended format is MP3. Shoutcast output are done using the `output.shoutcast` operator with the appropriate parameters. An example is: ```{.liquidsoap include="shoutcast.liq"} ``` As usual, `liquidsoap -h output.shoutcast` gives you the full list of options for this operator. ## Shoutcast as relay A side note for those of you who feel they ``need'' to use Shoutcast for non-technical reasons (such as their stream directory service...): you can still benefit from Icecast's power by streaming to an Icecast server, and then relaying it through a shoutcast server. In order to do that, you have to alias the root mountpoint ("`/`") to your MP3 mountpoint in your icecast server configuration, like this: ``` <alias source="/" dest="/mystream.mp3" /> ``` Be careful that icecast often aliases the status page (`/status.xsl`) with the `/`. In this case, comment out the status page alias before inserting yours. �������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/sources.md�������������������������������������������������������������0000664�0000000�0000000�00000014647�14773033502�0020134�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Sources Using liquidsoap is about writing a script describing how to build what you want. It is about building a stream using elementary streams and stream combinators, etc. Actually, it's a bit more than streams, we call them _sources_. A source is a stream with metadata and track annotations. It is discretized as a stream of fixed-length buffers of media samples, the frames. Every frame may have metadata inserted at any point, independently of track boundaries. At every instant, a source can be asked to fill a frame of data. The liquidsoap API provides plenty of functions for building sources. Some of those functions build elementary sources from scratch, others are operators that combine sources into more complex ones. An important class of sources is that of _active sources_, they are the sources that actively trigger the computation of your stream. Typically, active sources are built from output functions, because outputting a stream is the only reason why you want to compute it. All sources, operators and outputs are listed in the [scripting API reference](reference.html). ## How does it work? To clarify the picture let's study in more details an example: ```liquidsoap radio = output.icecast( %vorbis,mount="test.ogg", random( [ jingle , fallback([ playlist1,playlist2,playlist3 ]) ])) ``` At every cycle of the [clock](clocks.html), the output asks the `random` node for data, until it gets a full frame of raw audio. Then, it encodes the frame and sends it to the Icecast server. Suppose `random` has chosen the `fallback` node, and that only `playlist2` is available, and thus played. At every cycle, the buffer is passed from `random` to `fallback` and then to `playlist2` which fills it, returns it to `fallback` which returns it to `random` which returns it to the output. At some point, `playlist2` ends a track. The fallback detects that on the returned buffer, and selects a new child for the next filling, depending on who's available. But it doesn't change the buffer, and returns it to `random`, which also (randomly) selects a new child at this point, before returning the buffer to the output. On next filling, the route of the frame can be different. Note that it is also possible to have the route changed inside a track, for example using the `track_sensitive` option of fallback, which is typically done for instant switches to live shows when they start. The important point here is that **all of the above steps are local**. Everything takes place between one operator and its immediate children source; operators do not see beyond that point. ## Fallibility By default, liquidsoap outputs are meant to emit a stream without discontinuing. Since this stream is provided by the source passed to the output operator, it is the source responsibility to never fail. Liquidsoap has a mechanism to verify this, which helps you think of all possible failures, and prevent them. Elementary sources are either _fallible_ or _infallible_, and this _liveness type_ is propagated through operators to finally compute the type of any source. For example, a `fallback` or `random` source is infallible if an only if at least one of its children is infallible, and a `switch` is infallible if and only if it has one infallible child guarded by the trivial predicate `{ true }`. On startup, each output checks the liveness type of its input source, and issues an error if it is fallible. The typical fix for such problems is to add one fallback to play a default file (`single()`) or a checked playlist (`playlist.safe()`) if the normal source fails. One can also use the `mksafe` operator that will insert silence during failures. If you do not care about failures, you can pass the parameter `fallible=true` to most outputs. In that case, the output will accept a fallible source, and stop whenever the source fails, to restart when it is ready to emit a stream again. ## Caching mode In some situations, a source must take care of the consistency of its output. If it is asked twice to fill buffers during the same cycle, it should fill them with the same data. Suppose for example that a playlist is used by two outputs, and that it gives the first frame to the first output, the second frame to the second output: it would give the third frame to the first output during the second cycle, and the output will have missed one frame. It is sometimes useful to keep this is mind to understand the behaviour of some complex scripts. The high-level picture is enough for users, more details follow for developers and curious readers. The sources detect if they need to remember (cache) their previous output in order to replay it. To do that, clients of the source must register in advance. If two clients have registered, then caching should be enabled. Actually that's a bit more complicated, because of transitions. Obviously the sources which use a transition involving some other source must register to it, because they may eventually use it. But a jingle used in two transitions by the same switching operator doesn't need caching. The solution involves two kinds of registering: _dynamic_ and _static activations_. Activations are associated with a path in the graph of sources' nesting. The dynamic activation is a pre-registration allowing a single real _static activation_ to come later, possibly in the middle of a cycle. Two static activations trigger caching. The other reason for enabling caching is when there is one static activation and one dynamic activation which doesn't come from a prefix of the static activation's path. It means that the dynamic activation can yield at any moment to a static activation and that the source will be used by two sources at the same time. ## Execution model In your script you define a bunch of sources interacting together. Each source belongs to a [clock](clocks.html), but clocks only have direct access to _active sources_, which are mostly outputs. At every cycle of the clock, active sources are animated: a chunk of stream (frame) is computed, and potentially outputted one way or another. This streaming task is the most important and shouldn't be disturbed. Thus, other tasks are done in auxiliary threads: file download, audio validity checking, http polling, playlist reloading... No blocking or expensive call should be done in streaming threads. Remote files are completely downloaded to a local temporary file before use by the root thread. It also means that you shouldn't access NFS or any kind of falsely local files. �����������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/split-cue.md�����������������������������������������������������������0000664�0000000�0000000�00000000567�14773033502�0020352�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Split and re-encode a CUE sheet. CUE sheets are sometimes distributed along with a single audio file containing a whole CD. Liquidsoap can parse CUE sheets as playlists and use them in your request-based sources. Here's for instance an example of a simple code to split a CUE sheet into several mp3 files with `id3v2` tags: ```{.liquidsoap include="split-cue.liq"} ``` �����������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/stereotool.md����������������������������������������������������������0000664�0000000�0000000�00000006114�14773033502�0020636�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Stereotool support Starting with version `2.2.0`, liquidsoap supports the shared library distributed by [Thimeo Audio Technology](https://www.thimeo.com/stereo-tool/) using the `stereotool` operator (and `track.audio.stereotool` for the low-level, track-specific equivalent). This feature is enabled in all release builds of liquidsoap starting with `rolling-release-v2.2.x` and should be enabled if you compile liquidsoap with the optional `ctypes-foreign` opam module installed. The operator can replace the use of the stereotool binary in your script and offers multiple benefits. In particular, it has a **very low latency** compared to using the binary and also operates synchronously. The operator should be quite easy to use. Here's an example: ```liquidsoap # Define a source s = ... # Apply stereotool to it: s = stereotool( library_file="/path/to/stereotool/shared/lib", license_key="my_license_key", preset="/path/to/preset/file", s ) ``` That's it! You can apply as many `stereotool` operators as you wish and at any stage in the script, thanks to its synchronous nature. However, a current limitation is that **the processed audio signal is slightly delayed**. This is because the operator has an internal processing buffer. We do plan on delaying metadata and track marks to match this latency but this has not yet been implemented and will probably have to wait for the `2.3.x` release cycle. This means that, until then, track switches and metadata updates might happen slightly earlier than the corresponding signal. We're talking about `50ms` to `100ms` earlier, though, so that might not be a super big deal. For the same reason, the source returned by `stereotool` is an _audio-only_ source. Otherwise, other concurrent tracks such as video and etc would be slightly out of sync. If you need to use the operator in this kind of situation, you might want to use a `ffmpeg` filter to e.g. adjust the video's PTS to match the audio delay. In such case, you can refer to the `latency` method that is available on the source returned by the operator which should indicate the delay to compensate from the processed audio signal. The operator's `preset` parameter has a companion `load_type` parameter that can optionally be used to only load a subset of the preset. You might refer to the upstream documentation if you need to use it. Lastly, `stereotool` is a **proprietary software**. While we actively promote open source, we also want to meet our users where they are and, for a lot of them, this means supporting the sound processing provided by the tool. However, to use it, you will need a license. Using the operator without the proper license will _not_ result in an error in your script but the audio signal might have spoken text and/or beeps added to it. Using the operator with an invalid license will be reported in the logs. You might also use the `valid_license` method available on the source returned by the operator, which returns `false` if the license is invalid. In this case, the `unlincensed_used_features` method returns a string indicating which unlicensed features are being used. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/stream_content.md������������������������������������������������������0000664�0000000�0000000�00000015775�14773033502�0021501�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Stream contents In liquidsoap, a stream may contain any number of audio, video and event MIDI channels (though this has not been tested in a while!). As part of the type checking of your script, liquidsoap checks that you make a consistent use of stream contents, and also guesses what kind of stream your script is intended to work on. As with other inferred parameters, you do not necessarily need to read about stream contents typing if you're still learning the ropes of liquidsoap, but you might eventually need to know a little about it. In liquidsoap script language, there are three sorts of objects that rely on stream types: sources, requests and encoding formats. A [source](sources.html) produces a stream, and it is important what kind of stream it produces when composing it with other sources. A [request](requests.html) is an abstract notion of file, often meant to be decoded, and it is useful to know into what kind of stream it is meant to be decoded. Finally, a [format](encoding_formats.html) describes how a stream should be encoded (_e.g._, before output in a file or via icecast), and the stream content is also useful here for the format to make sense. In this page, we explain how liquidsoap uses stream types to guess and check what you're doing. ## Content types Liquidsoap supports various type of content to be produced as the script runs. ### Internal content _internal_ content generally refers to content that the liquidsoap application can produce and manipulate. For audio, the default internal content is `pcm` floats using OCaml native 64-bits float array representations. This is the format that allows the fastest manipulation. For video, the default is a C in-memory arrays of plannar YUV420 data. For users concerned with memory consumption, we also support two additional audio formats, `pcm_s16` and `pcm_f32` using, resp., signed 16-bit integers and 32-bit floating point numbers. These formats may increase CPU usage, however, as we do need to convert back and forth when using them in audio manipulation operators such as `amplify`, `crossfade` and etc. See [this link](memory.html#audio-data-format) for more details. ### Opaque content Liquidsoap also supports content type that are opaque to the application, provided by the `ffmpeg` decoder. There are two: - FFmpeg raw frames, which are decoded plain FFmpeg frames - FFmpeg packets, also referred to as FFmpeg copy content. These are packets of encoded content These type of content are consumed by FFmpeg specific operators and it is possible to convert back and forth if you want to use them with our internal operators. However, their best use-case is to keep them as-is end-to-end to optimize for memory and/or CPU usage. See the [FFmpeg support](ffmpeg.html) doc for more information. ## Global parameters You might have noticed that our description of internal stream contents is missing some information, such as sample rate, video size, etc. Indeed, that information is not part of the stream types, which is local to each source/request/format, but global in liquidsoap. You can change it using the `frame.audio/video.*` settings, shown here with their default values: ```liquidsoap audio.samplerate := 44100 video.frame.width := 320 video.frame.height := 240 video.frame.rate := 25 ``` ## Checking stream contents Checking the consistency of use of stream contents is done as part of type checking. There is not so much to say here, except that you have to read type errors. We present a few examples. For example, if you try to send an ALSA input to a SDL input using `output.sdl(input.alsa())`, you'll get the following: ``` At line 1, char 22-23: this value has type source(audio=pcm('a)) but it should be a subtype of source(video=canvas) ``` It means that a source with a video channel was expected by the SDL output, but the ALSA output can only offer sources producing audio. ## Conversions get a type error on seemingly meaningful code, and you'll wonder how to fix it. Often, it suffices to perform a few explicit conversions. Consider another example involving the SDL output, where we also try to use AO to output the audio content of a video: ```liquidsoap s = single("file.mp4") # Output video here output.file( %ffmpeg(%video(codec="libx264"), "/path/to/video.flv", s ) # Output audio here output.file( %ffmpeg(%audio(codec="aac")) "/path/to/video.aac", s ) ``` This won't work because the first output expects a video-only stream while the second one expected an audio-only stream The solution is to split the stream in two, dropping the irrelevant content: ```liquidsoap s = single("file.mp4") # Output video here output.file( %ffmpeg(%video(codec="libx264"), "/path/to/video.flv", source.drop.audio(s) ) # Output audio here output.file( %ffmpeg(%audio(codec="aac")) "/path/to/video.aac", source.drop.video(s) ) ``` Another conversion is muxing. It is useful to add audio/video channels to a pure video/audio stream. For this, see `source.mux.video` and `source.mux.audio`. ## Type annotations You now have all the tools to write a correct script. But you might still be surprised by what stream content liquidsoap guesses you want to use. This is very important, because even if liquidsoap finds a type for which it accepts to run, it might not run as you intend: a different type might mean a different behavior (not the intended number of audio channels, no video, etc). Before reading on how liquidsoap performs this inference, you can already work your way to the intended type by using type annotations. For example, with `output.alsa(input.alsa())`, you'll see that liquidsoap decides that stereo audio should be used, and consequently the ALSA I/O will be initialized with two channels. If you want to use a different number of channels, for example mono, you can explicitly specify it using: ```liquidsoap output.alsa((input.alsa():source(audio=pcm(mono)))) ``` ## Guessing stream contents When all other methods fail, you might need to understand a little more how liquidsoap guesses what stream contents should be used for each source. First, liquidsoap guesses as much as possible (without making unnecessary assumption) from what's been given in the script. Usually, the outputs pretty much determine what sources should contain. A critical ingredient here is often the [encoding format](encoding_formats.html). For example, in ```liquidsoap output.icecast(%vorbis,mount="some.ogg",s) ``` `%vorbis` has type `format(audio=pcm(stereo))`, hence `s` should have type `source(audio=pcm(stereo))`. This works in more complex examples, when the types are guessed successively for several intermediate operators. After this first phase, it is possible that some contents are still undetermined. For example in `output.alsa(input.alsa())`, any number of audio channels could work, and nothing helps us determine what is intended. At this point, the default numbers of channels are used. They are given by the setting `frame.audio/video/midi.channels` (whose defaults are respectively `2`, `0` and `0`). In our example, stereo audio would be chosen. ���liquidsoap-2.3.2/doc/content/strings_encoding.md����������������������������������������������������0000664�0000000�0000000�00000002354�14773033502�0022000�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Strings encoding Liquidsoap operates internally using the UTF-8 string encoding. Most strings inside the application are converted to UTF-8 whenever possible. Conversion is done using [camomile](https://github.com/ocaml-community/camomile) automatic string encoding detection. If the conversion fails, the string is kept as-is. The list of encodings used for automatic detection is set via [settings.charset.encodings](settings.html#list-of-encodings-to-try-for-automatic-encoding-detection.) There are some exceptions, however. For instance, filenames and paths are not converted: if your system expects paths to be in a different encoding than UTF-8 then we do need to keep strings representing files and paths in this encoding to prevent errors. In general, you are advised to set the string encoding to UTF-8 on all systems running liquidsoap scripts for consistency and clarity. However, if for some reasons you need to tweak string encoding, these settings can be of use: - `settings.log.recode` and `settings.log.recode.encoding`: set the first one to `true` and the second one to the string encoding you would like log entries to be converted into. - `settings.metadata.recode`: set to `false` to prevent metadata from being converted to UTF-8. ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/video-static.md��������������������������������������������������������0000664�0000000�0000000�00000000674�14773033502�0021037�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# A simple video script The other day, I wanted to prepare some videos of my favorite reggae and soul tunes for uploading them to YouTube. My goal was very simple: prepare a video with the music, and a static image. After briefly digging for a simple software to do that, which I could not find, I said ``hey, why not doing it with liquidsoap''? Well, that is fairly easy! Here is the code: ```{.liquidsoap include="video-static.liq"} ``` ��������������������������������������������������������������������liquidsoap-2.3.2/doc/content/video.md���������������������������������������������������������������0000664�0000000�0000000�00000020367�14773033502�0017553�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- header-includes: | \DeclareUnicodeCharacter{03C0}{$\pi$} --- Basically streaming videos does not change anything compared to streaming audio: you just have to use video files instead of sound files! For instance, if you want to stream a single file to an icecast server in ogg format (with theora and vorbis as codecs for audio and video) you can simply type: ```{.liquidsoap include="video-simple.liq"} ``` And of course you could have used a `playlist` instead of `single` to have multiple files, or used other [formats](encoding_formats.html) for the stream. In order to test a video stream, it is often convenient to use the `output.sdl` operator (or `output.graphics`) which will open a window and display the video stream inside. These can handle streams with video only, you can use the `drop_audio` operator to remove the sound part of a stream if needed. You should be expecting much higher resource needs (in cpu time in particular) for video than for audio. So, be prepared to hear the fan of your computer! The size of videos have a great impact on computations; if your machine cannot handle a stream (i.e. it's always catching up) you can try to encode to smaller videos for a start. ### Setting up frame size and positions We provide an abstract API to specify video frame sizes and positions that is independent from the actual rendered size. This way, you can define all your elements and have them being rendered at different frame size without having to change their placement or size values! This works by setting up a _virtual canvas_ that is larger than the _actual canvas_. You specify your positions, sizes etc. in terms of units for the larger canvas and they are translated automatically to values that apply for the actual canvas. We provide some default values. They are all `16:9` ratio using a virtual canvas of `10 000` pixels height: ```{.liquidsoap include="video-default-canvas.liq"} ``` The returned canvas is a record with the following methods: - `width`/`height`: size of the actual frame - `px`: define values in terms of virtual pixels - `vw`/`vh`: define values in terms of percentage (between `0.` and `1.`) of, resp., the actual frame width and height - `rem`: define values in terms of percentage (between `0.` and `1.`) of the default font size All the positioning methods are functions. For convenience, you can use the infix operator `@` to make things more readable. For instance, instead of writing `px(120)` to define a size of `120px`, you can write: `120 @ px`. These two notations are equivalent but the second one is more readable in this context. Here's an example of how to use this: ```{.liquidsoap include="video-canvas-example.liq"} ``` ### Encoding with FFmpeg The `%ffmpeg` encoder is the recommended encoder when working with video. Not only does it support a wide range of audio and video formats but it can also send and receive data to many different places, using `input.ffmpeg.` and `output.url`. On top of that, it also supports all the [FFmpeg filters](https://ffmpeg.org/ffmpeg-filters.html) and passing encoded data, if your script does not need re-encoding. The syntax for the encoder is detailed in the [encoders page](encoding_formats.html). Here are some examples: ```liquidsoap # AC3 audio and H264 video encapsulated in a MPEG-TS bitstream %ffmpeg(format="mpegts", %audio(codec="ac3",channel_coupling=0), %video(codec="libx264",b="2600k", "x264-params"="scenecut=0:open_gop=0:min-keyint=150:keyint=150", preset="ultrafast")) # AAC audio and H264 video encapsulated in a mp4 file (to use with # `output.file` only, mp4 container cannot be streamed! %ffmpeg(format="mp4", %audio(codec="aac"), %video(codec="libx264",b="2600k")) # Ogg opus and theora encappsulated in an ogg bitstream %ffmpeg(format="ogg", %audio(codec="libopus"), %video(codec="libtheora")) # Ogg opus and VP8 video encapsulated in a webm bitstream %ffmpeg(format="webm", %audio(codec="libopus"), %video(codec="libvpx")) ``` ### Streaming with FFmpeg The main input to take advantage of FFmpeg is `input.ffmpeg`. It should be able to decode pretty much any url and file that the `ffmpeg` command-line can take as input. This is, in particular, how `input.rtmp` is defined. For outputting, one can use the regular outputs but some of them have special features when used with `%ffmpeg`: - `output.file` is able to properly close a file after it is done encoding it. This makes it possible to encode in formats that need a proper header after encoding is done, such as `mp4`. - `output.url` will only work with the `%ffmpeg` encoder. It delegates data output to FFmpeg and can support any url that the `ffmpeg` command-line supports. - `output.file.hls` and `output.harbor.hls` should only be used with `%ffmpeg`. The other encoders do work but `%ffmpeg` is the only encoder able to generate valid `MPEG-TS` and `MP4` data segments for the HLS specifications. ## Useful tips & tricks Video is a really exciting world where there are lots of cool stuff to do. ### Transitions Transitions at the beginning or at the end of video can be achieved using `video.fade.in` and `video.fade.out`. For instance, fading at the beginning of videos is done by ```{.liquidsoap include="video-transition.liq" from="BEGIN" to="END"} ``` ### Adding a logo You can add a logo (any image) using the `video.add_image` operator, as follows: ```{.liquidsoap include="video-logo.liq" from="BEGIN" to="END"} ``` ### Inputting from a webcam If your computer has a webcam, it can be used as a source thanks to the `input.v4l2` operator. For instance: ```{.liquidsoap include="video-webcam.liq"} ``` ### Video in video Suppose that you have two video sources `s` and `s2` and you want to display a small copy of `s2` on top of `s`. This can be achieved by ```{.liquidsoap include="video-in-video.liq" from="BEGIN" to="END"} ``` ### Scrolling text Adding scrolling text at the bottom of your video is as easy as ```{.liquidsoap include="video-text.liq"} ``` You might need to change the `font` parameter so that it matches a font file present on your system. ### Effects There are many of effects that you can use to add some fun to your videos: `video.greyscale`, `video.sepia`, `video.lomo`, etc. [Read the documentation](reference.html) to find out about them. If you have compiled Liquidsoap with [frei0r](http://www.piksel.org/frei0r/) support, and have installed frei0r plugins, they will be named `video.frei0r.*`. You can have a list of those supported on your installation as usual, using `liquidsoap --list-plugins`. ### Presenting weather forecast You can say that a specific color should be transparent using `video.transparent`. For instance, you can put yourself in front of a blue screen (whose RGB color should be around 0x0000ff) and replace the blue screen by an image of the weather using ```{.liquidsoap include="video-weather.liq" to="END"} ``` ## Detailed examples ### The anonymizer Let's design an ``anonymizer'' effect: I want to blur my face and change my voice so that nobody will recognise me in the street after seeing the youtube video. Here is what we are going to achieve: <center><iframe width="560" height="315" src="//www.youtube.com/embed/E7Fb0wV3h5Q" frameborder="0" allowfullscreen></iframe></center>This video was produced thanks to the following script: ```{.liquidsoap include="video-anonymizer.liq"} ``` ### Controlling with OSC In this example we are going to use OSC integration in order to modify the parameters in realtime. There are many OSC clients around, for instance I used [TouchOSC](http://hexler.net/software/touchosc) : <center><iframe width="560" height="315" src="//www.youtube.com/embed/EX1PTjiuuXY" frameborder="0" allowfullscreen></iframe></center>Here is how the video was made: ```{.liquidsoap content="video-osc.liq"} ``` ### Blue screen You want to show yourself in front of a video of a bunny, as in <center><iframe width="640" height="360" src="//www.youtube.com/embed/zHikXRNMQu4?feature=player_detailpage" frameborder="0" allowfullscreen></iframe></center>The idea is to film yourself in front of a blue screen, make this blue screen transparent and put the resulting video in front of the bunny video (actually, I don't have a blue screen at home, only a white wall but it still kinda works). ```{.liquidsoap include="video-bluescreen.liq"} ``` �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/content/xml.md�����������������������������������������������������������������0000664�0000000�0000000�00000010672�14773033502�0017243�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������## Importing/exporting XML values Support for XML parsing and rendering was first added in liquidsoap `2.3.1`. You can parse XML strings using a decorator and type annotation. There are two different representations of XML you can use. ### Record access representation This is the easiest representation. It is intended for quick access to parsed value via record and tuples. Here's an example: ```liquidsoap s = '<bla param="1" bla="true"> <foo opt="12.3">gni</foo> <bar /> <bar>bla</bar> <blo>1.23</blo> <blu>false</blu> <ble>123</ble> </bla>' let xml.parse (x : { bla: { foo: string.{ xml_params: {opt: float} }, bar: (unit * string), blo: float, blu: bool, ble: int, xml_params: { bla: bool } } } ) = s print("The value for ble is: #{x.bla.ble}") ``` Things to note: - The basic mappings are: `<tag name> -> <tag content>` - Tag content maps tag parameters to a `xml_params` method. - When multiple tags are present, their values are collected as tuple (`bar` tag in the example) - When a tag contains a single ground value (`string`, `bool`, `float` or `integer`), the mapping is from tag name to the corresponding value, with xml attributes attached as methods - Tag parameters can be converted to ground values and omitted. The parsing is driven by the type annotation and is intended to be permissive. For instance, this will work: ```liquidsoaop s = '<bla>foo</bla>' # Here, `foo` is omitted. let xml.parse (x: { bla: unit }) = s # x contains: { bla = () } # Here, `foo` is made optional let xml.parse (x: { bla: string? }) = s # x contains: { bla = "foo" } ``` ### Formal representation Because XML format can result in complex values, the parser can also use a generic representation. Here's an example: ```liquidsoap s = '<bla param="1" bla="true"> <foo opt="12.3">gni</foo> <bar /> <bar>bla</bar> <blo>1.23</blo> <blu>false</blu> <ble>123</ble> </bla>' let xml.parse (x : ( string * { xml_params: [(string * string)], xml_children: [ ( string * { xml_params: [(string * string)], xml_children: [(string * {xml_text: string})] } ) ] } ) ) = s # x contains: ( "bla", { xml_children= [ ( "foo", { xml_children=[("xml_text", {xml_text="gni"})], xml_params=[("opt", "12.3")] } ), ("bar", {xml_children=[], xml_params=[]}), ( "bar", { xml_children=[("xml_text", {xml_text="bla"})], xml_params=[("option", "aab")] } ), ( "blo", {xml_children=[("xml_text", {xml_text="1.23"})], xml_params=[]} ), ( "blu", {xml_children=[("xml_text", {xml_text="false"})], xml_params=[]} ), ( "ble", {xml_children=[("xml_text", {xml_text="123"})], xml_params=[]} ) ], xml_params=[("param", "1"), ("bla", "true")] } ) ``` This representation is much less convenient to manipulate but allows an exact representation of all XML values. Things to note: - XML nodes are represented by a pair of the form: `(<tag name>, <tag properties>)` - `<tag properties>` is a record containing the following methods: - `xml_params`, represented as a list of pairs `(string * string)` - `xml_children`, containing a list of the XML node's children. Each entry in the list is a node in the formal XML representation. - `xml_text`, present when the node is a text node. In this case, `xml_params` and `xm_children` are empty. - By convention, text nodes are labelled `xml_text` and are of the form: `{ xml_text: "node content" }` ### Rendering XML values XML values can be converted back to strings using `xml.stringify`. Both the formal and record-access form can be rendered back into XML strings however, with the record-access representations, if a node has multiple children with the same tag, the conversion to XML string will fail. More generally, if the values you want to convert to XML strings are complex, for instance if they use several times the same tag as child node or if the order of child nodes matters, we recommend using the formal representation to make sure that children ordering is properly preserved. This is because record methods are not ordered in the language so we make no guarantee that the child nodes they represent be rendered in a specific order. ����������������������������������������������������������������������liquidsoap-2.3.2/doc/content/yaml.md����������������������������������������������������������������0000664�0000000�0000000�00000001256�14773033502�0017403�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������## Importing/exporting YAML values Support for YAML parsing and rendering was first added in liquidsoap `2.2.0`. This support follows the same pattern as [JSON parsing/rendering](json.html) but using yaml-based syntax, i.e.: ```liquidsoap let yaml.parse ({ name, version, scripts, } : { name: string, version: string, scripts: { test: string? }? }) = file.contents("/path/to/file.yaml") ``` and ```liquidsoap r = {artist = "Bla", title = "Blo"} print(yaml.stringify(r)) ``` The only major difference being that, in YAML, all numbers are parsed and rendered as _floats_. Please refer to the [JSON parsing and rendering](json.html) documentation for more details. ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/dune���������������������������������������������������������������������������0000664�0000000�0000000�00000001615�14773033502�0015322�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������; Regenerate using dune build @gendune --auto-promote (include dune.inc) (rule (alias dummytest) (package liquidsoap) (action (run %{bin:liquidsoap} --version))) (executable (name gen_dune) (preprocess (pps ppx_string)) (libraries re liquidsoap_build_tools) (modules gen_dune)) (executable (name subst_md) (libraries re liquidsoap_lang) (modules subst_md)) (rule (alias gendune) (target dune.inc.gen) (deps (source_tree ../src/libs) (source_tree .)) (action (with-stdout-to dune.inc.gen (run ./gen_dune.exe)))) (rule (alias gendune) (action (diff dune.inc dune.inc.gen))) (rule (alias doc) (target liquidsoap.1) (deps no-pandoc (:liquidsoap_man liquidsoap.1.md)) (action (ignore-outputs (system "pandoc -s -t man %{liquidsoap_man} -o liquidsoap.1 || cp no-pandoc liquidsoap.1")))) (install (section man) (package liquidsoap) (files liquidsoap.1)) �������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/dune.inc�����������������������������������������������������������������������0000664�0000000�0000000�00001220175�14773033502�0016077�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������ (rule (alias doc) (package liquidsoap) (deps (source_tree ../src/libs)) (target protocols.md) (action (with-stdout-to protocols.md (setenv PAGER none (run %{bin:liquidsoap} --list-protocols-md))))) (rule (alias doc) (package liquidsoap) (deps (:header content/reference-header.md) (source_tree ../src/libs)) (target reference.md) (action (with-stdout-to reference.md (progn (cat %{header}) (echo "\n") (setenv PAGER none (run %{bin:liquidsoap} --list-functions-md))))) ) (rule (alias doc) (package liquidsoap) (deps (:header content/reference-header.md) (source_tree ../src/libs)) (target reference-extras.md) (action (with-stdout-to reference-extras.md (progn (cat %{header}) (echo "\n") (setenv PAGER none (run %{bin:liquidsoap} --no-external-plugins --list-extra-functions-md))))) ) (rule (alias doc) (package liquidsoap) (deps (:header content/reference-header.md) (source_tree ../src/libs)) (target reference-deprecated.md) (action (with-stdout-to reference-deprecated.md (progn (cat %{header}) (echo "\n") (setenv PAGER none (run %{bin:liquidsoap} --list-deprecated-functions-md))))) ) (rule (alias doc) (package liquidsoap) (deps (source_tree ../src/libs)) (target settings.md) (action (with-stdout-to settings.md (setenv PAGER none (run %{bin:liquidsoap} --list-settings))))) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target protocols.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md protocols.md) ) (target protocols.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=protocols --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target reference.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md reference.md) ) (target reference.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=reference --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target reference-extras.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md reference-extras.md) ) (target reference-extras.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=reference-extras --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target reference-deprecated.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md reference-deprecated.md) ) (target reference-deprecated.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=reference-deprecated --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target settings.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md settings.md) ) (target settings.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=settings --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target beets.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/beets.md) ) (target beets.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=beets --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target blank.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/blank.md) ) (target blank.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=blank --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target book.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/book.md) ) (target book.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=book --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target build.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/build.md) ) (target build.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=build --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target clocks.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/clocks.md) ) (target clocks.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=clocks --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target complete_case.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/complete_case.md) ) (target complete_case.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=complete_case --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target cookbook.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/cookbook.md) ) (target cookbook.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=cookbook --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target crossfade.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/crossfade.md) ) (target crossfade.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=crossfade --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target custom-path.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/custom-path.md) ) (target custom-path.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=custom-path --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target database.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/database.md) ) (target database.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=database --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target documentation.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/documentation.md) ) (target documentation.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=documentation --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target dynamic_sources.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/dynamic_sources.md) ) (target dynamic_sources.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=dynamic_sources --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target encoding_formats.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/encoding_formats.md) ) (target encoding_formats.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=encoding_formats --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target external_decoders.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/external_decoders.md) ) (target external_decoders.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=external_decoders --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target external_encoders.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/external_encoders.md) ) (target external_encoders.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=external_encoders --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target external_streams.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/external_streams.md) ) (target external_streams.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=external_streams --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target faq.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/faq.md) ) (target faq.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=faq --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target ffmpeg.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/ffmpeg.md) ) (target ffmpeg.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=ffmpeg --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target ffmpeg_cookbook.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/ffmpeg_cookbook.md) ) (target ffmpeg_cookbook.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=ffmpeg_cookbook --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target ffmpeg_encoder.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/ffmpeg_encoder.md) ) (target ffmpeg_encoder.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=ffmpeg_encoder --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target ffmpeg_filters.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/ffmpeg_filters.md) ) (target ffmpeg_filters.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=ffmpeg_filters --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target flows_devel.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/flows_devel.md) ) (target flows_devel.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=flows_devel --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target frequence3.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/frequence3.md) ) (target frequence3.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=frequence3 --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target geekradio.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/geekradio.md) ) (target geekradio.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=geekradio --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target harbor.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/harbor.md) ) (target harbor.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=harbor --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target harbor_http.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/harbor_http.md) ) (target harbor_http.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=harbor_http --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target help.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/help.md) ) (target help.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=help --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target hls_output.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/hls_output.md) ) (target hls_output.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=hls_output --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target http_input.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/http_input.md) ) (target http_input.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=http_input --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target icy_metadata.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/icy_metadata.md) ) (target icy_metadata.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=icy_metadata --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target in_production.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/in_production.md) ) (target in_production.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=in_production --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target index.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/index.md) ) (target index.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=index --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target install.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/install.md) ) (target install.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=install --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target json.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/json.md) ) (target json.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=json --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target ladspa.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/ladspa.md) ) (target ladspa.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=ladspa --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target language.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/language.md) ) (target language.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=language --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target memory.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/memory.md) ) (target memory.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=memory --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target metadata.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/metadata.md) ) (target metadata.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=metadata --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target migrating.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/migrating.md) ) (target migrating.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=migrating --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target multitrack.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/multitrack.md) ) (target multitrack.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=multitrack --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target on2.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/on2.md) ) (target on2.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=on2 --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target phases.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/phases.md) ) (target phases.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=phases --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target playlist_parsers.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/playlist_parsers.md) ) (target playlist_parsers.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=playlist_parsers --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target presentations.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/presentations.md) ) (target presentations.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=presentations --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target profiling.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/profiling.md) ) (target profiling.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=profiling --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target prometheus.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/prometheus.md) ) (target prometheus.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=prometheus --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target protocols-presentation.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/protocols-presentation.md) ) (target protocols-presentation.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=protocols-presentation --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target publications.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/publications.md) ) (target publications.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=publications --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target quick_start.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/quick_start.md) ) (target quick_start.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=quick_start --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target radiopi.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/radiopi.md) ) (target radiopi.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=radiopi --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target reference-header.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/reference-header.md) ) (target reference-header.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=reference-header --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target release-assets.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/release-assets.md) ) (target release-assets.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=release-assets --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target replay_gain.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/replay_gain.md) ) (target replay_gain.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=replay_gain --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target request_sources.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/request_sources.md) ) (target request_sources.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=request_sources --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target requests.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/requests.md) ) (target requests.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=requests --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target rolling-release.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/rolling-release.md) ) (target rolling-release.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=rolling-release --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target script_loading.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/script_loading.md) ) (target script_loading.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=script_loading --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target seek.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/seek.md) ) (target seek.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=seek --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target server.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/server.md) ) (target server.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=server --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target shoutcast.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/shoutcast.md) ) (target shoutcast.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=shoutcast --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target sources.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/sources.md) ) (target sources.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=sources --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target split-cue.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/split-cue.md) ) (target split-cue.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=split-cue --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target stereotool.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/stereotool.md) ) (target stereotool.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=stereotool --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target stream_content.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/stream_content.md) ) (target stream_content.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=stream_content --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target strings_encoding.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/strings_encoding.md) ) (target strings_encoding.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=strings_encoding --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target video-static.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/video-static.md) ) (target video-static.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=video-static --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target video.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/video.md) ) (target video.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=video --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target xml.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/xml.md) ) (target xml.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=xml --template=template.html -o %{target}) ) ) ) (rule (alias doc) (package liquidsoap) (enabled_if (not %{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target yaml.html) (action (run cp %{no_pandoc} %{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html content/liq/append-silence.liq content/liq/archive-cleaner.liq content/liq/basic-radio.liq content/liq/beets-amplify.liq content/liq/beets-protocol-short.liq content/liq/beets-protocol.liq content/liq/beets-source.liq content/liq/blank-detect.liq content/liq/blank-sorry.liq content/liq/complete-case.liq content/liq/cross.custom.liq content/liq/crossfade.liq content/liq/decoder-faad.liq content/liq/decoder-flac.liq content/liq/decoder-metaflac.liq content/liq/dump-hourly.liq content/liq/dump-hourly2.liq content/liq/dynamic-source.liq content/liq/external-output.file.liq content/liq/fallback.liq content/liq/ffmpeg-filter-dynamic-volume.liq content/liq/ffmpeg-filter-flanger-highpass.liq content/liq/ffmpeg-filter-hflip.liq content/liq/ffmpeg-filter-hflip2.liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq content/liq/ffmpeg-live-switch.liq content/liq/ffmpeg-relay-ondemand.liq content/liq/ffmpeg-relay.liq content/liq/ffmpeg-shared-encoding-rtmp.liq content/liq/ffmpeg-shared-encoding.liq content/liq/fixed-time1.liq content/liq/fixed-time2.liq content/liq/frame-size.liq content/liq/harbor-auth.liq content/liq/harbor-dynamic.liq content/liq/harbor-insert-metadata.liq content/liq/harbor-metadata.liq content/liq/harbor-redirect.liq content/liq/harbor-simple.liq content/liq/harbor-usage.liq content/liq/harbor.http.register.liq content/liq/harbor.http.response.liq content/liq/hls-metadata.liq content/liq/hls-mp4.liq content/liq/http-input.liq content/liq/icy-update.liq content/liq/input.mplayer.liq content/liq/jingle-hour.liq content/liq/json-ex.liq content/liq/json-stringify.liq content/liq/json1.liq content/liq/live-switch.liq content/liq/medialib-predicate.liq content/liq/medialib.liq content/liq/medialib.sqlite.liq content/liq/multitrack-add-video-track.liq content/liq/multitrack-add-video-track2.liq content/liq/multitrack-default-video-track.liq content/liq/multitrack.liq content/liq/multitrack2.liq content/liq/multitrack3.liq content/liq/output.file.hls.liq content/liq/playlists.liq content/liq/prometheus-callback.liq content/liq/prometheus-settings.liq content/liq/radiopi.liq content/liq/re-encode.liq content/liq/regular.liq content/liq/replaygain-metadata.liq content/liq/replaygain-playlist.liq content/liq/request.dynamic.liq content/liq/rtmp.liq content/liq/samplerate3.liq content/liq/scheduling.liq content/liq/seek-telnet.liq content/liq/settings.liq content/liq/shoutcast.liq content/liq/single.liq content/liq/source-cue.liq content/liq/space_overhead.liq content/liq/split-cue.liq content/liq/sqlite.liq content/liq/srt-receiver.liq content/liq/srt-sender.liq content/liq/switch-show.liq content/liq/transcoding.liq content/liq/video-anonymizer.liq content/liq/video-bluescreen.liq content/liq/video-canvas-example.liq content/liq/video-default-canvas.liq content/liq/video-in-video.liq content/liq/video-logo.liq content/liq/video-osc.liq content/liq/video-simple.liq content/liq/video-static.liq content/liq/video-text.liq content/liq/video-transition.liq content/liq/video-weather.liq content/liq/video-webcam.liq (:md content/yaml.md) ) (target yaml.html) (action (pipe-stdout (run pandoc %{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=yaml --template=template.html -o %{target}) ) ) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/append-silence.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/append-silence.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/archive-cleaner.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/archive-cleaner.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/basic-radio.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/basic-radio.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/beets-amplify.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/beets-amplify.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/beets-protocol-short.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/beets-protocol-short.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/beets-protocol.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/beets-protocol.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/beets-source.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/beets-source.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/blank-detect.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/blank-detect.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/blank-sorry.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/blank-sorry.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/complete-case.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/complete-case.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/cross.custom.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/cross.custom.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/crossfade.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/crossfade.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/decoder-faad.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/decoder-faad.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/decoder-flac.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/decoder-flac.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/decoder-metaflac.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/decoder-metaflac.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/dump-hourly.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/dump-hourly.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/dump-hourly2.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/dump-hourly2.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/dynamic-source.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/dynamic-source.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/external-output.file.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/external-output.file.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/fallback.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/fallback.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/ffmpeg-filter-dynamic-volume.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/ffmpeg-filter-dynamic-volume.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/ffmpeg-filter-flanger-highpass.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/ffmpeg-filter-flanger-highpass.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/ffmpeg-filter-hflip.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/ffmpeg-filter-hflip.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/ffmpeg-filter-hflip2.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/ffmpeg-filter-hflip2.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/ffmpeg-filter-parallel-flanger-highpass.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/ffmpeg-filter-parallel-flanger-highpass.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/ffmpeg-live-switch.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/ffmpeg-live-switch.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/ffmpeg-relay-ondemand.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/ffmpeg-relay-ondemand.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/ffmpeg-relay.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/ffmpeg-relay.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/ffmpeg-shared-encoding-rtmp.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/ffmpeg-shared-encoding-rtmp.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/ffmpeg-shared-encoding.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/ffmpeg-shared-encoding.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/fixed-time1.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/fixed-time1.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/fixed-time2.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/fixed-time2.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/frame-size.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/frame-size.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/harbor-auth.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/harbor-auth.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/harbor-dynamic.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/harbor-dynamic.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/harbor-insert-metadata.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/harbor-insert-metadata.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/harbor-metadata.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/harbor-metadata.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/harbor-redirect.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/harbor-redirect.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/harbor-simple.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/harbor-simple.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/harbor-usage.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/harbor-usage.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/harbor.http.register.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/harbor.http.register.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/harbor.http.response.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/harbor.http.response.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/hls-metadata.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/hls-metadata.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/hls-mp4.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/hls-mp4.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/http-input.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/http-input.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/icy-update.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/icy-update.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/input.mplayer.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/input.mplayer.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/jingle-hour.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/jingle-hour.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/json-ex.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/json-ex.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/json-stringify.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/json-stringify.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/json1.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/json1.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/live-switch.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/live-switch.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/medialib-predicate.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/medialib-predicate.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/medialib.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/medialib.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/medialib.sqlite.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/medialib.sqlite.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/multitrack-add-video-track.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/multitrack-add-video-track.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/multitrack-add-video-track2.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/multitrack-add-video-track2.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/multitrack-default-video-track.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/multitrack-default-video-track.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/multitrack.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/multitrack.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/multitrack2.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/multitrack2.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/multitrack3.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/multitrack3.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/output.file.hls.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/output.file.hls.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/playlists.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/playlists.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/prometheus-callback.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/prometheus-callback.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/prometheus-settings.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/prometheus-settings.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/radiopi.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/radiopi.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/re-encode.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/re-encode.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/regular.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/regular.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/replaygain-metadata.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/replaygain-metadata.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/replaygain-playlist.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/replaygain-playlist.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/request.dynamic.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/request.dynamic.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/rtmp.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/rtmp.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/samplerate3.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/samplerate3.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/scheduling.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/scheduling.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/seek-telnet.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/seek-telnet.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/settings.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/settings.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/shoutcast.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/shoutcast.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/single.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/single.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/source-cue.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/source-cue.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/space_overhead.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/space_overhead.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/split-cue.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/split-cue.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/sqlite.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/sqlite.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/srt-receiver.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/srt-receiver.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/srt-sender.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/srt-sender.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/switch-show.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/switch-show.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/transcoding.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/transcoding.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/video-anonymizer.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/video-anonymizer.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/video-bluescreen.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/video-bluescreen.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/video-canvas-example.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/video-canvas-example.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/video-default-canvas.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/video-default-canvas.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/video-in-video.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/video-in-video.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/video-logo.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/video-logo.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/video-osc.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/video-osc.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/video-simple.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/video-simple.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/video-static.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/video-static.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/video-text.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/video-text.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/video-transition.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/video-transition.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/video-weather.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/video-weather.liq)) ) (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq content/liq/video-webcam.liq) ) (action (run %{bin:liquidsoap} --check --no-fallible-check content/liq/video-webcam.liq)) ) (install (section doc) (package liquidsoap) (files (beets.html as html/beets.html) (blank.html as html/blank.html) (book.html as html/book.html) (build.html as html/build.html) (clocks.html as html/clocks.html) (complete_case.html as html/complete_case.html) (cookbook.html as html/cookbook.html) (crossfade.html as html/crossfade.html) (custom-path.html as html/custom-path.html) (database.html as html/database.html) (documentation.html as html/documentation.html) (dynamic_sources.html as html/dynamic_sources.html) (encoding_formats.html as html/encoding_formats.html) (external_decoders.html as html/external_decoders.html) (external_encoders.html as html/external_encoders.html) (external_streams.html as html/external_streams.html) (faq.html as html/faq.html) (ffmpeg.html as html/ffmpeg.html) (ffmpeg_cookbook.html as html/ffmpeg_cookbook.html) (ffmpeg_encoder.html as html/ffmpeg_encoder.html) (ffmpeg_filters.html as html/ffmpeg_filters.html) (flows_devel.html as html/flows_devel.html) (frequence3.html as html/frequence3.html) (geekradio.html as html/geekradio.html) (harbor.html as html/harbor.html) (harbor_http.html as html/harbor_http.html) (help.html as html/help.html) (hls_output.html as html/hls_output.html) (http_input.html as html/http_input.html) (icy_metadata.html as html/icy_metadata.html) (in_production.html as html/in_production.html) (index.html as html/index.html) (install.html as html/install.html) (json.html as html/json.html) (ladspa.html as html/ladspa.html) (language.html as html/language.html) (memory.html as html/memory.html) (metadata.html as html/metadata.html) (migrating.html as html/migrating.html) (multitrack.html as html/multitrack.html) (on2.html as html/on2.html) (orig/css/homepage.css as html/css/homepage.css) (orig/css/style.css as html/css/style.css) (orig/fosdem2020/clock.png as html/fosdem2020/clock.png) (orig/fosdem2020/index.html as html/fosdem2020/index.html) (orig/fosdem2020/logo.png as html/fosdem2020/logo.png) (orig/fosdem2020/radio.gif as html/fosdem2020/radio.gif) (orig/fosdem2020/remark.js as html/fosdem2020/remark.js) (orig/images/basic-radio-graph.png as html/images/basic-radio-graph.png) (orig/images/design/background.png as html/images/design/background.png) (orig/images/design/background_page.png as html/images/design/background_page.png) (orig/images/design/logo.png as html/images/design/logo.png) (orig/images/grab.png as html/images/grab.png) (orig/images/graph_clocks.png as html/images/graph_clocks.png) (orig/images/liqgraph.png as html/images/liqgraph.png) (orig/images/schema-webradio-inkscape.png as html/images/schema-webradio-inkscape.png) (orig/images/stream.png as html/images/stream.png) (orig/images/tabs/tab_API.png as html/images/tabs/tab_API.png) (orig/images/tabs/tab_about.png as html/images/tabs/tab_about.png) (orig/images/tabs/tab_developers.png as html/images/tabs/tab_developers.png) (orig/images/tabs/tab_docs.png as html/images/tabs/tab_docs.png) (orig/images/tabs/tab_snippets.png as html/images/tabs/tab_snippets.png) (phases.html as html/phases.html) (playlist_parsers.html as html/playlist_parsers.html) (presentations.html as html/presentations.html) (profiling.html as html/profiling.html) (prometheus.html as html/prometheus.html) (protocols-presentation.html as html/protocols-presentation.html) (protocols.html as html/protocols.html) (publications.html as html/publications.html) (quick_start.html as html/quick_start.html) (radiopi.html as html/radiopi.html) (reference-deprecated.html as html/reference-deprecated.html) (reference-extras.html as html/reference-extras.html) (reference-header.html as html/reference-header.html) (reference.html as html/reference.html) (release-assets.html as html/release-assets.html) (replay_gain.html as html/replay_gain.html) (request_sources.html as html/request_sources.html) (requests.html as html/requests.html) (rolling-release.html as html/rolling-release.html) (script_loading.html as html/script_loading.html) (seek.html as html/seek.html) (server.html as html/server.html) (settings.html as html/settings.html) (shoutcast.html as html/shoutcast.html) (sources.html as html/sources.html) (split-cue.html as html/split-cue.html) (stereotool.html as html/stereotool.html) (stream_content.html as html/stream_content.html) (strings_encoding.html as html/strings_encoding.html) (video-static.html as html/video-static.html) (video.html as html/video.html) (xml.html as html/xml.html) (yaml.html as html/yaml.html) ) ) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/gen_dune.ml��������������������������������������������������������������������0000664�0000000�0000000�00000011014�14773033502�0016554�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������module Pcre = Re.Pcre let generated_md = [ ("protocols.md", "--list-protocols-md", None); ("reference.md", "--list-functions-md", Some "content/reference-header.md"); ( "reference-extras.md", "--no-external-plugins --list-extra-functions-md", Some "content/reference-header.md" ); ( "reference-deprecated.md", "--list-deprecated-functions-md", Some "content/reference-header.md" ); ("settings.md", "--list-settings", None); ] let mk_html f = Pcre.substitute ~rex:(Pcre.regexp "md(?:\\.in)?$") ~subst:(fun _ -> "html") f let mk_md ?(content = true) f = if Pcre.pmatch ~rex:(Pcre.regexp "md\\.in$") f then Pcre.substitute ~rex:(Pcre.regexp "\\.in$") ~subst:(fun _ -> "") (Filename.basename f) else if content then "content/" ^ f else f let mk_title = Filename.remove_extension let mk_subst_rule f = if Pcre.pmatch ~rex:(Pcre.regexp "md\\.in$") f then ( let target = mk_md f in Printf.printf {| (rule (alias doc) (package liquidsoap) (deps (:subst_md ./subst_md.exe) (:in_md content/%s)) (target %s) (action (with-stdout-to %%{target} (run %%{subst_md} %%{in_md}))))|} f target) let mk_html_rule ~liq ~content f = let liq = liq |> List.map (fun f -> " " ^ f) |> String.concat "\n" in Printf.printf {| (rule (alias doc) (package liquidsoap) (enabled_if (not %%{bin-available:pandoc})) (deps (:no_pandoc no-pandoc)) (target %s) (action (run cp %%{no_pandoc} %%{target})) ) (rule (alias doc) (package liquidsoap) (enabled_if %%{bin-available:pandoc}) (deps liquidsoap.xml language.dtd template.html %s (:md %s) ) (target %s) (action (pipe-stdout (run pandoc %%{md} -t json) (run pandoc-include --directory content/liq) (run pandoc -f json --syntax-definition=liquidsoap.xml --highlight=pygments --metadata pagetitle=%s --template=template.html -o %%{target}) ) ) ) |} (mk_html f) liq (mk_md ~content f) (mk_html f) (mk_title f) let mk_generated_rule (file, option, header) = let header_deps, header_action, header_close = match header with | None -> ("", "", "") | Some fname -> ( [%string {|(:header %{fname})|}], {|(progn (cat %{header}) (echo "\n")|}, ")" ) in let header_action = if header_action = "" then "" else "\n " ^ header_action in let header_close = if header_close = "" then "" else "\n " ^ header_close in Printf.printf {| (rule (alias doc) (package liquidsoap) (deps %s (source_tree ../src/libs)) (target %s) (action (with-stdout-to %s%s (setenv PAGER none (run %%{bin:liquidsoap} %s)))))%s |} header_deps file file header_action option header_close let mk_test_rule file = Printf.printf {| (rule (alias doctest) (package liquidsoap) (deps (source_tree ../src/libs) (:test_liq %s) ) (action (run %%{bin:liquidsoap} --check --no-fallible-check %s)) ) |} file file let mk_html_install f = Printf.sprintf {| (%s as html/%s)|} (mk_html f) (mk_html f) let rec readdir ?(cur = []) ~location dir = List.fold_left (fun cur file -> let file = Filename.concat dir file in if Sys.is_directory (Filename.concat location file) then readdir ~cur ~location file else file :: cur) cur (Build_tools.read_files ~location dir) let () = let location = Filename.dirname Sys.executable_name in let md = Sys.readdir (Filename.concat location "content") |> Array.to_list |> List.filter (fun f -> Filename.extension f = ".md" || Filename.extension f = ".in") |> List.sort compare in let liq = Sys.readdir (Filename.concat location "content/liq") |> Array.to_list |> List.filter (fun f -> Filename.extension f = ".liq") |> List.sort compare |> List.map (fun f -> "content/liq/" ^ f) in List.iter mk_generated_rule generated_md; List.iter mk_subst_rule md; List.iter (fun (file, _, _) -> mk_html_rule ~liq ~content:false file) generated_md; List.iter (mk_html_rule ~liq ~content:true) md; List.iter mk_test_rule liq; let files = List.map (fun f -> Printf.sprintf {| (orig/%s as html/%s)|} f f) (readdir ~location:(Filename.concat location "orig") "") @ List.map (fun (f, _, _) -> mk_html_install f) generated_md @ List.map mk_html_install md in let files = files |> List.sort compare |> String.concat "\n" in Printf.printf {| (install (section doc) (package liquidsoap) (files %s ) ) |} files ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/language.dtd�������������������������������������������������������������������0000664�0000000�0000000�00000047060�14773033502�0016730�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!-- Copyright (c) 2001 Joseph Wenninger <jowenn@kde.org> modified (c) 2002 Anders Lund <anders@alweb.dk> modified (c) 2003 Simon Huerlimann <simon.huerlimann@access.unizh.ch> modified (c) 2005 Dominik Haumann <dhdev@gmx.de> modified (c) 2008 Wilbert Berendsen <info@wilbertberendsen.nl> This file describes the XML format used for syntax highlight descriptions for the Kate text editor (http://kate.kde.org), which is part of the KDE desktop environment (http://www.kde.org). You'll find the "Writing a Kate Highlighting XML File HOWTO" at http://kate.kde.org/doc/hlhowto.php This format is identified using the SYSTEM identifier SYSTEM "language.dtd" Files using this format should include a DOCTYPE declaration like this: <!DOCTYPE language SYSTEM "language.dtd"> You can validate your syntax files using checkXML from the development package of kdelibs: checkXML yourSyntax.xml If you see any 'validity error' lines, you should fix them. If you get a lot of 'No template matches' lines, everything's just fine. You've produced a valid syntax file! It's also possible to use the (much faster) xmllint which comes with the GNOME (oops:-) XML Library libxml2: xmllint - -dtdvalid language.dtd yourSyntax.xml (don't use a space between the two - That's just because XML comments don't allow that:-( To use your syntax file, copy it to .kde/share/apps/katepart/syntax/ in your home directory. You have to open a new instance of kwrite/kate to use the new syntax file. TODO - find a more readable way for the - -dtdvalid stuff, it's just annoying xml comments don't allow it. --> <!-- Entity declarations You can use '&per;' instead of '.'. This seems to be useful in <item> elements. TODO - Are there any more such pre-defined entities? --> <!ENTITY per "." > <!-- Boolean type Attributes that are of type boolean allow the following values: 'true', 'TRUE' and '1' all meaning true, 'false', FALSE' and '0' all meaning false. It is encouraged to use 'true' and 'false' instead of the alternatives. --> <!ENTITY % boolean "true|false|TRUE|FALSE|0|1"> <!-- Default Styles Allowed predefined default styles for itemData, available are: - dsNormal, used for normal text - dsKeyword, used for keywords - dsDataType, used for data types - dsDecVal, used for decimal values - dsBaseN, used for values with a base other than 10 - dsFloat, used for float values - dsChar, used for a character - dsString, used for strings - dsComment, used for comments - dsOthers, used for 'other' things - dsAlert, used for warning messages - dsFunction, used for function calls - dsRegionMarker, used for region markers - dsError, used for error highlighting. --> <!ENTITY % defStyles "dsNormal|dsKeyword|dsDataType|dsDecVal|dsBaseN|dsFloat|dsChar|dsString|dsComment|dsOthers|dsAlert|dsFunction|dsRegionMarker|dsError"> <!-- Language specification name: The name of this syntax description. Used in the Highlighting Mode menu section: The logical group to which this syntax description belongs. Used for sub menus extensions: A file glob or pattern to decide for which documents to use this syntax description style: Attribute style that this highlighter provides to kate scripts [optional] mimetype: A list of mimetypes to decide for which documents to use this syntax description [optional] version: Version number of this syntax description [optional] kateversion: Kate version required for using this file [optional] casesensitive: Whether text is matched case sensitive. [boolean, optional, default=true] FIXME: This is not implemented yet priority: Priority of this language, if more than one are usable for the file [optional] author: Name of author of this hl file [optional] license: License for this hl file [optional] indenter: Name of the Indenter to use for this highlighting mode per default, like "cstyle" [optional] hidden: Should it be hidden in menu [boolean, optional, default=false] TODO - Which matches are affected by casesensitive? keyword, RegExpr, StringDetect, WordDetect...? WARNING: due to helper scripts, the language opening tag must be on a *single line* and *cannot* be split in multiple lines. --> <!ELEMENT language (highlighting, general?, spellchecking?)> <!ATTLIST language name CDATA #REQUIRED section NMTOKEN #REQUIRED extensions CDATA #REQUIRED version CDATA #REQUIRED kateversion CDATA #REQUIRED style CDATA #IMPLIED mimetype CDATA #IMPLIED casesensitive (%boolean;) #IMPLIED priority CDATA #IMPLIED author CDATA #IMPLIED license CDATA #IMPLIED indenter CDATA #IMPLIED hidden (%boolean;) #IMPLIED > <!-- General options --> <!ELEMENT general (folding|comments|keywords|indentation|emptyLines)*> <!-- List of folding indentationsensitive: If true, the code folding is indentation based. --> <!ELEMENT folding EMPTY> <!ATTLIST folding indentationsensitive (%boolean;) #IMPLIED > <!-- List of comments --> <!ELEMENT comments (comment)+> <!-- Comment specification name: Type of this comment. Allowed are 'singleLine' and 'multiLine' start: The comment starts with this string end: The comment ends with this string [optional] region: The region name of the foldable multiline comment. If you have beginRegion="Comment" ... endRegion="Comment" you should use region="Comment". This way uncomment works even if you do not select all the text of the multiline comment. position: only available for type singleLine. Default is column0, to insert the single line comment characters after the whitespaces (= before the first non space) set position to "afterwhitespace" --> <!ELEMENT comment EMPTY> <!ATTLIST comment name (singleLine|multiLine) #REQUIRED start CDATA #REQUIRED end CDATA #IMPLIED region CDATA #IMPLIED position (afterwhitespace) #IMPLIED > <!-- Keyword options casesensitive: Whether keywords are matched case sensitive. [boolean, optional, default=true] weakDeliminator: Add weak deliminators [optional, default: ""] additionalDeliminator: Add deliminators [optional] wordWrapDeliminator: characters that are used to wrap long lines [optional] --> <!ELEMENT keywords EMPTY> <!ATTLIST keywords casesensitive (%boolean;) #IMPLIED weakDeliminator CDATA #IMPLIED additionalDeliminator CDATA #IMPLIED wordWrapDeliminator CDATA #IMPLIED > <!-- Indentation options mode: indentation mode to use TODO - Explain (weak) deliminators --> <!ELEMENT indentation EMPTY> <!ATTLIST indentation mode CDATA #IMPLIED > <!-- Treat lines that match a given regular expression as empty line. This is needed for example in Python for comments (#...), as then the indentation based folding should ignore the line. This is only implemented for indentation based folding. If the folding is not indentation based, the emptyLines are not used. --> <!ELEMENT emptyLines (emptyLine*)> <!-- One empty line regular expression. regexpr: The regular expression, example from python: ^\s*#.*$ casesensitive: Sets, whether the regular expression match is performed case sensitive --> <!ELEMENT emptyLine EMPTY> <!ATTLIST emptyLine regexpr CDATA #REQUIRED casesensitive (%boolean;) #IMPLIED > <!-- Highlighting specification --> <!ELEMENT highlighting (list*, contexts, itemDatas)> <!ATTLIST highlighting > <!-- List of items name: Name of this list --> <!ELEMENT list (item)*> <!ATTLIST list name CDATA #REQUIRED > <!-- List item contains string used in <keyword> --> <!ELEMENT item (#PCDATA)> <!-- List of contexts --> <!ELEMENT contexts (context)+> <!-- context specification name: The name of this context specification. Used in '*Context' attributes [optional] attribute: The name of the ItemData to be used for matching text lineEndContext: Next context if end of line is encountered lineBeginContext: Next context if begin of line is encountered [optional] fallthrough: Use a fallthrough context [optional] fallthroughContext: Fall through to this context [optional] dynamic: Dynamic context [boolean, optional] noIndentationBasedFolding: Python uses indentation based folding. However, Python has parts where it does not use indentation based folding (e.g. for """ strings). In this case switch to an own context and set this attribute to true. Then the indentation based folding will ignore this parts and not change folding markers. [optional] TODO: - Explain fallthrough. - Do we need fallthrough at all? It could be true, if fallthroughContext is set, false otherwise. - Make lineEndContext optional, defaults to '#stay'. Reasonable? --> <!ELEMENT context (keyword | Float | HlCOct | HlCHex | HlCFloat | Int | DetectChar | Detect2Chars | AnyChar | StringDetect | WordDetect | RegExpr | LineContinue | HlCStringChar | RangeDetect | HlCChar | IncludeRules | DetectSpaces | DetectIdentifier)*> <!ATTLIST context name CDATA #IMPLIED attribute CDATA #REQUIRED lineEndContext CDATA #REQUIRED lineBeginContext CDATA #IMPLIED fallthrough (%boolean;) #IMPLIED fallthroughContext CDATA #IMPLIED dynamic (%boolean;) #IMPLIED noIndentationBasedFolding (%boolean;) #IMPLIED > <!-- Common attributes attribute: The name of the ItemData to be used for matching text context: The name of the context to go to when this rule matches beginRegion: Begin a region of type beginRegion [optional] endRegion: End a region of type endRegion [optional] firstNonSpace: should this rule only match at first non-space char in line? column: should this rule only match at given column in line (column == count of chars in front) --> <!ENTITY % commonAttributes "attribute CDATA #IMPLIED context CDATA #IMPLIED beginRegion CDATA #IMPLIED endRegion CDATA #IMPLIED lookAhead (%boolean;) #IMPLIED firstNonSpace (%boolean;) #IMPLIED column CDATA #IMPLIED" > <!-- Detect members of a keyword list commonAttributes: Common attributes insensitive: Is this list case-insensitive? [boolean, optional, see note] String: Name of the list weakDelimiter: Use weak deliminator By default, case sensitivity is determined from <keywords casesensitive> in <general> (default=true), but can be overridden per-list with 'insensitive'. TODO: - Should be weakDeliminator - Explain deliminator - Doesn't seem to be supported in highlight.cpp --> <!ELEMENT keyword EMPTY> <!ATTLIST keyword %commonAttributes; insensitive CDATA #IMPLIED String CDATA #REQUIRED weakDelimiter CDATA #IMPLIED > <!-- Detect a floating point number commonAttributes: Common attributes AnyChar is allowed as a child rule. TODO: The source code allows *all* rules to be child rules, shall we change the DTD in some way? --> <!ELEMENT Float (AnyChar)*> <!ATTLIST Float %commonAttributes; > <!-- Detect an octal number commonAttributes: Common attributes --> <!ELEMENT HlCOct EMPTY> <!ATTLIST HlCOct %commonAttributes; > <!-- Detect a hexadecimal number commonAttributes: Common attributes --> <!ELEMENT HlCHex EMPTY> <!ATTLIST HlCHex %commonAttributes; > <!-- Detect a C-style floating point number commonAttributes: Common attributes --> <!ELEMENT HlCFloat EMPTY> <!ATTLIST HlCFloat %commonAttributes; > <!-- Detect C-style character commonAttributes: Common attributes TODO - Did I get this right? --> <!ELEMENT HlCChar EMPTY> <!ATTLIST HlCChar %commonAttributes; > <!-- Detect an integer number commonAttributes: Common attributes StringDetect is allowed as a child rule. TODO: The source code allows *all* rules to be child rules, shall we change the DTD in some way? --> <!ELEMENT Int (StringDetect)*> <!ATTLIST Int %commonAttributes; > <!-- Detect a single character commonAttributes: Common attributes char: The character to look for dynamic: Uses 0 ... 9 as placeholders for dynamic arguments (in fact, first char of arg...) [boolean, optional, default=false] --> <!ELEMENT DetectChar EMPTY> <!ATTLIST DetectChar %commonAttributes; char CDATA #REQUIRED dynamic (%boolean;) #IMPLIED > <!-- Detect two characters commonAttributes: Common attributes char: The first character char1: The second character dynamic: Uses 0 ... 9 as placeholders for dynamic arguments (in fact, first char of arg...) [boolean, optional, default=false] --> <!ELEMENT Detect2Chars EMPTY> <!ATTLIST Detect2Chars %commonAttributes; char CDATA #REQUIRED char1 CDATA #REQUIRED dynamic (%boolean;) #IMPLIED > <!-- Detect any group of characters commonAttributes: Common attributes String: A string representing the characters to look for TODO - Description is not descriptive enough, I'm not sure what it exactly does:-( --> <!ELEMENT AnyChar EMPTY> <!ATTLIST AnyChar %commonAttributes; String CDATA #REQUIRED > <!-- Detect a string commonAttributes: Common attributes String: The string to look for insensitive: Whether the string is matched case INsensitive. [boolean, optional, default=false] dynamic: Uses %0 ... %9 as placeholders for dynamic arguments [boolean, optional, default=false] TODO - What's default of insensitive? I'm not sure... --> <!ELEMENT StringDetect EMPTY> <!ATTLIST StringDetect %commonAttributes; String CDATA #REQUIRED insensitive (%boolean;) #IMPLIED dynamic (%boolean;) #IMPLIED > <!-- Detect a word, i.e. a string at word boundaries commonAttributes: Common attributes String: The string to look for insensitive: Whether the string is matched case INsensitive. [boolean, optional, default=false] dynamic: Uses %0 ... %9 as placeholders for dynamic arguments [boolean, optional, default=false] TODO - What's default of insensitive? I'm not sure... --> <!ELEMENT WordDetect EMPTY> <!ATTLIST WordDetect %commonAttributes; String CDATA #REQUIRED insensitive (%boolean;) #IMPLIED dynamic (%boolean;) #IMPLIED > <!-- Detect a match of a regular expression commonAttributes: Common attributes String: The regular expression pattern insensitive: Whether the text is matched case INsensitive. [boolean, optional, default=false] minimal: Whether to use minimal matching for wild cards in the pattern [boolean, optional, default='false'] dynamic: Uses %0 ... %9 as placeholders for dynamic arguments [boolean, optional, default=false] --> <!ELEMENT RegExpr EMPTY> <!ATTLIST RegExpr %commonAttributes; String CDATA #REQUIRED insensitive (%boolean;) #IMPLIED minimal (%boolean;) #IMPLIED dynamic (%boolean;) #IMPLIED > <!-- Detect a line continuation commonAttributes: Common attributes --> <!ELEMENT LineContinue EMPTY> <!ATTLIST LineContinue %commonAttributes; > <!-- Detect a C-style escaped character commonAttributes: Common attributes TODO: - Did I get this right? Only one character, or a string? --> <!ELEMENT HlCStringChar EMPTY> <!ATTLIST HlCStringChar %commonAttributes; > <!-- Detect a range of characters commonAttributes: Common attributes char: The character starting the range char1: The character terminating the range --> <!ELEMENT RangeDetect EMPTY> <!ATTLIST RangeDetect %commonAttributes; char CDATA #REQUIRED char1 CDATA #REQUIRED > <!-- Include Rules of another context context: The name of the context to include includeAttrib: If this is true, the host context of the IncludeRules will be given the attribute of the source context --> <!ELEMENT IncludeRules EMPTY> <!ATTLIST IncludeRules context CDATA #REQUIRED includeAttrib (%boolean;) #IMPLIED > <!-- Detect all following Spaces --> <!ELEMENT DetectSpaces EMPTY> <!ATTLIST DetectSpaces %commonAttributes; > <!-- Detect an Identifier ( == LETTER(LETTER|NUMBER|_)*) --> <!ELEMENT DetectIdentifier EMPTY> <!ATTLIST DetectIdentifier %commonAttributes; > <!-- List of attributes --> <!ELEMENT itemDatas (itemData)+> <!ATTLIST itemDatas > <!-- Attribute specification name CDATA #REQUIRED The name of this attribute defStyleNum CDATA #REQUIRED The index of the default style to use color CDATA #IMPLIED Color for this style, either a hex triplet, a name or some other format recognized by Qt [optional] selColor CDATA #IMPLIED The color for this style when text is selected [optional] italic CDATA #IMPLIED Whether this attribute should be rendered using an italic typeface [optional, boolean, default=false] bold CDATA #IMPLIED Whether this attribute should be renederd using a bold typeface [optional, boolean, default=false] underline CDATA #IMPLIED Whether this attribute should be underlined [optional, boolean, default=false] strikeOut CDATA #IMPLIED Whether this attribute should be striked out [optional, boolean, default=false] backgroundColor CDATA #IMPLIED The background color for this style [optional] selBackgroundColor CDATA #IMPLIED The background color for this style when text is selected [optional] spellChecking CDATA #IMPLIED Whether this attribute should be spell checked [optional, boolean, default=true] --> <!ELEMENT itemData EMPTY> <!ATTLIST itemData name CDATA #REQUIRED defStyleNum (%defStyles;) #REQUIRED color CDATA #IMPLIED selColor CDATA #IMPLIED italic (%boolean;) #IMPLIED bold (%boolean;) #IMPLIED underline (%boolean;) #IMPLIED strikeOut (%boolean;) #IMPLIED backgroundColor CDATA #IMPLIED selBackgroundColor CDATA #IMPLIED spellChecking (%boolean;) #IMPLIED > <!-- encodingPolicy type Attributes that are of type 'encodingPolicy' allow the following values: 'EncodeAlways', 'EncodeWhenPresent' and 'EncodeNever' --> <!ENTITY % encodingPolicy "EncodeAlways|EncodeWhenPresent|EncodeNever"> <!-- Spellchecking specification --> <!ELEMENT spellchecking (configuration?, encodings?)> <!ATTLIST spellchecking > <!-- List of character encodings --> <!ELEMENT encodings (encoding)+> <!ATTLIST encodings > <!-- Encoding specification sequence CDATA #REQUIRED Character sequence of the encoding; must not contain new-line characters, i.e. \n or \r character CDATA #IMPLIED Encoded character; must be of length 1 ignored (%boolean;) #IMPLIED If true, then the encoding sequence is ignored for spellchecking --> <!ELEMENT encoding EMPTY> <!ATTLIST encoding string CDATA #REQUIRED char CDATA #IMPLIED ignored (%boolean;) #IMPLIED > <!-- Spellchecking configuration encodingReplacementPolicy (%encodingPolicy;) #IMPLIED Policy for replacing encoded characters in replacements for misspelled words --> <!ELEMENT configuration EMPTY> <!ATTLIST configuration encodingReplacementPolicy (%encodingPolicy;) #IMPLIED > ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/liquidsoap.1.md����������������������������������������������������������������0000664�0000000�0000000�00000007360�14773033502�0017302�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- title: LIQUIDSOAP section: 1 date: Jul 24, 2019 header: Liquidsoap @version@ footer: Liquidsoap @version@ ... <!-- .TH LIQUIDSOAP 1 "Jul 1, 2016" "Liquidsoap @version@" --> # NAME liquidsoap - a multimedia streaming language # SYNOPSIS liquidsoap [ _options_ ] [ _script_ | _expression_ ] # DESCRIPTION Liquidsoap is a programming language for describing multimedia streaming systems. It is very flexible, making simple things simple but giving a lot of control for advanced uses. Liquidsoap supports audio, video and MIDI streams, and a wide range of input/output operators including Icecast and various soundcard APIs. It can perform a broad range of signal processing, combine streams in various ways, support custom transitions, generate sound procedurally... and all this can be assembled as you wish. Input files can be accessed remotely, or even be synthesized on the fly using external scripts such as speech synthesis. Finally, interaction with a running liquidsoap instance is possible via telnet or socket. Liquidsoap scripts passed on the command line will be evaluated: they shall be used to define the streaming system to be ran. It is possible to pass multiple scripts; they will all be ran successively, and definitions from one script can be used in subsequent ones. A script will be read from standard input if `-` is given as script filename. Information about scripting liquidsoap is available on our website: [http://liquidsoap.info/](http://liquidsoap.info/). If the parameter is not a file it will be treated as an expression which will be executed. It is a convenient way to test simple one-line scripts. When running only one-liners, the default is to log messages directly on stdout rather than to a file. # OPTIONS \- : Read script from standard input. \-- : Stop parsing the command-line and pass subsequent items to the script. \--debug : Print debugging log messages. \--dynamic-plugins-dir _path_ : Directory where to look for plugins. \--errors-as-warnings : Issue warnings instead of fatal errors for unused variables and ignored expressions. If you are not sure about it, it is better to not use it. \--interactive : Start an interactive interpreter. \--list-plugins : List all plugins (builtin scripting values, supported formats and protocols). \--list-plugins-xml : List all plugins (builtin scripting values, supported formats and protocols), output as XML. \--no-stdlib : Do not load pervasive script libraries. \--version : Display Liquidsoap's version. \--build-config : Display Liquidsoap's build config. -c, \--check : Check and evaluate scripts but do not perform any streaming. -cl, \--check-lib : Like \--check but treats all scripts and expressions as libraries, so that unused toplevel variables are not reported. -d, \--daemon : Run in daemon mode. -f, \--force-start : For advanced dynamic uses: force liquidsoap to start even when no active source is initially defined. -h _plugin_ : Print the description of a plugin, eg. a builtin scripting function. -i : Display inferred types. -p, --parse-only : Parse scripts but do not type-check and run them. -q, \--quiet : Do not print log messages on standard output. -r _filename_ : Process a request. -T, \--disable-telnet : Disable the telnet server. -U, \--disable-unix-socket : Disable the unix socket. -t, \--enable-telnet : Enable the telnet server. -u, \--enable-unix-socket : Enable the unix socket. -v, \--verbose : Print log messages on standard output. \--list-settings : Show all settings with their documentation. -help, \--help Display this list of options # SEE ALSO Our website [http://liquidsoap.info/](http://liquidsoap.info/) and the HTML documentation coming with your distribution of Liquidsoap. # AUTHOR [The savonet team](savonet-users@lists.sourceforge.net). ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/liquidsoap.xml�����������������������������������������������������������������0000777�0000000�0000000�00000000000�14773033502�0024126�2../scripts/liquidsoap.xml���������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/no-pandoc����������������������������������������������������������������������0000664�0000000�0000000�00000000113�14773033502�0016235�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������Please rebuild with pandoc installed to generate liquidsoap documentation. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/orig/��������������������������������������������������������������������������0000775�0000000�0000000�00000000000�14773033502�0015401�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/orig/css/����������������������������������������������������������������������0000775�0000000�0000000�00000000000�14773033502�0016171�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/orig/css/homepage.css����������������������������������������������������������0000664�0000000�0000000�00000005167�14773033502�0020501�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#play { background: transparent url("images/blocks/play/block_play_bg.png"); height: 124px; margin: 0 0 2em; } #play-left { background: transparent url("images/blocks/play/block_play_cl.png") no-repeat scroll top left; width: 20px; height: 124px; float: left; margin: 0; padding: 0; } #play-right { background: transparent url("images/blocks/play/block_play_cr.png") no-repeat scroll top left; width: 20px; height: 124px; float: right; margin: 0; padding: 0; } #play h3 { text-indent: -9000px; height: 124px; width: 58px; float: left; background: transparent url("images/blocks/play/block_play_arrow.png") no-repeat scroll top left; margin: 0 8% 0 5%; padding: 0; } #play p { text-indent: -9000px; height: 124px; width: 203px; float: left; background: transparent url("images/blocks/play/block_play_text.png") no-repeat scroll top left; margin: 0; padding: 0; } h3#abcs { display: none; } #fleche123 { background: transparent url("images/blocks/fleche123.png") no-repeat scroll top left; width: 393px; height: 155px; list-style-type: none; padding: 0; margin: 2em 0; position: relative; } #fleche123 li a { text-indent: -9000px; display: block; margin: 0; padding: 0; height: 132px; position: absolute; } #fleche123 li#fleche123-is a { width: 113px; } #fleche123 li#fleche123-dl a { width: 135px; left: 113px; } #fleche123 li#fleche123-ej a { width: 82px; left: 248px; } div.step { margin: 0; border: solid #C9F; border-width: 0 1px 1px; background-color: #FCF; margin-bottom: 1em; } div.step h3 { height: 20px; margin: 0 -1px; background: #906 url("images/tabs/tab_red_bg.png") repeat-x scroll top left; } div.step h3 span.left, div.step h3 span.right { display: block; height: 20px; width: 10px; background: transparent no-repeat scroll top left; } div.step h3 span.left { background-image: url("images/tabs/tab_red_l.png"); float: left; } div.step h3 span.right { background-image: url("images/tabs/tab_red_r.png"); float: right; background-position: right top; } div.step h3 span.text { padding: 0 0.5em; margin-right: 2em; float: right; display: block; text-indent: -9000px; height: 18px; background: #FFF no-repeat scroll center top; } div.step h3 span#step-download { width: 68px; background-image: url("images/tabs/btn_dl.png"); } div.step h3 span#step-install { width: 92px; background-image: url("images/tabs/btn_iands.png"); } div.step h3 span#step-enjoy { width: 119px; background-image: url("images/tabs/btn_eys.png"); } div.step .step-content { margin: 0; padding: 0 0.5em; text-align: justify; font-size: 10pt; } div.step .step-content p { text-indent: 1em; } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/orig/css/style.css�������������������������������������������������������������0000664�0000000�0000000�00000011202�14773033502�0020037�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������body { text-align: center; /* Required for pesky IE */ background: #FFF url('../images/design/background.png') repeat-x scroll top left; padding: 0; margin: 0; font-family: Bitstream Vera Sans, Tahoma, Verdana, Helvetica, sans-serif; } #wrapper { margin: 0 auto; width: 600px; text-align: left; /* Overrides the previously set center alignment that older IE versions reqd */ padding: 0; background: #FFF url('../images/design/background_page.png') repeat-x scroll top left; } #header { padding: 0; margin: 0; } #logo { background: #FFF url('../images/design/logo.png') no-repeat scroll top left; width: 515px; height: 106px; margin-left: 2em; } #menu { list-style-type: none; padding: 0; margin: 0; height: 18px; position: relative; } #menu li { list-style-type: none; padding: 0; margin: 0 1em 0 0; height: 18px; width: 83px; text-indent: -9000px; float: left; background: transparent no-repeat scroll top left; position: absolute; } #menu #menu-about { background-image: url("../images/tabs/tab_about.png"); right: 344px; } #menu #menu-download { background-image: url("../images/tabs/tab_install.png"); right: 258px; } /* This is for shipped documentation only.. */ #menu #menu-doc-index { background-image: url("../images/tabs/tab_docs.png"); right: 258px; } /* This is for shipped documentation only.. */ #menu #menu-doc-api { background-image: url("../images/tabs/tab_API.png"); right: 172px; } /* This is for shipped documentation only.. */ #menu #menu-doc-snippets { background-image: url("../images/tabs/tab_snippets.png"); right: 88px; } #menu #menu-support { background-image: url("../images/tabs/tab_docs.png"); right: 172px; } /* This is for website only.. */ #menu #menu-doc-api-www { background-image: url("../images/tabs/tab_API.png"); right: 86px; } #menu #menu-developers { background-image: url("../images/tabs/tab_developers.png"); right: 0px; } #menu li:hover, #menu li.active { background-position: 0 -18px; } #menu li a { display: block; } #index { width: 390px; padding: 3em 10px 0 20px; position: relative; float: left; text-align: justify; font-size: 10pt; } #content { width: 560px; padding: 3em 10px 0 20px; position: relative; float: left; text-align: justify; font-size: 10pt; } #sidebar { width: 140px; float: left; font-size: 10pt; padding: 3em 20px 0 10px; } #sidebar .box { background: repeat-y scroll top left; width: 150px; padding-bottom: 47px; position: relative; margin: 0 15px 2em 0; } #sidebar .box h3 { background: transparent no-repeat scroll top left; width: 150px; height: 19px; text-indent: -9000px; margin: 0; padding: 0; } #sidebar .box .more { text-align: right; font-style: normal; font-size: 8pt; padding: 0 1em; } #sidebar .box .box-bottom { background: transparent no-repeat scroll bottom left; width: 150px; height: 47px; position: absolute; bottom: 0; } #sidebar .box-say { background-image: url("../images/boxes/box_blue_bg.png"); background-color: #CCF; } #sidebar .box-say h3 { background-image: url("../images/boxes/box_blue_top.png"); } #sidebar .box-say blockquote { margin: 0; padding: 0; } #sidebar .box-say blockquote p { padding: 0 1em; margin: 0 0 1em; font-style: italic; text-indent: 0.5em; } #sidebar .box-say .box-bottom { background-image: url("../images/boxes/box_blue_say.png"); } #sidebar .box-see { background-image: url("../images/boxes/box_red_bg.png"); background-color: #FCC; } #sidebar .box-see h3 { background-image: url("../images/boxes/box_red_top.png"); } #sidebar .box-see .box-bottom { background-image: url("../images/boxes/box_red_see.png"); } #sidebar .box-hear { background-image: url("../images/boxes/box_green_bg.png"); background-color: #FCC; } #sidebar .box-hear h3 { background-image: url("../images/boxes/box_green_top.png"); } #sidebar .box-hear p.content { padding: 0 1em; margin: 0; } #sidebar .box-hear p.content img { margin-bottom: -5px; } #sidebar .box-hear .box-bottom { background-image: url("../images/boxes/box_green_hear.png"); } #footer { border-top: 2px solid #C9F; font-size: 8pt; text-align: right; padding: 0.3em 1em 5em; color: #666; clear: both; } img { border: none; margin-left: auto; margin-right: auto; display: block; } img.grab { display: inline; } hr.invisible { visibility: hidden; clear: both; } pre { padding-left: 10px ; border-left: solid 1px ; border-right: solid 1px ; font: 1.1em monospace, fixed ; white-space: pre ; color: #444 ; overflow: auto; } code { background-color: #eee; font: monospace, fixed; } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/orig/fosdem2020/���������������������������������������������������������������0000775�0000000�0000000�00000000000�14773033502�0017162�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/orig/fosdem2020/clock.png������������������������������������������������������0000664�0000000�0000000�00000043047�14773033502�0020773�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������PNG  ��� IHDR�����{���&-��1iCCPICC Profile��XYTKL%Ar9 "%IEQ"DTA (JPPDzA%szY�|##P�GF;{V8})1vvV�)Yn<Y0P�C G5�*X�03](!6XDK(`uֱ&qr0D�xZ_ �⏧!rs6R?5a=`J?�txNs"XrCA\6 ވiCfs^[Nu=E0 ϩx28y%g5D07#l:Ts#G9Qc͝6w:lG%;ƾcdDž:o, 0-fR릞x <h1)7OtúΈam L5VNf}aon q-X-0-~% t.1"mshk紩9nS$8 P4 6[L/A �lQ~ph@ |FP�`5�#?ͧ h &,A;nWğ\{BD05aͰ&X 4'ZBzHU@7?fӇyb^G":lC"RhmD>"͊2h%d$}.22Bqf-d(A K+nbS/?20sw-pfkh"rwku}[jpghjPp,>#( r r�a#C=(P7-�H&l,�ՓMzDlȌ zX�[܁7b`dF@&GqPNs\WAh>x z�x ́E A@R@Ɛ�C>PA)>(*JP tC]P >@_%E1xPm(5>ڎ BEPèbT9Վz@>`��,-8.KMQxƢh~ ^h : .AWѝg1Íh`1n L&S\CfŲbŰsdž`Szlv8pR8m-ĝ]qx<^oGZ|+?_&0D[?a!PAI!LLD16щBL#/hhhiii4{ii.<IK5=L[E{7:::Q:=:Xt5twF~ГeSK0D zf {Ko0`\`"332322u1Mp$Q1ɟA:GK'd!!BG #O0cŘ͙Cs/2?ec!($feYEYYXX.e]bgŮ^>aʑ)iϙYysK•uk-}{ǔ']^V^=c|:|Tc|m|Y; <XtL|#DR :&!4''l-"\'<$BQ )y ]TLUhXXذ8xxs D)^IdddJJE*uJO#.!].BVF_&^NfLUJ6]I6m=KNY.LB<IB>]WIBsE:ETfy%)�2dekʫ***T> T}ƬfPnޢSCE#VƬff洖VVָYQ~3:*%C/136nPp#(1ٸxD$ȤdT4,99ż|BbE%e;+Ih(k 6"66M;1([X{;RIydNNyNŝ;\\\j\ms؝ӝpX4<95]l{.oN0;vh.R(( ,:!X70xjH-·jZV Aɻ3qg_TdfhFh(f{Ls,3r7_#%!)1"{䮬]SI&IɔHJHk:$]. }nfdo.>3:R>R:+?Q\NaJ.%!CŇ~Wv{$`n~uSARQ룍e]JEqEV'O9R\2PjPZdO]:s:ꙗgM6Þ?7YRJʜժj՚Zڼ:T]\݇ ^z/]l$sl=k}ep9+>WZ^hPktMٍPƹf7:nj޼~KVU@Kmy֌ֵ;wfڃ;vtvy}{7[4n<R{Xqcr'O?Uyأܫ{O_ѳ͟?t|Kӯ^-;~p{QcFc߽z~e"cnpofZaɇޏ'>E~Z/ff&׾~V_ v #߳pr nxUb/_kkkѾW@�V@��fnU`B., =L >#VD  m?]}C%c=S3%[iOK\<(^N$Hbfae&-''lbjfnai'/jclD4Z3f2iʬۼŢUu#v\Qnhw'vo>|~,?),HQ 5 s F,:]p8xWuRk`tꎽ'^ع�PBAcnࡤyG,c:.ShQXDYɍO-Ɲ8+Yuζ|l.|UkQ׳O57s+mwRwth%ݝq9?yrɞ^>rxgiDу^tU0eX 盕Dzw~5!ũ>}L6CL93w}~_gߋJ-=]I+%׀%h$ihW_2d|yeum*"7YIa~{7+rDD%bŻ$j$Җ22t)(R TTHjk_45uru|-  9PF\671wPd\zk}צ6.>QIYs]r>y۫~;FQ)NA6TP0p4ȕ?cV0D]NI1Ʌ)-'Siɤk۟y@̇E9_WckEv'BK>|l+S*T#qn"b+yW: _ڄm!ySEض;i:<puzt'9Ozf~<j``ؗɯ}]>ȫ;Ҹ{ I)3>RIל|K//Z~Osj{a5WQ6 s�o244#P L$R/- l6v#S%WgR^3޿-/*-"Z,$- "qTRJMQjR:MFDl6mU{*))F* (+(i}V/0X<eKN]{]Oҿcioknd<eRbj;nGYHY,f~fS``G?(s+[{' ;zI-R:*k!Na7#w&DFiDƠcfbv%%$޾-iC}>  ͊N9[ppM^㑻}#GGJy9qd)Y)W8PQr ]UP[_䥐˳WU^nh kw+]U[K|DžN{W<zP30o[㵓OsOuo~[/X��`;C ���{P�Z�BO"  Ԁ_!9e]`B2@ǡf I\Piz� ExҼХH&9Uaxp6<Oŷ8 q~"BCyBN[MNKOǰȘ1eXId5r/s <1$Av)^XNV.n<0E>O$#bz&\$)* :)vA<FBCʗDVlJ=T%Ge >UHuTEFְvN^OaMV]f=#SV˶x;V{Q5G+'sKk۴S+r{W/=P-(9=V,,j6F+6'n$A)Ȯdה{S[,32ˇN+/(1<)]&xF\¦2EPzŦzpSʍ㷮o[4̣ݫ=2}۟<D823=67%㋙_Ͷϛ}KadqK+lďMK�U`\A: F A6Ptj^�JQWP< ΀e6zÎ 4b Xol#{^DYb LE3FLΐ^6>dgA51, sy6Ѱ5rr<%5]cˋo x"X(+,+"-Z&%n,%U9TOYmrYqʏTn^P+Q?e-è;ת_opŰIi#^ombN.]Knyz nqƧGNRCB.FҊJ(JLRO>28:>}׾)$zc}!o4?(;'KO]r+Uq5Zu _NuMiƵ[n۴錾5>g"L_:xyxtǃ3_g_̟gt%a@ + 4/Y@qP4A!;i+]!`1oNn )B:q0]2=?}C$8iYKk2''; 7{gw(X^ב{Nq e Uw~Mf-w Y=mCosL̴ -ZY_ (­փIz-cτ!2�K dJ5gקd7dVq@Oz#JZӡKG/%_$U{b2H?}^&vŖzyWoi:<w֥vRG{vJ?xL|2k߷ڳwA/_y}wxە1n;&&?OL?_fqo糾z|E?Rj_:B\i\uu}1 Dk��fdm(��V֖V!0�w68k8õ./j��iTXtXML:com.adobe.xmp�����<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.4.0"> <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> <rdf:Description rdf:about="" xmlns:exif="http://ns.adobe.com/exif/1.0/"> <exif:PixelXDimension>471</exif:PixelXDimension> <exif:PixelYDimension>123</exif:PixelYDimension> </rdf:Description> </rdf:RDF> </x:xmpmeta> ��,IDATxXTGQĮQc,1v%1cFhĖh&"F`ablQQPHQ#, ͽ +, >rwʙ3{2&"@�(4e M "D"@D\#"@�(d\ (#D"@Ε� DB&@ε8"@� \"@�(d\ (#D"@Ε� DB&@ε8"@� F\^JM� DPܼyje0Oa}*}Dڟם4y#2O\Z4+J_4IAFO&y8YVJquJBt:+v+RQ}t1 y&&bйix8H-Ք@j٪ħ"yJpԫJfy_"<13objUń!^f2N<NJBt:+v+bny/R{ݐMz6{56\cȋB� DhEު9w|ƹi%"@� Jȹ*D"@ޖ�9׷%H� D(0*S6$D"v5d7j*(ĥK4R:/_VEDRK 5(^P1JxvGP}Wރ}2^nT u{:=)hQ1?O�0\y[3,ϫӻr<ХuUSЭ:wz A�06*>`ljm84h{vNdp!c9[8:UWM^sU"u҆2"@J/#ZzjF� G iD� %�9ހ> D#q`բ)@w "@J៤^mpuGie|DNq Lv1Y8RjVM#}n$ N©<"@`|oM0ZxqF@TlY̝;@#@ %KB B%ɘ?>*Voȹj6oތvڡre5i)P?_,*@H$hٲ%ʕ+W,RSSKK></ŇYf*Z?7oDϞ=|c,T+_}ݫs0Ҝ0axAjD%fW^-6MǏCA㑙#F`޼yR}ooo06l8{411)r֯_.\XekE21JO )66ҥKISPnՋ͹apww/333,^_:64)aO 7oWmRZ"@�0n)P-內1a(g ߅!)h_g=.2X>M_V{3GոVD� M g~X\zuQhҮ~tJu4G&r./fxlοH+h6'GA2w@p,1qƕEj]drv4zN\��sr!WAfBQ0Xs~8&BܿGYATt}+O~&4*)qO<:o6A֊P@aq(`qj>sKnV.p:Ch4U'q aX"ަ- i*OtE<Bp)0e4@~M<7#`Qt"$:Fu2"qfYH*¼aMsZy^}gQ,v8G<{"bN)BxiggДHM, ?-).QxU`gA3kp5r4]h2 UU'ù[?ޙ>=+|-Jpu$gxQ#q}@<Y\1A%4n]co�1hhZ@T%Xm;Zc0qtK~= g2xy1\{e>Alͩдht ǢJ .<'@_o0uB>뢢,#*dkGI=zSpuet5" h4tJ0='nѽ|0a`{yVl~Dc02?�#aVz: 61M[29p eZ,*5J;޶x}{}.zy0⃺ ,s߃onfg2^\K>o9Xv105dBQA'))8:l0gqcnݕSR)SNV"ʾ*FGG&\O.~7̓qivUȒ% *(7O H1Vus4_懯A/! GDBjv."&fGki.V̝P$ZVSw;w|#xU?H}×/[-[,Ueك5˗c-ؼRxL:ÚZOk?}+.r"n݁]Wa!]ʽY̶˰jq<  L SVIqxǩ_aQ@,nj&vG`޴鋁Gw~} S1F1gqi]'ng 3G/-j @jP18[%gpg<׬>8Atem)lQbx:LR^?s86 <$aI GhC<L{]ÎxG m>8#E}?| 3yFm:UxuavGG0i;t*쀻+t%=޲8Rڏ2uqa 4Pc'{r>,жC5b2>9C~ͣ\%æ8Vh 0X,8\/fo0?Zp.u3?Vw[\%aaXw_>^haѡKlV׉X~)8S2&M/x8&:k7$OSl6یMªQ}ZXO- >ϗmiJZ:Lv67GoU-b4_]V$Tqu2RB]qS[ S̚5>y?u[&VzzV8&&t? ,\S(.z&s#KG-G H֠dup8;z~:UóC4kc=1 ۇ! wQ..KWr'v F*v"\ڤw;)pRi;|3ul9?WdTi&J_5 ^WX|O+[*\Y12swC`ax We*c"/&;X =Okbv/ xWyF>Q/Hx]LcȐEbX;kOgF2j.<9t PŹ8+T؊u񵸒Ӽ!m{h"ڎR,یslqzlƺiX!o)ذ=bt0AghdΝgAn =pؼ`A(SI/b(%lX=epq J [C{Op"4{V‰$/t+SKQ4vUIh$n^m9k@$-VY,C~i1?ݒ7o6kH=�͸Așfra*"=<v+Uې2/h0pO6ZR$;&7_n9XZޠ=ڄ:mgØcYg,|[քϩXe<eeIŒ#ce[}obnڦ\dq$mτc g;ռ1{jyKRd:Z".4\h#8֩>"~vj<As`nga|EHnVc ~4Rc98ͅu*Šߺ _s Qc<RakA|xڭA:<Gl9a37)NX:#B>G~ $=K~˧hT%'Ckcnȍ.G8 #jӻAc>]%mZCeN/ ӳ˱ger^!T)7fz<Tq};\G nZ8Uւ4 _�Y"dOIK�@ep0,9"%Q#=\^A샐Z8 Tm7,aO_ #a�f\ �->DRVIkv=)>NFx迼XOU*F%ևI`2T,#=d#+JjW]mb2I};Ue^Jf_XeLa2.[UCX\W&{<>_"dl jsfsrrbwfVuVT{~~X盜&%-"e,x7'\JF9-١'  k_NcRNʢ.|+ KMː* i.N{Q^& fs-OW_12nOOO6~xEN}N+gVX9<=v>d;wfW\ɕ2^%o|ވu3I|b_!70flSЖ`feB ? ٤-f[b_2>+zsd<b?:`72JnB6DB@Qwm;}ΦL`'ߡ'ٽ_.0ՠ7,=e ׿Ne17ZM:M,/žI6BNFiy;L;;`{6x FmH#/.CjoT_%P԰O4q޲e ;vl"C<E~1,#X۟i֘_τ~tp$>&r:$ 4徛h=Yj䀘. ۝,3kKbCƫ+K0^;Bip%fs } nيwVGl/~.R:Ug'o;nO7ۺje%YQ7V~A:[F?f>.Q8l ٮl'Xz4RWL231</ll`sl>ovv/ =b 9rs}[*8C>d 4 ˎr[4`y0]˭,# t<D(F_g#8ea[v)*SH1^(XEx6Uׁ,1=~:\U9W" H`,U,Jؿg̛ASc4]JGh'_,ri+V 0>ܐ LC̴X*7jLE\~8B1Q<S\0PgC ,d/v"7EC,EÎM}{ί U~ȍs&a's9ߞb*|&*+S"{IM}}gS!Lse 5;cYvVn.۝kx_* &ZA3sǹybME=}>ŀ'9]*b;[Vv|fxŊ␥z?%/-_*�vm>rXnǥcڲcF>v}/r{Y 7ﭸgDG6r(vM1=`W+͔De7.eG7ilA{-].Z6b'[~YLR4IT0;oQtTU5s }nTR%Gu%qOAnU',P|0.(CDu5GyʧrqE&SpYj# \?H!_na_9@M #u}hE\7gg rCd]45U9W >yF0>iI|#d0ז:΂72F-end*M\ߔ|ι*%=NεS+":d"g4k>J U4 *.#1EE\둲9<Y*O- rUI9ohb=^fľ_݉J%'iQ!lulbW6;{6fMMaA (@sM B{-|irq!ka62r]P/}9Wyyũ,B/, @?2 ѹ_MQPCTEiuqⲰǥP묨ko7sݸ5[n~?ה3*.Zʇp3}77wn9zHnxo*([w-zCxLC63XzZW:;ffʏg ߙxhPehcMn DK2p#,o_@ڭ~Gɵ$&JҊ-y~/OXݾ3k_iZ\"=uc鯃H9 L ]CJ#q~ } W#Y=)OJ%&&w3T"w)lXTGL-<HCVyWCBBYK[|⫓%΅}N={%)C@@kq<ydDDDW^\bӣO>x%o"'وant1>>cƌ$f8}$^\[o{~VbL]]][loo;vzR ӛ9axuM/GJMs ѹrӹ .oݰc)B\ʗ"Pt"CʂЊ|MAQ&H"P <n3�biH3\Ŷ~sIpx| c.A/ȹfϕoLC\2Dɉ@NX)S;8|3'%`$5lbokH8P~Tdڸ&@~vF\ifjߐ k=n*T>x)fwLL'DjgUاJՇ H"@�(![3ZBZ$DRA،2<5ԯwxahfjG"@|[8?RxpONUe$l6TU λǑȲR՜xn,,dT \_ɾ_hxTxzܼiwϖ׵0a7hr\W]5 $O"@0tc^l ߔ]Iu>zOmLrMr|M~qۆ{U~FW]ZW"@J)MvF\Ǯ^CI] {:VO'%) D]qx#8%IE"@�3vš�$"@�9Wos ^}R"`ȹ|@ "@<Tr[ܶ#͉�0^4s5޶"@s5mI, D�fkS/#D##@ ##T]"@�(\+N$iD"@%`$\@ � Dr"@�0ZrDO:J�0Dv1ZrrDO:%E:"`hWJWIgcDڄ4"DRNHKy+R� Ds5 e� D4]q K,�d9&&&H{(z κ )@NL[ή�) Ѝ�9WݸasBHҥKӛ5kK):aaaի޽[tRIFGHmKX'KlRW%L}Fp=>ynnnzL"+NhQQu++)"@t#@]Qݺ"DUhWUT %,_MNٿ˗0ֹ)(2n^~IŽڅ;X_3gV &+%N(:L9-Ѥ]%:)jHGQD@ĽW Ɉťm~{-j4BЪ7\-~'`$4j+!18MvGyT4Щ=C])?SkYe)(C[>FSypf]:ZBSg4#^!^Ƴz`` V[0C8v5Zv~!|pSZtH!Dd炳7`@* A]E^\Z+.,Wkl#/|;,t@@F'z& y?!Ce#Ei/zÜ^քV&ŽӮaGt]{~C~r%X4þ.'\")f(JѢQTran^n~_`gWt(NFv1k+*6-`WtBob!rf x=�~�udO"z! gm>d p[(,Ka�{8=HlЛibH 9 zO7!&քڌ60d=xߞ{GFr°7 u]5>ȾV?Ldž}ZtvݣkD, Ssch1h/nJ -,wEBK(QY$ERhtvmnc4ltt D>}`=?g׮z0TtR53`TXV�[xu(D+?HUD5`)DBrUo QJ<w ٮZL4<ht'V5Zu D\c;3;/G[5"*DhWc:YEPto.7&6-z㣦xolkI2<�hd+LV &qq/ͰFf|^# خiXmKLJ$= QHNJAjWzD1C Ua8W\e$f 19dHdOoU ێO<>%-C +S`_1+pr^)6#Y.ll333EV?!\߻i1{•u΄Gvcί@fZ V S_[�!X=3'<khWJM!*a,_UAk)uzO j`+7R <"j7S*V5b:`O8M1#;f<zhWWo2Ch70NYr(\Iͮqs2K1 "_#/kDb8G*߮`"T^ 7C/<z*mY< V}_<JE E[sd樴5\a|Ƙ`lZ@bA%: g_YO/VU,3+@6tlW_~P(3FGHaQI9ʓDITbcs x85%g$H%e1_grfЖ`W+R Ѕc؀re Rc>;{m_Jxm~<po*c v q$[76FiCCCYzAIhKNe*>׃f:̬ac+Dۿ 7-W|,0m2 01ˎD@;p$L bi=|?ݏ+ wڄF<VړH"J]O %wWELjv?uМp9VW,37�S̽'D2c$3WkNPbcclX)8^|w}ԄPHkq6MJ6N:]v-5k/Oo߾4T`ȹpjDϭ^CPt HH>oI%"@�(Ĺ?Vj[*F� EN@Ӯ8F\W"ou*"@F ]qLNtL� D [L� Fk� D ZZ[E� Fk� D ZZ[E� FH+f*"@JaW$52 J�C� oWp�튣}סD"@~sU‰� D>rϔ$"@ jO� OHsM2 *}JDڟם4y#2O\Z4+Jm6IAFO&y8YVJquJBt:+v+RQ}t1 y&^Ǹт7[j gCaĊUrr6ebq* IUWT O_"<1Su>S;8֯2.&,2 VG.I|%n:iT\n&ml=\oG~M~݄UTlpyIS DP1]_a$"@1 j Lu$D"%@εHqSaD"` ȹC+S� DH s-RT DrTG"@�(R{>1����IENDB`�����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������liquidsoap-2.3.2/doc/orig/fosdem2020/index.html�����������������������������������������������������0000664�0000000�0000000�00000020712�14773033502�0021161�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������<!DOCTYPE html> <html> <head> <title>Liquidsoap liquidsoap-2.3.2/doc/orig/fosdem2020/logo.png000066400000000000000000000120061477303350200206270ustar00rootroot00000000000000PNG  IHDRUj"sRGB pHYs B(xiTXtXML:com.adobe.xmp www.inkscape.org 1 UXIDATx{\}w^!GheڠD[*$MDM)nMMBK_H6yؤPBU )Q[T%iQ0`?13;{fyܹ~qϜ]9Hb/~Z\ N-vErǭtIŪUjw94f`'UZD 56YITJщj`AiJ6ms7VV[V.LTHJ%*~X^v'D\&ɳlkg"@%N&It{D~Iѩb VΤ>my B] lߚU1 AHBl.OQUM*յwz[&<ױQnI?eZ}E@ -D(2TBO^WM< ƃ*u 'd:iRTeTP[ylS5uZ\kQACmbjQlF@vL{QBJX~npJViXmhp=~>rբjhOH|~z|;0]\jzOsgF`Zg"(wپo6#G^!xu- 3Q nΩYlts:jDIɅfdG5x 9*Fed" *-{nmraYK[7FR+ y:pb'MsT,th/jq!lMTsIѱ\}b9,HҙQ}6-ZQF&P#Vfڤ!cG'=Kb72uhVϮpo9:'hS53roWuTIIgc6D5[N+mek`6yt@`˭ޥNC`^pxoLD猋wHm\Kuk~lj TU&pkP}`C_JƒN+;?#Og0=7}eB1~RDubb.qVDn;t3 eABN.FS"lw DVWլr 70H/i1yU_kI;>Zn ;C-^sHzv:o3}Ku ;"k ,]Vv_ٌ&L1;Iqg&|$D4p'"WA@W<.6\oKaU-DC.L8s@rzo/WE%C-SZ C$?00uTiktMuZ*C KO(h#q^)yzvp~v й>ٲ0H E>kgQGR+ywKEkc}c6wv+c2ϸ,Gc̠j%ȽK* 1H'6:\Pph7Nf\g7]|`'`P `47):+d-.|4 4dr {fږoTo[X[^ibO}tTOi * Eyd jG n(P1 El!j/vk߫ <Xfja46{51UZ-@XN; gቕ4dd L ʿvy3 nًˇ3[@-&r긮 p rC&j/3E~y{Ҹ]eNwk^ +xV:Q*сdc]4]@nk8HՓ.tD"޾-ɸjkx@j!U;@M8N=d jFqк2{g/=4[<ڍ3>Qئ4hQf骚j3X\(lS4(Mu9 2 eBPUjq ?)`pNP{~q|'oI=p-OƠvc4_e׸Q@;O= :OQp{Kg$t.8֣s?<ʏ KDž,=6~7iԵO^Nqvq6ceT^,~//fϞ_$~m c7~xC2RJȚJ,nTmk&'\q;hWvً gI T?()|䴈NIr%x/V3x:SI-`Zt|; 9X+g⸜ {AhOYLJB65AR˜q~x0|JٖۦɞP4HNu <+ZRW@pFRAmYߊy_ f\*eWUj_X -q2{@; Nu*ɵ*[=sJ}HwVtTWKXs-i,ݙ_XItnXFAYpfDXNCm wwRTӖ+8UbsBW`Y{qdqz[N'PQBNhwNO-W+u0~)Xh}*W2Np7{Fo>m/0HDP)N6;Hh1zoeFr2-}kj䆖T1 v1=MzNl!N6T6b׽-:UEYOi9E PV+pz$hNh;>^kYN~wr'1h%>LUT1}do6w.*V,:/IȤMiIbQmET?z*72) P/ Ձp]%^9l;_@B< 4I#qN%Zɢp8 b:(:CܕB&Go;^aَE:P =+T%]@Ǽ·}bٲNdUIENDB`liquidsoap-2.3.2/doc/orig/fosdem2020/radio.gif000066400000000000000000056343341477303350200207710ustar00rootroot00000000000000GIF87a^         #&" ##"",*"#"),#""$#"$"%,"#!*#$$*$$*3$*%*3%$&5+)&2)+;)!*%!*H#*$$*,%*3+*,=*-++2++>2-4)3*$42,4435435;+6=36+;6*F6(-8P;84F94;:<4;>;JeCJRNJC9KDK[OKKML;bL?LMUoMUMRB?SKDS]NSTLTLBUSSUC[UKTWK^WDbW7vXXlY@[[St[@u\KI]oR]Tk]K\^Lc^LG_^U_]Sbt[b[\bTcbScb[jbTkb]^cy]dcudYNj\cjZYkdlkZdmdsmZWnndol~oZkpd~pc\qsqcksl~slkttstnmzst{m{{k`|rl|zs|u{}ss~{t~gil{t}uuutu{u{|zum||rv}|||s|wrt{x}uyz|vv{||{|w|}}~~! NETSCAPE2.0! ,^ ʥi[ȰÇܸyD~2cǍB)ϣIRvcǒ;0Uc&#sܩO tF{"IS)M:S<aVQt(jmhKM_o\ Emһx6t߿ 6È+^̸ǐ [pΥ02ƾq۩^i˖Ė]8۞nxn!qu{S~DC3p`nB*dZyU O|yY_^"˟_|V?Ͽ1 eFudhNE%w'zx䵧z'X> ("X^ubꉸv&7s=A fVY5X;/xjCY$N;#(OXf%Q<#J^ ek3  &FO͠J34JNyD*N&%Zf)#iޚ`~VixjjofjiHЈɜuL3wC>蠏G.{Ɇ6zf[hn5\׬%E/uq^ٓ-;%>fI|rQx^4@^| R"Zh;Gf1#j(ըJۤ6i zaH%]iD:&j_``,cyKZNl~ӗ@zt*sL_#II^jK ^"ܦ26IOfd OIHiJN:̝Qaϵq[Phd `W@F*Hp#3ycK_ᦊƱŪ $zq6K-QήiJz4wMNyWO +Ԝ&vmBJtX;]^Yz6-DVMKc-nyVC^h0| ɸ*pK\7Z$̥SrI4QZwTh]U=kY7E;F׽霮E۟ͯ2^L3uM4:+:2 iN޸fu+ksNڪԫmmMaGR$`c6Ϭ^3ԘƎ&n@;5יaNw:yPƲ0/ ϊ$W*ֶ`V?h>94Fgjbtq"(3p*Qk-V-EڹN5fN>3qBKXj>Y ǖkG$969[ujoD,f{[5lS㖛Wԭ D$/:>V[,/G\2瞄pg;\r+>NmQپAfR؟pCtyuxRMa-isid8# s8{yqvemIo,#K<Qs[8`8cvF"K6u \R\C/zY~a/+=:_n4K8#E Js7U@{w>OƜަ HA~ۣWwW1PB_owvXs6AS3O:@wi 7e}SO3ui '~;D8w~~W{c\vswFcCv7-F<yhgGVS@G &98s hHwwƀfʐ~|GCc(x+|8<,ATkfy\jp)rxFْyszu91H.i[lYF̆Z6j8x )Z=Yv#tv{锧hpGh$))0()ylvc ɐVI$XJl `C)CRdi(9YSyK'IDC҉ѩRM`Nذ Ր)Ci))b`sÛo9YkʙvdXs''zI7)ZMӠ M>?7lXT[ o"J$jƝiբ'sl9Ty -E ZPeassԹ,O {[yȟ.IphI)b1d0e -ִnzmـR7 uJ*{Rwmoy^(i[J, cx@fd/$JV&^y~JzvU%z%I٨2]M:4@ jڧ\MoĉǙuj<~`z9} t[OrCg:?Wxȣv#F (FF1$в0ek?a(Ok}v%;kj8T oi$7KsIR0aV?,0o2 V^HC{hAv '9*wB@Q{SۀK3xSsڞrveD':FKb74}뼒ZC{[$&ۻ&Yְ,X?k¤׸X%y61ث k{g@ D;Xӫy[L NܹKy쥭8 2ɭ;"<՛=w}(7w^ܽ70~+at̢+= zxH(3GyE?(]ʨE,`0,<.0 Ŵ;ŭ{{p[o |'&[5 p<^ħt<=hH˽𲡂|;*ÅcdiJUy\[<ˑ+9tLHK|ɕl {@![\,qYƆ,40׊vR˙eTEK:F2,ӕFnI.sذ dMl<Zw%= ac.?h25nl9nkBq[)L,eN^npY>?~|ϤEv˜-G~zߖ><|~7OUM,դX-).۲WRSڔ>:彝놎ڈ>r' K%l g.K$!{~z/xfŎgTzƾR'Ӟ%Mk./;5Bf:}nVR6PdoWLᰮފ|ɹz]nUz^Nd_flh?.%m,n_X-aq^ؠ }?YMiؼ,ff^ =Hsn{O\joN` 7-)edo_zVvtl3՝ ;EW`  4(08գXO=ӥ#x|!E$YҤɏ*L%1_Τ9e͗t{uۚjIp2eפNņիPNuWaV*VjF!ڳװcʘq5W.[3nDIC֔aĉyOv@5T)S[j9YWfE[Z){p ~e?hnziM,7I '^xc=#ZҥMFz2IoJ[C4eaH{ 2qon|o{l90qβ0nhN+"gP+VR)6:=ؓ7 KI@$kʠS3a13a<î)P4.ԐCkDڨQMnDh 3sj,͙r\3@zd.I#R=&{Ҭ( J+8ڼ 0Y<2DS7Au 2D0F*aҳXI&5*C7ш6Zl!œDa-5OUvك$9T*+:aZuu]vE$L$V\u]*؛IXހ4xMg'{"H("F6ϫv27r˵tbwWDR.WXo~{v&Tp-\tQ^vb8ۋ +C =7UH.9̓UܴE _O倪p;)-n]芯Zo;NJu]w168횽/ S lC(Aߎ{1";yXxۿ)Vv2jé>gqB9eW~i Bs1'sxsn;b G[hct")VmxpuwG&^ek&L}TytT̓,uss[ֽ}OUۑ> \IwK54-/ _2J#䍍85 fzI<61Z`0A(c jv2e B8W>jcE0ѼsC'l@z;@p2@{Cn-.b40YDH/Kd&bLZtPD)F8p?u!y<. H"1uT"DFVj G:E!4d\0Iz'i%8 pS)fBƑɫ+y0%A; Dv@%TQ UpB* Jy,% ȁ<zN .tO .-HxP0h C[Iʥe UHq U b0 U&":72?TU(z?=|iLetd{>PLՄBMz0^|dW}\ZQKX"joVCna2ZD9G]y׼k pA"t {Et~IC !58i鶶 Ȭ4l1 Alw'>1 DA Zi=: 2Ӻ|D$[ۀmWmpXȟ ֧ȥO 8*q`BBf; KVrD rBb'4nRaZvp~KakoӝΧ>mq58UQmc na K]5qEL7AAB`Y5pld=Ə :!ȃi ,\.YOfd@8TRWI9öPE ;"мfN*'f Ztg6A۴¡QgQ(ŵ*oӶ5 nN$P1S! /AU1X5JgܷYs峟o&]z#_y3 ERSGٺajGH䡎cd}Ba$DL*<'Oy 6:"hc>8 蓇}# @8pgҢWZ?{÷0 p)==:ꞂF@}ڠdXh Ї|O-グC>;;;` sxKc";d @)?̿;4}Z { D*"|4! %,f`BC9 (t)*Bӫn`h $4,H'74۹dNX} C`O,p ufh[G=(.)@ *,;j9@9PBQ=2S#F.W$WXY;8D cx_D `rHHq+p/MNY5^CeG4 xm\!}pZԷ^^΅R,/Yݖspr#i'.9 iN۶O`dL0q3ѳBc T ^%/a!l;+}r8WnﺃκCVHdMbG9۾u(ϥ(U4-`8`,0Q`l_5!"K3cYւE>WU8>6a=G!U9*QzˉHu0DO0R0'$L dpTrbхbY5`MSd8 ,xFeU.XM!UCYe[anS-tfJЇi+Bb,15Dp [BhJl.LH@) 9H1&&0vw6Ap|.m,BRO@RsY6nK()L^|4,P3;Pqj{fhvhV0Β@MLTNBVhec[H+8jD+{$$Ȃ7K-hȇv\";DHHnd*P LDj(aĶVu/z|^ l&H؄-fꦮ&n1&`ORʌD#((5DMy9 2,>zM+@g0_n-sH)elb ~^~nFf!K`mo*By i4r,H(H#uQ]/D?(R;9f[FnoL>,t`6֜Xoxq c]o/.>r6VXyim9C>p*7r#E7kiS=Ԏ(giD*3p9Gv8l6Rt׮V&o[X3)4HEP0Ã1qsn Uhgxǜ7b>p`ilT+y3y'K9ðOjNLVLzw΃!;CKw!:a6Vu<1(k >錟{ {(q?Wu7yol\` .ZMȀ ?HmrUʮ@K=)wk72d!%J(c.բhbʼn6ntVsJ>rذe A j4Xw'Оg(QֳʼnQUjTSxkk֬ "DcT .TpF$N31d-'S,;fU3"ƈm|gkR"Cez*V\@\c@j=[p=0u5x#_0P8P6$ƘcQvb*tEmDiG|K @([B+r 5ZFR&G4'O7 d\qB@`v.Da' c9&_1JE;෍~p10WYhma^)v#V labX8ZфbgA4Lb8>XH! $`A %B@B w\TR)r97 +u9\ LQT,.ƚ'YQ _NħW`[tU@ `rv!P.;*QjYf9O4L29bX $ ƪaACNBTrN3T8ٍ/p&"tSa?ge^ E0dP(su@d)\-qxin<"89!-Js}=;ǬF#m3 0-a'L}RY%v15I=xLwLnK#hLR| QTt0%^h&7!C&!0%0;n fRd%YѺUPMDoh0BtiPQ*҈v0m4"~#d`LKB izYV\収48X} HueK\"k*NtKk6ƴRM%Vf4cH$֜ f/Rggth@Ƌ'AP! B%͛)ݽޏ|{"H> ?ե"PuJA 8Y׉Zupe@_*Z @ \&0 e-V f9D0$; tf}-]RA:iO`"y͆ a d1LܮZ]*zAq2|$zkWĒ=Q wAmgw>褃<_ vE^('AyMX^| n yd O :^;1( p$BC4X?_A _[V WUޠxb9u1d' LyjWeVܒE.`ujF)m"BUL\@pբ.RB0']dL1HCb(Q枖'u5握=})V&dɅb ^ۧ^,Dv"5"C>C5H64Q;CJ 3%PB\:> jCiک?N"&vD,uy^DHʍ Yكp.QIPcPNnzu@pJ%\@!@/%x/1˄FB @-[_rl\<@yO^r|fWFݡ  h \bv0="Dhn" %-0Yª.4 $p\ 4 $Dp*s0Ξ fe}x.K SXv[U bFaLHYJujςco(V'f%an~󍅫'O (^2]1y]@ أbd$垄 @4aAAt-i'wX`2^\lՇҜ  *\Mo]H@"Wfߠ2rDJ\`]MyoW'{2iAr™*_@>or-tAB3:?Pl2Xp}F;s,L4Rv2'|V&t^/Nr =օ l8d$1dTn3 @[/C-r.'H?VMVoi45 Nw!%uH2]./K,ʹ`ΦsU@ h25@V'OMa)0X tAUic*Yz<#ס5r5:<ʯ,+\U3bV&u莉tJpBtMlbMN/5doiRD|IoKȚ P670Aj{&T͸05mvnv>p GU^Qa]ts#2 ތh" @t] b' nZ`cwʤ (VJwy7o(ADI!Hs|7}Ci7ȫ l 4S(1x Kkp;\8ܕ7# (ʢ~_+^Y,EkbSr=.9R$ 7ϞG Pz̒7~C,xb` @<X5Ht`NǚK[Ûy`H /4_̞+w88EI5*F1 gdôx#ARb!xcz(ۀPy7 (쀫+ڣ ӹ*S4k HD;4g *Bʮ`H(ܠO;k8N`MV8 `P" 凈;7y#;` w@df S?)sZ=ODD7؀PvW=% ޘ8f0^tn+# F/,_T%Ћ;u |[  ̀út PSlylA ЀTɳ>?DA~G*wD!C]?'ڳxcFQdH#Iweɉ'\޶TR'N8SE C9p@၃K (@V@8 ,z4BXt=Bk9de*U -{Bp`A$qrdZ(Sr͛9o$5JU#:lpAr DeK[$G-4~yI)ԷeLW4mn P4CոY:-p`RMzT`w+b80/+'ȀT)S0{L +q352'\ Xk-؊(b|X\!2<2N:9`1ji+Χ#J@"ϼ+/.o jr0C+.p*l!= %5q*P"4AftFo1(n|"="rX9BZARWqIx| cJvˠ;zzJ6j+O.F/2݂+ȏ# {_89"lK=r65}v,NuW,F8 *%ˎE-nGcn1ʸpW;B_Ҥ{ '9ɩ4]sEL̯ t p4)ErZg@xڥT ^Nd ۾>D6y̍W71 ,Q1^."qGr0M>8V\<(CZj ]V"s ]@ghJbA ^)`wBN34cmT bܰ{;$yDK|Yߎ>%NѢ8d`7y%=]|v1ڧ]%WS-G} L ^A`j0'f`khI36? ( y#z3ݼ8 q! wIw&c@z&'bJUN(sIAr)H%7O2GRŘ®e:^E<HAÔa׵1",C-h3\!yHި 9y DzPU|G'wMmrxS)B#dsI`".btZSr'Q5i\h0QeOUrFo)UdE6J} 2U,l8,B5!j籣~J'SCLQ$'P |+Nfv֏Z+1ԗ@~\HHSKY95kRZB/B2bop:v ie/~ogCڑENg/5U <ubfk1`J.ny.gJ}$PKҨ\^+h"V,C.ܒ4*ueδz7No'Slp_2V}}Uꗗ~W'qP)B`Wp"& mR\i&O[at@2dNHn.C'~ޟb9Hldx"9S2h '֩DcUAw1Arh{%Vr|ЗhJwVcEĮ6[&+*[֮MBlt 7LksLeim/FT&:.O@=34SҺrDEJ(\#yxm ) laGipp;eAtv?rFq`I,fgX>mܝxcEoS[P.3`P-@Ux`bS=KyApNT8giG^8ǀ^7Ѱ1׬q(-Tq D+IOZ6z)`e6(8paFx&' ¼yBFk)靠xwѾ54Sxi9w{F)dkK YAx KbzE>!=WMt?]V*'t~" JD pOS'J?F@E< Ng.@2+/wֈgNX8HE y`*@ Ġ xẑNBExܢx2HhMXl,mTH( 챃n@ݫI (!p!M]JNZ&@RNrEDof͢ăw|/%bv80+'8G__r('E` |@O@ U~J**] 2  r+.+#r"e 3qKj%ȪE/r0(*qf&?P,rk_Ɔ1S | K/S6sH>OD*K+OS+S}~*@4`-d@F~62/ g> +lu(@*2L.\"4/PO[j,)&'G l@ zs2s<7R=#4,>̀k&|B!'T;edfHl"J\b䐏"nN|r?NDMSR\* 0,fn#3oH#S >cGթ @.gw~"fDjsRQ)(B( +JNJ4AG)B7w: j蒹r9.ffNj̲F -.N Td_NCdD0\ f63q"-ވHP 2 PU[)fRQO dRְ,Ki:U&${L`(^KC.͊ͥJ gl-LWOtpN`u"Yo9>q u5[UQQ>t\~:C[` 9 fdPevhhU$@-Q:kVq\a)#X;1T!!",|0V"46c9%X"#&˅h1\Û:nB*?ks\-Aǝ]n_*(P\O"|t BXɉ,!Ρ.]Zڹ-55ԾS:V[5X\AO'Y|kwڲE&w#qaR"=ڸK8/Ӊꏏ Z%<=.Ի3Y]̣+-'Y~4:ZK\P.OBC7;AHp\{лO-MŸڥV!}:!|Hʡޡ<ԷԱ;]WeՉAV6e_V '͎kX!톦׀pCgil`zfB[7VEY !<,kpDME ,[2w.FƺLェ)jՅK5NO*~}GhYYŰ'y'}@M˞zބ/6AlX>+XH= 9Uj1~:6;r6鵶bC]bYLĮlø)ʊ>iPN?k'vx`WI- b3 yЌK/ƍ;z2rlydIDB×L|gڼ3Ν<{躡 ҥL5TTZ4 THㆊd#kmx".<8VyR(@4nQe0{"Fћ7;0l 5Z!)zII \C1M̛S/[&Y"ҎvD >LJ(RM>|+Q6ޯVrU]W_PX嵘Z(Qu=Y^Hb6*:PiG ͖WƛC!rN=5F!$ uX vȱRKsSL9iey^t>CSQI^TVR=hj48Xa[ e ~6vY8Wd5!^"f H)(fh,fpY\j#DNTAbTP@j"cNIB':U%vSRYȴLCH}ycޚ~{AՉ(v#jpe(]>e|! `x1A]ꅙ]p0d{V]f"f0k65@QAE r--'FւN2Iw,"SK<$3t-e2Զ n;.-z*Ǫ ryaycsٕ@c>\8VcFuZd:*2B%"]93 K04mFs4IۤMgԶf|ZʻZE 0 ruKa&^Hmsh50c _a=[8 TW޲>4}n !);(dzt¿~LuG%LO]s*K]IJƁboAZG4T!@ /h^ t0 5Fm=`|[@'30APoWbH1$X h@&Y%}Nu[ v@h.\+8p @d^[Ø4_ )J tfL0=Cv1Rv-3l Ptب~adNJ9K>,lhx(G'vkgt.M2L&ӲINBfov(G=ta̿ަU2Dxib=k.J '1PB/-2בsq9 8dЦMLA'{" r j,wRq.g=I|-yK"Jyyja7I({]ePiEZ|( Y1I2t<;l7NrB~&< dMtMJOAF-m*F%aLU.UJU%`kQBGoZQ br5IV҅]u%#D^{d}#!4v@YT8f0ϒ'NCiKVxXDC*8O໡]zC쪃 F)GE1! ]Gˁ2$WolyP^7]<'ʍa lwpVҡbƪ lLt?:K_rS%-ݴNN9<`يE ^DټArj G?.n-cgp|n"ŌO G:qDfjO"QG3b Kn2YJ$DN؄CsG`yݲ9& zv^Xt.pj" ;طb@U.*Dkc{kʺ+szdC m~DaJ SfpyZ_uT%U7"**e"cytW^9]’zgLE +cBUC 'BTġߓ;w^-|1#(M e/kWLXZ7" B )k8Uq0tE>U=0 b-XL,Vr%{ɽ9Sx`{80ю UTzly]P&xYǖpHh9߃*uGe a(MS|ƧRȷkq'3;g7r1cq Շ}'65C(x2W"yomgeegAtsvryDOh&2u`]7u(w0t@e~V7 [wU*gYħE|r&d!I2;hKqъRES RVx8>PPVX<h w307-cgb=hiWEb`؄@T0'-$B.# +:@kGX`2&"Y}$2^"\ ِ SՐqe(x 'I0uY\  k%V("w\GdV*YQ=i2EEi,CqMI0?7zI5D*}BȐ1Ia1YdU2 7J0R!x)V/!I\[B}kHh=@z55vc$d;&Gd_OʥN#.)aYcY0ƚC@@ ?P5(f_f([h@;)pC}a6x*|]E)qCPsPG(vy`VeS. y{Q1y͈(2s+u{w[I[OIX.jM!Ƞ jX:e| bN(@}ݺƊ{+;h`xuɨ @r/{ 8'x98*"sB^::"i[*GKôڥOO%c11G͘.f7%,M @ pI9TPO{<*^,8~w'g\U\\P eVKQu6a%m!6Y$W0t̵I{gM/Ó5{z^`;X7dCwIH F(ʞ 'qı|Nk;wác̃!Ua]/lǕg.U0uQ;l3P?T, D6z*\Bay 5zY0S0<[[I,[b^@xx@rh0  @ ݀ T=|i˺,]쿿Bzadu['se(QwR1[.Z͌ ]U)9htw' R*u5O>`=/-<}c[,8OM}Y.Q„rPpi 0 W&HS}'؋f+"DTVQqJt6a i!@lƅ}"țu.'*C!5o)uVLSX\uޣ{0mk"edn! ,^ ʥcعǐ#J(QE2NdDZǏBb(S<ٮK0-lcƛ8)ND=ySgNHw]ʴiҦPJ5ⶫ/WN^óhӪ]p[ƜKWݻx MJ⼿;n\+^ذYƐ#KL2dD-k-Èli ͦU6kzI%Gus=Ro4};!MX4G}nFE|jdխb"D`̌M0凯^{&Ͽݿrh`H1l%tw[vt(S u6Ly;1A8QLS_Kň4(`MXpùNqwՔbQm[㓎PcYՏ" $~e"5Xfvɨ&:5ɟ:nҨ΁sSg!M9H ΔI:(^ ^ܳI%gM%i%JBȳnu6]꫰9;+JG$ y]BwF'賍[S9>!OХW4drOpsߙ*+~z߮:ln9qhlVϳvA+1gG`@BͶtmB>$#ٕϋl3JηsaKp9Je7-,7Ȝ~lQ>Mr}_6ـv67Moζ޻fjcM-b(i>Cݨߌ(pU-d<#ˣW+jXcrk] 5ۋ49lY op/;?,GN[.P؇[ Ydz餃n5r^_Ѝ+1ny:h-id5tؾ(, B"45#z_ڬ3B䰈-jЋ`73PD!U7Qs!*ٗDb|Lb%!t!ICz|Η+NQ~ +^׳drr.'}J505ضU}̣+voKGܥ<ԯu_1f%O7Ct0әܦ28LcBnv3q+(]hNTSh~ډw.'^:"1&OyQBusP/j,ˤF3')hrqa FHq&:f% +Ju?L/? Ҁ{nJ{(n#Oݷ9&k*T1?~FA "a҄T3 &^Y]`'β*E`Z@Uqm.#ӺVqw]'EyNf:PՁmiQ1mYk`َh, w:ϊUeM vǖ㵊[ X>nVgoFm"&٧b2˜գEa|htb7=tYPr/lO e%i*'Ge(ý5E:}~{/v\Np_GV| =|YY C e uՁL;&Fq2Мf5ݫo&WujRl,*c`2 HNɺd&9耲,RZQ+^9Ĉ.)GMQo6|Ub0ͳ9i1gVnJz*?äq]i.괴OWu/fTV^5CX&K`p% Aom 968=i_stY6q/pnsaZn +@~xtVaW~kgwT \+2J4SUX`Їfa8۹-&NnX[|Sqߍl" ~rzVzhYkmGOpGׯse ~j*#'ŝ>.eϳя%c=G7!lv%6{d{ZS,/1u.{{WЅ>_YM 8$>[rDOǽG٨w^ٕ}$6z|H8J ݂]G!Ѭ ]A.n} ) hF;0|Jg].xN}ڷ}Xlm*K}|@e12fWowG{t ~{ܦAPMg\[u^qrXh>nz} h]e7~y](c-:Heu=w &t7CfweN(4jQEC|ƇGcio'CxyEh]dg}sv-89,grWq ȅ&4>achw7{hfRG6Gp4q68أw(GXh B`րv8b}2eGdo%v'*X{6GDVGuqx\U=z|؇~X%Nx/sXzhHˆl0/ʄ'f܄ wwG7jHdK>Okr8 8xzxCx7L}; f}2;[RHRf PtlfxH(vHYGmxJi7$fSՒ-|M0b^hF4Ij8ySdE9iB$KYv?P{r?4aRY^W}uCC2rH[3Iefۖjjln)cp= s`;r{h}yHaW[I/[&Uffə>DgHyaٚ*931,,euiBy3IW9.&flj&@n.Ֆt9y+XVlC9p"Ş婛F1rذ*5֜FeE7suxQ l~&I:d`ZȒǨWd`Woؠ `)PỤ&z*Ydu|1YyY5zU&9j<:wHݧ^r v:󙜣Jmuuo)): ه:8yRÂckȇH*٦!I)].ԛoTLwFʨ%NA8mBȩ7 *l}aۀ Vڟjd5d>Jr"&bŎvyJ!z {3[˺ +?D!Kk)[[ӻZYȘYx7ۺqJQT:-bYYr+DK1W0;9 x;zCp7,SנVyBHKLG\RMi•w5}|w:3o 'Y?%eCUQ0C4lT|ն;y@B[D(E~;-|`[';W[7W,[ٮZ z\CYH,Vg<lBknxY MtvǏsD#5͠ Oqq+wohk˱Qzl}$I-)Yy[>l~.fQ=Iht:Wʤ| w@̱\u⬺e5ʌr Xjj̸)f?$ʛ,֩I<7l洘7׾ߝG)״XNM&r(ԃLo/ҕrp,7u>|]_Ϋ.Q9'ٍnnyB6 }^xkӝnȼ~}nE NfqtjK9;aݠ~MȾ^ Ğ~ZY_·點큱B.n[؜ Τc,,G?HCѶ铜8,Oƞa|YYYi/ g]y:SO]YɾjLM̌m:Zo-9 A -[~K4.-k 5nw[ Ϙ?ϛlu>NHqw1)ZY;yMOkO>p60td~Ϧ!-5QSZKYyo>ƐsdMP*ϻ,J  ֨_ֽօ }`:ڴ_T $X> .dAB.:ѽȑGv5Tt2ʄ SMa1uO@ 4(QI^6n8QYJZ:uF[Ŧ#(l>iբ5om[qR(.ݲvcGE6eJt hÌӲdɬ%%ڹPKEttsu_cFGv]kuko{ +^x`PMNDN 3}2̛yflG6ZkRax+@ :΅"hD u ̔93BKK4DL; tT2"Эrԑ7xG Gn:p*pZrE^܅^nʰSFdvb SDԼIQEbxsNRm!tA;*`dr-x,+E-3I&N$S'3E&52M sx*sbuO=O\TWB{?CC Dmt&!ItqRgvo'c:;DRUUeUNWcJYmUum_7`u41ƠVZ +' u.[>vej>t EME݄8yCz-QЁ~ǰBF:eƜ-ya皨8h٨2\b3ώ\}ᦞ70ap;!pZ8n"/!pd@MYO!L';8qGEw:RU-Z 6I&~AI1+lJl&\0AG*#'aF!wHONl$EYxģI,LD3 (MU6Ò.U)wV)Y6ħkSڥ':q[#\4=E!c\@9g_4 >뚧-qhpEu90ZtTu *ƞ9G5`T}TIԟc,MScPVO X,t7TuP})^^3Z!6.#*V4PgErQl/v9쩓Խj+[Akl'L7 ;q\dd#<;cD:P3@+F*PL3!QB|-ma Y+DvۂmE`6$G5KÐBt1Bݏún^oP^Bjs*PS24+tm`)޺/LH䈹F-8-F5P`v;q]e%wNm6=߄ScԨtv7Ưfq_]W@|W~1 Uf<&;7jpg5wXή2%` \B<]c.o4X4Ysҷt֭v <~@>n O NB̀UDv_}ƓSeNҒh N:i0Y̤!(\,7Kyօ7xkHϽVK;1c&v`Ke-Vƥѝ2;"IcW00O>-^!.4M\Aaӫ⽺[&kT=h1TA34=06".c}NW,*?K3s`@J$HNmɽ27ҋ9o"펱nP&n*wVdKv\|Ѓ&ؔUE(,x;!&i֥{ q?W ޥ‡YB1ⷽKeuu t}·>aD! JAV|"H7퀈e4AW(@ Pv wcag-73'd lkt9>#8H }Ї}V;h @9HIVо;1-Ђ1?'?'ۺ2;-3`д B!\9*ӎ0p#`%lgz9k> dVHt'1IKHD9> '8d{ѵ3+=* hAC!B"$:Pθ X#>3}L~k${*x9ȄAS?FC,(LF ضTčiS#I0I!tI^ܯlɲKzZ D I~wmlL@0RŠh8LdJ_ ʯ 9xJc@l HLKİ˱ƎF(\ˤRzK]BEʇvh ,νtmM8,$Jl0FĝL_qʧC2 AxMьȉCA$PF"?-xMjiFo-@],GcMȈv@N`PLIB N EzŦ\H4k,HxϭT,gLO엘Y*P9Y Gb`vPuHmLt``%CCDHJ>0c8ȬLFQ%]և$MdF -TD}$i+3;0bh3s U["wTOHWUXW[D{sxzK7xQ=`_R0wP~N0Sj9rE(B9 2 ed$Ѐ4'Ϳ( TE|Yj|L2kbp5ߊTH|iƓ9VnEDLT@GRxTHX Cx>8XKSޔ؄XT?JH>A3Y*@Ė \Y-mEױՄ@B9 A$C%#KXWD{2WiڑWW RX=H.G$*K8%VDتHHh(8Y-T$KgmSyuY%֍WL$>w0Qഷҍ䧑W_(]J9}&XN\FPZZ]{Ŋ[E(= ][e=] XFmC^^ +]ő|_[4'GE8UhwǕ(N~~MRCH],'a [UZZȆlp5d&x(֋}Hu?̇f^6Ղ(@a\1dcXzK][w ,x$YC6jbtLd܏[IXW)@G.ذP쨅d!fe[Їw2cX `6>k?|9C?T.ac>V?K8ABN va-0`d,H4$A c,}]}@fګAbN[uL.drV}`e3ƌX x6vkAcl24Ysߥ 4AHFIxoI<sFm;8dCjpBd}־SK2)~d]NQ!yb`2GO^S;km V[E@ Ȁx;N.\Ԃ,q7cKgc@;O<()HEoIX>q31Bco]tk@ruxcvЇ,A)!feZD WxY(W# xzճqcEH 5?dVwJ%\iBq]aݹ81d[GX4)p~G4Y'K"b:M_UBX2[hcR_E55 S&4Kk\)Ƨ  m$1ve;`bFMfFDಋc Ȁd̀ @ih.l>qɳ:?w9Yc@'#v@xr>eTLN-(jidzĉŇ 8[`COf -~lLi/4NbxBvm: ]@'wk;Cw ff FwMgeO|C';.lYO%~UQ v G04x#H=>ȃT7!؁h8D Ka~l3d_h2yctLVy73rp7z tPzzF_㧷̀ 2dwc>>bbpP~<#Hx!xQT%6*HV V-d|O ;Xy\X(G;Z=u8Gkko,h „1lat'hČǃ ={nJxJ=A̘x`SM@ hP4BΝ8A5TԘ"ň .8p$NFK6 ZZֲe+fq4U Gz zNI<7Xya %J6I*jǽzG#wnhi҃G2bb+ңpHqQ>E@@fQz $$_رf0 WK/ʂu٠bY"˻qCD,pHqFRΑQ7HULt$@ <@)Wb͐v4\%,4Vi E LyB @̔beEsԣ w$yZmk. N WBvQ,1znf=EPedyRGֱU&CiFGik |kI*UPqVx숤DeK*TvYrdguCb;1 VԈbO`xZ#B*j$y͈mZPv]}0/b<#dŗY}խ|l+@W>Ց58&_Oъ%pK0\AqgP(mr!H`qKQ#J"~ShIB"`5;xtݏ1'>~wMZUqk#cWUPkW\_NwL,lInYhdDpDŽA żQp10=@D1;XϪY-cf,2#%:;M~z^@Mt=܀h@Ϥ揆Z9Hgl쀳iDV6|"4(V-:HDQ z22R;.}ϸ0E $ڎ_]VZT3 \u#5#1wG!|Iw?QUkNt'<ל0sڂWM_pC5Eo:Y*E>o$QH5h6( :5/!w_M"Y!!Ǒ$ X:%`IG] =&K:tNysQNn` UFXD&(} dCq P籅Dl썗N8xK6S =$EXh}#)I:c gNEY6 -ΑEIa+ @5/(ޠ՞%5B-XCEoɐ '$o`qB-$@L}D`A8`u T*QIDSy&P8b)V䡌XJld Pۨe5$H4<6atn,oy9=\X`P lX@x ] >>Y\JNL_5T`Tᱢ0 لZHQWȁxJeae~-/43ʡ)HȍD58`BE!qG!/ <@ p!$AM6?&f"mtC|8a"MФUDD\X8}~@` afcKF!,)c3nmp p L x@7d:\9  cA=3% A< 02d%#@6C#A"E!1)>R`PR||=dTNEeT LW;(Wd͙TU\dA$Kd$M0EOO@E HoKQF(Ğ#)LBUc x-2 ?e&a_=(!\BalPOh%@z<^*G:|]'Fe@;pLRv^IU[d@ AH|$ <0Ppf fx&$LB!f=fTބ ov@#jZE%)"=C-\]@PuҢSb._B^Aţ@ TF}yzjLA{2i8p |1 T JA(R&m̥@' ީ^gY@fE8ى"#g騖\ߎ\_Ώ ֜0Z1iB^LpEL%RjQ)pID%F(A<@=f)'K&.kRiCIFT.$SlOB%$^Ԣd+~ǬkFNNj %ˑn^[\*M܁~Q d0@ DdEt"jj!D#=֣ ɩ1v0+%>Q98TÀdG~LLBTF)Tq\kaq:G=RN P+Ӓ5 @S(T9 h 4ۼüSn>3%LlA0 XNJc,CDH6؂@CM $Z`9fs~+G]j$ ~5TkTPaԛc^է:-*Ԟ؁Bы4%X'B0tg(>Cڮm!>4  0W&B %f FCE CNʠ^e\JPP,: LL&ǤPW{SV\ʐl@.in@(G'L% !."/5KC=TGB % @  1looFB Dx&ð/x?jzJR&߹h+hUv$*IRgaUE0+ڀl$pa  "!!J=4<4!BP34/Ş  "dCB")/ #'< CJ*^Jn&U`}OdnDG#'Лm^Z5 \   q2la!'.L9 %CoHh2#"2P2)2A8,2OJzYauk9.~!/#D^v>M_^N =⥈G̔J4?Q3LG`FEa}ǖ1WÆPeAąN*:0+8Գ&O>s 5H_@)iAr"t)+,2hG"@ujF0WIe>bVHfktZ4LCקueD@4e Pc2<>h %L(v3_lD]~uXXIBZsZN-_#CW41|kJRq''1.RdtJX #bb{f] x @4eO_fOH|A7} Av&v|JəWhn,*˕ozLEo|5P TyYE?ʇ]ks_ZcTxR=}9wGAT3SZ(A xgYNWveW @|KAATC/}԰BAkk A|f|yQi.ى58YI'nfDY1%]UuNaQGʲv@4EIͲȸNY4GUq7n~3X 7GbO=>V~9ìo7qYru(*N\ y]ǵD&hhNG y$+v^MMG~lzz@*w=U[5){W ںίBoO8|6V|`kaY'|D̝VUu13)Rv̌wx_A T@~\LX@ 7Mc4A-D<+|:ЃC)W5p끥2dPWdek RMZAe$Qֵ{J+{^%Et5p@ $wTޡHA8+Vh!DeC.ն=N\nfxp=$.\w} 9hIZVB& 4I@灞?)}4> @S_+p@ӦHiJ.J NH$ B-B "4+̃ s*2%aSԬ΂3LD 94a͵ MlM7 $)"θ"RNI$r%ˮv*H2`?ێ&;FA&ꨳ֫1B̀RXtR*rB$ 3/%hqRR0CQTUt3^40b;Lj^H!k#27# Rb b!'J)2PrT!b' 5)  =~@KQ -A*J2@DTQR1.ТR$½8b ЅS\ "DQj W]}W܀ceVنewމңg ݲESV& Lv3{ ;}jP4?C@`AaR*"/?-"TՎUlτa7{uX"ayɉfy!eygg٨^k^4kbMu?-;NFx)l*;ib\~L/RG5X 枴K/Ի/-A <0cXuUn$GdjYy 9E8:+]NgQx[I yыOIϘ廙MYQ;E- }d'8 )cSM/_3ZZ2 D.Jp, Cຒä pcnN\c~Y:Ptٔc@2IKH<70xpCLq@w Bw pV~^D)JP&΄ ^W0X/.!$CH/Vbȱ(q/<- B友]b@10Bk|RO*kt'iJRB:e8  ( ;O$Lz4L>Z b O͘,Ll5$ wD-ѣ~\r7\&Dt1l!-z%gO`{`%9*:)1XcMCejp—S9(pevp `ii jXS'~332#ʁr cC#jD_Vы"Iڨ:`V)i4НnK>-*xB:?fg;JR ;Ɇȩ+h dAQ6+ڪf9"l2qֳʦHlm~VO8suR];_y%ՖK:^")! UASji 2;Aݮ&!˿̓=>wAJ $m /P ;*ϾY'mZ8tb~ dž5a#sX[j\IuFFWVS۩q+K0+$0hYT?&rObts0U]uU'r`Ao}f@ ζWy݇+ʌяZQLh=6~MZ*0NjR6kͯ…!py7b\Zɒ\ -D+ڹbhȮ>/]s& [:d%b>bE.̮}d2+4ܞ VjmrF+o+cNtsrV24}ֶ ̉<QD>Kl NNU9-L;hV&XCYPrO^#G`zB}ߋkO~R;VAfN m 0kʼn'nd\17( / Ӌsԩ`"or* njJB CCP2@Gؔ+@x B0A:,&*kjIC#Ѝb)nu"/Z0 C;>h'BhhiPkmɴPip1㼜0=Z$m*-n LOaP8P!aÖfHlwN"rpzPRB=n>i [p(ХK&JOD ]13>ʩ6+`&lZ +* :B2c@ 'A7|+kn"tбQ ED%&MR'\OJ`B1fM>\ń#*D-NpCN@[ 0p xx AA_)g#W"H$ Q⥯ӤӺdIw)ͯ$0k゜Nb0ˎ kؾfN`-.J$r"o :4#@$_R%Y_RQ&}(Є*l/']"& `>L%@ *R+LR`TQ"b1J m4/L$Kb)?2oL'ʢC4Y3 )*E=JS@8+v¤)8@fB$dS(/p{v~.38AC59-BaR03wsg1%Ӹ&<..Ť[ kR֩H\*˅' KL,.(ab/$ 47eA`$Ҋ,T%186CC:Q:It&+ގ)Ӏ ;F~*0|Bvt\4P>(i=(*^.z@F“@ |SA.ALH74:I>4sGN_z"<O0fC&KP{t%N*^ĥ= wjIiJIXY❆!?BtT UoUBVeuVVW#"DW2!sXs&F[l`QY5^ʲ&$$u_ ÑbH^ZKH "R2R*@( zhf^)V_Vu\2:c2`_X%VQ6Kd g!-# .)N +c3(['l,uZgTSn oR2ēEf[BorVg7t2"TDWT*k:LEETI??1h:rHGslTP(+ ,w,zHloR gXA6oWrgyUp}w|q@:q]"l#jBP<=kQ+ LcH=`[+^*׀-,*^S~m^1Vbwyo%_wux6_JD`nFUyYb^p*ƗY-dBȸ{=_ĺG}@nAfma0 .C.~.r! ؔuxpBbؓ2%&(+x24Ȑ0 K D2+sJ} zva"o06IRhx!88}bPyЉ xFÛDQ3JJkØ7sz)-! J͌`~1DB@BFrB9X{wBb O1Xi*ɮ2j!2b%6$MLhY* LmtjaaY~ Ayg5*8t~Hh9`'GT*M9yT8s't5Fa>`$d&>%b k`WM~@ $D 湞mpM9IA!x GM&i("'L,J PdLSm ~9 (%y:"-uNUT4eyF()_\OS׾)JG{'81_2zl*ܩQ-2i^  aIR<rPJL؁߿~Ajb4` *H\ 2d P#RWpI Ta'\H|{^/Ѝ*Uekդ$QZΡJN=%QH2u Sn/"DhНV qE\uW^dbiWa(h0`$@eiZg_9@T@rnBApZ4WQEq\r@PqT]>vdr-GHMzG%TSOWiW؆'u~& UrA^<4"`g=_$6"Ȁ:PpBj vB 9CiHR=cNHutIY4e]A-!7Is*O&;{nWgQٕVej[) ^zvbDAX)`Z*aƢ{5_mj(Zͪ)TŮDKL8Qӷik:d$ZtGQ^NUn[&ካf{m箺5a*X暂|kfpi_~mz gHae)Z| #pډwPxǂfr\^@j<r! 3ӌQ,aJϓ$A{9\p[]<,y:ޛU95շO*hB[6 |Wمas.V7)2P %gfuZX|,\zm'`PTPk %="QNBFW:yIñ"n{$KPcSе+) :5#(A r.! m@ⰿ1Sc"u6N&O 95#hQ;T m$~6B"MW`MlQl$ d/V%t+H{BB\-J3b8/>"1 noU&6ʐ}%BվC jQy p@#$P!`d@,g;[GBKwTus\OYٮYJ$;40UI0.p[&TjS!R1bX5Ooz<5.@a h $ MH2)L DG9Sr5˃'R=d4CP'#"=w/}eT' qgC"qh{ ZQT j<@1Us8XF >hBҸ?6T"brxԚ4!0'4 $}B`2t5Q4V-r%4ԐZwF[y.ITUUār1P[Ce0-1/%(.ZjFo)p-mte8*5M;ؖ=8b"!q,aS|Ҁ5@nPE{3ٕV(D; ':`NX~תT-oB /nJmmWZ SAlel ?JڨpP^XBx>Trxb0D_`/f?~ ipuC)"Oux )H)16@cx4Z|бzM@1 Su΁'}p(rJ5{0M 0S މ )2V};Yl#AF "1SgnXz"P˭8,;้:K_V]gm}ME>ݹ-:g@q\=t)R%S Xt T0X/C5Nbf # pޯұzPKD r $ VZՆDPg9Q#O䒔)1f|֢$</_%uSs# Pc.N뒹tKrI=2b5j|`+AgX262d ?rKqqԞ}\yG:x(]Cxn „jA'|FST~y$:Wmm聉E-By|<5#wi692\{jepR6etŤ=S;EAY_ 4}DQHqf7lr;m65llvwruxWaxg6a%ڳhz1c! @\tq7cuPw>1~1'eFotv{HBAvrp[%rjqQTv]}XFCE=}oux~.~ kJb I%*QgKq!0tsnw>ojx1+^BG_\S07n@N=p+uMepǙNH IS#QG#ȓ$6(FW<[c:gn t U}RG[P/|{*iqYu{c$GwdЮ@9 9 ص#$*>E{NNڪ갱;greHPRrQE"6r*~7KsgK!SIt)z=&{Ac AEe:j9 Flxo_BQ+ KjJGx0*KফGqj!6V{"ᷓv z&30r6K#14"Ik|{?ṟ c_ʮTK{,; bPM`2Ck`~Gr6nˏ[ͫ_Uu="ty!#02|#b5bz)pAC;I N J{hU"I|HG U!T')0#|kKr'߆tm 7%btsV('!2DBt)Ò;{Y8L)J,TM;m9?qY9<)zX)n}lD]vqLJy 3h(+U2(YI1r# _-=W'1[FۮS9M+>RG߱kL Zf~wq@K'GU;Ĭ x,8"DmI"}ѲɄcEG=!"Q<< imCK۹H9ũH%2ڥcVRSe-p*lOz`wVZĀ SJ菞r{x{"̌sZŕ `5((bt }ҏQ!~n6W:<,uyY.v5U Ԧy@0sjטY=]P}^BsGd0 ͐8)Ã# xvrxC 52&*\l)Um7W 2/\#71vr;fLԸqEٙJ\@J02m5Q7-YZ4R0 f[MgL5x ֿܧX}<b}2 ٸC )~Ȣ E p{'6ƧܶAW{z}8Q{ Hđ\'Bd(u۬<^[*J/]+-{1l%Қ1VbݰH"$'r#VQeڀUKˮ󍿧t!nԇvEǨ|;5-Kq6 p `H km\1_'|bx6[#+ZwVkb*bc ӫa"|/ qqSI.MqΧhU>m(>$5Y?Yvp`f>þjntZ/T8>]3]u+.LHt\^J7WyY?t61~ 6 >{l8uhYb;xxԨK٬{Cɺ5 Z|2gocOo  w+ZUbrrmѿ|Վ`*E] 1.~a tKB 5ct¼Rc# Wq[V0qPn:n ʝhq@i娀kڤIph)o+$mL̈́ntA,H0A 6,p 0C 2 dppbE6l%Ŗ5 xB=}VB 3 jCRK>e"Ǎr`D>/0)1 OS!#E =|рƀ! ,^ dٸɓǯÆ IHQ"3jȑc C~Fd7vNXQKbI͛8%>'/'̘@ʼѣ+'"]ԧӧ]d)B -śٳh6Ƕ[ ʍXqavs΅᾿՞LÈ,̘Y#K^ϰƘ3k,o;'n%+^&vd~6'k뭶۸oct47SKsRixp(fSYC ε䛲ŋ[m7po%8E vw]MմgrQ(gv݆e݁ .(b F |wauMa_5vF$`;EwL ;5dF%un!wFRY8FX\UXyd#ng?6FpbQ81 䣐>iXe wr%R4} ;%mS4ؕcԒ~\Z],wc۞wkmz;ľ>&ҀC2Vdsz:dv{#ZT$EuS*&UvSkN߿F7+kfK͞G'FKv wYvu 9`#{+,??nnk%abDevEj/eҽ*0PTKl ?Vۖxuìڀ5.x#L<w0mxny[T_-xZmuM2ݹv>lCxRn:5+(l7znW]IǓ1/'󉮦+ojNףJ^z:w(MM%53Ǯ7CBxQֈ؟6.?m΋u Zҫrck):P'AtL W:#z5Ajdz_ ɗbrHfCBx k aE,VMf =iO;Ī;.by% wekю.dF($7!fG;.RKb DkbjN1Y}SuF ,Q*Pd@lRNéDZU^5d+FEut$YMK(zc༆J۪S=J}!T|oUg^YkО_*؁'2\1WyZnK]MdyYjP,x=loGK^4r'[vAGmk[Zؼ#6qG&E?R:k]5۝S< MG<t(| x4@GBtTǚ` z+. B+ .lWr2vl jMԶZlȸ |3l|٥rNIrﴧ} r^x҄LA4?iP;{ԤnlQ:cYf;Ezxڨ!%5a6-ZP#h6y.Iyzާ8AnmmN!>cr-ѭn 9q:},c|REyʚ=?-ބҤvxq;trn-S;xBp{ e.nd՟%O.s;e9"K 9|aI>/;5;HsM[Mq.tӃS!ߺkxSRV]FGEY}j/7lܓ xxͶK|)[ўWӭꏇ%vvwzW^^A0CPJߧsJXZ~Gt :qVtTh'lmhvuʀ|U'oBUV~8h8Uc>hEX@^rUe6by{H}Vq#af.x\TȈ}Y;?X^rzdVp#{愥{V3vqqe ȅ\Wh??@}!ER<Șʈ^̈xćP($H(8g_\h x\HfvH=pB*6z swEDWxO؇Gjm06phG ٍW9mfW?>PZ7:'%)tb(CB='A!QJ/ccْ~'DN#ME.gO G0'~0ImICI>Ǒn=2ԔsxgG8)DE[$v$ vW~Ha3| ư ^t>c9wWiٖextY:ECR7tE?Y 9 dIٓ@ZN)rjzYOй6adwR5\)5 /)x?Zz(3(aNɜُiu[ idvdgZ:YT%zxy셞iqoSAFIU_gz)\A)Vԝɢ*I9hYY z[j)ƌ 6W|YOYKE*S'k&<_:0.Z0YJf]Z99ZKDkN9OV<nD@[$SQZ{zbi4\ZZDjpʨ:*yDtצib艇;ysVsO}1Ù湪bjjSzڇt*wZ ʬ^] pȣrsP m#7+11RzT#j4Og=n Oė;@{k4GWt3^ǤGW 밥UEI8jرJ:۳@R'22s ڈ/=ǖ4z=:Y _A۵JG2*hKMO;=R2TZk^۵%TbHf{PKDr:˸ĚIp{:8;` 8 w{;{~&A|)ېd\C`Vi[7j'QX2C 귵{ )T+7PՆ)~WR++YBw˛TQ'<TKitڪz˳k߻s+jqA:Y7戮d ܾYz~;{X[K>t:h*kx zWpwk:EgkB (!I絻x0OY9,M<38  u Hܤ˯!N0t\v<0eP[ṾP,Y"`<OƵ _o qܿsǘۤ|,;nM8ja Q,_Oa-u nz,G,}j[`  Unint~Q 'd~2{h.o[P~T ^wX>ߌH-ŕ҈NJǍiP "Wp}^]YSJNM,j`~ (0U0N !dVh~,뮞 LɾR0~܋ǭnԨ.PU@ WGo ۭWEUNj~F~,BԾ636[ A fTכp/qgN^HJ/sP,*6P[PN霐 T _u_Y7\yȅ?󄞂K@nJߙPSho6Wp_ގ0` |4?npۂNZэ{/>\M!QPe GOoG?jro/m{oW`c0[` R^'T*K//Ak%L HЀ$QTp[6ԉ\4ҥK0c!E4I”TvKo.ͤYLs9uDOADQIeڴPQNZUYVWWw֭ T TJC#JӬ;~FdJ,[-浛7sYѱdn|ϟM#]魩Uf:4S^V-f @ x%>&N5zh0)Y2n8ɖwbd]yTS(mҮ]Vw\v]+Ύ8QNqN$;i:: )ӎ;oä{ BqDfC6n/?-N@@@Z.JI^hHQ + 2 -q8(eD,*RpOkpF1Gih l:T$IxJ ܰ4>{vp d-8t͈0"79(^L҅N:NpШ~-JPlEtW^˃3EHsR<8 2( 3 O "8 ;?F?zoZz ]jtqr߉ʞŕ՜{;E7Q&3K‰hth*pÁ~ƛysl{A&T^v %{eOudr$Paayc_]A"[_#E qu5q\#k G'` U@!%AY չ(;ރEAi#Ap1MS1F%gPՠhkZ<$@ᡠ]CH5Px:=`@4(}TVgP8_H %.\ FX/Vsܡ/~+# D$G:dٳE hjIr eD24qb" ҔWӚ*Ўd?; kg$ 0L0)(z{fEY &JX5x_8::F) :ru5R<ٌO(Rd@>YO(3Lh}#싁:\ 1De4"egG=zEO5< Rp6R L Җ6?)II%v  Sd2*N[ V VJLli]-Jys)reUY+PSk%zZ/n SJ$_nns]9X-T ΢ J>[F=6[ue-;`#.4Yum$j͛ZiN]aBB[VoǟT>ߴF*v#KW +0HUw+o nW5ZIE 8=o]yjb$`M>z2V,mB4H:ASG!yYGשd0 'Om>YFYx09wv5P.Xe %í-˲pg9;1c cxG;Q)4T'Y Sxu2 IeH% J2GZAp2 QOOλ͆#E0Qiݱ>)['jq !&{4+"f LQJP5Ms/*C*:q [ Xw04*a )%jSgxé,ʦ`~ r@Wh @$ /f*tC6oTz, [ B ع[n  v0xԃH 93=Bx2obcf3*TLE252qu> ! <]l6i䡏cP^OR`'!h5y璎Uۧ0+4ܬG=+/ =߫@D b X{UXK>9X&{&h,о;Uc'/9 Їdf2xA*hV\j)S7ԻLE3+;G36I.lýYӚ@81@Nk;` ~)X<+1(El?'0;0¢){8U{=AۇuK>H;'|BNP( <+4ywfkb;} F+3T+˲-;K xFhL 8yE(ĘA賅I+0,,E8FDϋ%+s7i7=DciAH3&\D@ ˘R㨵E\F0E)i6۲xFF 4k;(E 1} .[XrDGu\4$xHdPDz$;~rG; fD) h;H&C;J:z6č[:]l63H0;bz,;CFcjəyC .Fɫ$4Ӈi GK4P08.H7܁`'J]:JuAOD)c?8`OmVH@ `KA*ćBȉ82C)2L=.dܲ@(ʌ C۩Ʀg⒏G|3i0A@MuL?uv4$Ђ͚B|cBZ5!;8&* O`VDXQ-Ȃ3NK?Ҋ4'1,O).l&2LPet,O8қ3)ɟd+A8tGX?<vD S ҂C8o42.Ui}5wN8$ lOQ2'x x*|N ͣ8֣82L&F'=J,R.  Q3@|H_I0A)HEm}:u;)pDPjUXםC8H}p}8Q~؇m`@(p20O02q #F48bU|!U" $!U=dհLXYU)-Hs- IVb]YcMֿ3gm[GX4)plS?@se^ykp4`G)X& ,=;0sZH#;Q}؇ڮ…mŅK@L-ȼ+T}1Й:䝺?H=ŏt=۰KUT*Fcܬpx R%\/E\z`8?hH9O֜V7ł%06wDC8=x+.z;im]}]ٍ]ŅIMM$8BH8PjsH^v JU"]U#eO`lR2X$Ia Z:LI,@\Kߘm#hM<>XJ(0x@P;_A뿬KuGiPHY]yֵ`mЄҰa1bBaAP q@ޙp[aa7Ff6LQ6_&H5OU~v6Kʀ 8à T!B*P_9w(r\49b)00c`Xu;. HWօn0=Z?GW6dCKP.#T!Sa` N-\F8eTUk{e ;H"RT&6BEAcn|whUxs̃GxmP# eDD8u78^ިՇץmtZOކm`mmpg|gN T)tpᾪ>Fh"MO@fSf<^Ufk#_d倌Rh[[V hÐ&LDz#rBi@xLLI%8#`1P -HW k*aC5Y:5}`] n _`OC>{ʭ6Fk@kh-k9BkkxlϹ&̒뻾6U.>}R 8l® @ŶL[l7ޭ@NH_͖RHpm#Xwp$` 0NY8=AN.HAtfP'Ek}4nBv(1 dAB´6EČ 05l6He0KlVb&e΂, i]\ŶTNp &`,%@< oFڡ~A=_hvxڵ]_(tJЃ3Y`xʍrU.|nnm4{I{VGȃ2ݷ?xd x}q{qr #EqcsZZm4>ù=)k'_- *v22IR$I#0P-Ə$Qբ]˨RJ]IU%{qذiSRg#)#!,_ƜYfΝ,1bP7 *|7'8WVc'R:;)9e dP1\BEWwBU07FXa-!c=8YvY yde|fCi۠ZkAohPA @p=gk=@Νd 5Y])]#ذPxGy}$~2Sć)$;rV):=K 7wL5S$X9O>4ͬOYM2 +! +I$%rq"?0WyYEBc:a !e&SL 3i$ID,6 &S# 3ُ6es#Ibv$:6J$N^"Gqiq0%Ȁ8.@N0AB%D"HJDIKi](UiD:NY@PjQ sA{"V`>&ьf*jV .x`F$Zgaqد~s+(@ 7 gSPRq T.`t0D 4%.Нb!zh؞w$Q:$6+, h|+$ȰYre֣|f=tSG>I! M d@'09P!m4^h!l9JC)e%$[yH S$@J^&I'P"t@NJye5VBt"b *IO S  );+= gJs\Ѵ Bag Ơ2 Ӌ`x얐W n$$ȅ :(@"G89$)Q!,NP)*3X9)WJ9P%a5 5ŏ ǘbKB@DU@A@5 U5QGUGMkRyX1$,>&u0#XgP  ` z<$Fv-9!1.89lm08ސlc)Z9Iқ$ʔraai XWJ".T-NO:j!̨S؁ J 9` n6*U|UO\d2c `XQ J i&yS?i!Z eup{_r  P%8r*dRr'U\!Xʦ4ع hxã;-&dA k!ipzc*$&smfqC+!0ȠM0D,h볦-OX2 hd#"_p7hN@gH-,@u oC $qsQ&cGU}uJVIZCz"v7)mB Bxx02?<y~`}ũU1+6qn-UzڲjE2$"Ef&ll%_7;KpYHqT_j&o778Ts N&(lhjtUPi߇.x8 b.@i9bX*>!GTQpR$,%}T( =bx  )>a+ņٚ%®jVA;ـ ] ͗ʑ"i)i"$NŃXސPre n|iH- *_FI$DadCKh A05 -cTAM!*0FA 4꽙-AHShق% LB* +L!Ar2Ht1$&iuRgjVEJHce3'b @}RR+KE˛^V`PcbGA ltGi <݆byثELD&<^6N/Ԧ4m9Ve*H $"($u£7O#uC3LT ̎fiJlmPTnF֛тlg{:TFH IJ}k v(Ÿ!!"T%X*%W"-~lجF\))B.$~eT.]JG欝ZF=GC5T=&%q+EmnRT5-vΣBnZ.\AT$@:f 2/fAA0-e%AG " S{e.ү /z?^i,c"|11z֛bY(6pRT$-FCfٍd_yVqPopkMڀ8nV@0@J[Hİ @2+ Aȅ|@ت79eCL1ײܒ_ $lq`6ۜd%p+.h1~*ftzM*yhJ00Dk!/r"T ܀H2`As O3B(Kr n-re2,?*-N Yq@?C@n(cI{@)mC=T5_% @-KGn%AV%["UG^)77ZBڎvz s""sЛ ,=#AA>ĒB*p!AH x P^̓<Na8ڵ-o`4.{F`yL($xd2j41فcNLST"C26e2uLmHԄ-.sE|:J5r $lM$Y [ 8gC;9ڨj^OGL.?:%e|Jh$4b'9_Vж@ =aOE<|<ZVg>џѣrHrHA$4pMcϾ*F.؇=xgȘ:ڳ}D|O{rb)h"'9ePb\H/6 MĻ~[~w\̀'\)LM-tsPܸq(VXC9nXd#IeJ+Y\Ith{uC" 6r4ͱԆ HSpjA Z(D@!+f>{W5rȐp&@ Pfp &I$(Ir +Woqz"yFnݻuۇ<YqyH$ō[;t =fwELVew/G43k̹ϠD 8`5RKJƒ4Р 8@2 ڢπ*d 0Hp  T 1AZ2>4NM5ք*j2x-~ B8LrX&x[Î&N0$$͕8[bϜYG'| @- :D[4A P- D JA ˲Rs`sI+^+Iv`I&i+cι-IۅN2934b;̓%:딓NsWSO>IS[t*jrEJENILz J ѲPt:dED .`<^G 2X!xcAحe gyxZiaa Sp&lG1nbh\6m ]vLݯQ&SzŗS 7~XDQCObHձZMx(c9R *ѼG(ek1]i. 眅uMh!hi^}i*j~Loͩ;su?=v/)~nD5tnFU * VlQPpKKD٢z+)KUsUv(#]LԨnuVh:IhNnЂNIzeGxCR.y2&6w=DJPFgQ3ohDE~YY*xd0 1 bv9t0K<@ԥnHi>.C >x ΁BuB yb ]b9pxuֳ7 1` gD! AA0.c)Lf nLŬq0# F LAU@3bn+&uzc5 .kv4pNԢQ3#/BMR 5N< I94R׿!r)A(BV NP]JaD.L^v1 ZXT/fIj~R"ID![#Y"6̴YXBYOH3Y*>z=eA`:TԠ R (!5ڐZL]=W 7U곫8G' խJɁop1 EFW.Q LDTcL1c̠- gI^ .;kjn2㦏zƴ*m3W%x%}C*)*X R \!ԑt@6TITyGA;AA0*|/r9|fDAESJ&:+d F2FBtUF+f iftt3MG P3 "2b" tI#u7]ISJJB)n@*J@`*EO~Z .Ù,A{!BXq3NtUXSv&hb|)*o\PL W]C`T5+BW0+6zV@7YlAm4&4L@xQNGЈ)&Yal.d(*^;0&Hr2) @N\OYq=!<8HjPY?ҝЄ_Xmň\cp~١4UVTs,+$hrSsT'ي6ehs&G3 b13Z-8z!JY'Cp}ѷOz}-^_*TK2Ctp6duc d5,h҈=B_ܨf5Uldβ|4:?Ax6⪾,2ŚtY٬ϺRqY/O] oh.T'OB`B֚0\-=G4&3`fGp4ϓA9i"[.;4R7[1.1r;Ѿg䭁[So)8fUU nIbX{[) O f t0Y JO#-׋;:́7l",YBϤCA<{mjS?'V;k.~NKg{S7NB`$9|,[H63/i!lx2\ÛG=N.8^ |c#V| [oB*jWq/l$uA1:<=x%2$xUAA<#Sn˻ڀKj`So9&e|t.+mP: {S*N֋^&`;'G:?SkPk>%AlA{OciNFKݝ٢H`%K+q;ˢ0c ,my<;B @bVv/eНNXFM.NL] AnAA7˷>lݹݝD ._u[Aju'/B|@t؟h 2R.b6كڣ AfaHuMԁ=a, v tmݳu~ |.Yz,"Eޑ(n9`9;10 >e1dSeϹt 5,Á;@a<nx_n{;]n$E.g$+חe>wRF2~lz3x7yCԡma&a;r 1c!O1ĉY1ƈǎݺu|iBʕ,[| 3̙3 i <TP*UaL9dSxB \$@ @Wj85رR4xPah ² 7 DyA5 ``A8y$K-)S"ƌfb:{lSpΙF| ԤIcf3,5|ouċ‹g1Ȓ'S|=ɓϠC"w#8T[RRJ]XjpC|mNm{@vX%^QU2 Kafhgoo5QDtΊO!g7C?8pCMU#ް=}7ePrQ) _*Y<:ak((@BSM1;yH^ucãMʐ2~HQf8'D4F콪{J! )3="`4(T.s1$Ls!oh$ ?Cp01w|6"W̊KEgf/)?1O< LÜ cvlb R Lop$ЀSɼ i&F-(Íta.7R t%4E"*zxXXLQt_yKpqZj"[Δ3erYz +~1+aiVjBlB0 : [Sx!9'(r#!9Q$2O~{@ZZYQSDB H]"YDUn﫣 ksbV w{Q+[SZ@Lm\/Gj4pK4E(\hFHؿ1J+r`rjkqjmQ!o};EHBa ,m#tڦ:P`}f쪐ۍRT nU* mCYBLBi :~FKhE)]{QR2~2M>Uc;2! $ҟD3fbI I(mtpZ嬑·V0 ^Nr&$řv.2.,ox{F@qۃ`Ms\5m(!A~”˴N_az f- Th<53nvpJ ,Yy[½S+fx>"[DY}'a@nf4HK*FsE(,IWHeOPQ!. %kJP$躅MoGl8%rq>&\4A R ^kEܞON|eVƕc>@LHJt%.K_@Xݱ Q3PΚA$W?+q7*H":NO ا,ᕭx`Lfwm%b7}2z?13|1i=V4b<XJI0uMy0hQpI·HJP@kwvZ3uх>WBaȅvMER(& hC}"HGSDYu;ԁ8d\(7|#vZH!70f)r`O6š$1E7Ji=PI$m8-27ƅs>)&Ch]yQ{$%y]z2h@A.Ԉu. !ȇ8cp\@J`;@-H%%&qk8p,!IS7QnijAAQd4wD1{q:c(5- Ћm1%mU@Eu?C8HD0SH^#d4ݘ07X;ӷ^M1؇6U#`eXSɗRZIyZDsޒ]gv\BQVqØR(5c`)9 {cRXF(G = ME!8|5iITH,,pFMUur!bf' `9XXr؏)3aC$\!VL%BY8_@)؞I2ԗZ3!6BdP8*0Ԥ8ȟ"r<ȉIr+z^ &,h.YbR׵oR:qh0RE3:FuO!u֩@nD< r48ɨ<9EA2(3M51yS~h2B GwP:xR*c(e 5aT8YVSZp&k oy Ԑ\>cVdx Ҩ{6:>@R|9@%#v옮P#5u7Ik|xȯePVh{Q)b68dYmZ ЋZ(H{e*$yz ':+=)2u!Wɨ8{Eʙ2Be0IjxqӯJN=3guoMRJ\:UKeށLG&naz3áxj4}z8`LӢjh?}Q,{ =z˨>Pk2ZT0W q[yWḻVjF!-z\a\ PʶO}׻h1]AA&t-TV:?gTN%Z9.o}{|:d_)[79T0[8DʸPTbqK6? aU=; 5%!Pz\$#}Fzf19NGcSdLvi@4xӉ<в|z7Sч!'l)Jj7PNQ([4ҶFLqBO6Q0 0BCV\MR K' wZ6\ eS5xCqϊSn]хjSs7I6D `LqȄ6  }ZDVVTކHTGN9D&RA33̔o ;MUjXCdށؓ@P ,ҽN@@gv +.c5Lоd %:pU:FC{fߴ&%vwX{<01d%lܓف9wL0QB߂vN0}+6Tp$sw u6+~vZ型kbX7-Aq~]+ۋo:tkG& ,.<dݘ_+ ,BG~TRm #jAdˍB&+k/¢` +'z/p~p7>ԭ +-H "@^Mar‰$~ݎcI\9N΀ ӎ(;ϼ* rϫ"K> A ,a@*#+TnTzn&;AD1T\VHrt@Y!p(49$69(%i)M,{'. kUKRHa[23) +}) ڂ7؏ ˫ ,AsP k@:ihJ)- 1{i{znK꣯,Z[`^"l,(`k^h&3Ȁ[n*3\f+ּF] KK! ,^ dٸǰ!#JHʼn2jȱǏ!͝ɓ!4J^\ٰJrɳO jM,SǍMs<uԫ8Ƶ!;Q͛ǯ۷pʝ wݻx[s.CޥKp\mN̸1bP˘3k~{r[,,`-ET.魵ֈvDl ]ϡAy,j&s. s*vcjա>'u[))< lA7lrCׯmS$q|o?ds;=P&v^CC:FNPfd'DI)Q: )O6$v9=*iNyf)Y:™ԝwOp"YQiٞM6"*!mk2*hKe(*?6-67XJ+Vus*&:Vv^5j-ȢcP+T.!B0p # pSe:^o[%cwۤD7 Rk*$P/G5&nkl%l&Ê,KI٬^5#p묫܃=ޣ)N#Jx^5G/GߪƠv~6̈́ z街V L~ӘzhA&-ykRW9MO`WI{TFs@DWv=dbC݃՜6ر ?a@`'"Z1%(A V0SG5\iOS v $$ B9isrAza&`ȣz{fΈl_8=&6qqňdHI ZXP M{bMFFݱHj:j^.Frcg ݱ_yczC})݆ !" Qґd1 WJ^jSRM4!p?UYğFrđU ,OsdÖc.# sV; G$'j(eˍ6AeÑ`2ijRU wP)щUPC!5#N9{c ?oAG>:H )TCFQtq.ANP`G?:CxVK R5NMob" ,uY%mڱ8iNH 4vP cbJS=nR-&ܩ mLԢY5V3&AjVzJ$9it3M\lJ%k,'tkYxEJ>FEo[,XZM(+d"h ^Ei:ڵkB\G[ ,XٰOq\úp.,i;bKcZ }TKX ]DPͻشbo5X/M#l)}kja:`eCP;Ug68L7 qH2xg6/AG6W [lIk̕Ηt=MI_nDLhYֆn2se(O~0l]y7^3+Vvּ9S[f`##KRbR;G>d~41Ig9Èd?MsZ-q?:q @kwRVuB`۬&vyo~Kٔ2C}Uͪ2<^mIYn2c&]j[J[zq˾ڛvd7~t;?J˸eC!e@UgI܋of8[+f kq\8cb<<|~6ys.UorX5Yvq Ygco4X˚VTd )ozz=e_e]}k@<щ~՜$tw~2uwkzq`j'o,qV!5b6EyGq%wn~nQ.+~F5$!7SEH,;(ƔFBoCm C~|2~% H{gxGxꇁxMu!W%G0;Cry|Ť;gaFm/3˰HG='le!l~{5\X8\3do44lhThŇP05qx~5jc): =l(cb5~w(yV{l}eHNyYw6z)gt8~kh~qX\pdYdw&WGӉXp)VId؊M'.q%wVYrf~ PK+¨?"#+%(1wK(:kt%ix'^ '[09(E~RH`(\XwV|2a$82wVBeTX+{@7qj6>䃐 irE=SIHhM^<9`pHhxO+inlu*c`D>ب=4Dt4`)ׄ_G0DYIٖGY>ÔOtR.,X Z@OT; \yo"Gp[H?)Djm)rDGq(ecxcz|L~IiȩL@I HT8fKGqscyy{ [YAqb=Yvd鄬َlC eVTiW陘3VO4uv7Xs]ٞ e ԐV)i9,&99=9?=|3i鞐Y)U[ YVU}Qũdʔ 頲i6l97Q^8VWУАrARxe**kԀܙ87sӡu6~ ^9p(٠jUɤwPG8"=] =o g"e9?nɠ4Jz!¤pʙRء4ɥ9C|ک:*EE!i:G|Xh K57;s"W_?uorŦ`!XU¦y !juvr4Bxi ^svC(*UǪh *g jD,jw{)ʧj| 몚z*III*DZ ! _ښ:G7y*\u˧o s P kؑg[믔 |y~bH`y +|jȰD JfdRgxJ2&;IۆZd㹰CdZUe7\`NO"˝F\64L=G> 3TRCu[h|۷u$0 '%B5zID+Ce/ O 7w'JH`B;sZu YٺI5 {Pt{Zr}˻˹o+)$K 17fAʛH΋P5 ʬ!ZjJZ9Qj8 PDrx{f;q߫oˬ*Ҫ, +p8Ǹ|#BBTs>+p 5t( -T#9'GxK9,;X= Ģj$DlMjJY@ P )G6)2X,€[×"kh\IiEp 0Ǔs ?HwyЫ||Ʉx莊N[vaLȍ+nl`q ǖl 0Gs9dlL$L3|ʎi Cȋ lȶEˣ@Ӕ ̐sO$M|dfx`'$}8lڜƋL>vNPZpWRW[-])Ηgs7.K0^%NvuØ}'"PT@|nTn؝ (~мNfA3^T[vÈ|hG>^NnvP b?}ŝx1C0.Uns<VNb|7Q>c`l@^+?^YV7':ߕ.C\@_dfvԾ꼰mQYSO9Y_k=MբeL -MCdsos`xim2^nMEE_Ë_hP\woOOv،mbm?́Z_ejU?X?nTK=N Ȟ9IDBʕ/dȌa!3%2ӨQQ^KGDY̤k֮K.)9qҡÇO@{%:QE YSQ>UZUCnWaV Z,wiվSnڴm|]0H>$Ќ3WW"Qx$IŠEZL0oϳ?"M:V+Wخe+u=ܸg3ٵjۺ+ݼwDI ?eCǑmcw6,&?@hh60 z;mݮ2)/WLNt}{85Su(_ۂ{5~kg/݄|ֱ |  .wS?PKk'3ɈD192֤-)xk, X6|0#$@r>H 81!ŶLPƉVҒz]quHCHK͐G(Q,LԏƖu!$$g?1(@#g*Ryd:I'la =|V1utztm0(' 烋l# uh`TIqR l' rQrCq,nFUz->$n%irX*ni`HiYD^2eNёKz0"jm> N8Ysҍj8яGD;[zlJDX–ip%P  zy>(&TDZhRs9lH&-x f"8Ynh2g,ZZbp&B a>A"Q$l΂/@JmfSM j8`'@5+.a}&p\9-g*1 O&d!.,[lذ*dԅҮ|@h\DW>oaj7VUH-b0;!+G֓][aIb} C0) YNCY-|V$:1K@$=dKELY=sTrZg ث`4_IP%jAjd#(jr4zƮns1koDö88]vxiW;hі(QF@Go[ҚKKس2G~n h+Xm{c n7 o$ @y jP#W"Z1^t 5 ' "Byr-@z-_ q sJK&QY^Q!n(=`13H h7~+A@Bp+dSqrc8DecϽ 7dOzOݧ<ʟ h)7ZMܒ9O2*>\?@=&&*25cݣ;  c{)P|ș蓾 9;3JjC;;۝iH(c? . bC 㿦ÄG09)@>C@ȃ&8H@*S> }X&y>y|;xÍ< ܥӾ;+";рA<j¤ `pC+?&5ι¯ȇv?vhTث<գ:1:zJ3ӧ'`<mCyFyy+DCu\DDb+"x)ܾLJDPh1.,2J HCB .3@K@+] ]9E`'PXWK.#0؁:gt3H6sCkF}F_i@8;dt :ElJB!;D/j##/1g&b4"=oE$s?K-O ] H5-\/7>XE7)X!xHpI p1ϙFFxBh86C5YJttJքGD x 㽪TJ<P9,HXOڤ 5Nе8D+iF]+ &|ԀB3M`΢"Tˤ]UN6̹"Z\Fv-LG=dPbEޝH^U)[#̔Lȃ2V ;ąPhD'+uu_8`=Z]O,=rPN N ^]F ALޫ@݆XNE,8ޤ()-8NPb{jvp'=]};$X\IfhPDP\x_5b]eS+pŬMcMA;&@0 fQINxڌ4GfB1ZJǬdOЄG@R5^OV\3+| kx{`0Z}C>t\H<1V_D ;+b83.c%jh Lm&ݼPhAˠ,ֽAhhbw>WM4F]Fg˓dW%h`,S [%յuhtOOOV>6^"^e␶e[&i~؆G )*HVIH; -pY2ijEэc+*r`&8> jjgCm!s-daH]|%g&4ӣ<~톦3SЄ0kH; {s)]lw&iw2@΃IhVl'H F bppfվfckMjj fjumsrrj٥ 7%xNr{X3Ba`v'VhTP6جSUڋyy IWUID(g|ymd%_׷&f0LX7hPְ` dijn\8JtȉH_+W u9|߇0f؆n[sf(/̂&&I|Ԩ BA͚Mq'R"ƌ/#tF,i$s*߱TnݺmJ=AΜ:,P ABFx J]tԤ d`50XjV2d81,Y#R$I%JЭkyň1c%UU;tc7.V0=ć #7T(1C wDQ˖ ]~W(#kfӮM;_,wS7ױiNfiۺ1&/_̜o(O,M^a 4EƊ7o>$HK\fjԙ'OSEQN-U` `Rd5!W p!`Z)'55W\sݵbz_"4缃X:0/P#\6,|` l[lY9[>K84yu5njd6b#̹.P&B eHĜv .PcM:c[|&Ң~6E:R>%pi"NMVNX*bT*ZPA@X)؀+jׯfxb-HC X!rTp D*x@eܢkn?ڦn.Î}So7M'aH1sB.T'xjk>aqJq~!__uɗ́NW3;p< lϵrhD+]Я4fb*+ 5& [sM9YeB S9"3qܫE\[lɶNK춎o}6p MP_YD4=UB8t"H9X3 "ٜF:ρkT 6a\Wn(V` %@bb)KL6A)ȡ`F,$/x@'LBf"E\X(-*A9b~[0AA%52=L6hBLAT ۠I/|uIi]9rB0PĤg=#Xţ=e=>F\`?r^M%RqkX5$@%_:Sm)VJU3AYA 5$]$I .YMKb/#\ lQA Kœ2cK1&8A@ PA"d!!4U&Ą͂5x WcFKXXY>%##j?pFg\]B^_Q%^/YQpEX()[ HeVTԍfn&t0@LL܁Ө J kjt8/$8\Bd0Z֨!4dUVhA;!5tzXgۀȣܠtV u>>0`XBB%`V:y'R"Wg.-IMK*h6h8_N˱;H*$60#PD)'f4]V0TClj>(tMZN9 iv ` ܀\.Y@&lyg"RHwDs"T J|Ѹ- W8ȅgR@hEZHٍ/ LB*LǢ* $B(B(,@܋ZGӛƍjs };;ꪢK\>ևԪ  [WN%e ؐx0+m2]a^@LkbHN[e2 QYݝ,(EE /HK4-С!(\%wUg)lU Z6Dl?jtV\Z.b'v`U,0Xl .N'ڌbXⱺMB!<-]lAmld:dnT$dO"rP7'k8_ԁB-Fj4-*gX-<~R,)JJ?n@.Ygp-. f_Fna.ҌQU %LPynXs5dF겮z9/؀-$sw,tA@Y5HB,&] hDSAy|(z`t.'`CG62Y7C3Iq'h/Y vgĴ\2R Wf,&Le5QROZ.h"4mAb^ (Vg@x= d2uZ5,7ZRPA9D䴕2\wuEЏ_?^`s`{ta+b+#އb36J{#|' xfǥMl.QNߔI2ٜJO2uR^Ȃz@FTӅ^ (onp泞ȍtb/vcK צt~v I0q43pUPPrgO%sxAȊT2\ʮd-yn :z 糃 x @ PPWԑ l2 5z(sG҆=lzz`aԇL/i9A31ͥ9aޒSP%Ep68KZ$<𑈉LSd΢a;#oX#{o A;{  xO1^¼#WrZe=V1wfzw^J7ޭU ^Xo}ł"&##}~6vl Pü6| w~緀SlEX6hCK=EFK#w=tDۇ<cЖho8 =*@侚f :K>E9yͮ -GV]\{e^k;ϓp\ps x)wsgL AӨ/eڴ8={8rWF#?3yd=+Yf̘(ilygz4q4':t爾SwtݶfjT4+V%aDذ*l$J*Uaz5jռy+u߿<\rV!C (>L!È5HPr I8q$m(Th_}{Kn+VroiȌ߻s7\ ˗X;jFV-N:u%6lji^Nwִ~L0~MszY'fXZ0 `˫,Z-{F{n8<}?рU q B r겹(/)z֗W p:u$Ә XwVkF6񊗵KLZp;-^׽d>A7$P}{_z ~Zп%3 x&/ hS&i1]WF2I#Xe*o6c!o\ȹ v%E2u/g!6"Sb=Q<b-BJcFq@Q}舫 6p/Ԧ^0M`458iԻ'p[WX! [i!>J}Th,m),eC<ƎQ),0P,Ѯ/qmnp/bNYAVlKWZ;IOHNAB# N'? vA[PX.p!5, (JHl 4КaQYUa^գUsr9wvQi+H 5-Rjl֢Nn}k\UH k*4bH5'TT}Uj [+(8NL(F@Pk[gSӵ_4꣕qh"SEe]hX$&i+v kY <9{$!5ʎW*Am~1[ RTnu@mߑR S9M1(LTtAN%Yͮ;HkZMwVy=^oe!.H1pϨ/T_$ M2`VVr;݂e mp _%|v8%/.˗%V1ü_ u@ؒ UwIgAڷʓw)S`)XY`%ERZ޲a qA }f4[up+2~`ruǕhvRQLYY eT[]m*Q%ʴjiPm&)zU8q)LaAjE<y6fwņ\:]QfeTICcECx+8!Co/j=P=i9͓%OF({[[KvZNA^!̬/r>v@_TYŹy҄?϶؎#DjZC* QHB(,Y?!=U\`8ߓ; :R@,7E3LZ?W-Vwe~g錯shZ J؃`pBzD" I6du7}_"ݢJsX .mcq0s_:`٣R^.X=}3{>ڝ.pcfj 6>J " .N$dɚݔOj /M܌0V2ujDת /0d3IlIBf&VLs,H1j0+zkмZO ܮ-- 5"%&,04g܌~kwbBL7#Sְf0H3`dV gf4͖0̤ͱT0 @>d-.A*q4{ި E0ߦuceP+4d,iVE.بgipxe\}4JcP#Z@H5) lC8KR_# Iim'x)HM1LG^- Tr t f#ju( Я`/ʂ E@-BJ@Hv-6q-4 (RK : q /ב}BmcTqǢZ/4gVvb Q#!HVÒ3h1 龫p2[F.#5r5R,ŀ, g?vtor`LGHK^Hkr24HJI)pt䤲T(cJ.TT"(+03I24NT,b==esLviIf7c+Icu#1,=5vh6iawB%d+bi ^qvIôV]g1Ώg:UƖӶ*6D#,*+ul+e_9 nQA>%܃!lAO^o;_Չpgk/Ck!7r,LoI308OGr@gs=|;|D,FBQ7pVQSaw5 WO>cPcxZ5B|LF yDypqz) J*' S17/wU:^Q$+U4IUESSm~ A(W=#@Al,FetxCvo? 4 5F t0 Ŏ1dl\afe*H A,άC=|PcEuwX hvax7ArWmA{ ؇YH>GO)V.g8-|A6u7+DW#DOΐ+*V<VZEtg=F!(A#ۜ ޔ ]&XձlWyC*TXT֑¨Ff'o;b8j ;,֐DZ_~Za2#Iq- 4G®~<@7s##֏}$cEK^㷠],]m>`+zxa%=0= x52=u-=ywbʡշ!g1UH2 -4~{CsDCt2/WPbo 'jI$ZĈYH%Z4CҤUTӧICfJ !ZX *Ƙ 0d5pb9Ykih$`ii<`e \ pY=(A"Vn#pB 6D QQ\q1ɜwa!XwTlISN-pa,y?{R*%NaiQ՞vd J p .h@r"F͸ʗȀjZ:jY4bFe(*ŨnW:dd8ҽ6t uM.D7iGYS*A3o,eF!b7r`&N8Q2AAIB"G>R7LdG<ʤ{gPKz!Vm+\ `Xene_ BQIS8bAL0R1%7#aBa@bҫ<2ot:P* G J@;y:͞iS9bPQ:O)Unhpׂ}ϣGyɘ=,}C#9YkRLF)rzǹeu gBD$_xTpի:]O"X}&iTc PRm"D#JWcU^ FRJ!}H')qfEő0B"Qi7L?MI`7;]DTB &0a9mkU7j:Фp3,{P͔ C hhc Pv3PFq-r_f?h/_]"v4 iT.Ud@'1ͪ-խW䲸^"(%f[J|8͕qt!Z8G=I QW,w8eTْmSBb Eb Vր@ "2fA Dζ0[dH- 8pXc8 ix`Ax)JXeyA#ݗln-s T~FU٥U8Zʱ+Gµ:1zdBgKv4S88<@smCp-_)0'Z|r|ع!mu݇a?ؖ&o8ͷw^'}K5P@l }eB09 IqSa m~sM O'"VD)YcO} =Ac-˯s;1#*sK7L_&h,;#4 /#,e*&7j9wQ#k Ou5 o*Kxksw^3_Մ|[?lg /)jiaY*>3^d6:Y;G8`fA6H`6S9gB5v wIBV?Q Bv.;|||.0}cgт)?9}xgE}0~2XiSG1T_W1t]݅>E*8;cTKuA`'{%TO-a,rItGb|%|wr%(1n7;ȃ烑o2#S+1zٔFK'@V7Efyq[p  qkK%;!{c k`:9B†A4OçpV"Pym2և~=8U}s@'@!^}51؋E2u5*?dW8bi ,k'&> HdFf(A{_iI@Ypf @A|h@θ\#8Q~2}2*%!>%H8HxhRLy'Rb"Hwx? R~Wek;X3aNt );[&OYUa*;IPV<&QM'z8$('I}d>-y8g1s5H&#etR-?>Y1(G&SeL9Z'PzG[7dkjF, 8aix%P؆Ue`D[muf8\JfyT@dYC1P/p rY׈>e>ח1x!˩vQӄS^%EFq8*SSw$ԣf[ZL`UHԟ(&SB$0$w¥{ȇ88&GjxyBsZyX3D3%?L>CP@%R?ܩn}Q@1p=+XAd`H"2Q?0T`(4?.Y]BjHϕhpr(8I p:+pF$YV9)#, eby6+IhdS1OpQ:Ix:fl )4%Ztڥj#|z%i ٨.y!AhQapq&%yi)i1}%!YQ@>Ex[`Fe3Z:c8v\qIR)4l Y^ S0,Xu62R:X!h[;hRʟꕚoD#ѪW*w{_ғ8ۄB"DZG`'T>oƤ2I0ZB-s 0 0UDWY4ֵ+Ӱ¡aL⇶ȉ^YWv12{*FsUpySRє ,i,:)vY9[?9ʆ4pzw&k}hӸ#;>?jAKK!W41孏ILRҲ)o1E+=NBEA`ظa';IpO`[# K[JYİ*\S܃! |Rˉl]RN)[,9IF+"Sխ|+#_̖1L{k [705IB塪 IȏlQ]o#j(3T,hőB4_\LH&EI<@Y0t.p 0 ͌ܿ /UjM !~)V7 7 2?>{Z piqH;⼄)S3#[KMa`K io6nK"":-@͉C̭vN!&*I̾|þxoʝE_+WF)9++|& `a7X?;tό4/-4>a 4ͯӇ6j=}B>D~*H t\q$ꚭIPuy'J%ydOX@Y1z7@eLU:7f9kc`i׭yٮy,B05]8L#jLbA9;@akB8򅓓K$:#WLPw„9Nn}⻐}4&Mq7|j(݂M6}٭ۭܰ2룋ԁY݂Ncv=&j+_Sj* d=*W##p_yA  .ҜY>[ 7@ d9V<_:FP&Qrx9Sl-{\8]{-8YtnļSΈj2|nqo~(4\E3y:Yh6qH@Zi(_O\;h.]jo0qunw>4{1ovX mL#" kG$;~<6=րH*Yܕ cv tî+Ej쩆n; ~Xao泥3\:Ē:[d03NvIJ NN`"䭭Luqju<"GR,߻Zݸ)Ov;Ky( 66nNsQ#*ʋRmL:B|n[oN+$4ۺ1-.{Zek> jxʁSN5rȐƒIp,SkFue8! N\1c`… s *Hٱ >(ѥKf6m)?DZ B֭]zk 6|D+l'ɔ6=dM?)W }U,S{cUmXL@]ixa9Y#ݺxp`.,X %,R <: cDcE NM6>16i!ڭ"~(^D~19{1GU(a(j,,*+! *zO#aA/B*1P@%9 #PT /l& ;1DC EX7$JyQAk|F*F` $r(#C`:P}(Xk(h(iKj -DS3 ⤂ %HrHŒD:3`B TC EPu JDFLq)$m1:- 7]n1Ts@4DiNQX8R(AB)C,ܑH(iNԣiȻQ!>eH "\O!*PV @2\hӒdک7@m2}bBFp! ,^ dٸcaCxṛHn3jhƉ 7춭du\i×0QŒ͛.cرϟq O8[tiÅPjFj5iXbƵ+f`Ê-γ;$ɒ0=_wl-܃o}X#1XcM,U b,5#@ԖAXm:,8+4mMN9MKoBlqky^k:E MX; gʄ-v=EZvnNcTFL['-\LB654;GКuFW/MI>Y/夗\{ތ=0˜-; lF3:PaWUF!c&l)-PIE ;,k/3=˄ .C{Ƈ] y Hˍn1%yߘy3 뭫qEB}_>h`WGod [o5։~bW omHrۺ'1:JI@oVइz0t#@Y5JB.z` u~b_XDQm}PUz;*LW-4 !G1t.p_sJ&|U ䷭2"! Y1R:,M#I6R3| +loFl)Z48g$T>-tWW5. =|ؓt Kk\ Y2:O$s'E*bie&=8Ύ"H vD:ьƞZ>KR@*LIh3P8t>b *<:Q`঺MCg5N");Ր7U4M>}m, IJ&h %+ٕ.W 7-r2OD,7;9W{3'ыTɭmn%985LT]^eWV1*qXk5NON>qQ ^j_3{Mt򦎺O9]bWU4rwl~s!\&Wt}\i]4KmMN'J:8;8(&,Ӽ1b~\u2Xfk6!nMW6ÿ;kg8{wjU[Yt0kOz~x %sAnU~A'WyIU"J;ݣ_KgrK2K⍋{~ǁa?|e d7 g]΋oo/`ek}ABI 6~77 \xU?~Q1swBLCBdg0!H5y&hnWktX6,G{3x6F~q _~7p\W(Ƃ|sK%xA)*Ԃ#G:4hͧg4T(rlA6&:GX|%06(%Sh9ymM-72HUbd/8lVQIa?{m8x xlTx3eׇWmEwn^?gHiH\>8QL6a{7|؄Sh}w'_(?Hz4@_rP-HpXX {Uz}5H~Ȋ0hHk/m(7fx5PC+Kŗm|ytYdZ6$9}htux `c?h~ ]:rPIj s |&7 U4')PU%)wxv$26c00lX>B|d0S682DKqtJ0L}(؏،؎gH\6 y;S6rdudkm閝GsZv](w|L~iTY Yiur YagbՁ i9Q٩}*i8k_sc鵌oUXw¹ؗ$Tٜyv|ƤwvHEY\s^]x:_S1eXw虞iIt P 0I Q;IU( JkUr7G'OCFJBMLL ˃ [b`}H< f|m o׌5ME=cx-q%|Q@Oκ;  [9_\dmKp;W1uԦ=QvUι ۄD_csܣ{mu["S&FU[yP֍ݯվ[ȑȹ;>VsSk*|Ѭάaԧ]d@Op̹O 25.`č4|ހ 8[A&~*ڬ}_/Ѹ6~Œb[sN޼P0_ %.^9T-nc 3N - mbtQV@=U ywn@kR݄^凞Z\c~>0 ^8~UgcMƫnJN+n߅~W*M]M,Ȟ n柾SZFd6ٟFJZ~+׳ dN*Nδ`~.dt.}. kE0y"{z|W^B T4V~`-/b8?h< ?7g-s͕NRq eA1IW"5JN Q< XM;,_dWk_/ܛW~xɜO:p_Y7 e~ P{S@\o VлS ލOH+dW~*kk\P_ 0XŀUOCn,;WA "Hp†%|XD KGэ$Yw)Uֲf@M 0xPg#nxB ._,%/WnZʘ8:uzJ.aaŎ-ͬkiŭe+ΜiZ[ ݷ%w߿/R\xC#6\qq㍀%O&R:`ʤӦN>5haD DP)ԧReWz5NVKoj}kqܫWrɐFp3VAѝ;g9%fg {?+h'v*ڶB1ʫzSN9J/( 6|;;l"h6Di%2/묽F(p"?֐J5*1\xK,h L j/5NBQL0˲AcZ=t,aFrHb5" HTRNh̊)ݲrRsPK/IO.MD<QM4ST514H޳1n%ؿ*J2%7ai9/mBǰ +ݔ8~.-25dKË<\5^Y\m`OrXaebhMdk.krb7]5KR*Oz[쒷^e^N_ ޚq=8(gP"a VhȊYn)F%|KjV/r[B=iVk5{Fk]у)YndzozW 'V衛NJYI&8'RwΑt[XĒDAOl!tNlXoc]^Uv垻V؍wHw']H< G$9ǴYJUGԶg?tX(@aAAw %T>iTT~'SW;mw2ݭ8;F p?"м Z0GCrmե|P1T1 B! a&4! f@'TAjl! CKϐ |F~hqsjf3ݭ'3vV Ht-RbТ=bTz^,4CҰ$ !0 R@B;$8 o B."4Hy-<^"DǒYꕀK>AOlF_Ry*,pE0d)٥4G\CP)@a H@|P$P1mE}ܐD*ep殈$p:ic"HQ=.2ݠ[E"#MAu4 ЅB X~p_u5RtBIᵓ~)&KURH~sMـ4D7( ,kRӨ"@)Բd>1]`-& a,X Dnk\ZH xk]◾nl|I#M,򦯠3(Eޏ(=~HU};I ;q Vb^ C\7| L0 Xg}+D{ߢԮ H%%.7%l.8@ę휗@$ xٕOj3c~GQ r4 mtK=m|9v {`,T@|FEؤ+&d\6O|U@ڬ&6gq@F<jwgLc\*1! }y#t]WCkd?!H-EVb][j<8g$̼ f!904 G*V3/ΣuULbH.p?L\wNV3#O9,;:R9@Ns1H7nCd'! H\NɎ!X',.[睪oMЉ9Oלdz=<`)X^Ob O3g{㽪|@z`~m,, s+lr'/c.J7_y-&þַugɜVv3m^RoY|x)4%u3"}=h{ m>pA G P`g-vܮn^ͧ%k}tLJxΚYw c+ GV.տt΄=œҐ>)>H)0!hs7萋i»Pij7 ;8"\$%S?cb?3*ӣ Jixu{ m`G0CxLIEp,8#x@H D( Ħ,5j)u̾z<8 4(ڗ 1ºAdK="؜V  4!%$,LHhBZ#E#@0 Ddv; X jDJDE\D$ +HYpßk<[5C)s Kٗ;DdCĸzD6Ÿ{I(v,H'\y2<+HB17X# ȀDrE]p0F[yrzR<I8kLB1:DDDop 4HlAv@RLhL7%8yQ܁4&GP`;i@sy ց@ȆtH`aAysB'd̵ZF3K% jL Ĕ r#641@Z |H cdǟ4 +#Q$<dB),%K8?:jJHq=4!d#$F:)+<7#hjL?A *T*ӢǍ=tLMTB?#Lϼ$#tF޺s$[NPZ d(jZ˲ MG|dyKB,b7[,r.`uڙyVSkswwOhV`&j=3Y4Вe6]'\\sC;kH?-((׿Tۚz*k-!BfG+y$JB$D0tۦڸI iPK@[Ju x`I H]Pb^P[ܲeӫQ #¤ш K̓. l\pBC@DIAdFM-?S7n\pRxq݉BQ2'Pێ_ ީ4Бu`}h}[xA('HF쭅%G_!YDxwHy.k_=)1˻zֿ\A6ݣED<$6 {}5 fy)(B 9GMi7~a݆T1a-h[ !?.$K$N>:C=i{+lA%BbS 0@C(=/,=|# "k` Q763 ~MG;hfώ h$ӈwU@شfhCvPdEfdz! 0NP[ۤiNgId MR)gĚb=,Z[he]e@<2a,A3#e={`!X(Iת+́(HEdx+Ϻ9 N8^J^sgEaCHGg8MQjdd.AVRPZc\ŅN8ٹ,hle@h(ꮫ/QQivh+M`B@4&؁W 1\p+FRp6 ːj0cvp[އmz3h\nȆMpd2rM &)dT:TkqN̕\TJ6;JIHïOxD0DHl2$L;P̆ʞ6|xsFJv[v_`)0y H@lK hm!d".k87Y\$zhSk0ܩs|1NnvlVC%σ ߿t B(R3Vn H#m& ɞ\QF`kp. KKAQKuڙZμrJ%?k5"NW]qk<bI5Ix9*)*`Ƞ- B/w4i3se7mx9H:oMH;,Z( sﭒ^T.4h==)&k_ǝ_>ʵYz){OqYNNC%AnnV2;8Us\0H2BS(u]OPo) Hv3m0R/2Vx;0$d rNj?WmZbu:,hY眳(ntrBzM"[z}AF@<D%ZԬk1DhKKC *P"-"@~* OWj d?u[X`3#Xȃ) ('H`V0Mv*`o6z͢\*i&vw4Xh/8 XX *ciWٙhY$-\pAE9\i ZțP]"rL5ùr2)QFsvM̞q33yuL)ȑEЃma3֠ҧr1NU;બV)e+n%V,e,uaV1*uqc bKyKnmmYڠZf w}A Nq1TS5g i"G0>=; WP q, lb>ly Q򀡎JS9N;'Ϫ Uk5@([bW(\[1kWƁ6[ cµAmkUm] )`ĉY FDM:4xlnn#H#01WP"eЄkqnLf5K%R Ykn\Ҁih,dQVW V0R׽d}"ߎp3v=& ssuF Rx!3d,!aE-jašIcNªf1(ID$ LD '< _>gGmf;YDa MhpKtM+]aV4tXVN4.|ZFܠQ Ҩ&}HJ(G9uG\2DƀñK(XGu& Fr%1jBtxD0Q2R ]P'+_YXrjM(=eiY$f%2iXc֯jj}Vx@6<\mq[rP6~nsVy{ 8A;d6CЎ>ZA ;tH{LMHÄ!tHUY8GJS%2=-Liь'6M(/u*aUnS+ꐆ`e]oYS` V=A$%l+3ָy ^cZOy5#p1p vC |A L`*ڥ H* lN~2hj Z W0nkrޡxCeiv9cn/'>1l\NxB~ ( rrhͦX|e| U.1-,ob2oL :.L,/ǻ_jaf>rҕ*ـ' ؠW;[ dP?ynxJrݮ1eq,%t4 K脿GŔr@BjPrT^yȹh^T`M\W\M`X]<^Tٖm8@=t)uq (Aw;%,*A\4A]4C%AQ .MɀpCȇ dk}C<zV5^b\åIh94\D$ـV tZ ͍^E2SQVPOԠ^ZyO0d1Am}lv a0V!xP5XL0&y!L/yꔁ|@eoqƕM<\L" b?b#< Q "*#OILT %n`5p\*VheOrQ\K|\H\]`"A[` aR 0 c0f%؂-HC98-C6>6Jލ Xc"ɞ0'B  ` A-(:>DLs`\qP=e 6^jcrE"Z^ʉNHK %rϫXTDJ b$" ^0 H n˼tSbg-J9cwya dGPOZ'd44+/xk0ed`#T,IG} nD'܂51\Mh,eXhi9ԃ[#x=?B`#%x^ZG_BXQZDqMDA&Eb%`IbPOڬD-]id@Mkl!`㽁$ƸP@\O".& )*e@' uNZpe'y"[J#J"Uɧט_b\Janmi Q-Vt ͪ)VQTetI>TYk&nx0f L((|R<+&l $z]+%LB^fE Ao$f܅Kԃ=fN+tP\qTV_8שeJ `K<f_Do &~Lϴ@7tMƳEh눒h}m , P0)HDB$\B(Fꠐm EF-LVh H\R=xlZ.h.pMJJlMj*Kz@ 6 e6UD Z E-*0e\7elAiflA@ BA@d7lТz)o$V¯q G:c,VO @*E@X؈` Z&YnR0ɕn\ ЫH oXؤMV2 G go0p!p76"AAFMᆬhlR9>n/yqrqB d4@003VQO؈2/Jf n_ T\ NKHDFfAr$O2%Mf& J(BCByG@wɑa uZ7J2ίc/[B/[ish1g-33kH a$A:X2Y z7EG93Kn\TO J3Hᖼ02W9#0 u e0BuB$h)DBE4H:r`)q<rwf[4`,]s i4t6K8DA:yW^5VEܑZ0A iMeRZtRӲ4ͳosd@W?X$?  ZMʬӤl,%_C~V`; {.l ӮO@`PEPFhh nqV"JsMոseua( 7)o>C>GrXFw 8`LkONT:Aq-$5l4qv5wII7`w@KHhfSH8upUg{OX$ˆ^viUb\5M6b`u\VMh~|u$$3/p x  AJ+.@Ug29Cx_[9t:Kny"y1@;*ͮD"}Q8Y@w5|kwy\;Tp.+`xn0 $3:@ Lz#tmA'C15M9et#cz3yL)F$Rg7~6q8 K3![iyHy9vjmMnV0N/2ҡow (:3z <7_ ou_CkZt2Kt'RBǒ+;y4ѵȏ P|ůEEE1i7o`Z HΠ쩃Mt H~"ز?2Gp ;3H$lT4\؇B1`GV?|wJiXԳ%h5ڱZ=pw >P1b۬Lq$YQe=ʽvbv^TRu=n;@l`-,ڳe䫷|/a 4qA A$(D iԉV1k2aCaz4^c~#4yeJ+Us&ǐ3gC;٬Y,<5ziң6 ` rH"G(W*#  >0@PPՠlB=p٪(<@p׀v@`PpL08ȐF*F8B5|$I(VhR>~0#HId8!`kbƊ)H,ɗ7M5^ϟV Uz?N0h;*X(H+J ,-BL`3,L(4 lTL3NJKM(Z e p-7$MnɆnY8iqıΞ9Ӝ,b1:OJ/&6ir>>T0R! lPPb3$98mz@nAes-G $fA bT1ݙꑦN վH0jdNzul#(YXc3tvDGq4%,QP"N*35U[ѨRc0(/7?krc?aĨƙB4,`IjV6ԓMnBPUЖ+2[( ; hP%`Tf@@ ,4  2.ck=H3r猟IU!lD-iPڧ*9+L@mRm ]hC Q R\4QaJ!rz-46UȪ .H* XZ)&V!1̑&<J1$ 7KU9;Xpի|JF1ŭ 1k4D!m[*mPS!al(T؄BuW) ZBA!YШl>ϓ3R#iY>v'6^)m7`R#FVMn6 La%dˍݞH vu1/n`;\Bx3)eVw&oTZ_sXm-a_)Q40?l miHn+Cr`)w(? HӨ`ƻZ8";Y\g88*#渳qLߔ:ѯkld`)uJO IAlaQjjO4=CjE{!] ]K`YPgBs^ͩxXg_v(o0Q=H;S~߮:u؇+(Lg:m6i!FeB0-v_\ lԇ8u 7׸ ;Ʊ:q5e{Jgq&؎m]6u=S,P^Д'@Ԏ3V[̉dKG8XDZW_w>p1 |N<'i+q gE>rT,Qf`.TIDwe ^+ds? 9B]0NQ {W WR5`]Ptu^&&`QΠe.}?L= `0*j6\'#ͩ^O?<]u_<;6`Q MbImR" PN]/D1\{:wO.Cr ŊŎf؊"olT084Hۨ+,lZb* nBFӊeTg*ˬy|,cRN 5KX˻p P .$4*P% %n=Ȟ?hQG*,0H4EV)BnREkI3m6גpRl,(N31Q+3+Erbz.$:M.)0L_-S֏.g(D?(2l4Nn0Q(P2m!*qM22 APt#31"7S'Ⱥ3l7wL4HzX> | .,$4 7EO' O00:Mϒ\0' A31 "U;1$+3ʳ =⏨b=Er=ɲ,tz训-DOmoSްSp&.CB3 0 3 (!D 2'S(.ΔFCMF3M%VZ&ʲL--G?Hthzg5# }sI49jKaBW!2S:qLuLED3QMTCd ϏJHBE)T4OIDO].FzQܬ8I0A#(A+Ox1Wa80(ˡ2YTQUU=.M[ւP4@G.ZxTX|UX'QG JIMeN1Y[-!5\`\] H]NRƳ+uu-cnR/Jr3G5oZ.*! !Bm>4aʶd mKs] ea*c:5hT<FsjRdN[Y%籑 >0Tϖ Uhp/S AIJzt:oȍϝ2:fȊ<&8ޠ˩"/p8zn*FIzP+\hoXO\W\8m'/v MѭX!{ U-ݴ徟h[J'M[0>hipn?N&})¹׋IÇ]I]q%Ysd|Qyک=&Ъrѷ!ҹ & 'քJVIL>/=*~D3e 5CZI%{6Z NANM/}[ˠEWطBPqˣ+6djooa:puq=&4{NQ:ç~| , A Lmn2% ZQ⒪`̡3P\t6|(!Btѥ1#y|.m! ʕ,[| 3Lj0`N < DR8zh8}ZA)T!V *4}3O4[@g"D` );fК5W HQ5R8qbD$IPy 3f$OL/8Q$?rqF#ZFwcrP6c'7"OUhn!C.RJ6PZbˡ+We}YdT}:=Vz_6vB >DQhEd]Aefd&BZh@`jհk!&C AD!mfnUS E WCwrqH"dJץdӔVƔv߁%xEQGf< ^VUǦu~$SZaWQgvIUY^e J؃ CcF&caVqgJ bjca2Θ5$ѡ:'PCYp)ǤsOF'u6pe-e[zPjz0^yyiTSIm^{i_di~t9&QN9'GWd([:a2hcNp9JYdAHvC*kUÃA8q/'S5C+G\s=tS*lTބSv]SUUfJ[m>@NIVYzy-8 wLq%WK;Ab V ^]`aĐ]h,i *fgz5 7k\$j%* [S.s,Z`ENѕN}EXG÷r_ uWPep1&&)eͅ0ܥvTwR 1sG.!ܫu/~e㎷S2Um7eUp#<|otzO>')Lh=1?y]擗9V l4^ 6emT >uA)5L<P,a u "!: (%J+Zd=XyOV lj9E/l'3X S@EvQ!k kl| f"468iT=hT&FpGI2$-IރX`|[R|C9T^m-n1*BV9'l* ku'/c *DBJa y0̓~a 3A栐X7F6Y`B B E:8Gԡm#S0Y( b9;[_{ޓJ+c^ (&Ҍg Kd'F؉Є XN3(.ɝ aL=3 5 bq, LUAUPE-qqV AH=$RNS'@ibMTg h,Qw,MI.Q3)Y{Z75+w\&Qm9³Ȅ9m[x?UUdvp Apڨ6i#g?ÇիjmIZֆ5ShDgj=@b9e] JibyH=Pa-tcB i^ɂjCtBʤSAkz,mUQ;z-rK2*]$ÝI>d!6Lk(WSe,5^mņR}&Au`q+N-xQ|@+a BK71iu'%6DoSjqhk<3 Խ}̣uDI/h==j'ʪ^IyQۀwAo*8Y)F޲8E[u~.}F3DA&LyaÝhr3Bw[7 b y#U)yo=MwcUp.\-lÌ2, Jb[^ex6-3:cΎ˱%Ō`N-42 yޣI"H|'0Ϊnu&.4Yd=g=z*/΁ݚBNKh;7fDig; r!˼܅ db 4> ETBʀx0Ҩ=)roO#KO&z1BYZpXp E7k`V`<&Xt0S0fv {qx4{Q42u-}a*XQ]NCCGطbSBD*@{sv7z$z(E8eZuF{d{Q{ 6?emO9(R %9w+wDQ!'AtX/ه}#)I0HYH6x%<׃?B8W瀄JXjI1>QWIteTgkVM3 ua7%s(wGl=qpQwbF0Fs>׌+>nTGjZhgV>^ߺ`L n׈-p57Y7;*:2JA>eLiMjI2ov^~vٮ9ի n2E%ji@*q1{'*t! I);:6 n !]jQ9Ӿp^2"*nl`/eRe4 >`s>APN|);j"Q= 7׀aOh]Nn^WO,nc-]S(V+gh̼ 2.<ag'T]N &R  o ~]rS&{ܣϊt\nJ!/)[j3q[T֍K_oH,kr;1F1w%$ Y6k=|xܻrԭ֬2BRHL4I$J)d)Ll:=}>PAEG@T)RM>US9ƍ]nթD?,-1 PAnQ~69.p͐/…{3h)jذQcm eĉ-T̘!ZlV5I^|Aa ˧ 8A$20 b3>HDZy"h 7TDކnfk9;*-rd**҃j`cD1 N^*=@!\#TxvڤB sv3iI SQh@AHWCuEtDծ[ct) ۬D?j7`eNuŒffY0 i?5DˈS19N 5 >jJţ@@*A1hiLpKô 1F=!zhN2'p[r@&8̐bA"%G%/)=`5I%qIZ%:X;p~JOR(1ƀQ~ €?`{x;YO$ -n\Ύ1QVLd6G?H)'i8QIZq7bNtTk|;#来 ' W!kN2$M6)@l\kM$ƒ.˞İ-%35$Cm N"?41 )"n@z`uN׼~.Ugi.K]7E 4` K\LF f)uWtT$I&do MAD$v0VŐ2`@ E my;ZbMt4FD H&<2_;5x I C` [XДuMI|bz,[Z=K+.M jy)uUr@"hpm! U& QK < Z܌^ipMً-mNר(V* LS.xEoMYʩ\,@*j/P2)"Vn=X3n+k@.<)p3Y2rS$RyhB#tLChD{,󕎊ܤР[L{̕6D8u9%#g@W+d̓֎a!.! ,^h I#\ٸsǰCw"J|Hn/jQⶏ C\Gu6[ɲ˗0cʜɲM-O]N@dGhDjԦ)U*ӧӤ-JRX׭ eP%? W))4 *7O]y˷]} pp`\l󯼇leˋ/k̹ϠYnno;y튪~NRQ먺6y4f`+-\dN;frȎ$YՅcaMen=zχQ}*jRrvzmL)O@\[oq\; >ǐ<1mTτNGPiTTl sI z-am@vBXT|c1N)T~# 3x[WlBMr 6ɠPeQBlH! 5WvZ)Tev'ΚlvQȔ47ڈL5 '2;4>}4UHpm[[l N>yfSeA}Xrjv&cibTByҚz=AkI(Թ{j쟀%P%ɢ 80(zSi;"Qh!%5&!j,lW˾Δ곎濻o 7f'Wl,xh:IfBZrq2i:#3:Cpu 3:m4dcL3M/>&zu5Զj3QsM6'v1jÝ ܉x#5`m+^,M:3 :1<mE7xb'EN0k 걎NzUT6o.jn YNC~d]Ռ/Ӧ|x6Ns:`}6X39!Nn:{;w>ROv2Fj )lB6N$|Meݲʡ|X5Fx oUǾ$c=dyP~"vx ` 퀢򐒱jq) 3bTp^68 `9VXXXpVP %m]^Cvw4[(ķH57$Jm*ēT&FEyTb.8.#0iH ?gF4Q)p8J}tXӸvrhy`5LϏ e "w NH<$-Kl\W^iu/2yk![ %(q 2.{I4)Q.UZM#+Mx5SD].yKiy dS! L nє氨YM`Sy]#U7i'97ᡓ 9wӟ:!}~8^^ BBbS5b:QVtwȈ49RaSfbJR=)夸Pi+(kॗ);g:Jc^rOI^m>ʅȍFEhRw)L6uv*ժxƌxD6WWQVdjdVs}HJ[wٓ{ _ Ӧ${JlcGPnqTئiO廜5xbw3Ƨc4ZYZ,#ڰt=UXŭ~]X6˖aKܣU`,,ȆU#x[ NJ46۴#Qj˹2<6bpԤ~zzw<7 f} Y딪5dLS:Um dzWjx)ƆhXmqK8VO(DKtJ^hWN,*/X2W-[.pXZAr0T3P(P.>%t!bd^in6c ǘ&#AMq- K[OvfRl]ivW|&w1OvU/5T'#o::(_y..=A=:l-K/M<@C Gn l'>X5CɨL5 iU 8{gRsx8Ui?hyVju9 z]3+DsG].8H0NG|@hg6-GHn9G1}ԑ?@vDlyX/Kً 8NYms#guT9x?HBS2Ɍa4s9gYAm9lӇa% S)rgdq^YY4b=:9])0BIفTAҨ')()V aY[SX\٘蚈 瑪62c,9閵śI':)HqvI4W)Qt_eYwǔfH'I[8iJAa7 x~V'igătFX7)GtŵPJrx2L9%H%DDqS )_&)K,ڢ.Z0*65I35ڥ=4rgPV0 `l@ wFɑ GsBHZZw]bȹ0VqJBFm#D$ʜfӐf檯 (1'wꨩԫu7g`Xd60E0 `HX6THzm=yb6.K@(-:!;:  )gQu++a/{9p/*Sʐ˭I0?ADGkw : ~wBͳBu:UU0pd+й[ )˯Mm8+<;[ܚ`yAh9B;qk[ʼnS+A۸+[ ; kHgjr y8,>;K^tSOkx7+>+MLX PI ikVIWp qK й;l; Ő?@J3yӺ4)43vkjtŰ4xNL*j2#FSwSNTcL 6kÝ6r 6ڡwFP~,FT,砙l݋Q~OKby(\PIɛ,9YމzœG%})1T<| ɚ-l n€;n %[B,F݂ xLO=%4@S8,TTd@^glӝ{h6- [4U48N̾Wp]δר˥?{qQفZ =+ ͊؎Lnpܼg mF}Oٟ-3-ףMȭ}ڬvM;Q}UM$M lƜ0֓];lܗ z0Tѽ~twka#PdޙTe\j|MߌM , |Na MP=q)J޿=;=&N)ne⡛d =B0ͷ6T(:<>>-nAA[mᶭLNINS#^cd`&Lk m0 , ->I׭JBwv>Lڄ+RHpX 9ϜSN. =mӝߞánn}xZ&$뵝(((5@N!NΦ~zjES2.jr-=kKK3.e P#>O0 >Ɩ.*nܠnFf^/3 oѳ?\{ 7PNN0\4 ج ,N5~9PE/xJ] 2O beoɍMáh .=kFzߨ~^Q ` /%>H<_?b Ɲv -Cy涩q·2[ZO!#05 S\|!8 A_,šC+c4jԩWt&bŠAVdIs)U;rJt1YM2ug{A%ZNI4ԩQEB]ZwY}n}sfU7ͬm͘[ T(BE#4AÇ$SP!HfLBW.|%ĉ;ڨ#ȐŬ]ki+˖鬺Vԩe}ke7JtTVjUSʘ)Ŏ-{vZڵm B 5TW5|<1CG܉V.1kkgѤ]'ٮs慄 p(tj,zc˭;./*A; 3!ã"Z2L=aFkL5Kh+0z*#628&\CJ+0G4sk̊*28 {RfB*GS) % !O$,tѠƘŠAL 8`KNnHB tuD52(8y3^ 켳m@Q=̋ݽD0$^XeLY+;`=aV;(isqXYXiʸjzZ'E9*=Y. H\ K^-Žf!&&z4ߑfmQr[QjDVia?.luMNS~{ynAo%p>!BF81LI#{pB1Aw* AZ?]l@TwXRCۦO*Tj@:֩ *j!TBf3PЈ8 k}e9-'2A OЄ,p41ĬDd llxC#Dv aPɎj"D:r^lfeE0+R^0  ePuH%: FAGDxB8k~9$,MisY9_9wIKuCK3|aɥve @%@W0*t+ z#F5ry]qC:| AɄ *Ynt<Ԛ~ ٹSl)YNjk+lPmSmWjNRӞ(ϽT@(duD}V_)˅"uɓ"+p*&1=XGQ0"LZ Li^ So6e,cYX&9U OSv%'<2z.+c T5 5 emV]ZAVNqRPaQbi@W4` H](܁'MԶsP =~,,N++n짚*ZԔoR|F_TG،umWg+^BWzOp~wRBą7| L T885RYe_k ^vM,Jy6YNrPVTwe|@#TQ5r}mV641d> 뉱x۰{r $Wa~B`LI0C"pAljHvkGZGl ,{;IUmrBmTM9/URPf"a]rŠǒ L)YYa;qQP uĂg?saufZjlWHZPw%-(M/%Kb'7OTYjx ~-WSxkq"$=M Tm9 OB=XS`¼[Dܡ]pCz3'ύi[șזۨm}mO>q*.BRVU9,;?o P]8>is l`R?RRYq9c;[]66O@*-5]>pcֱ8VzǦ7H=G0B+8I%i]6=lbs>+RA*~:7| xOM$-*PUQ/uZ=0qڻGJbU.9H=4#0 0R$颮 <1j𪇝>ru:?fȋ7[z*S<&jy: ҃3(1X=;أ136# oiX9KEȃ<0IGX?Â! C/gD`z8C'1豑J`uô('4r/:"ħbN7U gڲ+DhjzB˓/LH\ZCxR8JHX=#@eD 1hj 0 S?||ȚBǮ@DSDs, HfD #B$RESJ褡#:Tr56U?[cB bE26]dJxJ R8LȄEp:@(8%谺2@ˁgăi=(lɚ%} IIѹ0GyTO+GǨ 5sP\RJ*;8EtB0;%KqCA7wXJJEH.%`)(6dj$63LЇŔ?yL јL|1xLʵGt@SM'zcr50jU .B0lȲ|t\NKc `&B)|s,LD? OE̓1%-{:~O<|2Ma!mQˋ׌MZ38-523 tRLKG؃ ?i2iS(LXEyOOz*&kTjyB&E!m< `M M+0W*Ѓ +DtfҎ̄5Dchd\FU!DsX7Qt8i 9ʱ$3"3T?@=#o)3F4 -IAjdAb=A֎͜chihc_POQ->]Yk_ ymٸx<R[e-Mrh@% yJxƥW|5U}KbЗe6(+>ػ[ϰρAMV fu`wy`cZr(uYdy|zJR;!O$tɀr w pB8ԋ-ս:jHEE!s5Rx5> M25m0¥8e[\8v8X@`Zӣ@P9Zf,`gd}~a睇xb%\*+sXaEnak}QJ/HOPa$ QEe_!vz=yMR& Ut+OX .N \G+b* -O & Ug[P.vwm\8\i@A&dCVdtNgFVG^n}/L܋ P<e_y#UUW6,pY[c)b3-XLP`f$P0yj!cP^v@Lmw؆T 1xFhĠ@&A~Xt>uvQHzOY-}䂶%r-  僞 0% t(89'f~hZv=1tiVD^<)@--HjzBlwK֓集IJH Adq.^ѥ6K K^Sf2Ѥ7|jzj߀vOWWP P\넌Ysn,u8O09;.2%ৼV9j8[6vfȊ`PD`<}UJJwu 4Yj(E>ܻuK2kY*- yKOz#fve_xTVh&fڄܲX.}ciMyƾc2n  fMlw^DY:fʆ_l1<ol [(g'o։]y\m[,{)%\p &jm 9Z/^',VDWdVc=M.8%È XG>j`^llw`I+o-G͸/r0'GZ'IJ,YK"\HzQ: mܦ >Ԏg K}hYAOp!OMxFPf.<i8i ulf=އm(<()h lQ84\A.d[G\u!,)D$O Ks5w|m%s>\ %-q-^DD@;聿 qEMxiH g~|}_fmހx~8uuo!rO3pl@Zéxxtx1GJGaQ/j2"p?w8{f)Px쨁®p1\Ƞ[pLvxxG2%1 WӴrI"waGb5h8n8b[r5Z\!bu_8 3 fa@Xi<eI6f}ZA(fj>l1٦Bn)`g >$fEr17EP xv6tʀ8*Uc9R{'|"c d:N3'|T3OAU d$G"Ҋ)\fazHb(;l#> [*ء/H׌U689YB>@G2de 0i=9j$B*PQxצIDg٠~*( RK5ʔ5٠cMNN\jOhZPL*\/3N 38 EqUlG'Xs5Z I(αo%8̂Z-O5 [u Yܝ= Zc=VkB hcAt/ n6 k\ _A`Ub<(iaƅ\,T${:oŗ5s-P;Lsc*?M7D"b8fqEl1Suv5 5hs-ֶmh߸Հ")oLgFp>ʀ% rd*S / 8;tQwa.3v8ɬq;e&:4D x?Onl'n@Є$&^1O-vZF,sMYn\V\7Hrc Pn]Rh%$HRJP:I1D Nt!&,VBP'HAXѮC,])ŻwfHEώaMPa&y9exĐ57 8Nc(cΘDlŨpfMyȶ;H4 Jq[x,\\T nI@J& D8yPa";\!JUJr'p8cTd Gо%/CP [lPJfWx4YFq&iґ63- ^9-1[x[x.(W~MzsTLƐ\k hIɊ6kӛRHP *]J'xI D->Ȥ);|z'yՊ)4H(p%0H OTUɍTvXոՙD%)+[Țα%ke< 8N#S B> gJH[ 蕑)խJ>u8ÉldҐ~ #IYZ~5H X"Q!ek]{Op$JX4SzD"t@$0E)14`M@z թR.B#f\I.^G?cb$e}#Z3u+y+}]0Fkޖ^LF| `7nRhD'H_O z58(St%Y9( dq [<{k!c ` Xq,RILb&bDCL{Wȼ FV&%52Cn2L.Z=@\s`ܑWa41=ŷC R"!>Vi \Aq _A #J# Rf@IB,tx'kƎł? *@)<mRUP\#¶d YYcljR3>5yӧݒY۬'2Cy r v_%v+T2\#8ވ|DKQ*18D*p ,+D3DBH@P01Av ǃHy%Ob1**"AtcLV(E4Q!8^usDA:촶{ =DoK.:iFwz:7*n|>4Zoob#l&qRE]0@mԀ$s(1= IHXx@}hAdtM/B" Ȁ B)О&Ԋ)VL AYԂ|Q_12J,]eaMH[ wFح_ ře~yIͶa"ΒDDaDm"X] eZ#9>hcq9a[ZaU@=ƶc>??͆Dݍlp@䓟cȆB d~ˀAFa#D@9I%FPdFny'6$/E7H+34C,pѾ d$`TL:h119@R2Z,q.#T&g5JbU7awqecXO \0ZD}I@ f[#==NKMɗe\╽DwNUac1"$D^gQ Me6 d&VLzi/(B,BP@ \d"HQT!dmcTAr], '4rB%sfsZH9c*\gve#IIxY6ig ҋJcݐ nh)_be]-Fr$ \L#.m(Ie:eFh GfA,%l_I7EvubxΓ>IL ND`)|K!Fޭ vK}'iߥܟ1c9ܚL@0Y->F aNM"yWU ]5= JIƜ! &_ D)нWmPBԤ%`D6~kȩPenevmoHBCR". V*V2Xm. .B.€Ȏ,@0a$چmp8\ul ߰| bgT ODox ! ½ǀ:l"n Z~-BCB`lPiDIoZr0 :norzĂ/j&nuB/%=A&A*A= W t^q#A[ؘgDLyDn&6pVp׎0"w1Q+05VE$, '%Ld1i,q:r/1n*1$@?pbpL=v|lt `]U@@ m.\8senA~HSC!R#=`G6YQ tr'4& gLDD29fNBfv77H=˛k0@e`Lcr- 4d_@J!(DܤMpAO uz s85kc$T$X5 WGkA)7xR"rT"4ZSZWLN$\Ӫt]7 ]F_=> abUA26~6RCP"e bZ)t7l$8u=2 @ ,8/,R@4vXrnoӂ2h9Cp/c?g_rn%+?+˵vZtvqCp{y D7yNbϕƙlPίL } Ն]@Ҫ ql8h88o"T @7@ l-d@EÔ2(8`3-%8_t]y>@4F.DF,V _]层%Z|86! A $x'x PElD[8pvrOa:={+}0hQBfg`=5-'ڽM-y] 0;}D\)ȋ6=%`@ $lB$$d$Y7Rn>嗵2Il>ӃsTOxpaB p0da JPQ6THq1 pY*TxР"Jtљ3WxpzN]4zt@ p U$ #ILC_."KJwJ)R:@*`"8@+ ` jH-C*.k*r" |Yxq8c[ћljNfᥘk>M4{;4ZIـkx7$g8Sn 0;+)<@/=pb%?l"9TϚ ժ2 +#F0*İ8L9. j"ZnUVIǛdZ$R+W'kͷ#};,CNuK1̙j4V́MRS*˓>*J@mSO3\bi Tˤ[4.0 L´C<}+E գ-lZmVWyV\s^K&Yc-M*Okӆ;睚TYvڞzhA6}iB|+ԗ9 ~ G]M!XRԥO/4u" Ԕ-]j` ,2Z!nas WY\7/|a oXC܄8̡HC%} "<=e`XĦ6)j๞BU@5i,RP1!'Qcq{1G3LЏQ yg"si'+4H%6Lv*k'Q'asN*Nd;3XN0 @ǣ\,UKN/vD0Ksp1#f1=_튆#ZC,Ӥ Χ[D E@0%" )`k v QxʢDeBjI8SfC#f1 4yÇI)9U\t}F xL[.+) vH?+)RH<dk"b)CLTU-*؄/)ReYFԀՇ(Y( XZ؀[9a眭3$' "9~ L>rWMM*їe0۬3/Q&N׵-֧NzrI옢kA)nR"ԏ#AM. nu) w]WhUPTWQ/VVsaXK.0^=k-x_jtmq)SߟL'bxRV>zp͔Ā]O)q0Y5y HU98 P0D8s~; bsM&D]=~)c)5)Ux+КvSnSRHއRA9mW~S.Tgлgs-Ƶ4FLp )6}-9sfQ8իnuCYuGw:RJSap $Aw^2#L utf.^ w!g>车.WP<%a;u>`LۢR@4]B[cif6^)3s!cD@}̀ol2d,8z-5OA"ٰvj[`-h0NnNOD+&|O maĊ ,Q 400PC 0 ENÈD%+b`*Nj( uP- RƂQ-PҬ P Tao  m5A!B+Q?78A|D ;&% "'n KO%D (%cn?DmQNFBQ.A,p^m,%o1؎EIQ1e%& W A.rѰɯ& x`00B#P.fAl8D%%]l& 38px~L-lTѯ=%=hh*0+վ ` 4aB2IR-Qڒn䒻4q/ e/M0=i+/,JµPLj1e o"2.(R+2rBOk_t4O3r#1OfT$--p- hΦX,%8q2$1P9a"N9>TQ(LB=.N&JJ*[!Hklxc+3بRr25T)5m- %,Lc6dB%ebBsI1sCC?F:kM)c)?jN;wn4k_@}ZHs5?!qfMa@=a!JT$5V5 T Ô2tCLє&DIhDTbNN Ai*ui^/Pr1զ+q?rQx/5S7S? 7oLVS8TUݑUTCSV=TV7ʬSMM%PɝP%,fltHN^ a鹦U4͆/a!@5\ƕJɠZXr6MTBLg_cu_:w*[VNJ~0L("f@m)(&֯tQ,c2![I8r&n3u\e5cw5K5^qv/1sցgg_郝[HxFG\_iU/ u]\]+JpUppqwOҥ?"61t='>T a]l2A7tɅ2X*x*Iu.:#+ZuʁG-m6e @[ hM?o-v#Xly p}zVװ^v"{ P gY v'-YJ6k=}qBtGW~Lx`,*#T:{Y {;4S.svԎ ӊSڰ ya֚\@ HmꩮBµ[QP7a5쁲;.{ϻgջ0;R/ & 3lIIrj2<\d0kPW9…כȀ @Tڤ|Q uomGadf7Ƨe|]bEFzTB; 1<)9sgץ%%x.UrĨ$ϋK=cL9/w_@]{'du6-rW/ac[>4CY[꽞ŜxC $ MB5$[@1{g5Ѹj/!C4)F^4K M=CEh2>ݯ0",zI(o-Xny,`. @bl;lֿ} -^AQVu뵰gg@~:^f !x`BE13t^X_49??*(m"'R#E.$yn`CH.( < .o@󋦊C#-BE89 ]_m0 < C2PC *ѢE=x䃐#>hp e4hJ+S¬I&{>p2hʄ!9hp` #jxj#ňTkĉ,ZxՌرW9$@reqmtSH1=826 &Ō;6'.bɏ+7G޹sީ[z6_b:լ[~ ; ض=`8`q"Ţ1yDI/<K $w@hЈK3p@{6zaC #xH5 Xeɖ= T89ܠ 7uC *u^'4uZ`pbK0YYd"("aFf}h&jHcvnQo#paE1gFuܑDL5%`uNH$AOE$A`tz9pB)_ y`V\UfE'i}Ju R]YafQ'xhbeg,i66㧢X[hPBjאAȐ"GEiv4Mדw e0Un@J TAgSvՅ|'v'z &]NXh_Ap`v ra<ؤXb{i&i~ ÆXo) ;)$$ZGڕ H@:dRЩӱ*kv=Q?]jVjvޙVS*81^RW;™A৅b f%HsNRZ9׃). kz*Aё 7Q#'ϪԮ M=:TPU  RUYT@j!vw橄[ ~SSeu F%UdcSVeͶnטp0wl\Q_.Ϻ*ώ&5x/}Lѥ@QM9v찟#Ӗn:`+up!+f;I/ʚ=#lbTӯqfm-RNm _:ŖS *|T…Dg&)v%CQ>]n'ZC,T-LO遚d% ۢBѮ`iL lpt wvv'BL0y/BeCIK.>pVvbpqCD= $!VR%8@zJĉU N{z{'/fq]5.t* k^K C S+aMR5S) %HRfH6>YL0Dj9I72tT`sҗ[QB ]I35Āѭ1C#Jl3h&L͔JNb!Tc4կ8sRJH%xp&,)TȄ.IQD'E.tQDſ3Q@Qh() OLF39ҜARM !L;j`Ap˦)&D'إ"ͮ(ʋdPȉt"cp\=Ԭ8A\3̣ӏtfW]ǃuTl4JT,ch'C6R"jj.tFYP}rE$3 RS Ň, AJֵֶ",: i0. A. 45+r>LA19FZ:S%4=neX[%'A ENqER6g9Kb>MMwrvXXڙD42&VLňA.Pv@{SЁg+/6 c(&LDg>[ḻawb6xL,P9XL-ыiusvF;|D=f`" 2[%yA;a Uj+&7^.߱(T݄y2U-$tuBirC˺X C2(F!"`QBӃІ>h5:FNu'0}ӆ鲕: pOcl%5M]dI8]]ΑOsQ;OlIFg,0)V,TP 20O8mq !jR`6 <hCv#ZьwlR[ƞGN{l!Jjnvz9RI\9&yެ~ma{*u>9kt5mF,Ũ s*$aŸ>k2 aqc=~nn {tYE $: 4ٷvv0|A>A̾^WK{PnOfGYx?~u,R0@;T ( >@#.A*Iyը?OSnmM'LcR'1=/ucc'Q3d{IEC ?@.4j=uT|Ƈ2kv%@,!}%dq1x]ax.v[J@~s@C{tEG\A6gKoGo(i9&}.zi*=XN=>C8?7xj7PI jUY-+d|%UZS%.Tep;'pp/ [R~:jAb\Syyy$ՄPth:BiUHhi\u5ՀC7{jqq%["5QkBI0DAZBuk[ˆ_!6zBmi4E?Beщhtu5'zxx]~Ȁ?(X{C4_N4{5p{7+#Th% v|72.OqlqYsbg1@T4mi~f? x&Hg|E%)ji5{d5,{f>XkĈ?âks7&?a?KI":95`f" &Fxy '8;575c1x BEGG7zKiM0Oy>qVS60V .ˁVpVc8#ᆷCD$fq8DQk>!^%NH!%)GA}[ DfxR `IaysDQYS\r9 'yE2b'&NU=9V%fuVj'pٛp!32E%)3>q|B%&#?TpY- Yj}t2fm`Qe!F 4I'la i.,`s&}B Ѐ2HhyTo+0U_'N)>(rVٳSX`:dCHx`h9/9$ Orڜ. +=fa2pRr,b]'?` 9h.6.K2qPE Q"GJ P)cz+DӃ>[ 5p>8pw2 SEpa^ǁdȇ!,jJIC pM{zc9Ar- vKl4tpKIPc!PCA!0 B@Nw򔔖2ڪXXqԩK8va#(ú^*k8Qd:NLR=8=$3ZʙX݁!X|X(JlE3xfmLl p.`Tʩy0 ͠bRF7TXw,j YSUI Bg(I q';8@Nk!j,!3D)T #EA?#-$A{jh!`t[f$mx8Ymv[*FZG!M*QS%5fں4&<&pa'j3# JisˇJbq*3Pwt}i!dm0 6K7Z ::`=[ @ W{tN:oT UZ}[T#P53;oث  N\S;U8F#e߁J9<%a!NV[aW~ M#uQ$E`.(;Ny~i~Eڍpd3 w0|]W~Pڄ>=rqZڰlЏzaOdJ2ǂUCiڼ9B_d"-UrlT wq0 ɠ ,t>3#*KNڍY&] |Grƥm۸v m+>DlΡ31g}/cݼ r  ؅8cB?܈p ɐ ٞIĎBy)X9YMAc9`==]vL .&, Wd%[&Q2/ud_Cg4@lPw0 ɀ *NNZ?7;+,B>NVN?;"m{߂,y͜ PAB*j<|H 'd$*W̐R$1wJIil̉)L5͝;WNuۚ5KFE=@iM>T85@]@#̞=[mKV\pUЕk]|o|o߿5[Ç95l ,3\A \`aaVH:ÃZu3Xmn<4q#ع"NsiS:s;y*(ԤKoJUU[x=}zZjծmbyFm˿j샺Ta(Ͳ6lBs<#(4P, FM,F H,&6c i 2fQ醬n~ j(jI'ţ<(4 >Ҫ./ϰS>T 1kHdB ӳ4+5!jhQz8-jG;K.%gp2QkBg"CR's<(R+*6{/^ۼ4c ,e ˋ-nx?tHT=1DV SAV3%-xQMIm410Tg9tyPKɺ#SrIZ UDJϴk{ vկ%yE0KS^΁ُUl/cGjŒML#(2X hGs:gD0EkpqHgB *_5%i '6THa|᫰:-Z3ٲ| 3eM>pvQyDy*쳣P^Z P\4R(*غ^9[1`t2r$m J"hvfXqgf( 5v+ ,;r,[5ȃҙgKǼ1*@4 ̠Ar0OBA5kkZQeyt3Wg%E/)H^rezhr\Ƥo0+ G2Ɍ.@\|r־gYXT3х}2ѹ> $6oR&hF  qv5B Bb9OsLg9Af66[ - |8 )R @Sl$5`,u.TA"? `xPFCR)ebӠ;U,aӼC.dDIOddŬ90<_<>f T9hC,4GhuB Ed*Ы8cБ&h Un'y: Y;$$tC>B2 R. wRn?$=eO0C"?ThK "tgzSHBcf3ˢZDHGk,;41ԏ&=)6Bf=- 7}9& wSq&gł0NO S Z{I_2UTuAZ4K?`Ȫ*c X4V #AHk])(EJW,mOguX2"KYL&|Cfb W@faE) \fSVU?5 h@jk,/oظY4$ÕCrn7"w'd|t)V]A1,sp̻K񒷼DP'Մ,@\ B iaAE:O#e F(g ]"GɀʂfQ> ngm62r;z.6b.iYv,0j-YL.K(}AJxU[,ZTZit+FD"֪0t`-qܹSذS*zMPU4\X<DBX'V|JfP2yǹqm u6j  [fhO{AE(Gm$qʒ7_ KZ0DZ[A;LZ٪,Uyϴ4Yf1] 0ы>ybx b7i!.D;O-~ &mkS!'I>_Tem?Ҁs1@c/tȝ#S$2%'T'ގ)(> =>oV7Y2E*A 3 s9$H=7Ѡ:c ֫32rKwk42@ҮC!.q>kheغ؛j [ ?N+㛷ȪiЀG3)x 1<8=J=x1@'ҙ)Dcam;"Aܓ' 1@`A{A9A5Qs*雀 )*ԙ-2Si Gy6205,8(;@/c) V:3" Rڪ3,^2JA3b< [8j\c Ι[4 h-yhD)+(jIhdZƬzAGp(! ,^h I#\ٸcǐu81"D3Z#FwBIIRL K'TwH$|ɳϟ@ riӤ!Eڬ4iM1Jujө)V5f! Zrٳh1#pPx˷/_~L n]n|8t;">޼OϜ9|Rؗ}h{'L9CĽsͭZZj EН3|8CسodȻ;oB_Swp9QKGeʕYfiU.}Ofe3<rr s El6lHH];zx[I>8^Oh"Өߌה:6޷{.(\WlBMsFL6;"5ِm VLVr("㩣eHy&f[҈Sk>lԌ2uwgs4)IsmI6H&)wQ6yYVХw^N@騤z%3y̪pthgI:"R)3*  s4묳 H c=_Emnk%5z4AY놗ާ`rhRk[k{j , pqԢl;F\%^R+TY (5N,;2ẩ[v #:/< 25 +ٌ/& ;7K̭%g-9tm `a}^b).㖲:0c;oF< Ŗ 21<#CsDŽ-MƘn.E҈M4 +[hYru`M6s_Yrm*kdw9xsx޷_w5^c'S[+B y `Gχl0uDT)rT5}Ր8:g-0 ZRpj0Ҵu,f6ұw+ͨϾsXA KXPr'\?搈[EIK1mv ]oJ"GҚTE(kTdZnU4)+PmmbaT)Ma[]%tkag?]ΐujӼ0&pR1 _m5[O<]wK.%$A~\ "l( 6Y#va$kXn׀| l$.1':ٱFֽruhc'9^ @;pߒ׆84 NfUUj9@ƆLbnČ_6DULds1[9vƳx_󹼥6An9:R)RNW'J?\2a1jȐ=]W(- q,Xulí̿fa9zˮ0g]8w Mi:Yq.Yh,dόf{$ wnil:P֛sAfIomkC=`w6ruFKĦJAtsYv9Sָ%n-iImb VUt ,Q> {R^rgoVV_(owE|ˮMˍ꤮4s>yZױG Wv-p9ѷ=b߮c뺏C<|75o|2w3!y5o}*'o!F׳^e_:|{5v4!-8w/|^PYxTqJwcun}bEXDٷeAfv5}1{Ypg~g5@P3s"woKUa]&r{%~ڤBa'? x|qwfy#D bR4EsIkeń7DegvBws":lJ6D$<5zVa2w4x^ccaְD G|? Db758W| }g^vc1Vxd{6t\`G~ 1Tfh|Wo 5^X>TE,ndEu6_ $`_ `eEVhXeLaaaFÉ=W`4iŠeA#Gȋ苆n0G@`X%B$wƅh!eި@W`mHH~hPu`kFVOdXZ8UxhhdX%'H0h : tt8Vy \x~VRSƘt u\48lW&HҘ-9p.ǔ‰4Y|x|:Iy< T>e*4}B}舘fH M#(CP=gW)e1xo?H{zU:u6 n9v~dXzٗ9D5Ϣ796(wc#Jxy)h(WF:s*H =?\>jV&lțٛ$)Cdw87 LpgxLTŒ`ysU$Wy ߣȞy\t:lY(UI ڜ&F):xp':g(^V'ڡiS(FF՝u>4x-HE,Z:9] j:EvTXd&3sٸ~ֹHzhP9l5xZʣ]ʀn7zjdejsx>i{kʦ(rohEVȹ\jӇ@): ?~g w騐z0X6hYF ʀUvٞd5ۥhl@{aQv~y{V ZGx p pf=@NCNԼt w4fu᪳{  LEWXZ^5\^`~ߎ,[jg(q#IS NvV@B{Ś$ܜZbJ{5ϪcLY跍,N`nն}/}]>h>P=CbP@s^DnV= bZ ޚ׍m堃v< ,.M~xTcU~9D28n~`9>"\vnZ]~NĞ#[~;n,'O u5r`0*H S@Bꘌ$| ޭt^.R;snՍv|1GNj %p>[_( f qTFM9 qs?c?MF?,Ž>t  `;Kp]oMcpB]v}hVKpVٓ4@;ù._ȳ{m/];ޮ ?HP c.cʕ-cDQQ^ʥGa!E 5)cR˕-͡YM9u֔9O?ZQI>Q|gզHmJϩRa *SDRSgOq#obA 8JPh`rBl"m*JV3+ W6 9TgpygtNUVF:F6oSja܊:ϲ٫պXclȜ~H3Pwe^!{GiO9)cAy!Q{+Z`$w)Ctr93߉aTCǺn ƨvcE)^=nNB[y)_Ɍ]R+T|C8=%-WS 좽xb$1H@ i,G: n$(A~k[ppo]1f4 x{8ƹГCUÂF<"NAM| IE *zFP2J@ *1ß|kM^(rHcҐ-UB1Q b p4 Ѹ9@_J)x9i"S1H0XB\NS wt@vCLj1 kfbk tdS&hw$2%!;aH%qLFp4n)4JQVH S9OZ\DL~,P0F-qtbH3f,‰Y)b55xDn4a=ULw(C,dA -މySK "'~GS}XjЂȕ/<h.z]Gb}a4[ZH@Ԕ#xE&6eM!EPN'12A }Є&hA*jqj$U9^YX U2]@7UQ=OءZ0C#Emo=fQ Tkqh.ĨſJYG3lSCA],~: wl0A&.g窢T 85i]ù $q-l7َqh X:|@bc[c2W٪]6׹X5dt УH(])@ SXE5N-_索|BZ}~[~ա+_nKQFaVlpBL(9 kq/2wâL8ث0s L@Bn5815cg~(]_%L6oi4u2ـT;Aa&a- Uy9k-x [xtC.AHT`$̺04ܪ=ѐ6Z2Eɗdr0 drJ'WT !xŜlpMW=WZT,̋AqZ'?suILE " )ec=AUP['@3vm݀6hp۬D=U[߇;bl7mq Jxcz|ǑߨZfہ@4n"'kn]#@~g ApBAC[EL1AH|瀴ل K/B_{[h|^'Ӣ>ynMo5%(Â%0@\D(%B0Ĭ1 A6KoPQ~)"88;$ <˛Cɮ,;}IYIIA|z0۲v5DĈ2: B~ۼdYkZ FL(H4#M#`*hYjL %˕\Nq>DmfXHPE];, Ww`rWFWZػ<Ӻ $ 6I_-%I,։&H\G@HTh}j]FeJOO"MSpMr'TG؃O vmEDh-W\=[IN^ۍshwb$[# Z%Z5 T^u8}(C+Xc%3@Q[B2Vk 2kl`YJN݃(Y  DRL8@ZE+UP pd@tH%s + i'G׀4b~8bvn&R&&AU^je~,+u7.-\.3ʥreŨ,[b.p4.9Dfc7ri΂X3HE)vTfNp؁4̆hʦnz6qz=iwD;KHԽ6 "avh8L|-+lhv^cUB99MDEz|!KZapKR|@ȸZi3ަHVjb4Ơ_[fP3b^&8 0FY!I$v嚃eD?{DQQk `4v($idCEALiO.KpB(2H2Xg$Q8Φ!U$_h NAHPnZN^q&_nZ=Z>nz;ʠFD9o [;U$hĦ8/U |ZuwvO@vkgwmw`I1Fr?Dt?tx5)~xd-H\[CqO xkւi&z9 D@97@Kۺ~Ƭ-Sqciy~:72x PCj@vG2_"I/Iγ ?Ns27V 6^M]V=O96w{3 NI`VD-LS!빵[ȟ @ھgzv$ wV*O'?hq‡KpQ5k-bŊE񚸐"C+i$ʔ*Wl幘1MSnݺmJM'P (j4J(i"h!FDM![(@R VaĈpG|p"E AuoT\)|e,SĔw8QH*8|}@!)\!J"QjI&%رOʮmwԤI;vٴi8 pArC=DjEQF9ȑ/Ǜ OeƔ= `QI0} UUiTDUVeQ8h~I10ZT0ht}W 6ŗ!N<SHFdc Dfuf )ԕBUa"9NI9;H1մ;׌/Lr?pav\b]+$rcG'G≤^Kǒc |LR7q0|t RXE`Tx)$PVQ66P[*h&ixb#X)5&Ta8Y 6Hi8: vprkMfl<9.:QVXOL'| 3aT#yq J(oE`B6րGҞh] ;lT: Ow ZB>@S"Ԩg ._S U%\•iv,< a(æ0AxՄ,҇?KDLdxT@V~Bdahژ1Wk&4Q(C0Dᚶ8H *r'@Y9U AI -UPL)&Kah,BFVg*%YDh˕h\PD0^0BVaHf 1ڽvG8ĖVtqcWʸܼp[;֥}DF3LH$3bQ Ca OhB~@Oh4A*2Ҡ?)HЂ^,II:P1`x*AD{ RgSHҏp,PL o'4JjaYLejE> մjp\Y:nVUYaG|"x ՀK̐IRuR?_ՌX| SBt)8h0.W$Alv*4d b1Y_1>Яrpy*&%g?hIa#DXTsFQU=\DW4]ԟ@ W pذPuR*ep&tI"b@'>-2^!6s'ԣ6Jy$QW{B9@x]&L:.(Q(RA;^ ղs"3"쀘8o{ 7 WQOKآJ;񎮂rhx@A5(9R[J&Ԑtfƴ2|魇5rسp?KJł@{ݯt@Y%T>!{,TfM(VTQIe2A@1qDžm aȉd ^qL.0)A+(<#`Bѕ$   ͛W w=!8=]:hq}_L/݇X-@dIEfeVXTHmTFJMVEEEUH[R `%Dڤ)l(=`mHf܇e%"pq9P' B,[B@ b C6d`Z4ARy- 'RN4 3$%A 3dIZױ)]Y `]ɴU#Ea?b>$O@1%2&R!$9]r tUR5^7`%E$H82~ D #YE,#SM>? Mabal dOQB6bDdd EVMDUOބ;w9$ԋN< vq  eН\܅ ayEݤ>I$Oj_PP-nRzf<%T5*\YE0@\Xg=@ YE8@' 9,] ^! @.P@m-B&bRc%P$}1+B*BZ@ 4A"BIn$deݢhW 2xCm/^Knj>f2~]`]3Dq{r2'TM-)u+]XwЩyAڪ8'%\ i dF- Z|%g똆 cyeQŻ\ص~Zz+C hc:ȁ%+)PB! %ԋ)=oVߎLo}D&@y F@PϊHI0@rFI9im@ &C©,frM=H 0trЁ$02.lB"A KhxxmZ#lmBRmo;PRLT:(T/WcY@XxЩPqaDN1͊V.\[P[ UvȄcX쎈X#7$0%@ahr't'# eI 4B'$r*/.w4Oʨ/oq[Rn:/&s5J=m ]"F"z]AZHXɴ b~%gУ! #b=[A>3ᨰ?O@WwBZU$ HKCK,7\CEjqj~GwJt0[I3Jtt I[L#"[FX1=S'zg8;X8 - dۻ Ӥ)UWU+f ?OWvoC@#,L1SOK]I] -$\\sjP.uQG`Wnj1a7a|Ucg`dS0US4TXtnNQ<I)dqٴ xj?tlWU3EAVnowx px'Oxr/׬57-(6j^w2fvW1wUzPI72POgvU@.!O`h @Qg 1@whq6G%h$O׊fޅ!`kek@n˶bs arnoĀ﹞@ &6&ܒCG-x7HxOߪwDwNr`? y9TFz~]c.f,9StKM)y%g_H\k)JT9CiSc D@ L3 `g@J41CmVw5n%0ꭧNߦx{z디hhLdz3 !:ɜJ,8Y=HMxJ[}swnh?H7b~=T.`QZ"rTi_&>5>&Ky9 0DIA#@ujP::y烾/}J8飾fR=A@B 6t0@xB*lI%J :rذB2\|bTpxΤ BЋvVDtD -Iˆ&ktM1T>$qkmkU8I 'R@9BdH-`@r'(\nJ UpZ:q^hџI Zt~Xvvl٬ն}zx[\pq{5@>~}DUãH&QTrE(}tf ?@pQ0"Ыj&*`Z02 (,T˭ ˮ~M@`؁?<ᤓ[f3:+M7P3δdKNH؂$u#&{3ηYι;ػhr!J*%Ts%[=𩧊f" ">" =GHpAFx0+ `u'=Iӈ;sr,ڇ$ "Ȇ1,σl6qZ'\O2=t)OtpC;04G0:*,JL/T $zaQ4XA16lV\ӹW;XұmX׈$7tgxe*2 -ں VA3kwpT<@&=::z;'>ɧmTGJ(RK '<]>c@"A~M^eyu]Yk9I$o˙sdwwαh :UH*(U_QBϙ h,4;mL{# RУTCJb|9+T$po8Q8 7͞_YaL<G't@0@%@I 5.r=_+SS P`%sJ5<ةO~Z֒W کy]Ҿ @BLQ!$)Klp:pr>}I\̆toBMh$D,eDxmZɢȅr)3 }꣓:#,ѝSy M#HAv(\Kl2!Ẅ۟9&6щb:XgˢUw-Pޠ26H%"D襵\$#e❭@B r!ANȐL$ - 2dGE:YOfD(WޜYWTeXJ/[.!Խl  %"O@3?(г=d! `\]`KM&5rrS'Xw8Det8zҕR4=M%S]/p W%Á}_ R0E^ Zad$eNsqKȔt*oZ"IK3qO0)εA.**oOd9xUDr8*TYh | jjX+I$4$S|+NnL-NijKvi%D@V_&v fZ%(\KX*V13qԶmn:뷩 .=׈T-]G [>^lUxOCԶCj E$XK7ifJ }{ U7;"[,`\NdR( |`'%Xdpk肩y?9pNG%Vrm^JI10ղ(` Ki-E//BТF0g [5nᱱ\_1E0!hgG>isJNAv&k~y{fMvgzխ~@OYm}r԰QJ/",sIQTT6aWTgQ&+*#5Q (O;[m W=6r "L(nŲڂڒ4@H@KH 7ˣJ9OȂd(9NnKFl, CC4L8MMQE\TROifePPQSQcO#$/T S%jN\.0.^ dmL`X2AT!MVMVml&aEYtW \uRWu܅mJPeC'܂rLvw)_boEQ6&TK-ྒ3 zhvE ! Snz +̡I,}WP}'~m]d*kȈ ʄRNX.(] 3>VQ %x@yJ&EϨ@y1 w_׸zAi@ niz,5YNIJkw}3txTQ7uW$RwzuJvwQlVc'Zެ>w?$j+$j 's;/-A|_-A瘎.1S|M2W9.g} ZWcnP#jȐ$ JAhbVbፍ掘"%UYCwd9@ @oxxoTq6 ^8NaO!keqn&8P{T~NNu\玒,Mfl2% 'eР%b+Vo iן@` :Rk\hAt#φ-58M?GsFZJb[g~7yczPFeۃ.(wAC5eBZkT,YL i <hZvԕyy}z˙[I7&O#yg1΋?VB42me; Ą{+JB-ز] ST Saslqxk=ZKq)F"*JYzm­M^mSn; A#pn K4 o~6 v/d LשW*%们黵e{Kurx )fɲ6ɕ[El͂VNaN\;YbFhågA)-. |@G8т֭ZʗdPQ$GlkhBtI1|Ř"N=f͜bs;~8ɔ%ܹwԭn/1B8:լ[~ uj6p2qbr9?#Ņ+rd̔+]Z2f"4K=?(y2!n Nr6زc8AV[l5WW]LDZ_Y‰aDXTa]YhFb&8mnraC38DF$]v@4Tt?M7dwO $]O?TP'_|vUuVZt_XcA%Z69\W 2h&b:aChy!hfډ>jb*(OD)EGBQq%XtTt&MYyII@%%RRN@O Ad OA|VIEaEgiVJ .4{=`'A%U矂Zh&٢#BJoIm9QjڭBt} Xj=nA-:w* POzHJmSP h雜 `UdiѢ)Q۪Lז)._V*c˴9n!6joՐ⛯@*e#GoDp-܌%%RG3smLC32R>3y9EV: 4e3:\T>u_C-TnM3RHժ(餶i/bE%^UBg6LI\RܰjgQ$?QjxS<lڳT#Dc^dSVNB[ 9`E t3Bvb%dv3]%;K6*=&+EH11H5U ~10飖Ye9LA#%l! $Mij5u0^DY:RswFnG9-HQq)ARU孄cydEB@'ls2Ф"D!ɈUK%+/Z9=,JĖqN Ls!F1UT4Sl1D-e57qИRAlYAĥD 2+IxC4BA g P,>k_4Im"Sj JAf 5s!*VhtXOW[Re#HD!YSa$ n9Bw>R[u7-KZC)-U @5?$@YQ ;ra 9 Z꩖{.AH@Tւ*KKVK22 0`4:DՈr$(, 2gֱw Ǧ$1;N($U4z|z2cYyR3)x*! @J.Pys%f^]L8˩UuKtʎo pr(eǤK^GȄˤ`Y'. !TS'bU)UB a; Q WȬpcjq:(rN3|;`wc|ຄvô;݉VR*c{k,Yfm3qϪ, r5`/C ~hh a5mm8Xv#STg jo1ы44h\U1uS6 p)=u,Y~3 |gL^0,lL!3@b^W 3PGu ~$VutX?~'YwfȏY>zNo"k.u./۷K[W#* i< '`G5mh:r(09Q厼@sMG;v{@KsALBnW)yOdE`7~C2,L`wD׹Zetx!>֔ݷ836HDϺ4ޏG[g=5XHsbI Gy()lA睚_ -*0;`9q r`.ѧju=c?lWqh{ 1]A)6 #*F!JShR]37t5,Sa==Mh>sqƲNB3w~rHX->7.{"!߰zVz@XqnQRb\q_ p;68'Eh;w}5iHG6f}s%h2;%`x7Ԃ vkx/;LqG$]a̘8YamaNII\h@r(s_s8qfP؄`Q/iEIfg5]n@h9uw{rWhAdCc10AA#A}`CGtAO%CKSyD~;yՓZa쇎r-oR..H LQf ͐ WiaIFy(9#obA?jn Hmj桜})Ҝ%B'نU=ۧ=3T߲t^&Y~c&>L?- aM ^5P ̐Bњ+RQǶVIw)ԆAl1Dr:Dr(kgDRZX:KI1'o60c|M,QGYǝC8'w@;Dꨎ:AZcД2` 6 EzIʚXcT)ʡmFm|76>\'u)եA% # wiz^$|:=7Z#*t^&_be*-痃@i:&[hBzFKslQg* :?bGxf\4a&]|5RY?]1j*#lqO%. "͂>79H-H`ʭI 0:檩)f:F.B]Զn e~3Lz'FYtbBstux?ĚB,DS%Ϻ 33y0J7JH:P p uE7[pڳ>[@;"qGk6vdYaAQT+b\buМG}(Yn ))12 z1&%'KhE ʀk8P˳fEaZ{3lyƛk9zhbk7#10c%C5t#l{jo^T;I38~.E`0 kEث芤XaԚ1gȡoCAhvR:]kbKk4w bm?#Կ"*.1B41yC^a29J>>q 0՛ `(;Qؽ^[f;%nڜZRqr4I:칯t 9D,wHZ%j,2OK;?:x m\sܸ{ܛ뺤b)3$$[F*Q>j[yqRkS<4htF:WAwgAYI8+W ˟Yo;“q9fS5f{q0jR)~\[-)eR)5Xϑ]')#jSz(npgBn˵X<ʰjEPUɐ \!e(ѸVRڕoQF$wr%wpan1XtAC͌ɫqz$Bl># 4]Dk\y$0c"pmsf%hNnމs#4ry~{ڭf\g]$.J`< CҎnfCb74]xZM;ts>|EqNrb#u] k"_$\;dOƧ7TPa.!  B *1`=rR%TDrDJ-]0e*nܼ#M(iN7sY4΢,z$z#4k #V訵G ̂8AȮ!fumW !Vల޽)FH#-Z1CFr؁ 'U|fΜ1Nm͚ #j֭ JE?lI)֐p@m*T*MD;?9t)S='͑Yrm.ǭ3吶ںnͨ[k9a 4! k$Ė`L3D30~5 74(, #6 (n8nlι*%*O'j:ʪ*K<#+'HI; , rA?A,C@š%DA!34Ѧ&20<G$ďND.lT?1[\GpRN]G8 ۋ&4H2>8=BRKN*i/|kd'Т 9ȘK*Ss)wʙOTPu"/r ^Eo =J!ߖXŖ*{TiGx̔{CPtꦆIJ>X J"7ҵdU,afipN;i-0\754DyIX?і߀TS.9*.2P`XLӀhz+ה~h FJ*FkY*N2 ԙK|!sNWi6]xz͚Jňn8(|kL))N#jJأ`+IvpX 񙡥1 c ;=\hrChMQ<$صmlI] FnUv0RaݧTDeQPXOzcx緼GYbWvݭp2@:E5x\-}c@"agP1.M(HTi`]DITS90Z&GgTXG#@H'j ^#fLE v4p6_ '# P@vjuY z1]٣r -H$,!'T@V5"CJfژE`PIwJ-,A<e2C^l,g\&FJNRBF.tcV)SmUǺqDѣ+6ӛ ^S R9V"ujUfnK^kJTJW[ZԫM}/g6Սx+б ;0($7WL(NJ䖛1NB1kLp1q0ùm:ăVcdcM$S l2ub{g3` ?[%qv1:@ &d=-{xr:|l5 #82?NA_Q5#\SPqB(\ݾCvCڔ"鰛Qϩy>|;:~MoΣCnzct3$>yQ֠o-zƺi솚c8\/Ǐ[i˷{i,|G@m}H<\WQ=tc@9P~/qn :PF0CΆ*ݧ eΈ L"eu B\=Pⓟm.Y5ʯ(GïPkʴ;0e@Ld/ %.X~/-)8q^6F9JLcVҕ{ X\z| K^rfF"QEE#"~0tڙ% Ln<>e\x3nOd9'>|,HZrcI-sҗ$d0^(LBIІ!*7kp5^6&kfq{"-RVLzұgٳbNxR%-#wt.M Ȁ1xt(2P&áP ix;chzWĦ8% aR~WJgSXU?өimүǂJKè*$Â.uE/ 6}ؠWv&xMVGկc-Y)Jms ~FN6uu'lf# ;p֨'@׌n5t],5eJ0 hWZV0j/VlHklվ6! \_`e\!N_sx"Nw-&dnea DM Z/2kZ$H-G /6H-U %ùVmc~>5@ާq`U[mt)YaCl2+{aeuWh7^'Tl^ز >lX;&rc PCNnrXrA1Bcf2 jĨ؅Yb\.a3Hɺ١ðלWζms۸κ+ng]T>?˘.",]*zv4 oHyH\fD|i+f,4 ͺ݉ёΦ%+Y4.7Jv;%ZMcMÕMEKZ:V/gy;fmTߎ/q*an:.}ZIkwژ$:+!2LEVZE|C j 98LǍs<| Sx3ҋS+ﵮbRcp֑z'ޭom!'| lX~:s 00j}@5Z}/}}p9GD~Dmq~@{3#D3{\~B;7DB7t'}%}в xa}#fZn7zgՆͤ${g_~ C$sB)Hn*R-N'zWACjՃ9WUHDⴄ |FCY{x2+s|&C^<}jWZHzDxzzRBvw)ycdXcxH6CC>D8+NmAT~[yo6'l-TwwvgxXD8XDGVe 3S)dXFi}VkXЉ.Gw8Ê4nHt7:,E((+qw EҪ:qfO[t.{B* ׃c[0 Ѓ< <8{W%jz~ETG5K1mC`{w7@Ϡ;DF/HOܞtV6$<[E>+5[ ^`?do~(ҝ ʔ 5P T<zyo=|V}Ww.dNeܸRM#h`i (" K)\TTIf_`ƜieN9{%ThbGEܔxԩ[/Xb˾5pԫw߿f=-saFfϸɔ_N،;S pAj4!Tk5^m&jjhy%|Ns8bn*:;kR,/ <[= jϪĢomO4JA@= \r-8T2MBC@L1lΪPLQZjѯu1T>" $oMA*;T$'J`P0ԅ'^TALNN\Nf1ʱO|+P} %V-[tZ&s,H҉ `L5e-$bTK B0ǴVc7\ED^{0g*Yh Y%YL ["]['0UqG㠴OXW5w[6^Ud\%j(ZWD5`B~Vcnaf*)nLN貕J 8**8p=-I]uTc֭ޢFkIJ}sgX sak񮧉 E78:l *@|@B[gSΉ^l.{dJ^gLOÓ+i<9JAFA ;XF5줗ΟQeTc™E$j>bm"X+JdzB@c,`.rU9HB]vWRS-,1XE)xb>lIX!T) F|-0X%Z"lkUe@Dљ"ζI%p:9n| [̇LuNp,lJ3齄*: ] EdNvcĴT5 NA K@B~P$ wz$,,g|J$.zzu^K9*`bp?Zm@&XWa$$ťppa ofCs;[i'$^U4Xl,$z H8P F'ءU ^uGNr_89)2zrXYfD0Ab;ztA R6rF)sL dgVG_$b hp W@D+AJW_S#OwԡѨ[jĜ:WF6`.@=ә>H g9cp. A({blvV-J{ 4an4UD8s+(p\d y5{\`r:F}lŇw"@wW[uiЃp,~9 NC(zN'fc!:MKUη:m-zz=ѷ &[_;nnp'`ր5Oo6{6,:MOUjƖ|q NBa s>d&Ic<+v1[y׼Zr@bQ-;(n=כ+3C+­ 뵊2D>i(*ycfXC23؃+DcT(p1\?jgK+Z?!s#';T?O#:כ#;*B ;$=۰, , |Q@A 4=46#<6cjp;?̑EpE4#h >*:'@A+BADl$[D I,BKZ$)'&3YK89ڜC0*|ǭI,ĈD1,ZC\Q.JȄS8Jp=#d($9QX FCdz` t;t!H GJ\u(sLњ2ڨںQ48ŭ)(K\E730={ӕ`HzR(SI.X#<` Ft3kKFyħ=Iuu6DGnњ9ij?jL =} òKl)O踇ڊ$RȄt,%%0؁ 1Z@jBܼ l-ܟ KDq MC;143ħ|jJiTJLHȳ<&;Fk h%4HN\N2J[8#<%/$TQ5E(#hUm(UC0ťP. M s` Vam =RSDNuZQނ 8HpeFHMP/ͳCw>B*3L3cHj#P\ŁTmXL N<)I\e3:^EӽdSbcE9mVzQͨǎIB娭ae VqՈȀ#tUW!vJP:CWH7eHVKt?HMhf2Z[ƅ 5Һ o ։}zM%e; 0=rY̠$B]G)MJ pEO OODhOI_E*EUU ؀́+HI{%3!w[%V]SX֛Su(B.[ܚ WPH 6&iĥOܬEaHf\*UE;0sߩFwkXч%r Zȗds2uصmI" [źG~HJxM^Mތp^rLWW$RDHamRR4HRK8]tˆ ]5UuOҤZ< 6iSKV lm`* dݚBP@ p TE- uPJvXlxdPQn]R=Q`j[)L +[eL2K0TM F+evT/\a1l33Dȸc`IxC2CDDod-]` }dvtpwx&{VS=#=}%ĭJ̼头G]+Lc˼`Έ0Mx'hcTj/kiC(.H)I)!Po8!1IէЇއu\PY͆jw^jѾ*mPmdh>ljLeUg%p†k>0=ϙ hOJZ|j_@TCgf$ٻ<4P2$14+ЂK] K)1[ (J\KL;Ђ;h."h2m/mobzFDI֬bQO ߚ &T4 +鵶fE>Bn"D$N1L$aL.h*pV^sQ8᬴* '&_,_1h H3C hs p/6pEqTRLtQ-.jzpu#>0ΜOXL3r`,DxDHK.H8e_:yQb&7u 9H=;I*g \?m1^>4mn<@ 7Y޺Bj>_ސ@Wn^DwD 3phw"6Vh>PF|uyʮEЂmFm]w\ou&2v47mcpUp3+9޽B\NʖǸst {!0*(;xV`KH;ıDO}YW ULb|F s~x>Wow'(Db@$kh.mpxx=`hvn]5VYGjt'E-nh1ķ;Oy)mT<9n@b xDtDW$) pMzez['iv}zw`I('h {\UbP'piǐT @Tx^:L%+Re A-iYQp0קl'DB ƒN0B ;| 2dx )RdLNرd+sfCW&Ιv'PtEΝ}sPtTuPe@e{ǃP4~45T3)5CR#dLw\Ҋ^ATaX*(Nm%=5,Z}V45Nscb:Ȁd*d%yvYk=Yg_zxAi@x%Y eRF,׃sm4'p*C xpG*(=*ǼS6⴨B4!W;6d . a\rQC|F,tA!vpz_\nh_k,9*6P*ɁVkٶ歸x /&jpt sl0Q$A0G}Z JrLfNou15Sls,+0rtT7۰ȩN$=gXs|hvHыz,+:t_VN5VbNdr5o-h}!fI fskpHdN Ax{A  4i}Bc2>va+lqg"#]b @ng;WBt \|$'0 @%*vIUÊO [؍cu1hm0C|Ġ9= r(Qdbєg4Zi+WdtQ0*?0Q|{ %+HG:lͬ5k.1CBH"{ yH*!. @DdЄ,!'kJrq ¤$>"XxBPK8s/ `CU0?M 9hʈڳl3|6x=Z!׻Ǿq]\FlÌoP{mRH7M (`Y%A#:]Y. T  F0:I!4TeD"TiLiԴ#i(Ekˉ jf2Z=ijFP^n@K|cvr4ptXcӾY}GNgzx&KT%VAuec=mTo URye@Duim9ĥ/mkGB3ђ|+ Ԁ_ωCFA J(yk` [cO*+q\bIGS2%McӈE LZMxj{%.B!]d56N~ٷUFc$#=G 9nT6[ +P)w얒¦Xo7@ޙ hAG =#ѕ.;a`P1`. 5%Hhr(1Ҡ#92*clOrۣ-1yk&Mts c1W4з́װsvmdތDq][. ϨUGNnv` ^l Z  N?;8U"e#'SP4Ny$ BdLP9CrDUm>L`)Y5{ lԝrshٚr>"gWW,\7zޓ;"{ ޿¼!wFJp "LNuc+V f"$l&DPu -mmAY2\͚顃] ̡[E_@H{@ݔUjgdۗHt  KYٓסq,[!y\a x@)e@J@X- U:$\T`+)HB㍁:$-=q0^' Z8"nڮ]e ^:0SZO`*dA ) Z!*ʠOFZ JIՍ%]kT[ .Hx]%%١ݟ= @_A'BfՇ[Dɰ>>l$AU a,B$$Aؖ<9!*"Z(!2?:"$$dQٯ-^u'bcJiaVEٛ!m zu[bȓM|"S;UA tTG2^*`.! V! _(N8>,/ZV/R,h ap^فC<"@=8\%]$%O֬\d Sy R$ "k*r@ 2Sظ)$+8buɸL$ rD W]2J!lỲx@Q&n_5 \VH9:LCmC* :V 0/!8A=@=K^A' 5Z" d@@ ]eCZ4e_L$b6h*BY>J IGp]"/"klg[p :Y mȑ#IkfT&n"xfC9FȄL3B*B,,M T%_*%2ï"+V)F⏉ޭp0\N$+:=܃p')*K f2flkAS IӶb[OfHdw~=kfNX4k~]%B ) +aBA @GRl,5AJ@I. *Q**b^pҬ:tplE ϒ me8KFf mlI9!IxmE{mpDqO]G 4aYZT!C!L/ܞ"`!A&+PLiډ^, C6ծIA/{Jb^抩v.N:rH^I@ nY9o/d$[Ikd8O$o:!A:Lomsh tBȁΖڔX^TzBMpL`P9$:,p60ɍWFnYaq.]zWF"xIL f@jIj2FaJs 2 128vD5>"j4N5E[)/q?Ӭ}񍄮3Ɩdfz ~-aZ"\hbre"I;bAKeKאHY>ɋ}0'{a+(ϖr*AwTBʔև\g4IA@ Pfi=G ^ sp*a0s3;4(:מJףFn۸hٓ\ { H$h R󙈄 Mk&?RjPo(KAX5L,` AD۔ x&BɎ4t3τ蕴IJJ)cc4h"ߓN6aF8SwY4hb{p]lDZ{Mjf@ct ģZu>lVv7~`7%a{2'vyttu .(Ah'|frnط3g~ wf8v +YJlO^0n_xvpw  Z B2å=Tw3۸_xI&ȏ+J #4OIF CNePeh3V[ƺ܏ }I=9KtDkDHy?Vs0`Cy9p( @̂F'Ō;bvュgqwK)Gn%֜ɹeIڨyH;3/>SۜDf5#_d >ù C<d{po;/ p G{:Z+06N:4xľKS9yoɔ+ddhf<|gPc}ֻFO-k<[e{ݞMU,G-UԤ ;  $ &BDa-,TGz;G`? 7[y7hz @uMLWK淗wb\f3պnWޕNBjA]d`,> (H.7 Paȥ{1luOb#G{S3˪~S:5c 6tbD(98P""?H%tɠA \0`J x>pPJB) aD#FpD A$q5JذT\1{֬*Nz5& B@H[Q ј k:{KهlZe RkРjB5|tDB,j 6@a/"+lᆛlp1[a̱ām9 Y'3klQzj x[ȸ"t(A#J(L2$F lr)R;)k̠j @! .8|,$,Jx@>`?FlLe[Tt;rܬ EtNXcqspނN#}-28(!b`%5 "o=1Wr'0דi/Nzkx?AzJ" NK/tQIVTNQֈelq7]׎&p*x**In(42ȼ&s9jT ++B0lI+S[ F,{x~ Si\ܸW(ʴ6M,qKZV9;*j?sj?T " z!;8B A5DQFm}FFqn!ޛy$N4SzNҲh_܀]%7}RTq7\eR#D瘟 P@x ډ*V@I&w1%LxtH *bpI8B4+Vh¦0e'D97ݬ[}4i*ADj('dٟ`;\Fd XŃKE5`lhFPЂEo%@ 8RG#ց|w#SX2%%z*ǘ<))Ђ('>BЮxU`YTVdne-ʋ 4#)625K@v6}[P42ؠ%tœlKyDH|gN&<ӔO禒stDw)S 7:(\9esT @Y+ry e.g-DJcPJ3&0;@䈒E=)fv-LsƴRß$QЩNyJYLas1za @1F9.wP J} T8az(@R-iDL z\3f80;t9Y,buGӈl4Mc!ܒAU̲QFڂz0&%_ pj-{.eQ̩zݫ[C)f 4mkJ^vS݄~wdJޢ[؉z.eoy`>/ghM [u{WL!Z djİ0Q ւKUBl*q cw=<OL̪xNF"s0O(}z_=QE6r 8 Z道Ră!1 F~KejmvqRԠss^&WɭdM~$Ck9=P[D:Q)1`KT7/C]Qb.ޖ+fٻN12`&Z-Si]9&ߟ>ņ_"8eՀ:&\îveԦ2){erq6gTqjT3U7x&*zs6E}іPn%D`&[%GнL,™0PHx(j;O=sVJ &Nj|*,73 ِy +KW9[-ZM dg(2QXR0&,꣒:Jr3M4Ŗ r:89~d qB;Am95$T 6pXUHµDD&njul匣=& ;zisVo"cB? I7W#ow<@<&dLjG:I }Ocbwc;z:|d1nU2J.`K, w ?b=biO`WXsk⦶L42ٛut㛾黾[|v4YD <يuwwQɒ͒U9iyM5jբ Mk&` | >\v+8XsSQ̸|;uxQ ȅo)Gc/yTe$>=ϒh0zif.Y;&}hᴢԌD͇& } X4wUofImGs+kSp Z}"ǒ ێe@H}`O QQũ'NWѧ&c7xt]ɚ_=-XOf9Yb^.Sˊз˔g[]%³]MeYmYnZ%%ȹr@Hww߹r(1G#)LHYs$: L-2&/-,޺[]ۺR_Y?b]cfӱXb 0a 90D!*(xqA4<ؠJh%[L  G1z819y2`pD#N#H'QjJ%++\pJ˗+T䰁1mEMI1bؽk%UUf.>8׻޻Y^͗!x :ѤK>:j 0lg@%ؖh$ ˗(t&)Vppf΄P0ES%(@tȡ6eE]wᅈ'ua- cE&eag]a@k=yWAVl!nFP~iG!%q3Ҹ8U0t6UAJ @ U]R yTpB =LWYw]EfgCe֐[PN5 ^v `9XdSem֙z(A8d(ۚcCp}9pE%|Бp"tH:&jq&syupPL Ri LTNleW^Ti6PO妀vyk^VS͝zkeZh!k#% &ꉴDOT~#LoB+XC> ,6j+jM I8ŪRA|dS?"!@LRn{q>mXbQAT{>B\~k]fa^H`橮 Za~u=I7rhQ\õ.7purl{ jh&-D792?7SA/,yLG6Z~k%F)ySVhI g cX ЮuS0K$6qwSRŏuFB͸pxve.9TC!=#+mR2RڪeV\yc0@-!~4H|?*y^5EU# Y{v}E  sV'*_\;Nڏq "6Cک_m܆~Kd~F-Tu-a P,qld'ebͻhQJTbS>]Xx`9#n:`>S֡o"":Go bml13#Xr\a|aaL7IQMJdUT{=wO Ss74d),{ӄY8O`co)1=>פ2i` BapE;%v7)+X&Ȉ[ ޷ ֘(bJZ8/G*m7IgIyE&WHG?bi9!+_Tn 3 YF!vnT0 j׋_rf+X6F А}x$i *),)Mv{čuJW(hk#CAA|%fNEK HdTF"*2eD [Bp3A7[\ ,K,!rqcEf rq–aqYwB36| P AQr0 IyY^[=yz"is覔+T)7`0R1[DJ\ VVM#pLj]? kcFɇ Ű̊ab* c#e³L%#6+ &6BqC6[TT'`7Tz]&Hu#B֠ckBRC5Vjc?Eƚ Ț*e&F琶;*B *Jȭx[kRm3T:tɮvճcZ&8,Vyl2@4ą3kzdvEPU`Ő z*'i˱5ʶڻD##Ļ"j*dZ"o6FZ#708[)N d=J(CBDJ`eVY4j|Ik[P ); ~u{ ̍R)<<ŲK9TjHh2seyySN0867uF߻Z*hcA$f(!¨TM@7Jfv0q@` N P\R)(x#e-D#0;1۟3i,{Sɫ7"Qש9 0!sǧDD"D<j`/iS6-=W%WKkP@0 ;,aXqxɠlf" M Q9mU ˱jZTctAT=2b?XI{_*[7SJֆWlxɧ$yR<ΩL//1A?\_H_"ӛC,lH;SI,<=I!К ]IJcT5U`NRq @"cX)m+҉RFF[!;2ғC6ᔗX-C&uGwH>J!o=`, -h*֘C!rp @ 9XmΘ,ygH`J#)87ªעE=ʑw%ID\F)E#t!SZ]õZM==ەCSړqmjr@]q @gp$*]q!gZg"P3+Zgruҳ ׷=,^\јn~=K5&d;;픊a*kH.$19T.+٪9ĕ\5ԖfeVQ:>'@oB.D;^2Dë3LhP)LzC."qH4^=B|CU޿2,V jY"SuB pL%!rF.6#.˅JHqhAFٞ垢ᠾ=;2UxLCUu6iZWD=~4lTs{R.A.׸}GޒO6#M C݆Dc|{w7_I?]z >[~"_ZLOtOVpXt@d 5R# &? ? 6/_t3t?I(SCk+ƔTb}P$"_I+Jٍ6}0H%=] eX]nT߶HӤF_Ia-e&ʟB]}i/;ynó}NU +Yy1z@KS7N| 45]P?R(?UOd6ZO,P/b?@Y@X  "LPB ć lB-I> #8YI*G`A2DА$Q4ܬ 8e$-b̐KF2v̞=H,iν=G9sSwZ34!@`… FX 0p@`^Ɯқ2VaŌ-=WDZT)6t8Zᇅsp8gw0#' ʠO], 4D:fQ R*RiPV*^;|DlܹUg l1D03 2*L3 a@+.5nk# S[M*$!nr3(Q6>.󈅉p )) 3 &`Jj "0Jj#HxXo+*j’Ìw*@ 3A'p’0ppӭ5 90EQCkD?s۔ŏ`DKs)PTP ' ,J2 Nk3 hUh!'b is>D`9o<8ND7=@ͬC9P4tmNa$8ƊrH3"N"VɝbP_y,G+E Jl -1jp+0VL5 MmWpŕ: Lg |^ $ ƗHg;u# ~aQ 6DmQjUȊh);%(L*,dHagXe˲lisN/_ vhB-zD#4'2;~@"N`ԖäA:u`(>Vʶ3awG80_*C " hX|̱ƷCBQS{wΝ1=sA1N"}ZHF6YhgA ECYװTR*YIxL2հ" oU80LINFP!|`th4r"Wx?"`~Cv!1B)Ё!`[ukԌ(nuC eH[ǜb"} Ji& LJ۩T*"5+ U@\7;|=!e8x:b$ĞdF f0N` ׌4PAq7 ؼ5kR1V "NV<*-WDR憫 J{2X-I,rC#BIÜ@Υ=]2A eAET yfH#"D~S<"*әRQ,裉ʗ`uFf&$Mp4@B IxU<Bʮp=:CDD8,;Sw ėnDys[p3c{ԧ>uKȈ)Ы q*5T6UC#l;55QHNd`,;"]K$=@!#@0$AD f= v MO4bI Xaϔ!4/ MŝdvL"8:8Sj.2ِJ5Nv<P Z^'Q:HL+0SwPR@{d%(@Jub&PrK.:)(ܚ.) FAѤd=&} fx2uG#>Ar"?x.=Q0l&{&OxCULݝJwG L_\2LM<'! ,^ &\ԨSÇ%H1a7vin5~R㶓ۺᴗ֡IM0sl0ϐ<2J(u?eSӏPG&JjCu5u)J5}.5jƠ w,I#u[RSxM ]K:;sս[{2S:_3')OIq1Q (O-C'1s3<@d[`5V]B9Om]Pވ!%x}\_[$ZI{VT7!&b!7hD-!"p)XUyo1gzf:TI[aÅ @f0ـ8t#"w oD0Qc3jY+z(V͜4 8zWV:, i9ؖ9r:lCzX^m;D*콄RTx4[44*60V*˨/ mqPL6p[Մl$4:Dcnj{٪7s0o&5Jl+,M5 \OCp\MtXnl3dG-l uՐ6RNBT=؋Z.R2.~kk-Ӕ,$NXL2KFd5 yk/-9~*䝜?~4?I}'x^l,3Nhn :tˈ*3[G7ԾXk~gqa^NN74f\#e.49(iR]o&6^BdC JdD.8͟R[&V̼ QkbS^(73ɐF1ϐjVsFuk wӕ"HBbSŦ'<wJ'庭/acJP0x%bey3~laXeOickq^gz:2:`7eb1K s / kKXj'0Zfl N9 lTgwaWY0ağ0\5+V-/7Lo66}>p26d"M55ǸT{- I-;TXse w=_~[uK2!e1Z3R}r 32 #ǰ3{cIeŅ M^٪ͬW U8;FLcF%E}RRxo'Jֲspnzڹ>me5LPrAl հp6cڦ#]6ob]3 3ZʯrWQ7c~B wc&)<̎1\01(i6-!k'׻`<[+J+ר@PBYɜ9өh$/ɒ܉#(8Ǡzꤞɛ%cugIiɟПZ+J2FiDŽ2:2:2P٠h@Q(6`8droʡ_p B+d ɜȢ҉OYHdAʚܩڒ= g*DEaidXz $/Ѥdc6Q Y\:z.fIPP3Ri"؁IṁpJxRV-JL[p _^y6bjꌷg~ښZp `-6zAj4cڧcs1`[ ʗZ]:rDN` #KɬڄʒI.JEbݷ@֧.U :hGn:G* Xzw `e{C: ` %`EPU0: Rqn-J\HS\+3QΘ^Xڧ;!>[l; 0 Q V:R{8%Y$ɷZ9kbj keE)j[Qж[0q@ @[Kk-{G-ɩ)[v4Rhc _g!۶Zk\JsV.|ٷRz5 8IhKu лKԁ!=QP۹{ Ej wiJ֠ۚ;{{ںg5$J0=֣\Ǜ7Фibmר]o{Nˌ e8zѓԫ4ZމyΨհ05HܘN7}݉Zݗ~?*] ">}Z4 ɞ]9T0̝n_u७K KUk|ᾳ({-$#P(δy,S.^=` P7H;;0 @fPwIHqΞ6WQ>U׆`^ޑ*ԕ d~6T|1{anp.:^TpxN . N |OaлO?'~ޗnuv->`S PosB .cKn‹nJf쮇[R-߫lWo:6W/MP@>\Л~~FiVΨs(dZ~}ÝZ>:,Ȭ3OUnXtNƿʷ($` l3QO__#0% H :..][`i- UžN`e3Z_?.|_?H4K߇n>ROB NPN\@NfkMtl_3_OP77iTt!0no%4 @ŏFL .dȌXq̖-ViԨ-^!u$b)Uֲ51etM9͡gO %Zs.eS:ZTYߡkUzaŎ%[VUF6+RΥ[׮\k:V6__&a 4cHl-qx)$&l%˚4gά2hΟyoZNE[U񥹻F5yqpf|jwޜ]m5;o3_0(=<cKd!C."̢/02p#>:$Fjנ Ϛh62԰ +jJ9 )|6$DȲ*1EÊr jB+/Joރ/$Jo Jˆ'"@&4?WxɅAtm%h$qMM݊bCbjNqO>Dd{Fut-zG H#+6m'5쇅+*.@0sL3L)M5783bΟfQPf ЪEQfԎGu3C&R!SOCe "AJ#P Vp[qU%^ãm)cCDPl]ViQVE|VGaovJbʹ{t]1$A _z} iX"եNXn{RVx@).ZfkXs 5ofNN\, 1Xyw_:.5[5+X%nFL6z볢jAXѲ9)ԞmU^YIu5]&oHa J' 7phQz cz}:(3O~U۫ҩS+ee׍Ocmlha!pxg6R1/$Fm9Tt=/FeG=HaOj+Q=wݘ'Ci_6riJ>t!6If4Ç +@5 "ȡ ֩k 9qЃas)԰R羶/)@*FRר??PfCe"H'gd{5l'FRbE`rZ`>1}do=HǘA9$H|%83$ GȏЂHk7q$SP_8#DE%1AiU,(4TPFL@;eG96o%DN/H.j¼EJ ӑM Hj|ɮy$1*8lZ9O'pXݍ19.UAUU81YܢLlyjH$BuC8-jcLCFɍb`DGgAGr4RDT 5Ovs-eLāP&@?{yTVvhOoF)d*Ui!!984"El{ڢšI>iWt@0Ge㪶Te<yꕆ,:@ХaI-$3h,Y>*S* -,!PE0ۭb^L -mm 䖭ᭈ~US1,Fц픙L յ.v]AŌUE-Q7 {>ULbr'4 -J{=|pmmUKF=nW^zJWs)0qkx@Ѕ [L={1Gm)QE*t^) F0B1ȷ-WdA)L+Un!**焅ٓʖ _|\Tnqۉ7S1}x *>Yf v[Sf(C) ?ApKrY5ּbLs3>Y}"0X(;a^W^YKl\2sX ˁQ %죧g< C8g 952:D ,,A̐u#dB (7(gXV}#z\tq݈J}".@d4q#٪3``*RȘvxy7 X Txh-5U#>s:75{OpLώQK]UYc+yk1#x|rl;^g13-ǯRرf؂:v9а> L@rɇYewu~K^ߺ:0\K4:ðdS>B> ( s/(ʊ7>P091,pŃ9&`h B!* ,4N(?R¤@sW&B6R3; JyiPq3` {s⋻3Br9[KЃ0.PD7@[s I$-j< M-Od0ϓT+# yT+EJE[4ɰ`+^u°\2{(FcT[RF{;Ԃ,PJ6>C19>=(0@3u G& | 'd! Qw:֋*O5*ӫ151k3㵗3bHd'@d̂FOi,E-A0JJp?p( $PGhӹdLJLJhJl|UDJfʅ]T3²$Oŗ'N9,FsJedF#I,gfRԎfO\Dta@a%l.uPP LБK%UkS4QLLIpr?e$LQdҤ,1SS$RӤO05v:92@/ ܤ xH\@ C@}ۍ6^mVNa!ED؄mI[Gm O-Y=[ԼW'ۗm]C)HPhKMxm'}2<`(FUU6e:31uZ\R܍)]vfXN-V %Sv]^35'& U,=PNay^u}a @Z~A9D6;˂'ZKm,V#؜+v<紴i5EG- -bn>4XA .]`RHF5.5PM΀}Nja7K](0ӊ_bK"<$IQ(=JRo0SNe/bV[Sٖ M)Cut#R,u6倗q=>\b̀UץeZ@#)md4dm44dQOFz4{D&-{VVg2~IiR4AW굘囝^-K;Yp xlfg<>tƹlvdHmϬLx>(%7>}L(5fʼwxgy{U>e} #o=5U+\ً\`2c (fdv~agn봫6(:˒iTmfmi>Pp81ij%̯ jy|H)9jhXjvN^4>YM +[N׆nb>S| f3NKFQVxC7H,@DNvOx V]^DA e /EhN@6|\Se (bh>~f8SN>ӌsJ܊vh| f<(2%0e$VnDzjjʞwr@@efo wʢj(`1×%N QVW^-mvP+mU~if;h\Su;6ޠnsX+G炆7&N;ksG o`(`xJ/e /VYhf(?V0'N)%^wu spU8k 7DOD0& `KOdgSG*@G%s̀O YQknÎDGOPUZbxh8TMlwNw[Jr 4 @n$_ӶvzuEвsҳVa'T}f&K3-B@B1'~ h2'(@NN0Tv8MtCui(`JO~Lo7 JW-)Eϛs7YvyjVg'^.2\^˵NU r38 "I190GOD'U=gkgvafvfqc@28o<4ww`}؇} J}t _-ϔwp<@6^ٺR%OPY{- S|%H ?Z|*(2@@D|*p$ZηᡯZǪUV휹 *2,j(RkJ2U(sҦSn6f+2pH7nh[=pz-o?x8-؀ą'f`#F!É5l$ԪDB5S j"\tٌvbj 9Y %jMBRrړJpI\qbf6g1pBIP5dF剏y}:X2<Ȁh-h.$)_kʐC| B!z !\cV_27eva@9*16BIf-M;p .D9q^p1Ps!u'nD;!epK-q2_)TPwOjӄ\%Wh)~3e*b27H?D[&ZNU*m5 N-5ޛ E}ĭ<$eϲO@PkZ#UvB`z Y%":'!nEma8mh:ܡm h.B;h.=m0 k<#)NrO9!DŦPu0@ 89Jt zQ'#Yefԭnag_u32 Zd\  3S&)pP!4NCuR "l-2 vǰ/jpG3>q-4>8_lR}#mwIp~+2j/ hq W2]*&ƺ/t,_ƌ@iN׹5Or{#_O6<(rH3fC"D["$94!Ol R,mBA0Z;14imk~#3&BtL֌9N#pyMEEArW0\';$3kk!9gG8њ /'JPzlp]LkoMfEluGQ45Q*"@ F7 k&`3 C Ғӱt5N=ڊ>PlO q6e!ZxDrS1XeV` {k "˩#>qS+ưC@ŽMQiFm^|^%:dPˬ`U^U pY )m@&ux &_'€=QD6@ga<<7L)DB"L)"H]f@A4؊!xKG<4@^3`5)PԀ>7l RY xʬX- Y"# ٵm W b `2T:ȏCEam"7lC+DB$hB)#^a_ taTZGE]#GF8D9Сpa`Y"Sb$HY"Ə1bbԊdEprMR(^^ *b*"i4F DNZă-`"?t.l0nB$# "I1U#C`cjFpc7#`9);ZgM:S)QbPY␴ 0\i(m\6H$^ F܁'DBR 3YhThe^%+FɋA5TCLĄuڅOA$|$$"$$n|l fd>@S2\NXiR,$ř)Pj;ef9mNw؀ pǬ$UYα gH 8GI>Ѥt*\[n[^uw%TXuHC0(ʠ $ qH٧iTՏFV, & (@z̧%\'|B$84cA*H\쀮,FAt2xl6ЮB-Ҿ^ӖCQFN-LɹqLֽ |ƞDWأf8'5~ȅ݊ 2\ڠi< L%, AA}&6 |+&l<$A&Vʼn*j^kG$klU>0DTEUjn&KJyb]kKdN@DrW HꡖَW7$I+6zHkdfOsOIlZC6$&Dks+M/DO8mWmv~.有OURJ%ڎ'@NE׍ɍwk[x:yK B,Դ&/LAix"7wS3hOIsy![uL)Z5W,haB 6txBU،Y'<dH#I4 #Xrp3gƒ <Р4Ӂ 28zСOw2H*h )Te jĉ(kRq(Il@HI2eJҝ/JQ#E.4˚7oĥ|38tΣG&M:YvpwխèIݻy6 hc3tR 0u@P; -j4THjA^sبVl)zAm\TWu Sr@l+h[$g!RP4 OÇ!V{M!HΡ۲ȭeѪ*.&3*3; ʻ@&γK;+-o+>l"DLlp(s  lB KP "@9CC9 Qsh-;NA2T /:]򀦙R2R{ mJp@*ibi8-k8+ 2.1]+*6\8 2PбYO?DHEMAECE5Qu!=QR)eLaN8& T0saUhuHꔤJu'nKXf*6%ˆZ8c8,ftu5]DםwDGMwR4MzF"Har(ol@8S]5@8r 9 H.o,2Y%P-^"*dlkI̊6B8wvEݗEƚ{'#Fʹ&"x gE]bj'#1uAb>Kp™uĵU&2ˆ= 4d(E/uߍ`:8lD0ә,Y{8W~%U|'3"y(l wI'J駖^g.Šڱe0T ,D#*iVtG7pJ3J(\I I?gIq ,<ƨG+`:6h#ѭ1Lv & y`B֮U`Z ֻwzSc FZ'Qi^yBNs ?Hͯ];&REb m5 x%%lWSY!clK;YsصKs V`( puUXbXS*NY[tiu]V *t%/X D% D [Ce(DC%Ql[#[h*,(*҆)-?a PY0GGi,.WZXHx.qiAq*2IHъͣ!]mIv,0LȪ_ħNٝ> /wsP=j5@-l~jpO!3',AAE'>d{HȆdtedW{j`Dy3v7NaUeBnѦ*J\Qfn~30Y΀=;! [ a Oq"m}8qGSo[t%IDuN;1OW²#z-lm[`!GNAð =:T3BGz枒z*ñw:vE'W6qyo4%৵?[@u ot[Oo;?Z{İy>wYG:I %Pޥsٿ1q۷g ;Kk0(i Ho)V 1`+ˮtf,0' +oآ~4C/H cV8+(2伏^e(e0褀UJWZo+iK, x^-X AL.jC9+Bf31?T1(NcɕGcАHRRJ(@I8HTm'y*̓tghk@`5@IkIW+| 7.+1STjfRBSB1FL#12L* ۼ:O/S˾~%Kv)  h`PcS 9 t2S#TGTMk2/UdUC1BpJ</ ӸL‹EEQjm: ~J"1d.Kv-` u6V9J-Ө^=88/\4B't95`*R3^eidJGu_IJc@PAo)]:s;qFT`Caף=J Bb ~@ISlGUrtd|SbS5'/QVTUKYmCy9cGp8vRl&i3Dl$vzR@*r:`e`7׃,5-r). b b tK7Dpcv|6<6鮴d/B. !RSO(m2 #HW(ւvLT T"x)ED4N}uVhH8Xnoj$q^KYp džn`R%Ķ >4d~'RpKBULs*S1WxYx7_9[ީK'lh1cPx@ýb-Y`  #X@?+rG%d|AjQ$vkI]' 'c~OR9yVG_%휷mpv刂M nN4rYB.7{j_m ZQ6Z׆d/SB{e$K+),&2[p'[ŻJ8)>H7,MKoixC9hX-ɭ+W=87;p|ItJZ[[}A3^M~6I4 o$)"ל I[֫aʈk~K`ҳh`ZG'z'/ #)"͠mvIA PmEYAG^]j@#T4Xb@%TaÆ s8QEGx\AH `J4:լ[n v2A#Rd!D<2Ft\X 7/C jڼ94Q5<0hC`04|L-7ň{$WdEgEE9d$E5gUy%bev ,Tca13Yeeg6k2HcF[m2&nуq`Er4@#T ݀JdNIxTfe{@P4mWGW]ID }xZ_\іa]2CZ\h!_"",Xb)VaHeݣ;yhj#m;Jգ|)@%HxOt%p?C7J.ye85 2 _o^ eEi]E@,\C %_TP(餗 nc(*j^&{j+AL*&Aob5%F R@ aw@ӎym 0R@S| .Tk@@vy]N8!o*T8[Wc(֤ c#vp+"0qo1EA++1ABB/ %>8 IJ TLH-4> ERD%4{N?-R&' vDu ZE(;+#0o38)'B|wvV1~o# >yx90&!{峝78Cx8-'(Ip+)&E?'O ;/xg[p$`pA6)NԋLNj ^' ,Ād|#^|9 dH }Ui(ғa vC1=D< :JSmc&pg8DVkJ)RE|=Ĕ%ηJ!b$"B#,R&d*ʵtpg=irY 6rH#Masթ\x1*Q pDDS/=uȓ^>% Ҧtb@%oƬA~3g]$r<5ACEpt( b](DB"I.z¬2̗j+T0Mt cNsІY7hU6bVa RzN9թ4TyϫbucZ5W{OZJR,+!i--,#3)gӕ+Ef4tz% 1I MD@n(O4xlgY% eCJkڍ AB#,ethnTdm=V7̪V3UR6?\1#s-qXPOQQ0[/bcw*td̞Ų-Zoi "R|-S]`shQ0nJY5;)~nϵS%CYJzĎ+*L1|Ls^{v3;,[Pc4q_L^ H@9S&\,;rU,J\əI6r&k󱐅PPWIө.u76wl당[s@}"Xw*}ufu~ȉUzQtaPS{Kf*|ۄv*%g24EahauApK2A}OYQA2ۧ=gn~_Ji~7 ~jw@^I*Ivuŀ/N8b gyfn ph(4/EK"FQ?DRN#2Y%SfPVdBuy:~ :x WQo0D*7p[Up{v CDɁ{ +GgwζmOe|&w|$ hS5wZ"!\iAC~q0;H" yYe ͐ Ug1I&WPK^g%32Q?XoQqp&|1"ъWg;5:= F[ӂa17UAG(EE@o9Hy pBrz;IB+r^zk(o{9_Rq.le]g1xqxϑ%w1jby|¤-QAsx=Q0d {UP T N N!9XIWgrKЂR#v6@Vu?:) 8hg"wA~`RXU9X/6q@Q5RCrt/`QncYVP ~in UA wY#y>#EhRibGvypҁY]3%xk8t4Xwr,)RsySyJ猆iYc&ė$'%q{Ib%bVŅ7(>-fI"W:8A.@)?ٙX B9wLbivyb xI:J# [\ՠ0QO\MWס!f˖!Er%Q8$hX]D$6:rhᴚ}?2 ZFjuL*#N (%Bĩ\\X]13G3L^-- c2獲cdb2L5@dp!@P  ѨR)CemU֜'(rtVኪvMWRg58 Ц P8 c1@CGC5FfxO\J]?w#QG ֥Yq=5J{4J)MJ4QFmYLz'C1:& HcTq, o'  Z";ʱ:#nR;IO3کa%EMΒ%V7g!ZVb-cHY*74CA6^ h?#ar0\%P-LJ r +Q`[cN1Ym$׭Jz8lC$u[g_֡=(vZY4&R{-$;ˣ53%NJaOrٺ{F1Qʭ^kh1nU Vc5%Y"ܤBQ˽ܲnҢiˁ>C&E@Fkh拾J־q7xa(SWzkɉE?ۿ:|+p5a'!X%'Av!n'-WDN$G1VA!Ta;}7%(|q0 Q"­&é47\a6l9m(|FHQ,SfD,E"Eqƥx(m1[‡#PC.8^Q~KeR99[9TxYr!*\ }u68$pBla` VZOu\,&ɦ Qɒ)LgޛuX,Նc]>,lLFfjcoLrz<[@3tȂLIk̇Ol .@kZZMgW' K—җXq,Ԃs2GA&-57 nlSrT}  ÜМ:S""&%HGApFܙgg=39} Qh&]mM[7!<:T=HXj<Aߤ߀3\&#x U'b̘.PLט%XfձʲdY^ |[@ټ`E)ck|^#4"7.MWprLk9_-^ф8`4E.{'kNuQ,]\kv,%5g'OPP"@H P$e A4FX#*WKF޽v"R%sLq0MY22sH 00` ҥMFd֬C8;Am3d*Q7;ofaYNJ?{sD&Q.9>&NMZW\(%8T@pSO. XnmZ` #خu -+/iS8Ja*m4F$ѴN|M Emf(8~v~#H1I~;'h;)k𔂱7TmMEqnO] uEVz"d:vI2V$JךJfqasR}NҫڦuܪҀ @tdIѪe-h@v1ќEw '~/%BeLh6)?RA֤)F&2ңdB#*Rau 8v|3y_6?X$JM7%X9"tWxZj qqKdi 6M$ | YUCҘF!Va ,6[*Sx<DDqqQkL=]|VJ$Q-ı^f(czlFX"P#ֶY$蒟x26 XI{# @\VQbYe^!R|)bBrd?/*Obx [BӀG, n5ZR)P)C@XgRexSِ0o E 23c>ٓ{Ĕ)a%fFP~4ʚgۮx2$ x@bLw# @.u <]*פK T4tAB$cCO|S(Buo$NJJE c2i MO 2+Sa J?TlFr vX·(eAJH@lNgäYֹҵz:2#R p>EbB0'8!= ҅@ 24!2TC%iQ IĒtsňnw[T9\lI7Nu=mjW\BvZt+϶:rōluݱAߪ.Bj7i! ,^@&\Ӻq&‡ IHq"Ĉ&خ]ÍASI/ZLl0cʄ钤m8qgO'PH*uװ<\IͧUjݺ$Rx`Ê I)vʌ Jۻw_N_o5N1i/-{>-43bEox2_kLJ;]u{WEzkbWpvwM*lQ#^6(/s41A*R1H[:\'Gj]I CF`aZTFMrE96[o`TSiFӌ%IA/L`2LHEaX#L3ʎ\UhhM*D<4˄wt|L9EO2Qg?)PY2Iht:Uh*DVj<9 ]`Ԣh?z]_#Q:@c;1ӺJ[_A;f @Q.N c_ԧ>TQfAUX՝Ъ.ά8la&Τ4\g( Twl8Wbtt%.(O~Y0_+>O91:{@UtPX&},daJyY6tUgɺBͪ.P+]u&i*fq_I[!ќ茫KZM >UZ}ꦑJ\5!Tsp d҄FօW gKZ/={IÆҙ}ԢPp@ Hr|عΫܟY6^XF5nk^SJ#:9ZpT;;Xlƥ|qa 2ӰMlI-֌zc$~pd(9(IJ70^ ͆#16\1 ħ\ey-nl{9fm6/θV,xgz3J~(!jڑDGGz54LNN.CӜ!MZ>>,jm_:a q^}jֳ$iWayu=p=aAlg+yt&} "3#Ӯ0tEM?u;M49]0/Kjo8[N` vmxTk8x83G38qj;ڒ8;Mvr5 EdUZ4 ޜr#{T'Wb399 <IigBRo7.iKgtX>֧kv_c/T_(nq߁%M t*7!c WX!?ZJr*ӓ i?m}YnSEݫ9 Ng@MxdE ɤE|Vu7@P}v} 6EmߗUZʴGP~v~FjW6qokӂKgCjpkM^14'E@WUxZ0gʷp5 OP  PG hyE6; hA{DIf6PzKx0LeO`8⮹ɫ/hֵ*o-c˟jg6Hvn˳)s; u20{K @ [lc[C۱ZyCUYV+pc^ۂ06e})Wӟ׹y~{xJt۰{[v J[Qcz+;U+KCCsNjrhʬĶL4G} k`HdEKz+J+?k\+wEck+ﻵ-jid~۟sGvH&0YGpI z;۽;zǴ{cۋ{ʘ+-o*{4g6'p >d?Wp[E|}_O̓QLSL 5&h~7clFS{9,ps\u\WPc0;p:,?/V Y\;/ʹ[j{\Ɉ;hpG =sT@|[Zn'utz)˿<:18[ ó6{̻P { h-P|؜ͩcJ @LKu8)el~Lׇ6M/' ol#p֌ ۬ʭK T@[櫆X0˺)2Ĭ胸5l =I`8}W֥\ی HQ{_iԍ$;q|ĺrp]- _62IN0g}kz[APrT<Ȇ ؁`nvN'8ޒm#]#>H`j]B\Dд.gH(Mc^;8hK0Dp s<]jR.Jݚ{f9۾#P>`٘mߦ/Q.L7RZ?=kh:*( ENT^ҁIO.7S5Zz2 P$?L0~8BЍ5_upxm|E>SP>Ğ ί\ m.ә %p@J׬-HQht>X穇V<~ңc}ε~^FžH`^_a^8]z}{N3yMN->^b躎N(?ǎz>_ W ^ - > ?%ڮ+Dܷ@Hyw~y&1\Un}XdBhBN7ж;_.#xǽ٩qp+=8jί'k*]_0hPOjO e5oqOs>xO7@\p_ l5L1W+>ﰆ)Nwo +^x,0{/T\ڜD 뺰@C$XX2^cСCMX:5nX\GލyI)U$Ir%J|1eķ;tz;y$JśI7bihňQNjիҴJ̫/[|PTx[ q咸 )yP˅ +!L8NF:KW/ǎMl dnn(ĥ3~ٜȖ/Uۼ^љs gmعRLz3iUSŚukׯaǞen@m~(QbĆuK/ŏAܩ,^#SerB!J.7sGR% ܊mZZ Pي+K,++Bd"갋/0cQhAO=+>RB4HL믤_ 0J*)J,'Aj <&0jP+4T 8>kѻdѰDyQxhhHq̉#C/0c2?Z2)G0KT(8=GOKk2B3lar67\9L\!Fq YtQD-&\H00%V\F 5TU;ܷanb7ud`C"P.A#REBDP$ *J~r/ZPc$a}hab>O}Z6Wu>p=?flE4a3W@g4  0&L^b%2&1#STN'mO u y $3D|L$,E%Z Ik]*T'N&Re5wRb1zI bpYz, 4?e8OLs$fWr Q -gqS Ts?HzCfLdIy t)|W(Tmn)P ꬠ$J*Xib>HB{b JL曋_qTȱЇ>r \-pO3WI,*"a W7N 7*KuKxKC.;qSZU jau\hԧ.X" (GM#QP*u-a}娱]A%E$ N 3gdz[U&bkPRi["ިcy-l K$P-xyEMA5tSјլZCIo\pY S~PQ'ѱ䓪a29Bz>H{ٓFQɞğDBÊDs..:UX=)4>؃=H.'$r4aS%U` >gM&hYLGٙEY`$a[Bu*GRLLG\,O$ pZ<jPZ0} [0[xX0!s[Te[^e+<8 ܎ >A91Y]\Mɭ\,SGWͶ "DJ J?#Ɓ׍Fd ۥ] @ >[5^M^%NRx X،=S4 -5>:e8Hd>p7}VlL-ְ34QV_%=#XW G'N8TaE|(@Z@c^1wY[ſua6aAa[hi|@a^7YݪM4E<_"VL#Y+;țMluO LpTb-F绁$8B}93V0``E^;F^.?69I+c&BBD=$ݜ9aAaU<`Ȁqk!V_ L?xvGKYz;'T0XWG(bbBDWj%} @|𸞐L^ ^2c9%<i?f=Ѹ069()賨^m~u~PÈfuZu^NT%v= ju6,fׂHjmf0.k账hh 5^VZceh<s@V8_ilȀ A"vvgSO ejVf\TT+>6ȃL;wܻ3F 'o o}k BwnNlPEր㨰ײĒHdx8x ^rxjk =rH@FrW2HQ\Q\ &7s:`fk`=nfHBg>2^(2,>-tS %oDQMvoS҃w`i`~Ix@p4-܁'Q\Ɇڭ ~9=ok8&NqF fw>?$q3֪lvX:q_>tObv,8G>=⚥]Ӟ_Q9xz9 4-N1ھG ( /7N ?fv,^En(Ni-wVDsAPL+u1ڞjefL $X4rЂD[C' {-8G/a[x瞡15'0Nƫﺡߔo? 8>r?fASM_D(KY_#S62 '@E*W4h\*|g :̡CĈ'Rh"ƌ7b<\9u"ב֬d RΜ,JAC } Q.M ԟ$8` >hi 6j5j1>|$(z8ٛ%,b JN'Nb3w&nkSpP!C xDBVsS|w5Ѯm۵A]C|W/n E#$J-[ 6sD;B&H:*ءQRj)׮^nk?"E =^}WZ(bܑ'$"AdMA)pV1Ur漃m5˜@p p- 9Di F\H#tRJM2PBMNߕ^Q)Q{DmEWYYbn- > hW&T(H$wDmXC[o}FL"BԂ1Ո"Dlk#pQDIY7+Rk kGsD'%u/ 7e^~P,՛FSjrTWZi[UQpؖg$'fJ(h_:wQ! !beAV %Rݻw 'HGM8톏(s4 1@c3! ;4{(%S7%E{l;VPX$O^eT2`U//WhF 9Ŀ|SL10dLFE@BSLq\\ )q5T *([m#-*B8
lGdsɖ2tMKY@vUS,t%|-Tv5TzN;U{E}//[e muf1 >8Q\qE7N1 炴<%Xp1 B[Vt5MP&cuj wRB0 iS񒧼@% %pһ܃\e|"%>tb`,Y [8әI~_70P@`(F@@B4sp4F6i|G;l̂ĨJ(IRwQH4e!oRj@|Ck@K\ROO+Gdʜ)>UP pÖ0I.7%@@)6>,b` \Y'6GQ8918Cʣl:!Y g%9`Rl6`IO$pD<Ԋ{f++ T+;t_20cO&W33LRd1[CFV VmA}LC1)D> U{̈nR[# &6P/,!&=ZO8o˚sTßÝFqɊ\)`.Z%R;ֺb׹,bh * T{^ܳУwĩGvW+1 96 mE*aJu| \j&6P^+_ymd0k6bLSJǂ!iӦ΅l*  œ`-)gت)EDSrkW*7hkjqC:5WDmuDBL NX*[:9>oSoU lq_EӉ `fm0JǣB'TSv#:|a=Wb\MOQD>tv{%>1Ūy+Z դ , ?"H\2 \^X<bLdIlpdñYt9G  U\-AvcĶ+܄ڢIr紧EБ%]]7-t˛t-]l}jkW+-mӛvu 1V \b'P]Ve JߺQo^>Hkn3cu.;:!d*p% BA#tL]b >CQ՞D9A)OUw lZx یYpD]VTx- T8p_lmД\@^hA' ;u'A4AA"\"!܁3GFQCH:E W 6I` |uDiWR1qڹQ@X1q1\є8@#ғPB[뼎 :65nC,DB"<$xaā=:b!"", oc ]G$N]%PR&@ `?ΐO) vxh`5DUBLVM\$F-.@Ml Hх9!AVl}KF1:T3ކ5yU7t;I %QC7l+¶5Afk@D"DRUc,bV*DpVqWAXZ②U@^0@MR_>1vu\faǭ^̡Dbr!@ddcpL-A1'*}}Q5v.| d̅kHc9XoUfm;ޞo=XX3%g)Xh@i e^`xq\j.R|XCxq@\ Xdݐ'bOšg.^IjYZ̧dLFYOIC14*MhIe@#4܋ 8F bE(ąfhnh"H:HġRo˩(?(A.ɌhZ &A@/ɛx9U h| G9@| aIaH {JhXig1Rf,}i˱aPnQ;;l.&DFkdmmmzmYDXd׎bٲQP@U xx@h a[&Aր[&ED6n:L !|/oxN/44T@Q~&.\B"D&&<BƂdt Dn\(C݃xHhm*p/AUGYb"[f.LVZRضqJQCŨX` nі&ӕoo< L(Bc36#<&30+.&D I?5= M t0>ĭ$$]S@O8kFG,6NR蒋BAʀ1 Z0[fsWACKA6i#lQO(q4B$8([T,$C:psuW{u=Bl /( 3!u` !< Vwq3rCl&$Aw<RuvϵIw7}]9Eژd1Y ߳U=2=$3:x8v#-V8yQVҺwR ҨV Ít-@U@Eo.k9@xy_j&gAA &бO$+|A>^@\ݤ `5\8w?!6->4#3ksU@|OzmW>)4*zY8֝E~ f.´EX` 4!0m"&B B41EZ@kr:s)@랮qX WKB\;DkǬjz'l]>lE;ӥARu֟-"ca65ɣ|c AA$B,|BAmnoF_p#tfߦ-}!R>#gԿux,U ǽ qq@XrR٧HˋUL=}`HG/ῌfdyX @ |d,~,`Do,{#C櫳3nS7m7w>~qQ[X5rQ#Rp0B 8P 8>x`䁌EP9mG%6;*@1 # ȣZ cOkEAG| vN^1_ :_Y;w !Ir4pH `djqԑ4IdIo2+R@HPc2y I@%Y2(^Bg?U&L44sH=xrBXFN=Fn 6l葞LW$qP*@DNf#0`GFqT$%YKV! ,R a B|f)6ODeJ,ZbFрfԥFH'wlͪF $̈XGJ$ G6b7Јo Ȱ$9Kb26<=͂ր%)Ezjtd6O30;=;:Є1Bec>@G,XpzB8#Sdr"#im!&0B4`#xNrrg9Ware.\'qΕYEdJK eTe5pTCOD“fTI"W PkLINem21p8 Α49W *55/0R\%>̑>6iLm*=:S/Z*dfYs5tob#:%?n$ >յ q&[$D' 9s|C,9ws*b E AtZe-[jgYr 6XVy^fqebG!+dBd7a:Ie缤N<'`97]ѕ?knv ]z^D)k/޲#t7mR:)zKԒf"T1` /=lWA%" _U P!h)bDt{mj툭`%@?*X %sZpA6à`TcuJ%꒙U2MeK ưz`fM)0I=独c ַmrFL'$J^z8KBF'f!jTXt`MsHh*&]O{VTvi {õy+ `-! [ޓڑ5 ڎ9(Mit"<ՐMhDpxF5Jzǵm OxB>p_ztB;nʭI Otx'cm&k8oWzb^+]Pm3 9Ȋl1:%@!a [hE-|VX=˷\ p'( .OS|lq1GR I2f}hh7=@#]vC D1X/b9ˤB]4xq*n@dMV2\_;ҌJ'^NB#.$gpk'Mqہe ͞=ϡܧb|O !xG؂#`x9TZ8جXˤb; d|Io.%::.Ď/ZѮ(/*m, yo9LkJJ@CoTV)WͶ/Elʦ%]f@4Z!Epʬ]yjg9F 7h8ZPKϽb(hHc#4 XG (͐ʏTObp&,!Y cO΋ qvNHm/ȴЌWXkW#8«fD|pʏ\Qlo vQmQRE QyI!3H' iP,/8@@q"јdFJzQZܱ,Km&!jP6/ pF.  R a2";77E;016HwD5M5- Y!j@Z[B5JyrJ%[O6u3Ƶ\5]q$fC"dx1*U!"޺# ̪,ζU3zNdIi ~pzB@ͯ Ǯko)\w)R<ۨ!t#$唚4p $o`͗)qIDw[p5U{Y" 2czB“o$ۣ`HPf%2!z 4pY~K;Ϳ !4ƅ,k!8w=<6f+=87jn7Bk ot޺򂲃:y$c}W>"ћsȑy d Gxk̶n}=}]ȉ5^]޸Ujؘ'3v灂V]Tla@M3cU`atDmF%FvG}Yƻ:[Ud! \5Py%ɟ='_on)#3{GX[|U `1wfG}P}gɚC+!@ X.jAu}蓬ρ|bZ:~' v_scܨ9fw~ Y }_k#>cҦz$hcFk{Oߍd`e}?o +${[k)czRD9=W@l;у9{7tDa&U8Qsz: *"S$-TXH%+XH$k j13l{f͜Nsz 4ϝDw;u뚮KZ5֭\z5@5A 8pPId nݒ̑Æ^9jUQ"uŋ떈+p%>p A m<~:5S7=0=L`w$IPQcWxߓ&!1DiΝB˛'h=J:***vlnCP]%\-XYa`#(c _}`Yuvh6 4j$l9Zn$tRB'옂 IDQFAR1Qr9ёDXשu'!L5d^?zRN=TfV_YfZu dIr  =uug\7,ע &bmg9fdPjj:'T7PDYF9ǑH#oOTpNPArT%XZ O_'yiN=sO5n~ .WpEVYtVw6' h@6`+7j'iUZ8\s;Zm@C >4a\^T21.]5| ,5ALv$+^Μ9B4Mu 7io\V!~+2&QRR%,Nt #Nx|؇$d ; cYh"f (43%u ,#XHpꪠE8/BT GJe{D5%m%*f E }h28 z@!!#GJ@41CûJf3iDPR4.2j#B C1$.x wDBB%1'rLHXyI JU-HMVB!;hT y*(uh8c-\JT'`,| ̌NdGs.)({P~9u,7@ZBuxdDنoWɽ 840/aSuBxo*5z2Q\V$F: iGG_?f_ v2f;:)Qy3L@fF04O @%Gttѓ' K#d E ۩2$HH3PgP>B81, VƵ\t]%Jlk;n::%. ;aqcgkF*-L f:,I\k9LBJ5ɼE/)p `NܢGtJnMbeΦG%)b [Xj@ o\V5uMRt 1\( d@5 'uI⬖9TpUf" <RxKǴ0L n ;*P𘃭LB9yZwt7ifDYvbʡgeɫ@w+n $!ArK*и5Xj[,\"NXgKӪ`>$Cnw˵m ݦC>.iğ9a˥ 6T2M1Tu!5`WE]}h(x46GܤG6b1㚁p@k_4$2ԎK  :Yk(LiG5*C7PNW+TYSelk{qE2jW|Ɩ"K/'[|?;m@q`V֤VGnPhl&Flʑ9 ȫ/W0h),eǘNP wINJg< w˛M \q)0[e8ڑvɣ6\+XݏF^᧶wR^] ;c$txQOLp(6E"bC7Bun5P/Ճ~u8QFuX u؉^'IAgx'Q/mOl&fXB#rΗcN]3S7aOK֌(6G'7#.qAwnQ/[P@눓!SHh{ȀD#|5b/((8(g#CŁYTm*64~y5O&ő>TV&WPE0DaZgf=E߀먓;#Ux@&Yhs }"7M RPRIm( UiͶ[gcPH cu@yaǓpI#[XZd /ex |]%J>(  H`bM~gx)}uy"m9*sEn6t*X4_49)`C(v\fj=SxkZ(1OIwH?b%u Y3W !-$i$Ogs tLևtT:  J6Q?cahfYQYw'/'Xhg-NjѕLem(H70svy1i2"xH%Tzq %-hVؓ ͐ Rѥ p>3hsaMrGŁ﹐F$ps( (%D;q|l$Mwh)qC!B(2AHd'~1Bxi>`5_&9DVf Ġ%X *&*s)x~⪯HЗ/|T"`%(z]oH<|J.r4&T0#xkׂ1(*nv0syV"9'ۺTiBHU3SZVFJA0 Ю ^Z{9h$}a(Ư{($ °y<g [jiOW ,PYKcYP>{YÇ$1erg}5+Z ;h{%0t*vCzsGDL1 $}OE%}6DIꤐ({%EZ ^5. @QvY|jz Ʈ{!#9ÏZɯr%5"0"'hY= f<+*3 0Ia@aY|ܸ!Qx(ͭ4P{ v/)1ut{&eİn]:hv0 Csqpy{zd&_"3&P l@IX-]̸Z;s{#W[ٟcʴ7iº,,0)GsjrW|( zKC#;87Be 腬YT{ 2ݼXcӊK|A-(ehEݥq˽ѕ',$lQ Ê%Z~Al\ ϗM5.T U1"q쏫{7wYg5U][r&粊~ޙ%<|wفDZ;ۧ}'niV^a!Xx>qv@a7p.^d\[)*k">N\ȅ-B;hѾ]((n9ykb }(wt!|5"VY^^6`>BDDQ@,Y>1V%ȭ˝-cޘc1bNĿO&nb l )-KS%PP.޼J49TM,| # `#=eqe|G' ,*p'X9/zNJ~P7^^I^4Hρ"`]P3DIl%LP_8.%^oʨƑ9\bC7T@*:dD#.Ċ#><Å C(cGG4&69p@i7#д4d\$ *W|KFX9vx%ڵ޽SwݶfRB^{5` *d8 VxkY_̝|@a *E:Mf, ᇇ7rGO1"K,P(" (?K@N\DAe`f 1.9}'ѣ 43N15|$q 3",;,Isq . /<4J 1R*^(̳J mT5\s 椛!vGߞk#Tx4F Q.誣.ʅPȎ"r0c<*JZos/@1k`&d*@ A\++-d:GP ?TH!DD#Q?4XlU;F)$rn z2H,9\HTU$+gWTj&2!4-h@5Y^v"RA^uuguP-hHZRE`NHXut,G ; 8F\ ɚU* ? v16zA@#ʂLjbc% Et(]dlHQmW jּle@;F% BAsK9c"¤$4 Cz&rF`n5.ti QBjbF3Qf,*U4R38:)F|2+Ʋ4CVٚXŔHfK8@!&fuuTX:GQD`$NDSj߂Sz' IB 2p i]EA (^b%ޕE~s@ U 2Iw޷Tu^0ƲI1Jj6?`eaKWH <@X2)Tڀm㓬iRЃ = NP;bErYQ42xq ԁ%؈eFUU|H;EX"} FmMh _)7Se3L 3#"A ^0! V0y𵴝 ZZLtŌ9.V& hD#`@"AI|`cVjIRi yԎ=3cj%0Q8$-9fpűu};%]s&QIfI][ b@B"Og,sD +}i*q_2 ^)[J1":eR4颪]cOEٝb<㜅NuYȬX4-EXL)[% ]Uaԭ֚ `Ӎ0[Lc`E"@p r(J_*KMtb8 2yCg޽&Qޝ7}39r3v5])K=#^3+EVe6'G(?gw( wyэnlc02H] 8 (4I4٣+WR %rpG(*Bͨ;,!0(A!qrn*>[8]q>z qị8*ʬ󫁁@u zZ<[}I0\ncRE($ 8@|I7:( 8봻1 5,3cp1" 07A75k At;9E 6QѪq+y   (`.E]%P@0aHGx>(0)$Qb6 Ɖ4*"(M.Z"?̗ C;0 ǓXp5ҩKxX6ȁ-L,? /6;9ۀ81-+FS\?Sl9j AK4"=q+8xCPGž lKx>mœ!\i2k:~Gb{sBM6f~o T>]t'<~kdо -pǐ,8Ąm +rMz4Ψ[zab> O& .|ѐym^\sPCv2lZ]oɎ;R\?<ߑez =ES+]E`PXIEjv:VHaT5UQFUW=ك]Zl/GP^Nz}ci H_EߐDiL#LzdG5)>eaD]{H Rɤ;eNcJ+b0lڐ:#:Hܟ5'ʤg2asUD=iTS!^S} ꗑNb2^EOM7[U@F(16^PJM6-#:&KzbIM4h5f5z" mC*nS(U^:% F -;ko ;SE3E*\ *QklAbNi5ɢ :^Dc9Jk_5)2ʾKRFqڞS;՜ct9Ьt%ܲ5$cy [yу 8>8ᆻm!G8z>G,es9F#5vKI3wڴQ<| ylk265{,9]lQ!ۮµ%f6E}C Bǿ"q!(DD!P&0aq5y:aA ̃D¬CFqeϻX@4zSﰡQe(F1  !9-5ґ04-KX u6' ܝ]8l[516O,F2@:>&T8ֲBG(=ϖ<X҃$$%gDrCd0y?N&1 G;bV.&Uˆ LAh4-a(2^Ӎ3ٺ_pSi4dHG;'zf3ɀ% jVDb6Ba)&q'N* X(NV"8i,跢<3>5~z鳆 A)T,YLS9QdCjʋUo0rHa0 0)c,tAAΒpedO/s;w8^u_Fab F>2zBwr/bK@jRyiN]3mC+Lk*b;G{ɸ︇h ;}Zljƛy߯C}d%;O5/YQOTE,&7(Yb>qu.jVͶ c^cCL|";2Ԋwˆ4߀c[ d5 ,ظy@[^wx˻ƹ}礊6"i ܯ.aHEP;8ݸ;qA&KcU9[|77)oyr=2n[lJ+w4cA7KW6+sz]B뒦3ec8h_Ysz4;3*KVz] >3|PN5vew< h#V0Lyo^vї}caѷ9, ~@?=:;E?u2]5XU]|S n}}}݆z(@@~zmzq~n7ogS{2h{xMU _l&F F] x]7 ekv"7[Ev x   rN8X' m,/x+b*$Fc4h_5bɶXkm9(2^h׀ ~AhXEwtyw =dN VzS8~Vl#X &xm&&fx7y]ˤ2SOn\Uvb1vT3Q|؇OۣAf A(rW ngj#hf B6s(ւxkl:(Zx$7?7eNb凾؇ԅ%bjao'*ZtͨY(`fg&y'8OܨerOz={XUHBw7X_7lcg5׏:2b傭6Y~z)!~#Z7OA&pFW(8af/Gh'_d$X=zVJH8xBL8W`8tD D Arn0[qvGSdeVhhTWtJZ%>aUqCXfd%G(`7"KceKFhQŐד Yj ZQuwFiOTqÕ)q Q0Ùg0S>K`7߅lCIz/Ep 6yəyU ,٠9kɑ:4=I>yp gX7y7+`gQFxI {9.PVy iefdT^ިJIeyyZOWUX2c t6_C!YqygBKE>vZQ*W)qz"۩T_*^ =&=}e g*pk*`n:xu uw_sʠԧXtC髿YB Ffʓ㧦2e`F6U:v0 IQxz{fFzUCR |ّ9ա$FfCv~E*~ `ZcП=~n4Zꮻ]7(I !˯F8AW7 k_nʭB8: Kך_ȥhٱ g!(& )Rײ(z\^ `FG8j f6QWй˭ @ }g벋#i';˲Khb!'vuz0ePI$l'cJl݇$*3|g{u:<<# ċwcAقJ\ap*Q@łܼW,ܪ  6L+ +K:,#[%28uks.ٚ4FeGy {06ILńX<*XŧɓcLG@<ګLcS8``Tv e.f 5JSpUr; N)ܜ17̼J(w{͊3PX\ZXj^s'jov\l%&L,D mK8t̽f ד2z-:W ]<(='-::[Gig6,7;8,- =.<H~:7L頾P4J-##%J"=L y&loOq.J;N= {^nu.чާ#~^\ #@}=b>]8J OK tꨮ6)A.HHN7UAz2~ l[N(a.ߍp>mhԾ{K>Vsn|>~r.l~Ao #Q9DH[^>`&\+`k.OBxN1MqਤA\n,4/6 P[NBW|K Q?}_WMz)Gl.BnL?7,uDd:s/q۸>兏ⷌq0ϣ vX1wPꤟএPJzejĚD-F=~tw@q$SPaȐ˗+c$N'N`GaD"YHɒYcyḵISMΝCO$Z;H-]SQNZ*>YZJW_%u̞;w>ilƥ[]v+ͯi5 +.p$OaÇ9P rr Eu#HaLDYLk\73QojԸ̧U.gyղO [^tκE-p .[^xpË(P7Pr K؀8H D# 2@"'2>o?h -`n"lABNڬ8O*E(=G3Qԩ2tZjCE&*Rp5.i&ғSOOT(L &SV`.zB!2ou-P5 vXiHPLvYf#׸@͸Za[m2G7O!˭4ŚvA7+J"^T]U0 U -`3," ) VaqX&<'E6JڎlCƊ[GzHȓ&w+&gL@& UYm4쳄|A'urW酣\iǵn mfkSg> Gm^nt"YIwCgUeq"qF)?r7楘B*mX?EwJ;uS`7bkf?iLwOv% OUWj: :H \ӕ FЂGp{6Q2> }SU?9ZG}zp,NX.m9P£rOKF@,@A H  P Sxx8rM-^לP{*\!n\-Qa EåP]b#Tm[X'%Eꐢ'x~Sx lAXpQ"`BڨA82ѫ^ e/| LBAd9RZE^ő dUvd&߱INv? ɔ L%x@h% |' .;h 0HcN<̰)2S5 M|]Ƕ4)d` r«l` EtRgEFZD lC~N.MӻkW\h2;CԒDiыh<>4wO!ьw"KW^]b*шzKHiO8"(IHXCCMSsfUT?hW'xh7Ֆ;i2;kZ)< ++Sn[f 5מӃLas 7,Acr]vm i/&b(n9FA׍V,YjGi}}Lmg`d$ Q@.` ԡqEtk8[s >!!8FU%Tz㦌7uVc-;,~'*(i~c'qk|@#(A WUP+׫6E //~B2>= }ߔaV1 B bxB Bx wUټ! q\NjtG7C5:W"Wfm{ۦ\ 27%ۼFpbJ&">-ŽuSpP&Of\ )|&vbtB؇R)Ly4N>QRX:+rXG G/2.}1t^I!n#^VT)PXb֖Bˍmm;7D"DHۑ춡"76H=|OO^#XI>d@ׅ;5Z1&aaɖb]DvXR*&0 X@1laoyVF"YiU(q槣o=4IwꃯX|_[~,^i o, T캦SW UfQ dvpNRNw_iB4`a 0 z;pњ:WO,?͒v^"'%ԥYW:۲Q+)$xj(3D6b`aCTE؃K=hA.#;#?kZu!J&%<:p2+7QX38ZK02T,r2%@33 KP`;>)7>=xAHp3AiAlȳLAuk!:BZ% 7 y*ܲ0ʲ-2JZ2:3 >3d9\A;(`A>7p/x)0.A$<ДIĸ&FNI,-J &%?Ң,l٫-Rd+RI# 1sE3@}tCbEk39;cD;,`&`F3H[sFeaEl T#%4FoBRK܆[:Qb,|0XcGMں)z*C|@}=1ENpý8dO0# `|H 2iټ.4z3Iޜ$KBimKMKurYKUzGȼDO(<򻘀z :GxR8L?(8#HXM'Z(ؼB"ƎT$+ Nd-BŖ-˄:R5c+ǘ)ڪ1)̵cƟ,Ђ 1F j>gROP O?p\#0؁{< O*?H-űdB1fE"K 5 pDS|=.̶:(QtIyN ={jL|4DHM騤t"&:QL t份c H*h䫆2 Mt:=i,}-%0ДPi xDۢ+ۢ:Q+-S(1C?M@eF ,TЪXTuXLȄJO4X#OL(PJO͚PEQHeH,m,:zcYZk\`ν,8EVYd50J@  <LV',VoSB$UDm*LMG,O NŚ]6Y" Ym2O%U&K؏N{Yvd ق؍m=ST:gp@HY$@jQLEc44{ۤcTJEH#-@NEJ$ qeUmM Nw(rO'y:]-QIS\/٪J8ڊز]ݻMQx@i] Ρ1m PÍD=p]fpWOI !TM*@Vj͉ث%IbemXx [ݢM:T:-Y`P1kuQ ޻,E7<=^;W(݃47;\j0r#}_MU%U\qmRx 캡!)+/4*[ ^ 9=VQJV^@eL[4>lʭ䄉daщ%I:O݁%Q!nbEIbK?Ibpz+(8c* |5(Ua3ep =99a@ ܡ@|fRT4>#}X/HνKd oHgu^MdJeFQb,-`U8VV}:5`]\^ X`N@9۫ ;bM%8f4=Ђ=EsZdSHl%@n+St;b$!RgZgungK^BP)&z{`3OTWbUV\^V-;P]}>-ۇӀ hˁ?2eQ=pB+fLX>FT@؄[h߸DB7mdo#hpll(f*g{^jt \>[~[_렑hȽ:ux833[hkn`pfZfGX7 HAl2nv}y.-9g2 j]XӖآˀ:e :ݮ LejE=Zjxj}u vh+4p.A9(HZ0l_&o b`j}~j22Y:-voI֖[ w8cfYߋ f>c[(<.822PFqN>BZgnt=lbUVqFU "_#&r7cMX*|YKS&聖efDXu9 )xtE>ö$9P=Ovc9&rqlN@*1WgE//;ۍ+ f c5k_nBWeUP/C4͂&YBHB8 88'.*fM΀p*[^7֐}E `7]6`xԣWja;D+H p S R`0 Ad+XjΙh"ƌ7r4]}hRɔ*-ZkҜKh\Ҭiʊw{nmJ=AҤJ(A7REa* %T|Am װ -{냸 ƍc(T6*80FlGJP< fS6oe*Jt𐁂(dHau R0DDI91'%-#oʕ[qMoRRs=Zt)xpxAFKhςkaE6|O>,v^~_vb5 6!YeegRq@vje !Al!fЃqPDI YU^{}}g1&\ 8V^_u ` %1YR^QDi %ڠBlՖZkbɋQ#p5Gi nD$Qv 8%H5 Wb% SǕWWyXyb5sb z@=٧av*C 0.!Ԩ[ A[0DdM6ÉQ=$QDC3Ht/\c*!*PN^ $@1{Re|"հ ֲ>|`g BIT zL[jB{!G|G=#M0Y3 ׳dp8S(d_s6 'D sS<1,2ܵ[+, ɾ^ ]p x#l39g_1 A#p4F'4a"R1T#U-s,pءcf}}A=7ݰ@S @`~oee8Š\,f*WpWEA#4s͔QaF!ԤtFA @;vڑ|5qkxF)oy30Up TA+S5e '  K #xd U!ӘNhcQ@TAp»(xdWOБ|ȣuG;1 NkS!rX(셤4m4aݒ4.}K tLCceRPVv91P@`BQ3Uja8A *8n^+qcǞQpG7юll\ Al^p|ӓDN`F* Ianwဖ0IA"XW2XKJO+ 8edtE]rt,Ը00k501 (hjd:D i 7c*NF49ՖN|g<7ψadh 4ӟe^Hce2,3*ښ<AbG9D:Ӎu@B*\ac.5bȔ#4}㾤NC`٩| uԩmLG:XԤIGlS*ϩZ-\Y>UrKbۑ} dRշ4K: ).WnuX@_[n4ECxMa B >pBSrBxmQ3x0P+2OazS![Җn~i Le)Ac+f}e .q5N{`#$8@5 I ׼NFZ UxN 4<bo\j{,gC$p(68J;  [! j 1p)_]4b>JDQu,ˎ 3ٴB2Gw)!9 f0'Ӛ!2Zb#BV_8*. Ua>_LBSh@~<0z#wsQM~3;-h%+ӀhiLǺ?zVZ#zVHbRRsezpQB̫ PE-޻kM$ǰr0#hFmik,'^frD$ 6o4 N|a*ח$;wb,G-}! IԊMӭ1x ''-׼:p,~qoNC{!repw?M-A#H!&܁RhE{z ];1ȂՇd$A,;izӭ/̺.|R`qo2PCh'aver hy׻G0 We[o$95T7eGGIx|-*X"!̟nx4oD0^AS:%O!9޽؞Sak@G@*_UjX@ hvq_hqx@Um\ DLU'MC7m5_A$'B+|{P}HM nMu`E$^TTck`]A Řs ^LLH* @m.HU9i1HP0bM0RvC;16.8#.$܁dAfQ13FEY8=AF&k6#SP$>@`]E^ x"T YWl"` lEP(FD=ѵ`Xޙ/noI'qB+L22c3^L^?%`@N  E\>,Xقi:z;[j^G!f MjmBn ^%N?b" Z(tɈ РAL'\hŅ$u'ghآ`nzRRd<L$XB"c^B-L5]<ʃP&`B)#8$Ԁ"DlD^)Y鎾l%R3lQt#^%t x@>WV$@( Z@@@.%X(ȉeu ĴdCv_ _*)z>]F J"X$BѤi7NMBJBƩ`Un`)郥jOvB$Zeb pbhVY4lʘ [f%G*%^" d<ތyzޥEtAL+4e>i7tC,hB$D&#A\$+@uk1d+=X?8`lfżkX B g0WAZ Ȳب k"\n'T@_p,^}ǢR*eH@vħ>P<)$B$)hB"A0"A4,X6-6XE҅Vmr~mA:8 ؀ Y (`DgN's \*^$pl-njbH4<38b#&/)\jB(.LF0=]9Ci]0|*>PMaj*~(s2fX\oq( %tb/ NNU%  Aj.F A Pff"<Lr&&D@b DBCjك^0׃wpjnqdGtT[30 puU!b艥^ PYvn@ !+h)B$o1A$ǵnjOi rH!'%""#cGރ.IR Zq\ g@wvrI_+Up8!6FLATف] 0c ", 37q;1C3h5c(47@TA 8s^c::p;/D<7$3mY}/wX PL qr'rB?D@)>l)ɅZb)cP4dMXHtHJB$JIM[ fÝ/mRuk~"/99DTs [u%_%h˛+ ߿bX-Z 6CE\D?\$3k`c@ 3'!B,fA`3݂낛6(R!JVj6kz$ƿ5[m۶ H1np6~[Q,ؕ@jyfd @ ` $AB)`BMewW&5YP0{{9 9|Ϸ#Cۨ}w~b=R *F28)7\d5Lst,o $@A"hށ"/@ q'g`lg8y֌09TU5GAّ]?W ܔˁh\h/vTDA dey*ULE l'5AhdžNGA7^k2:ˈÓ?zիm(ZXcxo)&Y\ *2]``؅&[FyUGc tـjAg877B-$Cz=x P';Q&iĢ۷3^m0֩o)KXLBmkCj/yt]zTߊyմdO/"8si{9/^9Cc|W~U) n@mCkqbe*ȸY9إ'&_AmZ8_>!`aabsg>W6^z'-ڱCGoո+Talzc4Т . |NĬKg0Aq2bD)VwcFz%;aH#IxǓ˕Sf3f gN;yhP0p FPQiԨ8px H^m`V]zZns0D#FTo @(qT,r b'N|Mq"CApu[BqG/ uY\JٳA^y%̘3kxp<)_RmV+ݴaD+ln[ hV˷fHaGɈki*R([F246r52p -6\MhiEQ{j:148 ưʻ2 *j-*!+;,1B(H \p!1kP" iN9r('jIZNu,"~dh=&ZmɆ.+֯lw.ՌͰOf R褌9G`lQi4gX/s[8uW'(oR*+ul+gPNl'(;DQr֓٠S8#8-4W}#, Oh{ !HƯ WaǫiR9PX"mv= _#6hwJٸ~L ϧ.hgr*.沲b|F4آIUj*}VwdgGrL=-ǶB^(e)38@1R~iMqH)%moWn;.NēL\VmYhPaVo걆2Gv,F pU"ˬN:'B\+i``0b~AB.NXs+>KYWOyC#jq֭ +{P91nӲC0Q/^+\Z bRZ;CRfK'@2 xЄ gA#%0dLd0lά05{ꘝ@N 3u Q^yKH&J+qJ/ޓL %A9"eFp M0P8 v$/tU&ķuu;י2J9.^Iq[Q3W6\ۑN)20Pm+G0Ap/9 A~eA ҃$EJ5 #:)tqD줘;FEރexF1ʠ Ҡ  ,px l貯$OAN2onC.ؘH<%K@**-le0GbdHx P篼6p .l  =)Pz@xVްnEpLOĚ.紌8.<]Х,t~/G Fh+-0FʂNJ04H k DmnP>$- c  ~%~ .I`Ty<̡ /#a 5 C$pŮ/=P)@l~LF2Em+Ѯ- 6:1 &C 1UqYXquilq0w͚WѼ03/$1v* -~Rm6X.D.b#xe }%9%&mE 9V1C^by3)S I|2dz<3uͼЋ+9\>_ulU?mǾ@-y@_bАQxn/'B-t0e {:s"2I;2-33s6EE)4SF~|r stxJ s.nk|-[$i+{2l1J1SK Ԥ/(;aDIT Eה<44NQ=SK-L"U,k[k*hf (Ce-hS8ET930P&\񔎋y2HLr1*YU_U8NKl5OoW,u)0WyDR`ǜ [:YNf⃔L>B+S@K\Cuy^T?b]ݴE58b5T,uP2_*_GEƔM,ZtdϪîaopJb 9bx LLJ;T2UdE[esNuhV90_G>Oog3O` VКM;>vi#*ЎwC vn>/JbkRkS1@lL]SVeWUٶmB=,OHEԃER2R#A@*s,>@oUxBnf;^:uQ0Sp"+#9Wjsks2YY2@@&ҪscZkSQ.Fd[Ӆu~Մ:vQ37DƫQѡ9P&Z,WoXn@ˀ`تyy$+ȦT[[8.CP:cO>#GTAh; qM٨;'WzETo²9 vyu}7HZP&\d>N`W^WqbI4B L&xT4uoN/o  M;pN5[P;C9+^x;G2)\ofz׻[|s<bR)ݢnY}sS?ŖlyN_ DY3<9YfbfTgy5guGBfHQl$7woY,1fj&@W 9\L|)<nM)3sU}A ytܱ㏵T#՜{'{v9@gF!y%SP:U{OW`)gLe@Zkr=]]eFV` P .acMF^1Cq4B0פi/(jC]EM>wSZT t]> S ץP < Vkg~3 hV_2(ر cW;!6h3`馝آzJ[* IK1M6ժӓ5M%ԕ7$T ۥ:xL],mq^}Gr=Gر2 CbnboEG'5o:7p<*$Ig-evYuRe7u5RKp\uIr[&-6q; i4J/!:q/o-n]Fb>f۹gImPIS02&w7 N|bewV um}h2 SyR Z{.N /^:~vf0)P$ 3#IN&&+R(xiS78= ;زBH_ζ%[Ń}.t_p Y ZN,G =XUrR )0y:y$ex) \4gˀ dA62 2.-TZ1E16S4HxE-#*[rtwy5x9d[ʏuK8+aY B6IqQK)>('@yV G2h3u1hrDP5X"׀jx )KiJ@TF,U8p嫘##ITGG* ̟P*YM40dYWLk/;=ԕlF./,!#Λį tL%ɠKAQ-awZ STX N~3"A 0OOαn pX 2Zapy~r2ej#\ҖZ\asV)/\ 3?sZ b$#IEHU=+WMV P,vu_eL5ALò BRԂ^z>$뫓妙 (wZnW^&kl&#XV htB`DpbȨFgATsN%ăJ.""Bkc|`FP m~@dMKZ'Q4$8'-Ccgi|zxoSEM; z;ɚl P̘A29j}6|=mJC,8HUi:<Ӵ7jP2 pU?h^ԩ[G _2ѠOA} Mx>xgS#HXiXsX׈Xȉe+_p%&n@F 7bUՑMLRW:)9 nnKnS >G*8z4fj7B&<Y f V 8#ɚ,8hYe5`'fiqgۘGL9%xyQ8Ate$hBf8ߙ= B@W:M#byמ"!~qh` 簚П_1}j3. 9_ڈ87q({;nu1!Z82X1|顝B2QG 34CLIyw]&FTi i~MZxQZT>C.Eإ"M_X$[Wi’7mr:i톎̣QIGᅲsIM-Q\\K~EE))*:@(1gX.3h]و&<c1GȄhȦBt2QxwjnkjMOsb%/0pK9pҋ^| `0aHgXX,a8$(Ia h0)st [w*C9똘?{1Ӭz$ř4I#K!w'\.+dv*C.a*(1$> wUeIE { n!U-wEmG L"` Vt:=ABw5h7uT3R8tGb*~*I{Oɘ tB7?0 &뎷DkM_JQq?jJL-,F;a}Cfk\kMc%-f<8tKp Iʟ AJ:Ũ9@;XلX{K-9*`ih%د1Lw7ٴJjL+W%}+iK:2!23q-8 r}Q>@\G 2N!HZP,@܇Wi\qqoPb$2{Pkc^A,*dw|<&~ӫ$;%C1ACgɪ/ =, S"؇.T^xxLhX*-AS|>ڑ}H ƍPhLeAwńj@+iX#ӴsRamm/Hzk{Rm2ڹӋs$2K̍eHa7I+̇1\!DQ'ppYptHʩnhY'8O~zAm0HU.Cirm^RxVLTXQF ?>I$ąޙ- Sbc`NκbytX9 ɇWnB3 Mx `ˬ$6t:Qk?mK(,'$t|OiؗkfeBbкM8o==c ^ C)b0 ?94Ro5*% ),*^Dj+li{Kϩ|eH[9B|L-{Itŝ-,9_oܤSI`)P  vN'r`ھ )oA@ <0"E  k<|PB2\@<`A k@RE 0@Le~<R={VAE-!QRLYuܵ(bŻ4h a8PXrᡕLa#HIdAd"fbT8v ^(0GAF@vLcͰŊJ>A 4PA!o\iqb1`C!I.@,!6N *@;0[RQCr+<'Nѓe/m!XT-"޲r00 2$1A0` ^$c޸FӈtXnt4y eU/ *hlyq D'T"3ԓ%h`$ՕG1"Y\!ė"uqļUҔWeHUC) $dJ  A898-,6BV<b;GH$)d2t;ũЌ e{·P΅]~r qA`0X ! XLPv I2"d֐NMP/&!@n`QyJ}(R'ֳ,'5|^뤮n%/{b*R7M[׺ڕ]oOt7QP x1e >`j*A86[dcdM;h"o#-g? _O>``q:a Zems˜zKT\#ԉq.]oxR@+>>KPސ4˭*U+ؐ@:PaU,^n/!2NECcolF3|)HZVLeH܎h@9jbWqq!јreA^W\Ni Uwɿ1k;Ru)򕋠 ֕X)p _M@bhF7utҸ;iC%nvCV 3uPH YIRp@'mJ`i%[AqHt.z_ڹlROʶqAw$(7ϲQ Nb1biH/nMe 7yn6DL.ȽnpWˈX'AmH.T:>C,1.!?.W2YRj_3YE h2^sx=?&sZku\tEy ub#R+MzG@ fc}Ձ{ހ,7YS$X@ P3'ᑤ?}wDGlZȽmGG Q_?ſEj JUKͯ8;0#ѿK l4 < @@)yDTA41؂/'(! ,^ &f)\P!;v"JFqċ16ܨPGܸI )2ɓ(OnF-cŗ524 溛)ȱ͙{ (;u5VhMMCvc״MxXiLmiϪm4(”OL˷ǿ6LyHq38㑕 /G[8ӨO{L}zcװadO_jܪ0`և=fM< ?ސ]ЧB|ɬ\m6m!D z!<)u{-]55|xF?o!#H9~~ 6 i[huCMKi6mv0@'fjWI9aꅔ͌48fx8^XE-B_T-"YdPxxK2ThVNM>MJ#5kq %xI%'76ڈcX;7uccJ#n ;FԑGYZ():)+q.gXSM5;r(A{bߪ3ynfƟxÛQw&hE a8 Sc<_%eOR5 tUrC}2İ_Y圉=ms V. z<*P!JhA 1D&"/^ e((yc6d/ľXLQ!f!Dt6n;";J98QU1Y[{kPx]`?76mFdDJ4ZXdd^bcG ޮH!GgH$R @dMMJn|$'6k`Ls faRzBڈc:y_Ql´1#) QI8D S͈Me%/ GvvLLc;T$I m{ӟ=mm萤Pzlp W͂&ףl8F&NfyE h:f^G{VeBp%iIe#u@6@.&o0 )!ȳ֫3Nyxu冚TrKײIP8Ⳡ -h:69ʱr+q1cc2 6c"tol\+ nU$f9~"qk*g: !.{!jvs$r6[ ѺW iZd"Hćpl +/^~~ɜ=t7 “3Md/15bBlt[#|XߩɪSh#c8Fw0 `e t^])+IV6 F:{>Ȧd=zVkXMzBǑ5!/O䞢%tL mZօ.* eZ&Ifa±n hC=*-(ȖWlG᪋BXmbBa8:Qّi[%t3Qym Tw,׭%૭AfX:0uً6?rۋ3W&)A,j7voXɷՀ-nrfEmbl6wj94ohg/Wp ur }^#Xh{ylک6Q㵫xV;-`#0]r+W7]_֜~6qnR,.5˳TML9gf%{)fWX0hP`} }$0es({05,.}᜹sv -C =TijGg}I.=[vr'*Mcnme15WsjRFu;)U^г2FɇTzFmTYW4t4@ e*nZ6q37}u~Idh ~ t l-uws |SG{o`vwge%2^y|bZa8}ƃlrU v1+TTSuԂ 0z'n' 8:{oaM=BnRF}{'Մw.Rx.TX}T56 YVcHܗtwoI Z_Kev/X vxHƇ}ow{87c8\$<)58Rt7T8R^8 8hY$xdgLÈ8ZNt~ur̀ tHw&}x٨f^HDKM1E[/*CN.FYM8e4@t Xc?֍޳g}?9ՊCGr$m (m Uj8؋5w(q ]vajBx㔐 Y=ITB897}W]MY|WlXRD`XGn, Ƈ0S3k[Ite<>YX\ԓ WY"BH1XJTq^JS5Zɕ(%g@ Pjpfu{UIhf4{7<[5 —>yAj\uOFf ۳|c_3+>AI3uMXhE"aOUk9X—J㙚0TY@3Xb93HssyD JOq=J@(%'i0E`xНZLfɔFPyhg)Xwvɷ4ZY9Isi{B^oIHwxq9sX׹zRVʝ99eI&BhvsOӛ:̺{:T9dm*PF:: P $:azmc|A*ٗJfF}TI~)xsߊvZQFQcĨf a=دyOxa5XYjx7ǰ7W~ަiwH[D;vJ Đ6$f& ({T[0K2+Kٚv >K@kBUpD{˫ GMtIP+zs˪O_{DJ8he`i@A;Wۮ0G { ]j KK믇:] ٷtPĶPvn{Q@TcPFpK Uę%;{)1{3;b;6dKi pHNz5{ʻ[@q r԰DꘋHިʽV+ܩMH囻`Ev; p "I{ȫW`{vп0 ENl|aRDjs≮4۷ѥ&f(dUV#'*;ڱ꫘S8<6Xp9{6iHkG|42׀vljd R&*INX-2|Ǐj;0j-иrܺ9^{fh|uX*@~L<*P>JXʻ #7MĖ̺KbڲadJhiL CS|PP9HYܹRګs9+1ʼy * lƟL͝< ̬͠HgH , P#`JS+D Eset}/h?)4}Ӆ]-UkmN v9 #ԅ#(N/H 骱5~}'ZŊĥ}@~YBRo6NN.FM*FtVNq傽庽))~+ƭʱFu!(Z [?R ^r^GC`~ޏ^IMPp <؜ٛ욡}N(hw0Jڍn4O~T}] ξpnٮ* A^,EД| )p9NH苻v:Iv "nt  @  M\pR ra(wK4یgLf~g$OI^0:ڍXof` Nx?. +ɛ[ߝ /w_ST8lfͯ[|O$?l p .T?kO)ocۀtxxAoD_f^܄_Wş˝ w6Wo kl߽ 9 8뜯NG䣏ڕ_=/u?ȞA|?|/_O$JW!X`<:тŋ.u9Xa1ɒYڵkE4WҤ9t)ULyes1eh`۶m<аaC] 4PC 5 .$S,3 ƉE-nƘgkQ iCU *t;8{"}gҹKFwoY[ٴk.7 %>ՠwo_čoLdcli-'nعF悿jN{m tmڎZ ؚrj6bҍ| B#,Rk9P.@N;3;H!ӌ#~f2-@$SP5{JR@js2#M$ ˯4k0DF|.6# X L6/FŮ Գ=Z^>0!}zrQvj'QJeС +y&1P94+`E |sVl8xb> @C1LJQ"f+rMW %OR;LMW]5v|5Yi39?Hl .߂F8YHЊrŢB  Ri/$lUzھ64k$r"3CQ]vk ^w;>U_`V-hfVX{b$Y-&9RlVFud5 + = _>Ttӕ՚kvYmֻ)}= VZ^yErEdϥ&,0ڭT+xlVRx۶}ePv&y_0Uti㵹P#Lh |X Ts~1~pA5wL%7N;q3#%9A*) Ph:u ^\mF݋р~'({!ӉMat v гN)B8FQiL$96*ӓEyHPH'T6O}*N!h@ /Z͖U:F%aQ'L;&Bp䴍>3YY}NTJh!!Ab840Sִ7% `uOԧ+bPOѨHg@ҟ*™?c*6V<8%A N1+TJ+-hH.Y삪e('A_DtBWauX-KvbiL6 s:5]8KA4>%A8|-leBPkzhld#m9 vcY$Q"7<ɽ%T\K#uN.|N+ޤ U`/4E|g##xQ |(6 Z`2 *DYrŀ$i p 4?A`D"Of&vwݩ$)*<'ɋgz,o=*_|Z\ug*`!k.&ʣŶĠݙ/OmT *%܌TbFs)<9"sgՠXS MBu2^ wp;6h.X&=w5-T`x(kCG-:ʓC51Hj.YZ6'WA7uc3r( '*1 Ĉ??d. I.@7x_B&`-'8k)i@Er,14n1Z9,u[pҀ $x?<1AB9[#B'CBBKD@,X#h#@r@ˉc)C$C3$ A1"*z=K7*`s3(K(u{dC D?L:13TӿˍiI0EJG,D (3Z8b-$" \'bEqEAEYLTmEk l/ts*v#U(dLF-yK2CɄѢ8OK[O,: ϔO1,d7TWi R׬H8HlUܴ8$@ ɸN[пsX$O J?;DL;\N%kOfѢQ >/!ClH juCcd|< ʾ,RL:R"N"B̪1+Sr#8S1 B=E XmA6geC|&ϹH?H+RVEY7Ҁ`DƻHL)M8ARBiLDc$)NKWpSB@„_́+\jj6#L(B6ig= GϤkH\C,vvAF#?uHTE9/"3-zM,-D';DP8M)Y;SX8B0GӜT%QXjȆ)Xum1Hu(Y_,[X%J /t<&Td|<3?[2-*-:|m-DD;P.L1n48R@Xs%@#6|Xղ޸Q嘴uo].h}UӨ-Yfۃ M2{așcqpE<^ًKKMCڏ<@:dB}USΖLfpJXDv@0bݐݩ]v`UY E!^5Rx Q^ɁX4:T$ּȁ R'Ņz$68l$claTtN/WlRKghHfnl(;`hqגDB0o9hξ> 6h!/Hն1[dH2jFm^5|F0(UI_ٌNGm<ƫHuM=dy+)UHb]9B09KlӚiMPoF;uD` :>pm f Z EpuۙX9gYq 9vqU9GHD2p 8FFR^C e 1-)G9 eiV+W_n뼙 2;v0 m*c$KuhntAnfE_n˰Zs@HHv@XFwnW^lIQ0ޅv Ż GqxwxztrU# bk ki_W'X^87?XM%h/͕9VvIazv~!;@$Hn_yjlQ)MaǮDbN*JKju1c08Q',e?v^l5"ĀYHrh hmliP`ORMTf9U)HV|fA f@"- C$}-a*j4<%CM2xNI8`q~Ĩcׇ/4P`&wD "^d.^ۈ!18ֶƷqd%;{}ܣj :ZNAS@kcχNB= yQE@5!3qܳCm[4jz V+;"T7 $|!wf46|)8.Tq Dp=vK,1D3a"b Sl< 9DtGoFۜGML@ D]Ruɛ *yi^]+4>!>_ e;6_ UmxdxL n}H@@H< NjmގX?oe@@q@ 䀥TG<pY# EM LA* `!!B" J;.0CJ5/bفh "a_=@.FaMT!K:m2 Nd a׼Ɠ{,o@ "!$"D"X9@bЕ$"<$L% %'OmC)"?;8d3xA@\$TAp Wb.H"Z;0[e #"c]_$H"Dx $DEķXH`:Nu #n=>+mATA3 #d-LC7Ġ; B")b74C,4AXd  dٗ9"J…YIL9D/KlW v (L!Pqt[8v1eScu!"#=b]WKT$x“)$C6C34Q$=c5K]^a~>ygbrYx\cҔJfae.e2DNGȞ iOJ hD̹H}oDR'(T T 4nr`#feu%LAq;t4B$DB)),B`>iW t'd\4aj1fJV|B`\Ɛ3F5JAm՛騄غD%Ѐ"ޣ5b$  3L'3+HtB(\)Ăx:@ d G.&*.BnYҪ8t@fJS. ؈m|l9P]Ƅ::ĄmOPA$Y` B)03|])|B(lB(k,HBt+P _VA#3\Y .,>lI*eRlZ,u!DTv,ꁄɢ,P6`8!mTޜ؄Y%9XX#,#@'LB(.&$L AWm 41؉=X.-󢧴XcGrmZ R8往)%f:@Н)+l?Փ|mt PUrlMxn @A'h$8k,"C18=ڂH*l*H>oG=2_eZ/_{։XiVn`D@ FUuTݓ8Ҁ@= h'r+&o}_A8^N0 d b*p 3^{,LLnjP>p#S ;ZLdF jXk'Ă/~n"tB-@8C1q +ױ[Hf pblnLO@EG!gM Eԩ:$!b$_J߶cnOM8܀k )%خ+# ΂pp0 H  #13d6mLnR܆EV<-Qts) 0BX\`&An&FITDԀu>Kae,o:xA#s03DWE/EpT)İ"eX O I*Q0hA ȳbhb M@ 4NVYjR@d-X7p0g{WL5=dzbuVVWFW_W3^@f2Z q׎fmTDYFKF;<.`o9#"M3Ob:͎Xvw:wz]lld^'X"^uy}9lKֶay:ˡF d *[E"GyrQ*ojjӬvvl\۹w`s(=G $d|Y p82݋3|3 H:KJ; \zЌ@D؞&)nM+i|CA!['TTZ*cj ,}|+h.λ!$m@H .L(:C0lA-h B.QDsvI }*U,AD;0CK<9YQb+tN9O!TI%m벀 d'ʄs>[HJҼ8k4Mct O,|f9,C$9B,EשH[DNFFx P5/]I'+""X!RHzvKj!cQk%1<MC>V9Cˑpí S,R\ K!àvZİHH0s+:ka##H2fi WKX67!7oeq2ޒ}i&LQ^Sv=911 }`@01`6#:l1`կ@?' X!C:|$$; {@ ƩD.KTqB7~P)p̛ qV-KEbHÍd@w 'G9 maR #!ja(BA扬s 8E2M4UpQbxP>pBہ{.H#xf[x~$JAhཻ oay acd&Q nbNFDBB-| [Xd0s4{-el a/'`k= r S_3-Ԝ\^$[E-i {%H("g X,%y'&w3 F3@ЂP(R(g.':a*`Gg0dH!6p !%/,d_JQF}BJW ԇ`"":SKFH#6c A E=j˴Ԡ9uʼEj&ntBb.I1'ZuWWS;q/Y:>@.q+!FW63i,DRoއU:;bNO^2[^HgYvYI0F9Z5'-~{ֶ~df4lMyuL`001>s3MNU~ Xd%+=2{$|SeoS~H]տWlg.uHh\euj4KqYUڤ9mHtN=0;O! vBg5RhH2sڱ w?f^TF־$k2<XsNvqKŲLq`մ^@W\J԰}&W:iO}gc?BT4قNDHѷGWUQ L[br CSxnOi߸vrt01$\%33YvR .y:w*A NOA6:vhڤX)%-XZ>ӭcz/)@&F>2Ds7HgO:68+P!Is mt)LJk!<,˝^G3דlLPݑO=tIKt/yS ]!6 'G ]zwR)/?Cԧ„UPètG:~S|WwEani4|˰Yѷ V5g5g(kY?Nqv\Ы?A@]h bHG,l//B4hVOu&}dU(JcFcNAJҏ hr// @ .v$6<*Ppa~D8ʨlмУJF7!*#txE j8*OO:y*y ͉dIdBfBȢ knd n8-|08Bt>j抮a .<(j#`#j,`AC qB& ;qmqqJp# -{0p@ )s KJ'DPtnejgH#j`gl +nP=p# ^ >^}nF;PA>4/ ` L`RqV^Qf(#*`ډ$ zIFೂn f!' rQ "?,q o_>RLG/H,IRb@rU1Yh::b䲣h#od".BxH :NګYr'n)&ɢq*S*'*%+0B輲,C@5SLs$o˃f$5`ìoJR1~hkH3TNԲn'0_PŽH U M̆o*2y2#r3;G*qG4E$'&6G,?2 a3>j6$BC$ kjaՎS/o@$2td(x !mEB**ѓG* i=r&,? 7`6In`Gm ˱K 20cQ+ :#B.{QC7:`).FvTCEE<_FCF +σtı>THA5F) .c .PG%ƒ{2bY/K]N -Iެ@tT_vB Rt G{, 1%P 5b.q<.3PD5KUk0/a5VM,E5N}g֍>YGQ/> %YfTh?U}~؇Rݱoerk@ <#Vo|o'E렭h'1m4jI;K!_w53ϓ`Й5/4 $˲biLbrJ`6,9@06RR㭲{PJ24*SO $ҕc 0c @rz dG;e6R_hN3s 6iI=ř$6RU$Ųj ]XK`L6)$M1W**.SRf6CU5:ULM;V!']&r=g6ksϲnmbVt Ij,ߴj`6Ml.L>lOww=\WoT9ە]ݯ+Dq,dRbzzs*WHjn^8s~#PI@D 8,gФl{f虀+4GĬNKGbP~qI^kb ;);wBƭ'fEArE|gȹ@Q|h>+ j,vQJ&ǣ%J s/f|\ɳ@pt0ogk9f`I^u bT6qX-:DqMӍ=җX1#qj,IG~mAq 4?L<(=nsJohh-` X墁mO(+Xr?ؔ"gPQWXX6ΚY '$ }bZjXcs~#Q8{̩qf)ou{ ~`uo׸9Ơ8c:vI;X{۹g.W⹄K>IrIe:;v >]|׮ JtX`x<"JH,'JSZ0@ Le fAc9*#wbar{5is=gH:4w:yw"۠B/ ?IF j\!XM0'4G@']ڵ{ 2.h_ڶ@&Z.fp3{yz>Ӆf$ucMω85ە빹+V>b*K`. oBeٟ4b1أ_[(je\cڷPW{3}A (ה{-sWy?*M7c#IĨ_6*sAy2{d߰=ˣ@pf*1Lm.0I?nKg@j{Y\m AClC⯞~E^޸^H0NϞGk&IH^\>>*uE¬?5yP+faGȈ%dqU|ʮf_[V;׵vsz b8κhah/C/ pC bh5j #o7nir G*v=hT6ū"_,&,r0KsaΒˉ`=7J"fJ3H7~lΨ9[PCQ&c (\a4cMK RGXl1 ADIw~ɰkLֳWNB>yV.>?9q^t,mNl8J U˝j@ݹ+GE엣]0Gj20gx=;ўBf͂# >eKOA ȥ#EF͹jǺEu5]Ep$ T7+>m_exy#6B5 %zvjң d`υR!".a phaxx30ILDn qAE K5 MbXp &!⡠9(uH;X2*! A9VIŽ*ҖCJ @H^ݫF5 ΘZm$g!!$DP8OjlܠTVL^M9~+^P'ˆAj܈p1A 3$AG R9loV5:<```4;ISȡg=%芖rA%ƅ莟Z225`6DEMŠDܠ7+/:! TS yo*!g4ʏbR\ˁ Οiw*QdT DkO1r#D!cmЀeP8 HɑtԼ\9TIU]zוb a3/y@ ;0l*'? h+1 ^hmhXYNXCmwMQ#b&ڮ8 o[rӚc$Rau @DtO/us]F/ .v}Rp7&0w c;3s)'C EBO5+e՘zpƽr4[pvtF1yUG NumkEl+V+'a!d+.ıA\b80 bT?pΪu9QuAx|uEݤ@68-[5U, WYJ/DJ#?9]KG)PANQ {sчј Ǝٻ;mȀ%BJ"ԉ9+kP3siG:d>⿴x195}^\s3^ u!wJ~Uз0`ݸ]ȯ9qPmxBIslY\ONv/CO4wLڱj:iXif`̀$]>Wֲ4B`TUL}xP4(mB/gm|5LQ$b֋O%Cڒ>RĀm gla bf> q*:!+s6F l-qwl hDqTsPw}sds"LKd6tK~_2o`y48QvGquVGfR|z\m1 by$XyAq-v4VUSJPh$/4!(}:J}x2We1~a3#-~A˄t)UBhKBogHXaMMOQ cxue@ST8v5B9l؆2KEr]Us ehr(a$sc0h~or:tNj 54ңGH(e)׉XH;2.t$.ud|;*XGq(tl(@p)Gxrx@ xȸ4+/'sLFy~Kiqj%`5M"Y(p~fxQXtN6 m59b*ЊW9:vW6i Շ%ss?&hBdbX7Ls#Ht0Hi#*N'#G3$EWuUY72]ǎr2I0c%1EtoJB4Swf O)yEc]4R4K)d!Fgis"@f!qmمL6~fo"<#}5"D(CQz^NHB ?3l,gJi*ɃdCzR) ,}W4cI%m|eqKw;b6nYV22fFGBh7N`S2Ř A9I9 x k;j)𞮸NcHƆlqlѤ;s.v}$ʗ:t.Р}N]O7dhhjVH9"W}?d9)16*.SLيZ'?D D UfɌΗ~HFTZ:=曳USO'#tf`ycty[Q{TsSTZs%zе?/ o"Ǚ;8qv&X xy`O"I 9x?0g_!" /ZBm%,޸#x Ŝ~Q+)7lQB1Y`N=Pf@hϚ2s1Pdmޚ6;wfI<['b18Vc:i9K"Xyg"WOwY[Zb#;(8q3ӰgKt6o7en679JRw08;!KrlӸqX@:4eFʪvei: @`Ŋҥb$@x-xUC;^]+.l"wR*AP# @Z|<[,ᬽ2L\9 a 8#B̞L90,K†Ţ!5}e[Jwg\ ]/5ww#t1pl){$MtI`ӌ  v`%l2- hk lrں>|{*P(I>?48s,4WT!hƱ|2wS,@X{w6'~q,)e[tLё0p Mِ P `A$fPٚ=-<|NT;!0u$ {7@][xL|į^?UZ[\k}ȝn!x^D^;(So'ڝy P b - `f-33{{ w6.N$J; b{0?UubT/'}E I:#d/[]2Q3yokl빝2=20A0 >P ` "$< L>psmoțxLj*{;Nizb+B꺨 1l^oNx~q~Ĩm!zh$nonB(͍s6Ad⚪݌A02C4M~o .<@,B-#OolU^M\~;!%DYJ#AlNd@^q!!5BӐ\Cr(a3Gen~tpUVq RP O` v''ԫ \7353?r:JʓgM?Ij9jXe,(N}ayϺ1sEΉJx ]cdZ=_6xG,iBP? n=' o a0wOR%/M$g_Oi*1$Uk/{}nnя\ .g/?#:Dx7 FEO)hVNf-0E18JfBs-DDuӦS( N0*[L2C*\pˆ%l,BM*JРE9@4C 205* *d81F ]mQǎe mj(J&N,קMp8_zw{Cb,5A5\@xfΝ'f|h9LߨWÃ![7X|g-o.<\Dt'R>͖';MN\KRLyIK.eҤkfϟEuT>4Zi 'r* `` H+( ɮ밾628i2)X@. V 3_ 4F}P1V Ͷ.m~s8A 'cιwJi2#>CKH%4+ϼZ &J(F/-=:)\[1@&˂L2XP ~ءJEР q{H] dFs 䖳:mC`<#NhB BlW@3k *O|k ?`**s誆s*FmNHK8qL3,4+VJTlwqUE _4TpqvJ"2p2DFZh[pJmvTx0;P0{N _*)O}0[-x6 =7?+9QsU2TVAeTTC02=D]LqA;WS+Ჟ+6ք@SQ81o6[蒮w>ƒ\91 !Ɔx'DWu?"aC+H#xopmVLYg %t\sI EaDC3RtB d󳵴.XB"pA d$#8=r;#jIZ6}C F:rA BxA P2=~e`Lq~GqOM*`܇7{TNDp߈&r(*茋]0>U%xhf c7ގ"X"*P&Ԩ9Q wZ6EiC| dXcHANBy "_Uy <1|" u\`HS|j@t&3KG& h@[x %E(P̨$q @H4$ F5"Y#h$:Q*x4,r_$B8H t+(u#{Xno{ݓT( ;"kt顜 Fj*uMmL2Y`>qdTʼ<]A5omp3o&! Od-$-:QB0䰆E,tэfrhUVVpuf RZrE_L4Ԁ<rD_) r-P!RO'9^k6`:ֿw| :MWJud.N@%tZ t% ZC2 ="1BG:a q|)TS(nlc`!@LI/{6 fmrY>  jZK!"ltaz&LmVurd1+f_ SJwǷVO^lSܹhy7 W\rh%IBCtYwQvNwuZiRdOE6bNY|0J7KĢ} u[C΃t3@c}Xc$)e4aLΖ&'~*hyfzDIJAfIp9]5Y@T`F(YC9c=cͥTVY) 5&O]*nj&J!]I]K{2$? P {a AD+7c3e\j͸dVS`6X{g %:Ϋj+0yZJ@,d`*Î>l`B$e$Wk-\+tΥL3<-N[R[!;<,tsi_4IWmmh3 gLd9 Nl g>묭rc.uMʎ-|Ռ x0˳?f9LTvW֖末s9R^) >#cM1ڳl}Ъ~sFz̷~'4<2$c:9;*K/m*ϡ攏4_=aۜԿKlv vԾ)۝A 1%jO{EBe<-~K7 MS:t ]/}A B[ȕ.0{w#ga`RGlyLF1Qhp}0< YԠ5l>|cP/]ԤV5k ("QZ瀣eY@f$fcI%G1'BG>rfbX hЁ8 ^ַ3o$qH^J.HL+.<:ϫgjҗq<,ŵ/̌YKGKlb>Q4KAe(pe ѧB ͆i5J~c1Z:ol3'IҚ\ o+"ga&'DO[xgbC>Ebo^I3^@\ϋFBw:7*jÇ^PȠkZ bt̨Xgub m-3A`ߞ\Y׀5 | {*#ΰ_kϿB7&,rcvmx`=_RM|yV}ô!d%/L"9ЪjB,X%̘J".w~* +hlFreyJ3T1؎uFaoˡk\QH^R[4d2N\#0.et_-,܏5=.tы{r9)Z3#gCƿuw>S&]Gsؖ3?n+:DƦ1Ը5YIT 0^}m^bȽpq][uT-nM~u9[?eSt>6_2(FIk5=xnE<ѭR 3\!.F gkskOq!?zU; z9~b75.ns+;Ҋ'Kv\y4E7eT|.MOz Q҂2ѭbO+fzJF]z;avuer纤uWf&/jn8ƨnnnC' yhj<-{g$G3xK}mht q?ٓi8.{E8.Y83|YSWZ`.P]I D6sHolaG{)syU4:n)ucwZ9a)Phs)He VUVP0 IY҉Sdw։~ڥWyމ՗li5y^yɅx4 tF9_[[0y%HFiID AٝT@)JA UvcEJ.UpU2*zz甕`6U09ʣ 0 [zK|䅞OB]Ȥ$lyRuoy:Sar)WbD6Pjʦ;IO i5spHA H^T:0ȝx[U7'W&IRQ:nzP %Obp+wp__-Z>:#Z}Wǫ\৵^_4Ɗ[VI'4 ̺DkZ[c`o: I|ڭFTj@LjᣮGuvyu>FO{' j6jz8{9[`=: T{e+fKWVK"hJjkvE1۬,u5Tl;n M ڋ~opK|S+*PRW#%Jʞ:zI1Ί6K9mjsوJ zẴLմzSZTuX;_\a:7XR +IQZlۦcpc9 MiٷUK˸XJȻm2 k 190>Ȼ˶W d^[D1ɽ<(fxA5ؔbHa.P*I[՚dۏިA |Aػ]&.:+.-2S? kt @@ܡ= M^Fv 0HvY"N慶Xp$mo^dR w?L Lɟۣ N~Hmt -Ub>d^eKN4+1޾v>}>qypS4.~8묉[\ ~)BEߤ۾^xÜ;x 0 @\Pc ݚ4&NW <͓c~S3Ͼc pkPB o;i*\'iZ˩zAX@לCU97_BO=E~pXTboKoTOBN]| _ _$o )o-f WxT {?󄵾 rrNʭv%ͿW`_=: (~7 ` x:=ύӚ ek.d@ 6aC D4|(q$NPay劙11eƴONhϞ -V4Y2kIZ3tSMCWUtSJݻs%[Ye]{kb.ۺv+׬[ تT/f Z%?&A  Do!Ǝ EJ'*[~!3S-c'PCEtjc^uyܼiO7[k=^{WUUnS!gd+lV`B:{>q# 贍V3#n)V6b 7p7tJ 1{暲İSq::E\d+< æ«/l/>":t2:@3r$D`^CA.-&2jr ;,(!rN脑WԳ:z;q KO@1"0oШ'4 TJBiJh )2-j£N.P pS@kOfOh{; QekF IJƚJ-ԾPC#>}VHD(Uiu/ > +*dZ`\uh!VcMehCvЯMFo͉t'ԲLO<7rT"KH$$x`58X~ek&xR3lslkӆdoKM/"<ʬx\@<'-hj>" rдP{#„j҈̽|>辦0 ^6aAh,|٘秷-w`]6@%Q}A`! ,y i"@xmT x)mM4j+ C9MVYxXi`*lqiz i#vQĦ 0n`Xѧ>"C%*IfLU4hJm, 8StD s1䂏} e8'AAȡ r $lQIީd2i$HjAvnUL%}| .q!,/֒#^@#罶k Z7 moP*LBNn!)0y[4 R*)` S@xP8 aQw-KYe rTh%tq$ (7`Tk'$.t@խ!bbXl|x9;q}H<48 {@@ 4`]Xu+9d(n[U<[/—{>*] H`)c?{3BUs+[f`IR LX7׃ajK1Z3:I328 7R7ʈ>M3 hA  b5P[]I!$B1?5G8mhTB+,T1,$0 ;c823=PC ,( ,":;;C蛆?L0N* DܪSyFOA$:~Y΋u$B3D=[bHJL@=(XUD +6dx ū^; 9>GHɻʱ7H!cVcxpFɃ-"-2G tTLtML;4{JG.X30Z8HE3 |y,)@ܝOtC[%h\O{DHlc Ǖƈ&(H1I<2k,p:#͂Å E(R LE#1hU8Hkػ9"rJòT,F\˦f OK5{9TɗlH%d+ +0GKLLDN%S.fp̔BH%@@ȁ0\x8ˊldGMϴ4LbK"M D ̀"|D"WS:%K|N +L$ȼ \G-hLKjI9uX(Jp=#pJ0lOΒK`,a#OɬvfPw9 @Iӹ@PPI㌀P`ڊΰb(BI Y;|QfRHJ4*S HM$ҳ̐ϊdRۤTG܌8PRUX2|20/A1m nK(R+NKLjzIKQ??S\EẂ(Ss ՄԱ8՜%=## (=Xh)=R-a~FBDśnDjP"UBЏIc9.U=!J ORX=@,Vc<ܷ9˝<)܋H-Ɠ> ?;խA-р Z2Gɦ]-?B.m[ j`jZ6,f`Gȃ2PE,#pHY|8sR$ޙ5VM}\OMMH5ZxCؤe_}]F>ߘt'Ѐ;o|N0|زe:ijJD&D8kJDxk# +DPԿլsjaފ\9&a b礈xLAUKɫ{<xĢ -1 $3Kvhj$2@h/ (p@r 3h8ڶ _fqgossVs0nNш(#M /lFDTh]•@rO/Qr,7 HHvp&s-4gk2suI CsPub({d `RWA~KTv~>eHRjo`"8+u)*yT/zy uy_+J/q0WWxwgjiO.*-x&)#UyT>{׍\:ayg"?|{h=cΎ_hzkOWxJ1ȫ?&*I d {S>(紗P]"P5,8A+8*7Hw@yfP۩˶ɀc1>/x?x˿[7z^|e_LDg>H``` &"Ƌ#F )TF+L@LIfM4'=ع*رjJ;g)ԧR黬ZZ+ذ]őz,ZkjBk۵r2u*ӥJק$<.l0b'HC *j`qC&4a,ؘ􃄪Qbi5#GN" _(a"˘6qҔYg )zpbѤJ֬+ivZ_m}Xdpb X8f*\ AD`h!ZBaZpQ'Q-wJ`K(M=瀉=uwvU9s4M7,=ÝwFw%TuM}]\X~؀ e7fMrYixj&9m#fPADJHR)T܌.ńcs=! @jAj!uG]wE*O΢QHs]jY` _^ߙilogd ڙ x@V胀fxn>B % 4RJrjC,jOD)1vؑuD`Q%L*h e0LY䯿3r=%;VdYޱ-[srWլLXAf9حu@[FCb6/HQVR]S ȜM @ @QZl 1Q#TY3;T3G1!CVY sTeU]BY6Rs*LZ2A0!j 4PSg Yvݵ9ܧf; AHZkaF Ϣ]xSx͎;2kJ"yNȌ|yw+S^П^e9 iA0l\re(ЀSH:< )`~PxZ`/O) 'Cz1J-}ՐFyҤo.1Zdl}S"?q \f\+Y`@ j%P2 ] 5!׽5ʈ0XּDIA9$)WQ%x #9: *rS-T K:ر _LrC"B!apuyUh*ί/_K+#x!bc@ ֎3c'q&C(@jiK G J(B<;EzPL[Vq UXGF [B Ib ]U]b)r~`jZP;1̕hgJ JQ u-R]")TT.ϭ<4ԡu 0BPVvh^U~0[6|ILHpz1c7 8!ʑ"8™=T"ݜprk6mm̧f@ma B g]: fLBYpVs(T.K/?) x9"Hzۻ)q 9[j|:.D4he !:*e|5o5Yт&fP"R*o 6"+1&4A`n-ZFlcK_-9t.ixA OS48zq$0v*i `ȞDCr1`2PFDI yਢ[`s1/Za;26MX$?>$ۊP\x7twip q_r㨔nTfkS7ZqrPҒoV[ @:!!L }xW8s*T! T^ 2$0BIJw_mDD,֍!ӕ41o}ޔV;KIWpP+8؝ȀlwUk4xFݴB=9k4M@,c( MSao5x!\y8|Ps3Ӟ6pD,L ^C#hdw9ݗUhҽ]s;w“pDG"N4V6n.C!Zj@TyB4#QH46XCB"< x-)N__q]uI|Q'8A2ah_hUR( ^DF_4DuӨ`3I l G\^UDH<<$+L?)])6#X`؟VAp`^}TXu`iW ^R . E)AeL 5QέE)a61r OTM \ @mmJթHJ `+ Q앏CƂ)@],B)H P gQvw WS>%TfyL(:a\,BUԈN.m\˲^e e&&d+dg)+P&T FN Tgg: eR1j-V&VBS\DӤbWPaPP)8Ɖx܋F8 hl , \&\wbc"p(xJd.Vh`Cv{ i6C}:}OF@VbC @8@D_N ֬H\Aa5N'7:AqUQ Az҇t&6ZZDvigʝOKDїW<^-O-gu-f1D^EhDJ䈜Eix:xD&( $A]3d؀ 0PX=x$"`  . C"Rj9`&f߇|9FܕePƂj/b^oo8M #䀱@ ^R-d5dzzQj" z϶f.Vkƻ@fe+(~IbD-Zd`Kf 8&!܁xNRqF"tB2x5C{ŖUl,&V+ f|dUb%7AHE쉷(CtƬ̮kM!RjoI|m*@a&_2L~m/MnlZhn4.~E͗2lUdpSgH])_>BʨTk`Ζjؽj?R8ځ~m`-*ɬ/J+.&..e)ԯbƪN\-hEho2aWWInj_BLcԫ%-q֘0ĩhA.2yP,1~\2 ~&o2'+' 2 rPdnebWӴ+c٫}F5@jE03I s@9 *zrB-lm:,{b247$L3.c=B%X ݆1"bD@r r 8YO sI s"92(s@GF" A-B'!g15'CtJ4ZF۪-*ߗ9(@DH]13L7hp-5dS N ({rG84\5R' D 8=ԧ&^zc c\J;Sj2! c ͱZ1'G]e4{ѐY3(x)uBRi!6pަ ZBHF Yk-wpBIHC98j]. 6x``+F^YUZAٹ6 r{ ]zLh`C0؂'xB)IiSkjXaw'um&̪Abs%H+faCPGYw \UtV' !'̘ 3{TmG6T*yƓ5L5uFOkFNqKEyLx9 !X'O!y@Z,'www(ƸƋ7/J?ŕbM-g\O~NE8K:y=8ڌo9y(!_WR4ET۹CmMK靮y[Ɨ:EYTF<'7gzw})% u4Gc|_׺`0B:zytb=-x{55kĘ[:ܐROBAm ȁ3 )B&3 i}5p~B{6Kely]77OZ)2v-apK Dwm쓭S@ `efL9s 6tbDq)FtXbF8vZjҦlSXtfL3iִY3@8 C 5Rd 8pjT >hp@֬ 4 j0U pW0PaD 5T#QR@|5 %C4Q5g"K=3wNFիelm#liO\m#4О$iʛÉ7><Ξ>pbѤI5pz]ڊzװoD8T-nmnyb0`'QF\49 h/x3̰c1iA 5\ȶjOs.Gy@J}.O:uIɃT mbG؇+$/] В'_ӿ9r_˷陕)T<7RҗS"ֽ*TjWyV*#T,i˒ y!!Hȅ*A ԛMAG(҂BL3_CgVCB"?/I$>:1euZPIXu_o"A]P)P@F .?CXB~ TH 7PSp h5Q !|C}9X5~S&DTESv;%zŵ"uJwN2 yEm :֑ xģSqT~9@H"{F"a(k,r6#cLӸd&9YN¨Fs֔!ɎiMy;O@ܳ^gS4)ɕ{Γ . W0/wGaSz9^r *FgBTYLS6C@q48hN`2I4.>*'N%vx*jHbe]Ƃ0 oxAy'Q6 a0cv4 it'"nvt}Όɚ޴,SxԀR">S~Uˁ2>.~<&DOV)=)2JehΦ+-\/DJ]r<]y 0Q`-\+LיrV]brpWuI8sc)Sn)f9i @te:tC9J'։e~ٱD`>-oـr@f JRX%Be:3p>IWz8/qLDA@)D-J5#+91?)iVS1kw*2;W‹ pMV5%K1ʯ}I0~-ю2K T~lh#p!%_;[ٞr (''UeJc[n,?Rnc-\R`C*Mo:zc\{`xOh .nbX@FYݠB̬TXųp`JLAN GzmpyƮn;kpȠMO7 xPP.'RӐ>Ժk$Ѐ YFP0F VH*w.JGNЯ {t !OKwpq|PR<k n + ֜b nÂN:;d1o>Dm!"$)s(E # iRMV#O*Lr#'qMɟ\rκŐͼH ysLxDK)ܨLPlq`kzg(u23)))@n**+YD+7N#,,Rx& > 4+k ,7,Ĉw2ͪ兊v$¸ONKMW>O/J6aFSdh s7"3}&qr3S63+m̸&Qݸ$-3,ˋˁRek>6*o,0BO,[8˪zStr fs7!)*/G0S;C>T`nntlw`=_T4#=Vs$;@FJvlΨ2pnp8` p Ra49g(J!Tckr\9rb+9L@S-=̀M J欻԰xD>O׋5c'mQt P A}`Qk9\  "R+B1BK>TmBL/K/x̴=ˍUU |~@Zu(MT+N)_1X &)R*fH@b'YwA`Q# Ad-TdR4µSU&D4]o+uMOI*rzb]lWMuʂr^4˴c"{$N/my ̀b Pby [ovdG,JQ~TsX\]E+٫3f%pK(V_S;~ ,lXiIRv + jtA &krgc&TVmvݖ6eQS*&`TG'fmVQ- !0X#UoU N=:(ׂLm(0wtbK/F (v JA@ȶζuC6dcvtSu"unG;RH6mnxOth pU54'*WDqzaJ ,LƂdjވْ ֵDWb`}) RhcnA@~7v!ms; e2xNVa4UKb=E{NQt5= (7уπ2wp Êt׷Z=Amou(Ӈme`Ckߐyx6~zbEQ?` X<#X+KWlG_ã="Gw)gKkpVk؍5 &u8vю1}/TY2:F46 sfW-_@ٲ ̋*H">,ƍ戍w 9{!y,۴meW[F.:Sm5ʼnwoN̼9=VS]֨cGg`'C,.W)vI3W"k|+sxd]W9D&-JQy_E8ϔ9P@H]5^';#X0U&ČYW2dp+-{NUTgc[4xznͻIУ )R*,G'Vl.aO'C@$m'^ѷaT 쥑:L ?QJ]ENRIhK`-‹YXߔ!:70g 9a9h'#!Sn#2`az@.E~#]QӪB8@ o8D~0bDˆoh%V,B,| 4_`ej|YDCU I*fĐy UT3v *XszU8dv+֚ZJ,.TOx7@ *p0#p#*08`@/Ō;~ y^ 8pf\l/̈́Xb$լ&2Dg;Er[4xƼҥqVWXy͋{vW kHJ SP%tu4fۻ/;Z•K׮ aQ)ƀeE`MemhŴFAm8\nZI FjvnmI &M-pR1UY;Uu͹FTw9b1SrG\V]^|g[o\u-h@s#PCD0B<"6rɠeefEҍFP ,a#  7&m}bMzujj0X̙hZtՍ CDw)xPWUzXŠ|^~ 3IfiQE@-dz`Rh2Jb*aFp*8E"DUJ/jįn]3SO'1 jB ECĵD ծW9 9ZnV},Yp&6\ym:ߵyj-/tRj6j. GKoVC&iXu?İ+1 äFJ< <$a1SOayǂLp%~ڹ09s*xXbyz;CejN!6tBntbEƵMQ796 ?#l<GKIVN^l~Mf'㚹`t]U ̮l ǝ|'I,U:Bh\ S醀Z7:ӚJDSUEOSs5)((C eNg4Dz#hPk%"$Rq/'dAٵUN 嘪 p ln^%;bHuױdSrC#Jh}+iZں&}(mk;R2-u%B(0,]=H )YG{MvVi kJT;z#ZxW|xk TL!lgF(:Cg3rK(fR5>'?-C}VQV-4kC*Aj5d+[D$-F f] Łz1cD61uj҄Vh䦆/=#J}i`q9P`k0zB64i&!Rdm @?[T@PfRIт3mᦢ=^9e9 .d| @6m>QYWFu?U@([,:7"?:RIHFڒ2Lj9,Alg =Ab y;.$u*tE̦Yy{zf)D}CtL- .[bK| S̍Dsff1jP>;'{ I? v/5(4bgQR4|@|gfw/Q0|ll1Bv&A%=pw'mC5}9~4~\y>3?%3jTETQOwe`PE(jFN3K?fT;Y!!dPq0d3L̖!WlFA;B&d.U`v=׷m3g0S7I!r@CE6?1y,D CepI!?7eф4?`: U`7VYX{ BU#{sWƶQj0YBfZp `MpqA`18d(8޷ss(35qY +KTTOxNA*QLUra/zX$(WgtFq{\./Wd.0\fgdWȁl?E0UȀ bѸ- }_Xanxd8>7Rtl2#,[QGPa/`: (`i[[%LF,!(:l4ew*k!@SOSA0x @ ֐ p `X}/m[!8%C॓7F<NE9?8yV9CHTwOEUX!LcXfh@ :[Fs5C VV1ypI/9f|yyvb0K~B%0 l)ɒ.I} /73g8:eBudEdGǎƸM8GTIwKh;aEK){R䕫u lĀ WbQQG gŷg=qK2^?Uj =0 @Z*1f 87 eb<"TO6Tx}JWo&i@]54KSK$ Ltz>Q vw6kES4.`E @ %@ )I 'rc5sCMPaQa(1ui[:?CG3C-bx(c٫N %"/?Bz* ;tj&Aف+F>SrX)أ>P/Y}Ym`Nu=s*ima\> ᗡnCxY95#OpҬ18qnl:Gٖ5y[.e:!$k0 Saq9lL)c` )xR+ k{[uA""٥xw7`') "()\6)_ڐg?f`4x&Z/aQUӋ.D T.pp ސ6]}`[xȂ8`ڶxiu+1i{iy&Od2iJtM_ #k4Ѻ.Us? Fj: {[?Ƌ Ѻ[@y j 97 ,i(&Rhp7^Swۼ)nYs{w^C`Q`I0/00øZpy TzA#ʴ,ѨT)xP p~ 4e^{ `N2i h簰pxwqZ\Fi'd+y6??t&K'96THDwf JUX5@"pu{s[~ɴs'U)Ŧk(a*Wv@~UP=:]M1~hg쵶` fPf`o c p(^,'̺*숇C'&CUB @HWo+%1J"i [\[ʮvˋeCw\x ِ p 02Z[?]; im-hh꠶_"\s&U`,d)EgIHVMgxS4VfϻϒNu:\.z̜0pа yXav6plM,\q< p̬ iqh{('Spds=t<"?؁jEj1[Eb"l5LՂbB7խ|poè,j6v Ȑ ~( `Mp} Ҡ 0xXҼ]'=׌i6~9~@NFN26SBU(JʊH:4@șO[rħm?4v@%Ts1̊rm' ( my +a}~+1CY=AMn#7M((z^wuV:F}bԖY^N|)}FWv `%` }M̍ƈ@g}̼ ` $,'O 㬫8MTo:>w@NlXI)Wʭup<˧ g5)/@X CS(6kW|d3Ƨ*,8,bgs쵠Z 'b #,p!-tCy3@l+C;ޑx! ɧQY."a m9KD{pfĨ1bC>bb>@jsXH^bp+r@ўޙ=@#5^J~l$>@͓~q L}uUF q{2>4VP' -2NpВ-|=օ7\ 2.' *.壝 n#6C}1C>@I-_kcv Qlk4(Y -<\lp t !x 9988 n R183AF5Mp~Dz(jR/>lLAUDX ֨A#A -^hQ J &J*o9ldY={fH UQF ,Dfzp!DIV͚5s]z=6w̚E.;i4z1<@^*paL+U>\1C?C ʕ-WA m3 FC#7<{"& %";޸ ʆ8Ό1vH$M v+mxDc64U lȲ= Q/hR5XV\v=?t9k@vyGil!$ ⡉'^ / 0sl;.C\, *l4@4z`-9 .6hsHla%id$ TC!6:R.¾&8Ȁ cdLƚtD`T'b'Ė{ǚi]3J ½ x C ȓh3NILL1k1bQ4Lxl8~,h!4$M=N1( N2Z6- .]bL`BAv W73U *Лέޫc9D>yӓAp?sawFzYiD SMtTdC5US`UeѳNA[w!|rVP7tb E]veEڣ[/%\ss8̃*ڼZ͊ ̆cTAĉN *,j!iyDhXSN]G`đG1OFƨ^c|')Ԙ7HYQh8炶-K봻St4݉Zkbe>*v9-JkK&,>iؙL0T%2U9\*p[FW*A\)0tه3(M$&=;҄搅@&Nu|݅ &AC#8щ=?:1 \\bATBlnuQj/bDРbKw =C &9Ȝ() ݋ZFV^F8PP "!͏h,^@z$")AK:Dn%C ;\I"81 bd+@FDɨn4Ʋ&ӰE9W ϨDY/[dE)<`X@ vCF R%3Z6,:F@qcǡhs3Dsc^t&ǐy%tsHp%ArOPd Hru܀/W>Yw[ran$XF#I s0g:k>3,#% ZЩ0hBz[CɰԀO݌P%=i1P ZpB~z ! ,^I$_ͺǰ!nHqu2j̸mC:㴏5D٭i0c:f.ܹSm̂ JѣByʼnH^ "ӫX*9N[uF=ٶ|aLl}xН߿m*ro^u[̘ǖ#Kfn]㋄3kὝC :"b:]"ROxK$~EfjoZq8ɖԒS)ӣL1c;6dX5:f Z|9W}M\ܹM&<3,;-#s0΁WQ:%Hw^yw)egEewF@^{ PI3(C/.%4֘U(`5@J#2QS&FxPJ*UԒA^Xn%\E_`6 +k^b8 ;/.X)Q;4$^  YΠD*7fXG99%OsƩ1)բ^Ts襗5L*AC{\`*03ooygr1)䠼 诀JP蠲"UtW촉^4fF> piHzhc'믾 ,ܩC0kp,;MCJF۞'a4T欄Cz+M54J+#אּ>ħ7'-g1qnr<74SaѸ;-,5[c5Y4[вZ w9m~Z|Wxq=-qq#``hGmm+bٿ0:ZǕLDmH݈:a91E,u iTN9;!Q4>-gu-p _«D5kUrsm͋^V8kuzunڇD ٨qqViyhPEa9s$FM[Bk1*̻ޝ(-r,e};ޭ2.Yܺ4zc f.7k}vr i΅վo_&gYGмg[^Ĥ/wpm+}kG7o Vؚݟm{?nؕL0/R \[.f?XpN$o ?~OtNx)C']|>zuKy}hfvדu/XϫMӿ/O[r6|z7s,Wn$x;Wx6WiL' ַn5sjpc 7Iyy `uELS\Gz~2gf'gwW lwЄ ouRA8FŐwycr (g|M'>'0@}xu}ʐcKWnw S5ǃE| t~%d&+bz`7xHaeW9Ё8 &n64g:["c0}GE+WQO RhP0 0tnpЅ^crGv[Fjh9g6p8aBv/jTv~yWw8]LJxx HRzvy6wdIN؀Q8cЉ灡hW_Svo'pgcЂqh`_Nw2J2>&mX WyʸH| ҨX(`.U vV戎ahW'؎xwh6iX`;ҋ11#4`Phx5ig8 uǑEPc @ _x*;N/M2RGKVvzÓ0c#Bze PP3wSIim9V|ǰϷf.Up[ 0 eYw8si`gaY]pY~z\090Cgŗ7uV$ٛiC'ig 3h9[9ceE`#ىh`{Y&n0?3Y9F flzNy/2Ri%Fśi }yy (En|Ib9 yڹz]Өe|fY&jYɢ*jyq%2qhiiXǷ[=Aj`*U@[aɡX~76'%/ʢ. 0zn:9Z;ʣAzO9 ƇYƳpG\e'Ijje*{qPtyZ{ ŤW@cԹƨڊ8*՗dg뉦٩"զYuy_ :zzjc p^yix*UY)Nwꉩ{٬0XZmيwjpᚱ|Z>p*6PIʮzv0PHtE6A&Kc{z 5[7f(I܊kb⪠  q*PT ګ)aXʥAv<>C~he\HJ[ujN Jp *9PN@Z]fcv DXf{䗶 G0&r{0Ŭv;dy[{K^M{ P(б`V p@z+frֹ_zz6qkzXIJC@r䚻*PPKK^dϻ4ibg[iכͺڽ$jD M⋒4[^˾` #%[{W@`C3{jP"j ,::l r16 ?XT P#P>PS`&P x:Ad;K n{>LTz@ipѤ]E`ڴa N]'OVЫo,omM^Mm,m< BnvDw ?>p P>ޮ]Wm̮j#I *Mk̦mvulr>^E簬̩Q=$=Z00+Nks6Öcyγ@T~3jfԯժ^N?|[ q#9 + _gn.8>6N^皌 ڧi3S @x ~$]; ϶.0 n~^ȎIm˽ /ݾA<_RN ^%_`NF{gb[x3L,5N<_vB@oCyvoG=IM/omN@]V'_q =y&Vi@͚~misOqw>f4SO~%( 䴮Yo 7_daKc- Org}Ѫ? 9N:ADݷ]E?դ 邑/k40t<$G|dg/";ON#'T@Ԧ4+U@ 䒋/'F9 )5D؋SG:Ӝ4>kD@KCvDZFrMʞ|I/%9ըSv5@N7zOz-xVXe ^UæxU29z/aKc#Neٸxc5ii7'b1q5׳xMk(=m^n 4 ~k y (sɅֵ`^KVX e ?HbM?))ʎNZh8} NuŮfՎZ^st"wҰ$,a BP1"Plk۠wBCzӡ^qh{( WA!|IJ@60= 5E.ԯM rOO:]6(R?#nIU18@0źc:( KBR`c~Є,4):wĘ̏57rW"w$%4$J}ʏ6@@#m(|T'f\p)tN?LہEOX/V1A |KDs f>:#i(W$$ yR0ܓHiDֈ@"6 sJ (. 2ha<@b6uD=5`^-eeD?QSGC\) vm*?( d:n]i"s%|QF Ky}0ʬj-XG3RAE! XBҀ) )h8KiHO$uWi,tMݬ{]잃 +Yjb )a/%"ϸv$T%cY&~5BQ(6%miaY#9[gZZ1ZU.m(4a| :=Kۚ[F{X~ ZRP"oawX#$>S[DIm|S9(*<*vh=!R+ WL<۱i8 XQZ "s.d.\ۯ#sNj'.ݽ_;^Hٴ/k$ӏ!i0MM# l6YV$8e3! F5bmS|J tp*./ "D-WfNo0C(`}]Cu<] `8ei"~ӒHF)lr80S$?W ZW CDk Q#:V19ǖ3S([x>蓾;>i4ۆ_LEp.-,X$D@<; ۋ8?>?l=k t+TK 䩞A,&Q2NS ~ӈ2$b"@>4AЊK("<|i5ifHLL%B $f#"ѓ6ɹlۿ)<h r“L`6t&锖796@z="=;\)+A<ś8RLJ?@%8Á8?'];cW A#HI NL̸ɼ˼̤ԿcJ8iHJ9Hd ׌%$3\ٸM b\K*($&H-0N>ڵJu`_ J(:pP Q3POp L<Uϥ δWMY4MzN)7 |=824C M͍N, Ƶ@EVj%Zmf(,pȁD*dG|G-QF[O!M_)R_HRԴh&rԐ)͈˒+R%77R[ SrБ$OwP똸4DdtA3.#(Q@j.NToHWu]WuGo# J] "uKE=UT%U*X@#I' #BK~C$@Z7 H'2UIcaaIxCXE(=-(@s\tWc ʝQ`We׏Gi|}K @ Om/?ՙҨ$" OsC;h *\2'u5;+OF_Z j \ji(\LCЃK:-)Y1A٤5r-so8Z|\p#8PWW]ȁ0XZ$s' t2Xl"  X^S$UfK-`> -܂UO_OIXUPDC(,8#'@DkYˮq-]EE]}Km]3Y|-X H}+[!%CRQe\.+KH-ΑB.^UD8('Ȃ)+lu-I͆߆9M.]*=-VॣZ[,)CXTiMr`䡬 ҉R(c]'vvr^K[xBa`u DB $` `0 8u`\M(hA5)dmS'C+X6vM ^1`/bEz:y`>cKc'mMMi2'a$^l4 F .p$d4N#B;@ "0IZCߴQS\]e;q~gύe6uE SeW0(`4#cc/Fb24|4~Ԭ|˹T,10P hۋX^? f1eN 3p=ן<5g^Wh N.e  0a%G6ܔ|+ ` 5h.@7Ѐ6cl&d4}dp,@ px dJ6Ópѵz9Ծ~jj5ZN]e@s>Lxݷ%gs=Zm%cU ӜTF=9l>I@. p@MoȀkHlI0$1|$1mFm~Ɩfmp(}m6 /mnЀm߮hb^crrClZ!Kh: Tno&0N ovKo}WFV>p~.ZpY&Fp^p0R/$I[hiRS2lFP9<qp@/t.5W nA)A&`//P Pog #Og8p@4Er )uoo"0.brI&Ϙnahz}U9 t=54Q;溉9k\R>MlKC?t,@6&"H~l2,8 [Kl6Uҹq/KK77]nwwGZQךkPzˏWo~zhzsi[&ꈟ-XS v@B 事yZ5,ù,2޲kYWts{P~G,xFyVAƇBhCȧk_p͟弁xzlgx'!0s0 %]  <( A 0P"H#F#N2%/_,F)@BgO N!`ҪU;gZrz껰躒-Uڴjײmhr҅fzׯe^+\odȐk| V`2̚7g&P`%T ҤATA2lpˆo,G#8Re0YΤ t40: .a2 !URV*䩞;-lѝk.^}s+a]b9dq @ DA vjBI(l o9$QkTp#T05JK,]qsAؠBN]7Tv\0IMddv xw^zI#M0]3VSȷf:iV^wgVqfMb9XdU z@FXx7䐚 iB P "EnP3hcIਣ<#Qa+!$A+x@T|݆21cLAXa&0 HkLQ%b8bA!B9 6ė&X)!AL$ V @\B0PE\1!AA_TC4|MCBGu[et䲁d-P[#此 id `Xi؏`bX@ dB,\&+d5Z3)hBRB)<p  U^e4B-lW]"i XZSy/|%[bŤM\].'TE_rQHL=2 [s!2 @F T ъ< hd H&@Օ'hBY&X b}'\!VFlnmm>(B&z^N5ou'<qg,ur~huVJtJ wQ'.JeiD܀J5 `, $|B)+$ ڄ \a"@vKffnJDDYUa4h(zh(\~w!V^6XͨDBlhٔҩ J(&ǸPn ``ЀdA"L“Ƃ/#أs HV)1)ݙAŮ I^)iҩJFTHcx2.%A4XA$2MH2\(L iRsr@FX!JJ͈L!f2C*<%"$.qx(-!Ҕ)W`^k$ jOBfSvyRF,I Z>jRT@ ܁)ݔS>TԐ\a,4=ɑWn]M&ye:lY^P5)Z,[f LHjLUJȮOQkGvV JBN*HPD`&$BSB% TNTA[)y=|H]X]v'~-RZ֗g@`2i(pX+2Ub^e}F]&쎸ğQAH@ PH|j] DAEҝ.颃VD' APIIMIoԶ*gHF,벆glh!Jz| .m%CaE2rRoooWo!Qnh/-arněLeULG%,l(&R&х cԦ0o0 op0TRF* 1VJΡanlO.U08l0 ].VAp-0ƹqAjWίYt C(N,1ލHF)FӄS%D>Lh\X(8U T\@trZe&0EhY !pʲ"(3Ę iY!E|gUOD17N C%Si, C^ںXsz:K ;;חDI 0!D%ފ SADX.}p6p Dq{4H4tIөc1@&@׳=G[1^1ƟLmouR񎞪|qP5ŞmA#-$7<˘.,TgTO5_UQ|+xγsTܚȶ5C$J[1-0xidM2tWAQW9-|XIEt\eD]E~x٢xS7ApljHg^8uҔm 4BO'\T!%]E9H-x餎`wYbG Ɲć[-Ғf8JC(N-IE4Y VVXE5z[¢=mWXmBri9_|dr{:#Ke C.n[Bcss4+% _hU{[B֙r] ;_AKyyQ|:x{g1kRaD2H@u]G"y؉\;ہΔn7g9kV0u`ywc@Qh,>YxH6#_:7 N#iPXVL#'*|0!ÇyT *}y|7}M<h;!gW^ivR1wk-˷$r}Pۻp-l{ܛj"=}=nSZY }vU7_IG[ۮL^vBف%hx9?OW%?:IؽB=~(>+~ 4xaB * `T0d! #GAX8 8k-[JH-iI5 %Cw4&YxIsW_3|qbŁ#&Zdɓ],Z5i8Ok I6}t& 5E `"I"AxpReK@&@r'OBJL *^lZRN;3,ٲת⣆)ҵ+"U[O0{l1 m4,CVc lN #p+7:遡{i9pz {9Ji|r랂.<-KJx.d>4LA L,2d02 <M B ÎR! .(q&hMő~Kɹc ctd PSJ0RK,xB 9ĐApkJ,/uL0w5AkAӚ6%B:]6!;z ):~ Iixjq j(TB:AiɢJ!"B u 1ʐC9D^N.;ȳVr Xsz5DX&ْMV;#>=HޘH5Pʵݝ]juYG{@.$ԉ隂j1z*|>14yY27㸇lB~Bfٛ=6y;IE~m6vrrPۛ)kx}΂+UB'~s!ڙ) ;$j2m_m~^9ٽB{?~ӠDy)XMZ|qyGW^ ?Zxo+{iAHձjJ@$ˁ҃nI~RɁ$\Ysd"]C+t64E>2(wV_,iY;A!K(8&hd"S2EbiF)l1@hF!,"4' 2 *#Z*Rk`tfeab*؏ў'G/pvE/HdRzb.Ofn z`- W7f Ej |v̉*h8""<"rhDncRH* D J* Ԋx0(p$dX ^j i EЇ譜H"\G vpޜe8ΰ'O א@0H>h0pLԪ'1L"$(qN)~6ÖOkX 94cjG ~fQ8es6dF܉Dg^ÂT o+\FbjAcMP 1d,L"N.AFB{n˴p c~tf#~Be<P6im#"\pV #辶ke,"o7+=q${D v$PݶbMh\rē%|rL(64%n^  `nN)"*r+e3+7a,"31$2l:(6RrjgvKBn p°pdhqD)`"2ajhȀ Z+ pSS:DJk+ Q+d#As49#Os,W53,ђeM-&uCi*T'ik@lԌ]E6~G#Tpn+h1S;[P(NI1ԓس.R>h44Q352?IMr:nqhg6"lR`p+VkK|NR h3KN22A4H-{ Rb7lI QPĒJ,KωG>L&.7b'`M;v *l4O:^Gu@2? 4QEQ(hT%I $fb'T˂RPB8O]Vl/ A$ߐSum* _ Z VN?W ={5>XCX} O$U Y{[~ 1vr pJFGrDmqeLiN;e5dVm`WiHF24nޮ`iXa5PaװHCY%RH 3洌.*9[Iv>@ S$'P rFRv0;'#Uh,^mVMtVE(zC` n!rl2NiEAj'jjC pkL$zJ. VZT(SS[_v&jPi@O ǰj @Eeufғh!.hքbisUaG4p/(G"U[+AI?6@@vbR!+ oix 'H0EMh蔡P1O;Wh{wyi9WtWC-Dl*Ʊ> |soW~/ \r~4xx:VF"e]5""l؞۞Ls@4zj{O#,ɛ7F7}s m6is$/Ep\89]hVo&b`OԶ"ukiQ }&=989VV؈@6g`LHQD4EB]J]Y/Ȇg}1cXGe9s?A׳Td-1?Hk#rŕRۢ/k[T$vٳ冷 Q~s"Z5}eRD +a^Zڽ@nf!39Mnc߉ߥ!{>Z5,5zsTװ=S[IA-~'4[Q,Ɂ,B | C5=_yGԃŤՓ~闰 jl'#4Qs()I~ -ok6tk|r&"Fȝ Hg[Zʊ `g?b;J2s~>\p:R-[p/~쟝1i'&s|, 8B 4Aà LȡB x" &jTX"5Zl#*TشIfLN;< !L9sD;4iѥF"mz5kЪZZ,k1bǺJSʚ5 #Xڰ2ň p6޽| ؀$GNLyq9p(H ,qTPcr%Rd#@'Zd]urd L5Q}eR׌I&WYS4_'Y~  #pt@Yƀ0D!2fam&(h"Fb8ba4QD DYK=)FRR'd f.TeNlj)ߛitAsW u ^|n *haa҉Q&h&蹒> کxФPij2N"i-zEJ XF?E-H5W4CYP!eQMb*5԰.'%,n]vTnU\yrm$\poRJ@/ZB{*f.(bgW_q Ċz` %$r kԴ!*K4 ]S rfM<:K5LS'34/{/a=qs^yq8Ry]t`k\Ӊ5@ XfF Z%TZ) uQp~{Jw˻Ѫ 1M!TA'L˗[3D\*&Wft.`,ԅ$n)q$IPo[vgy՜W!dh3^&ӵPM]^T*XE$QG'IH+ op2~os%gXL CӘF3 h9.”nH+mpFB`;j." \u53Li6I!f7bw* Q#twE"E`$3g@c:1Y@p#(1H2PAQgY jZYf! %Rݎ!4y" ><!^F G`[%\9}|_8h4ҩ"% F5:9i] 'V&1taLg2&BZ:I9e '35*Rw]RUU׼N-ڪfs,OSyY5y*@XIjy!nk2NrĿ72s@Hd{IǮBL[[4T6m5lIAspI;61m-!zͮS'֒&axܦFmMKezdnmZ% &Y@/)F &5:3,f1ֺiU<-y POl2g*̠_؂kc ~F|ΰS)J(60ت0llf4 PEBY&xMIӕ/UJ @&ZB/ FA`22RǪtSNE[dyvO9Y-k |Y6ivR; Hl{m r5TIp(5, A)-I wӰl% t8I 7y&,/k tSnoQ 6Jutk%Hq' $9A z t[ntxCsvďc 'TF\F[є@QBOAHm4\''g:ɀ(rƤu9*I2pldemo9%`EN{+O.\G\u?Yk ~Mv 2nh?FA>KCvE, p%@.udKjXz|LkWtpr;=Rg|&p0%0€~sCG.栍W:[~$3&{n{k (;Iԃ 4 z9k F̔{{gTq4JT|;qҒTZ$Wr7B)T,ga}Ca"FW!YU}.RCC.hA` *]7$kAfHA=p pA{Y"9C,` 2pYa2%:6Zh}etZDl6-flІh M$L(}.//5Eg"U-\v$*K}~A*aA,q)pp'^[Sa{`8_gk  Ggfihfn3df`Ԃw3B'g%)γm1wyhOg4c-sR~BwDr\D=piCR>A#p)cKv'M`^ޡ9Ĵp^xvkeX.!-a[pU+Đ P p fx<R`D9,Жb_QrSxsrm yGX;V܇( CBiōRCx>$/hTU&}E~҉IySeSX0 ր@b9vEVhi{簜oqɀJ!*$!rMfn8lv 8ڤ'a xCIr)fC6"m1Cih`6Uhq7*C*gB~ı}Yyi[S8q ` ِ S@ `N pA?j"9ՠ% 2 z9MWnբtYI`הv-!Dl=Xrs .sD6R#NC.V$ $/7֓ÃVۈ/VAUiin^vM.0@ ɠ9p:` <fp X U`{XyQȨdqiGuN3#lH)_(>1!aUC)%U3sՙCih}%z!?:)) 7gwMkڦŠ9蠅Ж aɊ):` v"gѮb)pv ˗G 4ph`/!HT4yǩqF(*hDnA):"?s/baBZ6eIz[SJWl"|Ǿ-M")|-a-eGm(&Q> +G!G5ݻca ʡLyZ#hʪ:]ʎ]]ͺ]9;,p )pE@3v= r/܉7Va֙y+b6IInäd3ȝifŠi Ԁ @A ; ڊ3=Y]Pwc,ߦmlp=`4 GJhkɋ>l(!n)/=L'3wp ؝Ka%~@ : Fm !!kC۽-1kexl]觏 IywWCbRnҙ=#n)dߜ1y3'#6'18.rQ(Zd/C0 *& NN"bɢ>R5t׊>yToB:wwY*zЛ.nZr6jmxh7|߷9J;I=c]0?fp>aY0AkK㋟0 ҐF St~h56uA 8``@B .kB 2~(RH*LTQ9rܸdj~9SJ|TgOEh臎BB CKV[ K%8hh62a&QZ)w^iBd+3p<:IkQO6VSmUU}sTDjT!D QO,dG_aaHl]g6Jir*6l!"erխLrsCS#&UW{n -`J-ѯ^D <`8:k Fw l[, uDW!+v883BV)96y$~2U7Bh!o̞>2j+qΨ5 aL/<>J?dl 72PQ[ry<%$ژ4ʫ!̜zොcitbyX\QR`uĒX:N І0'@CZ!(ߴ$Q9wƒ#`zREm2Т``[\"а978Ҹ 8C*Bp_aVe¼flL6*CcZd@;wS"k1Iy;qpI1uǀld:2͒(h,iJi6Rx%Gr/.?h;ߡ bR>x c,fA bf;cd)1hBDh W0QQe) A I䐈;άHcj3ApkR2A*DZڹst`K[ ܈}%]N2Oa@DHNH%SB,*k4 QH%,?SR@^IV1pQ@jtܰ1׃9 rċ!C|"01DD(.'"D$X@G2=9cHzv۪ЈGА庛#AoH╤LTnwKyχ~A'+D@.p ?oi714FUp%LwS89hDH`Хs8ʣ*3-s S%xIsʠ=5Y *jI R  P1HʭitI2yz>:s C>FѨ xXC6͹kn0H0 8QN3Z6UD" ZаVKM0CKr. +~k=Ħ؈y H4 B8.hqYRܴj#'\!TBT;.>HBP뻿CFdk(0i8UFO@31H\jMFMr;P6F<v{w,yG|G|~Dž~H0@H 咱ERʼnGLLHTX[.ȓlT dIlIDɉd hMɘdɓ,ɽ0'+@20+p1@b`H H(-( +K%! ,^I$_ͺ'o!Cn#JHʼnݺqư#Cwfɒ3R[Ŕ'cʜ 溛O3dhѝ:k*]걩Cw4i¨gfخ]~V0`ϟ= s;URhåÓ_Y'<%dڐKKF z1lغ<);Y}d}_p bQbōNe6B!Yv 3@'t uSyX~]_xK4 7cH:^|Me_{MA NF ; NTQFURִFValj6̡VqMT|4iKsCQ䖋qHyk:J#K9hZd=$i v2 5l"p8ȭeNpNV=CG# n1,$8a- ]6FxV|!g:MG Gמzիm v7J&>1>\iKhW.=eMsJvTAgr5FeR`R#hɱ - Yu6$kZv"ih:%nΥŭhkq sdHmY{ A6]*?[]^QFnw1VZTz[P'umc諯'|Ј~+؛w+.V xN!{ѝ0ug9lDqi29lhxՆ7$Hj]Ʋ-~_)uxߵǹ./<B{Sw=~4ond#ACc$[/J-G1SwGUkddlǚ?a̶1pv+f&CVwsw p|e |AxP0ttt}J}!C&_v&vCvtyvCg.qeg??gU7zg{d8@ `wVtw ŷ  e$3_UjSgp!Vy  [ZFlhaGe=d=F{fm%czCDTcCkF1~Wwy_ P`{P{@3B(h+dlg ttLHxn6 Ő {'8b3j Vy`hf FCuk XWr&z(t(4WeXqwHpv=~tM8Le tpX{(r2C?hW w Q~VxK0RqŀSMQ s b4t `H eWh'^vsfLj|8_Wkrx>̈_vWyUG6{P (n&n{rpfՋGQL׎L8H.0p wj j;0SC3 b/ hcH8nh?N"E׌!)# v g&x5 pfp{#r7`;h|W 5 5)`Jא{*uoQVug)IhGfdg[w?ґ.r ty w 6KG9gVW58pwjhRYy才A (psM;RfmsgGC׳H&.FiU|2`I k'J/ZΙm5vR?y[{cJ^R^\rzjjh抛ۼETU6 pipk3Kpkj: Hٺv˳ZYBv8)NWԪIv˨Iۖ!3^@KPLukWp7a;b|UպU쒿˪ûݲy,WCm닛)bêX  [Q@\۽qJ el!,3u/jQdT ;<> |n D5{*TKlkXC©f,K8~ {g?{ǛKg f |~O@;ȅlȩ: ĐC|~2~Lɛܴ8éx+?ĥ͵< ,P5PN0ȸ5;z DE|ݒ `s nΎ%ά@~<5@M\|ˣzϡI[e8 lT|h- m֜\+ъFѭ|l*pII΅o ϞtlӇBӶ Md bZg8VT  ,a P#I Kkz xFXfȟ^}/6]Kf ӥ +5o]ӀsM 5J0\WU-mzz,d N+ -kQ2t yXB,G; کHLگ-N|hVFg#xW01<33~>[v6F^#P7H r `抺}+S_}d9DOZ4'[M}np< tQm>P;`~=$Nڼvm0QgDm2M=6␌Z٘WZ{4UVptwM?A=F ţp)٠ҹ?T?:$nzg:CףHiGrny煼ݏ^wpWaJ %^ HʾNdqQ܈ߤ9 %P%;P˭ݳ>EAPc lZ!마6(f(Ý^qV~4n>pQhyrK$yz8}@anLlЋOP @>,Ĩ[Jff`{:XvOw0~+eLT0@^]Oqo#+/3Kqmɪoо-=O.~@H#oħoX/9 $Xp9 Ͻc!{!FX:5f4Q`G]=z)Ud2!HͤY5֮'ΜA MVcIb5 QNP lB 6lčHLB-+qVeL8:b&\P3Cd1&A N~XѲ);̜I| [V ձiNvcLBJ5ZnUXaK[rڽۈӨ[y%L/NMM?Yf}Vo2>{$G:LH%cOZ鵞΋6p˭z #"@,n &r+:/h;ZŢIf*,Ƚ2lH"%\:4u$h5o 6W3 bM)"P* ʫ08-M㮼:䕾Xlѻa,ƝbÑ1'q!V+ˎD2IzԨЂ1(L,\ B$ 1#P 9,냳PB|3.FOsϾ`qJ@ SsM{)'Hӯt[n#%/ev O .K¬JUzY,4ruĸlbNQL\ VXacTYB uh%kho-mYts)eՒ]R ެj+6(T_%x [s-NTTXφu ʜ 0u{[쑡%id LFm\rYJgsYwY11l4V,Xhu xxm:`sX;=qIvN{[LmK kLs kq&nk@}v*+`, Y?طvɠ| t9-tg]w!aTv&.vko|)t oœo3o\w/YV7Ѩȑ{3Q4<=|Gtc!&3m=p{;%q Q4áe,;&49 ;h݉|\犂vPq%W #87!դq~ʸ3$Pɶ"ča,ԲLz̋%P,lg_.,hq t&TMu!,`SA h X7Be!tžP̭x4]ϴ ܈ y=bw%,d8ҁv8[O/PsmG?Dl* `7 X@BnЃX2<2T-O +EC{-<T_H]f3UՀDRFy 8\sZTСƫÉYp{HNcXͰ$)` |%VX;\x;Y Y>阷s[xjwskBjphGHbCۊ 11-r޳ ;@K`866[i+,A r(p3H⒆𹍁zQ?<3kHep??.UH/!86czYi*U#5=W \@*Ђ33TZ>(2cػfXB9=؃4#D#P 3DOZlD|D{+(eB! "?ۮVH%؊[8zP!K(;#6("0C ='(C`5\CD @HdQ;CxJI40@$HCDDE9*I'IĘ0JaM4bPPB&,3`K+;3 GRt"$@^|^F`-5UjHR;TR(RG>#`Ft TpGz`(c +h7̤4"$YS\LaLUWvuᒆj K{X~5@/ Ņ4\zG)MXcMQd݉Ԏ"O}@M6iY@a(ͪE `FHR8 DhV0)k6jveX?haSـk‰hizOŠ[{T(dܲIȞlU3;.H= Ȁ3;Ǝ3 \->|%yor޾h&n (P޵cc*UpT=Ml(&J9*HR qH[ gH_opq(+rpphZBkTm ,bp ] _aqmO ;UqpIπ `p0FR) +/K)uXu,r>]Pd.b sn n )f(<0{Tݓݭxai6M=/s(Yn'` s.?@oE0J HO4RMpQ&ǘ++-OxxF/ ]u_ڽ.v 6nvl}hW#*ͫD"ljvhUoρpS7 ɮZKVǔX,o{Vxx1uIHaW*/܊6Qbc%Ӷ? @$!%xB Jְ>8qAnEK&`ȉwot{.Ȃд4e9{r/v{m0ΚB^uV7&@?w{dpBlFڹ%h"Ƌ6r.#Ȑő,Io"AL %p2g|ͦʕ:wZ5eʄ+J(`ǖ.jRRZ(P @ժhPō(֮e\*>h.^ Mn>z0a8hܘ5|I)T6s+J@DHU>Z . $d1i NsǍXcː&Y3&gҧSg˜u0E! SRZ*WTǒ%Q>oh\]=W_7^ud 9`fiLHj -$R1v"Q=n9$aRT\!xĢA)0DZGM3p=čjt!]PTRVb{zZY6&c 8l_ J;-y ֧y;8jhˮ8b @6DvXJ0ԾErǍdRE2<6t):ķZqXV^|з}ܘWrlgb'ap0(TpBf?_E:தEvt >8Aav64cogtJ0F];j4EilzZMVvZ 9x߬^|0'x5 n*Rx@E# +.uv#Nk9H5nYPpx0qp`=Ty+R /yON񚇱mқ{w=чOʂ?5dًW @lyd}A 5(gB?/6߸*!1vF0jY|F82X~uaHX^$YaOZ(PHޯ:Cp &6^YE%Pl+zz.y_2I&1맹!ai(`b)!UH/R퐇pJٜ2\;FpV4cFp9oۛF`;i' 9iNtӞIwĹXUKn*6kz+8;b.`FA S"N63ⲡ 4.*A pm Ŝ3豵f4cӐwMc/&q' fH(: LoNK&2Ϛ֡>"k 2"mq^˞58'H+'%DI x^U؅<HB)+B*.C9]3hdA(U-QV]F蘎C5WL=QP_}_=V\K@]b<@p@ (^^hř%̵\LQ (el ꩞mU"!"$S%|+| l%(4A3T `;] V ZD "Z+Y_5!)<@gHR]!ԟFYPK4ʬ  f S$FA4@ A`A#"%\B(Sx'+6B,Ă=U($AUeNl C'z⨁b((G)2.肬-_+n+Ƣ"-Bgy/@Y_dW |E]˴%O^4FA56ZAUAp]AA"xB(c=;?@.h?nNDSA# j:$>($qMCCDJPXd*j"HN[@\}lJ&@Q_p˛d FxKcMO֙H jA$|)u6\J$QWᝓ __%`n\%~T-m=Ea&b|7GفLЅ\gj&$36d%?@Uł)4=!Ao[pg%Ze5 eEXY2/Wuj'8I~Ldy(zg2NGa f@ l A,B&B,0=UnC3'hB&8$x@ @B=Z"<XEdwE2 C(Ra׊td g@jA1&NVi%#ܠ|T=d^LLLo@H#adAA"|jl2h$L xfꚝ@ 'Ԃ&Fi=r*܉jLhh褆\Qmc,ɰH!|2&Mrd6@̶Ep  !`$`&ܣ&pBk @DAp†r+:x=| wC2ù$Z'`M@NZYM-dJ⿚jf,Yǒ^4< }@ h@$L'ܣ)<oN AxG 'Y,u+,H,P$C<8&2'2\DX4#W-5I,2G}2Kan:@ ! (])+B,`!HrjXkTAF@_⬟`:.B>~rntnfn]fPVdu®(}(|5 df4^xdT܁)4.$"!<xQ VY^5plC.;,sĝ/іoaa/,aЯzReajPXŠ܀i"*V@B$x%!A`pۀ @ %YoI|9pFh w/s~ bs5.\QLwPn-Z'Wl.o|@nD=9֧֗@ \@Q#N!1/dG6?)EGed%fd<;7tbگ9f}E0FIz^SѶo@ PooS" Lrrc4(wOT4skRt+tUnF$nd~aw<`6pkKv5 m7Ap52$P qp('H(KtnX x!ᵆt7,5fcx@h$Ũ#eigeVFd^ UrQ (B2d7ȸpwUE[DrDrhG 8 \{_<ƛ ^5ǩDĎ-%? !q&v(<)I кrR1 z{d_ctZWˤofYpJ#LQ2t͑9t={BAHCHr9B{[|v9܃G6(e}pWc GL- Ƶa<}M=]PU\x~Cȃ}kBE଺}I7wY>4  4 `D)Vxć 4lC*T0IRE 4tf „ ƒ,eIӥL}zFG(E\$J8ZuU-Y/Zٴk kVBlI{vq_2&Y}3X+@e&J4(@^ 2dR vbN& Pn*0 AÎ*1d`)ilœIںC*+O0˿Ï+!Lks9;ih!!:BZ>9lPA;:DL|EgrC N!HF`H:=i)#'Q-9h&*),KXb R001sˠ)3\M5!ܖ[t2HA pF>M Xj(RD&Yi `Kkx @º(DjLeU-I10A,4cʫsJؔ+ŠdˌYgyLi4Ӏn!qM! f`9уtzPҘ| j( r_ތKCK3ESA.TH_";&ʹ*T>> 3%Y3d͉~~0=om N:Sj4X  ygEt8}nzX;aQ=VUҐ;!coB+YWq r&Y@˩<"ݿ(tg|\DD|_Y"`~M8P45Li*x>`Y=A e 8('@6^e$ ;^ʸ=|O04 } >:*)8A#d@) R8nRlѡv’#Ccai+Yq ̃d{A 2\0Za e!㆗قxؑHA|_N1Mt"H8܆K ~5W c.]GM% SP)""FZA3P&pDXU" GF^9F$gRs:c!= icRR7(rRbŰi'Oq(Q v44ӗT0!|SZNpX)Hq`3?[BV̺9F L8ɉ9sAi_&٭pt ٓ@*h6Zr آKPM:'( J+G? ҔA0iN؝)eq&L7Ӛ^i@ͪN1ig@Poeٲ>Y+TO [ƈ\69rT[N \p%uR 6bDz. ,K+9dޣy[GEj|3@!پUhO܀`EeASnTԐ嵤N{{5Y],# rI^ 9ý-fdXJf8Z81D$%XHY#]&1р'w[5R~p=5ۜro .۟X+=7mBp^vH*k9o$ % >2*T@ݲO^ 9Ej4/Nz|@O07\ c?ȮK{/O{f8}5>KPoPI7^Lk‹Q@bu7j`FNDrN t-S )N׶C+LƒP /.n18 ІFn P P B!8DBPjqC͐2PkݾhQ¦&Hp ]9.BTB/,$qc]onGS خAP@ E- JbQjg6puqFbY٭缆&1p#Ղ\x|mx*@NovKg&G1}m :}0 O\%!p1(2rOӔO n$078,E鎢3@o*@ 1 #\o$N$g%% +֑ irA.MQ'zw6(uQ(99 o`ro*e!#_H0v'R Y"i96e*`6 Q -l\jTҐm/i6&93K# 0Qx'A (c32nqFvQ$2*26S*!'EQ'Lstir(6Ͳ֪ `-s-Q` (rS:.K*#s9ՄZ0:V:1;9tN(SBSp$3)C*_ɖLEF!"uD\Q,Q*lN7S@$H.%ofA]J+8@(Xa .3#46uB6X$ Q*(Ÿl HF>)2jN!*@0eo FG"26s`QH$ 삳4XTAM,#J'TKKTS1CLOɒ2EUT )P;(k#+]k68D0EoGRQ N CJ7 48I?Y2UXSC5K)Tkr% US0VP$r2sE'lDo)CvuhMOeiU8ȡt*(Qu?smEoKR` @[9UfĵS TAK]T]('1@T^4OV0M !NNRW 6pNj 5w8oYStV-+` dMd\meea@dfqNC)L hC`p?T*V)SӀq'vjipʴx22Tf Ȁ J\ ֖m:pem\\4oӕo[Ng Xp WI)NwpQb)n& jlrP6>k h3l=?MuY ^dvr8k{lO,\wWC+Np~V;66y3hD<LrR9X9`"5(Q wؗL"lƠh~ٶXv6RvIrVTgv ~R񝰖Ox8>ʣ(z2Ww$7FhLmXmtF1G#l}`0Q_8 n+0+vLfMoX]WmVxf^xV ?5׍"`QnEȸQ"Wq'L' 7 x9Sq Y+yZ9g9}xAo9qn)G`$Ȋh/9+D8ltT!Smfi*=9hv xqy}똗` `٘d CejT,fN!9x+Ï`5D+)xX2Ǚc$[՝5.$+%%k9[l %U]CGIqڣ5>4) RWR]Mޠ:XʷcFZ^|'#yG9myľ,#}X@nࡡ臞_)J4%nV'gGɉm#*ނ9=9VþR1<D4?P(*9C޹`9lr빪`>I_O6U ???o# H>:1^k. )?qaQSY`>N1 uiq3#`58dP=s=B%j1 Nhb&Ĉv*A)Xj:v'#ȑ$K,ذ@e-Ԁ6A"DHleǒ -z`%FPÉ-V4+s"C28J+[9S&6ϬEpF꺆S*Rpxp*z ).\> ~AcBd@hg=pV6`@Kݕk>Fmv'HS oK)S1A 2t/Q`dSNETW^块^Xm_|!T_bXc7e_`\ogWsi e5hD݅>RVw]6搉fH"%hΌR\4-sA9U^X\pk64(H9%VW yg'_[`[l 8~5] &Wl)IgYl6ala&uf^^ǁR|ЈQAAmyQidNq7cD14դ DIʺdNzfH+VVd ZpE_}j+_pVfF 4rJٖˊ&lNbɥ-rLc;D[QN@*W$Ŕ>ծ#e(I}k ] M3\Bxae"]( r&=(ZIx{nء/B5Y+鉈\ux0ML\JWЭrPFZg(YwJ=Q8rNF?nc9NN_ro`dzmxrZ]ނu[뎅.MQ%Ab1dMRA8ߑM# !\Ct67hFFň&tC??i`d^@ oHdCugE!HJʈ|w Ē./N"@fxޘ060;H!@B~yH|$s[IK$#+P̅hb=9Ӏ†7xic2Ř)?|'Yv z;$y` `x<v0,R *G"ɔ9(,CKzzv$b "Bt0ĕIh[ڢkid^bۉi=v3Z68W%@Wb PaH!/q?Tz\oڡ=/2i-[đ$0&6-I99lBp?.idB.; 7ZEf@ Y ;yK 0fO G۔W1m&eoy$׫ab2c%¬~Lעjb30{%rXur@s7Ls&7:Jˣ7*Qtq`rp ;I+b5'/RU3zOIIGV׬HUkK3%V8{{ 2 A v$KvZs00}'4˹+]:I;. MIq e)Z b'FN)凪 Kg^AYP*Кmtq ծ'JH7Q aȸ[gb9Ӝ i@ASd_%oxsjF@=.J..0㺯x0 viNfWU9yË>9+kuɫaדt#RIJ79bikXK>/o1Klj̢QuV|?LMFy:JsU `(HU0 Gz pzvfլmfVDEg*"hIoE7Hai0%1is4Wg Q 95J܁ yiW7g^)"! ( န@ )pZY|}l!\yl!/v3V$ ܫXɗ -#]\eX?4oEDǢ&н:39Lj ewQ0 Xɼ̖`AN9ҽ;7ی2l]Åˮ9J"{cI3*QM Kŝ)5(P7ྫmpa@F,}ib=d.˹'.`P Oy{@ ͌)wPZ@LJ]U bدȉ5^u!=s̮Igf{7CMkȺW@k\-36p햲ߌ۬ '}0W\{=˥gxUip BM" P `Ni ? []?$c8bm*|ImbFz>lC97$@67w8P>J?0*ddz00A67 Al"  `̴EU89>ݍX:J*!y,_Igng26zĂ"+]`إ3XEn9> !^DcEXK!+6wT~ZUU َ# bPT =>U)K~ʕz")w:Riܣk(^oIG^%p:>.K˒Q4 =+MU ٞ N Zӌ`JFS4_MC( R>_ -W| \J?g?YyL }^{r?+Δc!u1:HƤ4'=< L x!$3_-N p ,;ٓM #.@쎚 0r-L3OBߘ6S+H&=p.<96KE^73joupx+ѩ#IwiҠ+غn"M ^P{)x@[A蔞*ZXB@7n`1B >),^L11C 00# #jC5TPL K|p΁=}PCA*~p΅M l3g  tjT~A xYK-P.s^quh*<0S 'RAt"-). ɀJ*X:sL(JĄtzBo1K SAJ…8LBpó(,T@* ^ hl1Z.`kBO)wӭGeeODbJ߀c1R9Qڊ*V]!k3L /\Ad!UB4чܒmaz .Xӷ^@SXp*TG1Bn#5k=vD /`$uXd1[6wd>jw` aCA>*Q8k@*;  48C44 R:T2(bQ\&jBli-~Zhi8cIѥfߥNe38`oz]yGۻ6ɣ:9jOR!-!#Ys7%as<6 LVJFLRh%0C D1FkBitիq#JIzK~M SFK  +Cz`sJNP_Ap WNYn<,HGtӛ YE5PksEʂ si>Yoj;hAB"#Ԓ[ϱS'}VҝI})~x ]rP7UmM+{ Ċ|}Ӥ>f+N/fc:Ssֽ^ nj&]" g4CM:htbN poC1ء$41WS X(בa2с';|kF ]xt.x[Xe?uJȅ5,1ie-~Y,Q>*ϥfi OHHݠ-𠛰nGKQ"@>6l#d# [?'y *Q^XeNضB"!xjp9H2zcDjE:3C ܫ8)* @1 廯SXA ;>P&*3tl*c)kz[X1؂-F@B^mQhmk)*ЀI+5*+i\!'3|%$St>/>,ˁH'sB\iDj8H$Э&-';0zFt*G?yhcfP9p*?̓GyU/N$`.;$DUP ixeֈtPD j]x^_yU; )@rC p:}bJ .~PfΔcWtӖhgBߘyo0Fځ<uȩV#FQ(ԉ~&7-LzRpō>64udA{NPRﰳMc ijs& kiOe6GurXG"ҘPmH 뉴/$(f0(a 6pp򸐫Rj*b*kg*VjI]WEk<?7%tjr]~i~6+bbd4ua@-P8.c?|0D|`CF[PZcMNnv;%Ys'NSF4ǡNiZ#\.`/m%5 1Rߚl|5C)$ݒL3-d`,<و=RkQI5Tn9stR2턭u鄭o 9ÞN]?a+e؉\n ;p:6ƻ 2v]Eˌ ,9~끒B[~(>&bӕ{ZqlG+O_;H+/Ylhw<)O}Hg 9Oo`E$~0=EU/qζq0AkRؔ bPG3F?ac14%sP)0it:D(8A nKޘ06 y( r(T!:F̋hH)]!^s_7w΅*Ք&"+eCh% GFoPv.be '^< amظF.cU4}m I漽$ޢ88}qP4)kPb1H_zCp$! ?q3~\/AF _I? ή}2J$OISj]Lap9P-d1&2f:'PJĦ69ipd27ɉTd+56(N% } 4p~/meΊV[*6je`p#H_IӤ^f6W5rqb9]$A RyQ\+W`~r%58vx:n#:F(q05zulg;EV"GӺVj|nwi38l)W#V򯀵ijWKb|b%越RoNy"qnn4`A O}^3 [ٞp+*ݲQT pqW͔Mi+7غְD,~E>q 5+Tzox7Vμ,zl$gq<|۲vs߀ <կ V0E;YOt;eb8@"=ݫ1Rv/͙b',2!cEk~Mouxr5Z8Y2ys?CY0% 6aٰ&&nB|l4sh1[Α'V#cmnƛQ_VJOɇ4_n5'b Cϡ>j(g񓍫&wl" qɇUEd>g:rh,6܎Unjhߴծ1IǸnDvF< c۠\9!6gSi'9'ZlkV?phsw1M4W^ݧVFQ Frа4rKpG/}5 _5 |p _  /.Pg"BC+ ^X<,].1˜494_~p⃞頷1u;΋<.^ C@fzwC'x=6Fz֋]x#nf;Tkk%j땡!`ڇi?qKF1O[P~(ʼn}WH-jy_|[{m\~> a{8vsfngqqC7~C${8\cTng|Ww|0}@xw}'  (}vU}mGtgtp~6hW :~z7B/G_;6hpwl ; P{g%[dn{}}xǁPC%tJr}wW MHpXxs'tw2 w~;y` u%?::h [M؄9si|xnHltօw(LJP UjtGo 6 qx}sg2ׇw ~x~鷃: X@(( 5R:h(J8v{M:s9r*^#(N&o>vȋՐȎ Hvxxl{È~~@50Ōv g Ċ8INd"iӏ`sXn{wMhhphxCHw(y0{ĸ~ƈ Y icc ^6H},FyclcwX5h%74+)jհlɖ~M'Cs{P4Ix| 6IP~`xe HBYy hyXG My T~!xvpُuA'fmiHKKN֖Y  ؀t x7Ho&Gy퓘 i  0 Ps`xX} {4HDM}dojeh(Dpmwq)yY X}h^`f5 K  I؉ڹA 9IZמbbݦ|ሟǟjr$lCg9P ڠ#4'B {)Ex0pyF2 4}ɜ;*>@hB:*GM2YWɗnXl'kxiW~"ٜZژi cej @yqʨsn6u9|ڧ:tF)stPe}TꈾH١J9EPҺ*7 }xNgS!9{W% ږyzyg:gMIz) Ùm霒d Y*ڥice*Uc Щ 9_ڭs(4 &a j*:S"orr Hޚȓ N6)Ω9 {Q [Q0[v(:{`N9fNؤ!+{Zw)gw+D-[yj(y[:kaX5ٗh&zE\IMO046^G ;ntZAb{)r{4(빪fgu92pn*0-{& i\]qxtڨ縹{F9岬46P?WR˟Dvr xaDdqwMZUɷ{Y-˴M{WPW0( HB{E9v[fMY J#qB{(ˣK*HZJh+ykS k(ěQT@F;}Ct7[;i{lKd )¶Ac/Drpd8ML℡<졚 {* DLE|ǫ{ktQxŦ+YԽRЧ`(ɾ,Gelv[ܛgu8Kv5zov,ŻIH-#J0Ă,Ȅ P0;”xZXܶXyɲY{\'Jd{jh~qruH7-@˽@u˄||IxΫdLj O}*`-ݞ`||o~gHި @J`MǻU{xF>Vܑɜ۟ rs? lqժn.j3W @ i]}a0#%%p+>{-̴04j1Yv8'VG% ݣk+*;ݫnrIhMmPޏSN z>Pc]%>e=*N(`M-s~?Wy~|&7LZl(n͚#^,)N۵~yM\ <}TMq%,JVb Εw$hFsU~ i]^^?~,c0v<}wzKm?Xݎzʣ{x^Eչ#t>hO؀Ni8Prю-J|ȱz+ί=|*-mY}H;/O~0 =?7<:k+>Э~.TTԝr \ox] XuD9 i kpn.#NrLZ:ˇHG>yBoܒ?.~>Ov^x0 æoώ?,M+K?JOhC9 $Xp9 ;w%NWqň &GI)K֮ͤYZ̛2m֌gdʄ MVh1f-մV+SU4 W4l AY %Td q^R׮FfѺ_iΟ=id,P1R8Yb*IF6p$g͡Scͥϝ5̠C=t,ON7Zvm6lg9p(q%Nʥbzo=wTNS1fIrĊgDsgۃ_}cm@^{-hKƯ[n7* n8⸂ [碛N.#Y-M;=l>1y1!/S2! #&]Sp6t } J89DCb红*#Z;JAA̵~(B;yq"z/>4Paɵ't2J))^RB޺K0SU22 4cs7ߴk9Ƙ@=D[TFy|#'1&9QSd'O?-&TZp7 MEuݯXj̴[M7kk 98q;uE kۗ;%mֲg#(iZkm5} wd5)R2vu8xr@ &՗_9s+&X10ԩ Ԥ&vGFBGڌ𥳐ce#*ܿL-e.-dĐ !k4`t_\ģ]_%6heitG)ZV뭑llF^+OmF08b1e+z=T%>]iz`X۶@NoH"|{/&ɶ,M %n pmv6jnֽ_] p3,1 8CMzFL zd{A)0 "Spъ@u+kӾ}%߼@$(A\ _p 3"#% t`d LjQ 0 rO9|2! ѷ2ⴋnPؖ %6172/\'NjN 'Et/H #8-rq>Y8F<Lc`hpjYʤ!,Yڧ!X t…+AF׿4;$V<犞()WQ^`TY,~#kXQm@/3[>`}tܺ2Ґ9qK7mw-$[fHPX=0H4)h;о%{.ڭDD -̣X:K){?i ӫ?TqX, I@BCZ=[*6+ꨋDC)1R (I,7> .@ċ;3:* -|,$(h3 5\? K!8"! @`>=6S-\Ђ; 3;ë$kӃ048t/)p';ꦁpA?[kjX'Pĕ L!غ(hL=@`6z$L:H9 H-E0;@Kb9Ynʯ4;KЃ2@7p.>&`kF;ZX[5Fqܑ5pDQ 8s̵qQGDV88ȧ3ث%#fvĀBDxD@h fdY\Ⱥ03TYH`Al9 TuC0EK>H#=b`>zfJyD(9!=w…JLx ;3+zDBJ:Aȯ(EtHDŊB 0^tKR Lأ pth!fA( ĜL̡LΔ}Lsx!;.ѧS{,,63q,H$MM-4B눍FP㬹$_RLp4(X#*@YNiis0%AĜ 4*4 1b>ӡVOOZδJ)H ,xePM͙IlzUfPLJp:ł% H1оcL̑Q0œZO| ,ԩX3~j<*J+G|{yM0]H1K 5?E̢5E |SI4@#X%@P_Jp7X!DLk}{Vh]:B-Фl<3Lou4RR%-Wq8D3)a1Uf̂M1K=ؕhV\uf(,#HjМ<5WE4Y FeYG#җ0>cM~fTxmbLE;סǢMkK&PP,UƛZ>(Z%J(=ZFe=ۍGuh-}A[MY]ۦL76J.\M:*ĥ3=;, (,MU}սmMeڧZu@ Zf I(7S#0;>=[Z@]1&:sn=^( \^R1Y^93Ջ!Y*]RMx?ԏ}߯߀]=#_'^*J? hMx䥍_1 OcR~u%c5[HA=3hC#.Zl \$8̈[h2f8$LA^FF ifƅPcX:yz"B\^D#Uɽ#+s.ޫwg_-0DPi[PIHBwd1(A0)S)@+ [Q}vXn`쐖Ik γ%Zf#vF=J'ZaXBE~Hz 5jfa֡h?͝mF3KjinUO;ܻ& !xn`!C@'`<[%ql:'YTe"@p#`ȖiG!P#D m&=KmR d< B XEU x1"΂"{>ihOH)@f н 8vK'H]-]q4Sno6Əqa6Rle`ldfb loJDqJl֣VWMMwZꙉ GߢEZ$`Uu ݇UnBB2H Ƚܳpރi O(?rO rn$&c^ )(G b~=] ,B?/QpWPM:\1;9Piw(GÙʀ _U<*xHR{glD_6ݿ> ~Lt>%?_T/x^u+?R rtYw=}ܿqnsJ^'O=S}ʴhu<smx) v p@p ?ˀ 0`.%cw szN7yt 6fP7qApㅂ7iVX [%wRXY{}QSy'sf|52`H1y@ PPv{Zi(|zzz۷w8Ɣ({FxʮR%!0g=c|?=zR{ ȫ OДMM'?_.|}G ?6 " !KI+w!Ĉ͙h"Ō5>#HF,i2ƒVl%ʘѬikŊ%Y_x+Z4cJb5) RJ PA 4pAJ @C(X@6-$na $h\=` 7xP%>p1$H8d Р4 &;@P@lParw3p҆_HxČe2o$_R䕋sC5:G,%:ÒN){4NjݺC@@YS jP Cԗ.j CM1e0D! cJ6+Nŀ Ȁ=%g~ha.sVѬt W®>WqfAePH8.* B2hOXO ]&3CX0%pX88 :XUybnUwl9ins!pQXfRЄumdZc9ڮg]2# dg@ 0JEiZʙLRN! Q&I$ f @`9} +wHyTN'YҲ`ү$wY"FR* 3/g^/Ҭ5s!x͏#^@/K12,g*rӓ醴+Da,zȇDt2Jaf3;hDZ?iix(*Cъ^t$R"d`B,."%_((nC74'Ӆ'3U-֢-VWi.LL5\2<.cya!cQQY}eӾ4Yb0 VD7bg}b$St\9[ h;Pg4LBb=n#L$$%-Š) @<(,^-bEb,FrdGGdHqGa0n!1B($Y3ŬddOf\6:=@ GS:  Kbdӫ9ЁU<^VADAn `AAPA".Lم+DD' hB#yF &EPFփEu$mb'cczd$ _قeFBLN2n&gg]Y< E`W-O،# ͐8!(f@@W<"$~p&Anr&H \vBS\'/ d7lN╝ gTAf5UF̕afb'HާmT~2 ja #^Zf"hYa:&h4.h}5$}8A@~\n\bFOTfE|m4;=Ҋh ggdgH #D)L6LC3+%"$?B$܁n[5`dt]{*az$hqD2)y)g\8ʫ@ņ@fHk8 @ ,B),,v;&hB)L AA@ |4enldžȖN /ʾe(}Eά3Ip~lZKZ c)˃kڣfe >-V@l##&B,&)B)<<>@ ԭrB-{Y,ڝr2`ICe6ƍO\,JcV 2ȃ8Ǽͅ(`[<j HH& e$<'Ă)h#HAjZ X,0N= \Lti:5DCH *LWtB0EeI".ۼګy:LAf @ (ZbB))*FUl#Ԃ ׃ 0 ǰ0HԄ 6pWⵂ*X q lV 3""y1M\K~ d@$) hNi$!|'.+`ۊ)d m#9J C!g{M, GC4I%[2`2Yoy\'r03Kl +KT2FژY ZLN$̈<jo./!A@# A D؃ c !Z77/83$1%ߚX³'L`4lƋ_@ K$ARp$ h@T|)`F/ + DAC6]r * +8y4%:SE㦯ʝa uud!mNYSP*m82ʢMV&gOjWnݸԀqc õ]6tLKo^#Guq0w`aWNsHP;EdCt2 7ɫchj6ЋMqH |;EUÈB'#cj- 55H]7~up +~rGï,9s#6nZ4N<ҊP"ROh!zY.~l-lWmnq'€׵F|up7;xߧ԰8W0 Nmt74i&`H&b6ќ9dٜbMI \(#<"0Lm5kmA'5uo+8UiWd W94XxdaOyۚ1VPel2 tr\q(mW7f iAiWkTm N3MlFs@|]/8p7OU:(C7(cWw;ǘw X#ITbx!rW pnkqnHoH;/:J$:"gDLM@`17S4^o9"XtxKU kc%ezQ*:rނY,Y:It;u=𺌺( C6x_1bFDNWux 2ɘ8Es[ɟ|6mVye 2?O7×VngCW3FP|ŋ #Iz:W˧Wj/ٳaÆZoݼ{lB ,0?4PKB,L 1 Nb7(zG[Gr9QJCcaFch<1q*!\UnTࠂ,ⵁO* PsN-@ANf!gB Q#$ÂAHhDKEi]2W'hfOm%J l ʂֆ}t/a+$,Ͼ2Ygoh 4݅0\O&$P;( B.A4^vIbݠ_%q6Rz1^|y}UjGQW=8,RhXK8Y(k㍻ , #Y5<9sKTudD(C؀$-ӵFZizi_S}U^} :.x|J&V+,kob߮0ˋ;jHS~H {N;5?-PB#&8qC3PH/?͓nk:Л哩TSȕȎhJY c˚+`ML`b׻΁m j0 )[TD"$!,<̗:q)>hGKZhOJHn&5JҔK%+ EWr%j [,X ) fbLdJ[uhGc3d%dجAHoj b ar>æjvIE MS!r!Q9q9Soʨrr_tPf1Á8 P\ycu ,;%,r^0G$d6ޜx 2@-AZҢsݬښېI _)Vu2ASfk_c ޞjMŤG))if M +q3[1mn2)]RTx~%砰$7I +so~+@hdՁ"_ڢz`풢c(HM@\8͋zz"vfmW'Oi0&/C+w'= 2T1!_'QbH(dV*9_p2m`MVZ0F7*51ta2sgn Ҟ%.piG3Wd9q d <*| T`F;ڧ[!KIW_˝D'4j KI.*H\{U W8k4k=xLzgZSn]&]rFK0msئo3cv찡@(4O$m-4x?Ĩxm4z#U8 _؍K WםHMj ?pv u24Y.ɯ/pŦy8y{9٭[Ft?wүզxwT]-Z]Vrd?^'b[ӺBdQxwJ2XCS5u(5}FGz\[=$pk |`HpI\o > >3yXY.`bnox L ~VrlN` AT{"pi,)osb$FMBO:uvoJ ʏ8w cJl=pp woUiO` oC0PmJFi!7RPFLu%h+dc׸C,/Y* #%# OJ2nZ|, XI ̞/"h'-'/fP]npIoM.HMD*Fq>lK"=Ԧ̄<=֮WoߑFo@ lϻVqܶ^&&g1 txq8|qHJJ (0lMlq #f%FȽP׎EawEp2A1۔ Da4!%D˵h*i1!Ͱ F%q""Q-#c$/,%xѯO/.L2bx͐, a(HQd 2gDp%*j́!~t%,J" +,0I IR-KU`n 0ޒ8ҽN1<쏸0.P ?3nOX*(V`y 4 8Cĵn*C0Ӓ [K5[5I,asjunCQs8[8vDor@$kBYbpI*J;.1@pۺ ! }4T>!S&5p,IaP+2.ԏJQ./QdDO$դBh vLK M#mֱ e/DÌ O(E_Q[5SR-U* 46\ϕbG?l,<]QBo\v@4`"8_}Dng=˚ `DNGGNF %b3Fc-vRZsZu݌'uLv5-t$remI.^S;Off.hY`hЌgN]mUFĔŏs`MK.0`sZ!v2!-to "Ol!mum%sV|nS jv.2-TrLV-,P6,)Hʹ8HČMiζӁI<.I DC~oV2η eZGM+K3VmoWjx"}6_ |MT167yUJ.oo798U%8<,qMb?:MŒ|A.3Ya{ """|˪` a[\7[AQSHx!nXa X_#aVox$%BpVz3.O;-J/S{ ]Cvà 3(MN p ۜm\  fC6vvɨjW8o|8qwq߲aXkS!4?` xؘ-ΎKQ0M۬r+vbY;ar8#3F`|@P]]0M''] Cgn?ITs;-ыqBR$@_.x8c'M^?̎+ )㚋Gbc{`S2(v "(Yܰ~mVE9ۙ9=-zIU-9SKKRAxqq[UE0ʢ{iGL8/W*iWtik2遼@#i< GHSZ[zxuNgxAuxN$'TJr8P-T_K W$7j-71ac. qϒ5tSNیb+ 8[LS%DvRS躜9$]{k6 vە;pu88/b @pu'Th}FSy+{7UQ3jnNrk .APk( ` }ڭڃ<4rCҙ\򞡮qW֓<`Թ&27wS=;v`@߱Yߧ7GŌ.Ì(W"1<{+ɀ-W!=r[ƨ!Gm' eH2@ǯ6Kt;w>5ͻ.=_/%|XMaC`ՆVWמ!b xHc =\ȍNW^jTauM\-3xGn!CX$5gv>8lč)ף֎6]j|Y, "uuվ o+8q p o { ChY1b„Q3i$K6;vLU|  <"6lx`@@/=4RCxJZ!*J|P"X_˂ ֱbU+[_7| "ּyz ֯4Tmņ j5 K #jxN1b>8ɢ3cZY(1 g y#Z¢uV,G#KJ;reKC!<{Хۻ/4fQ@!k d5 X\ŁX7xd_o!WWwZvXbT-Va H~ )#ޑsR*W問˜E\@ong#dmtT:խN{./ H-&w|r'Ex9wK-Fy2QBU'28VųM !hE|߯WL #a.,I#rI "Os=wn)t\ɵM&?yY8A wsځxev "τtQGH:)1,:Mtø* b$t{0;8_1 W#Eqb@q:1# J|**gek[#˸3mR{-V{T.9#;҅1>τJ~4PU( 5*BXB{@$Q71)r'V"˫ds-93pgJ0sF09tj\wu,cICoTcGmk8 \\aLJ*3ĉ 'Tħ iS|Aտ}o F5QW> & T)c.yֳ(GI'ƤXͭ (+m3 N'wRDE-uxL8>[*U$"2SK̓ BF͒7 YʆXyD`i&:ë U`&0iBX,,e n(x,87ӳeמ* 0 K3; º*PW JTm/O[cF :!pŵYVx`^2|u9n= hΥO @Ÿ0.;e껸`_L0gha 75ŶpCX J"Bim*T<8+/.e,bng׽!(XL1I4n2mELV J,@A@ a92Բ/( b9iWɴa*4Kx!nͰYHC$0W]9BQNqkVOT3 ů0hTqt6Bx]]ko -\W^i BJvDt'gD#`'M9Sp ٍoG+k+÷Vm*$ iN}歟w+'ba‚ ?\] AoE[)cN֍)W!vh2 .T%4g|oPNsn&JB۹h|*n<4w+Ҙ 7/m9@:t`4bH@|#<NY֏*{i5`u!d̩ԒJ/ mOKbK,2^Yܳڱpu|t!o d n6x&"5W9tk ,S (zbA{%`j7vz>oSM~!9^ص- $fvLzT:!kv3|w<`wI=@}!$F2"x}}.R^x9fWg-5K$cSq=pXg{Za^w/7YzG~!$BdxzW4fXT#TL`͵-")dtH%P*p#!1l=0 1HmP7Hg9`p@3nR?pU%Wtxa-cnRPU؆Y.k7_< $MTsAj@5uXzt^hV"y؇|`d*"P4ffI5P \Jax 2Cx0_'n:gCKU@WTXHwNXi9?@_[Ad/$ r$Ai6Qϸkiׄ)Af``3Ily(x?Ѓc7bRdxcRh99-ic$W.%HTX5EUoucG {·"Z5APkdg@:k'MYfpVufA=989AɎBp21z`gMiւSЛJh l8R){hcb>:qK<];A .uw4(Z&[pwaL/H³w?`s\LKV|$`p M J[>feV#gqjl ק7gZӃ8-n"?ׂ]@S9 !Dj*!B瑎eS/Ghy ҤZ*%;l)Z Q`@՚` `HM` ] gM qOT0]׍0ݑ=]9_-]'15b;Ҽ.:̱7hYs\`Y6M;->`@Y-@t ĝԖM$Ykń.o/#=4.WGh9QCQܨE9(Rv׉M>35:M5N4RZa!,WE mDjp `~c \PjD͖{m6p}E܆m4W4WsE;i$ $*v+x+I4HK-pNA!/EsCfZ,1D[.c͌RK^ Ȧn σ#N8\23 F|A"袎"(D)QF*jL4)QH#v#*70I@OQiAJ?KU10v&H  'I;x0Ó`]r/FiS,RxkMv͘]&T-)@+]7LHzc3{(xسtzK=2 @)T0ɼܰOA]#{7bUBT#9 cޑZ`= BT+|ia1;*godM#ws}m]%[ AȴF7uhNn.y8^ &1}zʁ@ Bz;\b hD7Q}-](,cEA&p)沉q LV'(A 2;p (TLJ`49j.)c2H'0RNu."м!#%, y*[cT QB̐*{ _+MSm~($D5A pm@WNy}K٢̽4841'!E(VgA GY4HxћoʃD6fYnpkC R1vjl| ˊ97 b8/VκN`hT9qF54 `R\U5֏N(\F0< ܂Y #"k-qV|is,Ę!e(y51AKXٖv]Rm"kÙF┏ 'ΏwÂ6]WC$U&&+?LFԇP;qh[]w- Mh_1d\ONe!XKTqu6-I~@WTE#δPnwd&vSd'H_y@c*S:cKnnP.ȸ! n K-ЂJYkNxH.!X2!c&㬛8 7kv Z;#<[ K? X I<Êq0ʹ"X @#3ԣU h:Y+ FRŘB(9 ;(';x[YhDxdAACA &-c,(`r 9  Y* '+,5=TQ74b=:CC偺= kxayG`  Ad N@1;eH|zsKDM2?5#E14"2EIXX9_EбcH0::fd&Ȃ"q {$R${z[sLFXGvAL"{B0$Dz,YE]cC#ŊӢm{Uђy :/ ɑ4ICSyziv`k%乡$;dHL[Dž !yD̏˼1Q"S㬥 'GK" tCh@90flD+ye'Qj0j[`eHeHLG4&aLǤ6ݼYZO𑗼aJMw2MH$B@bt0B3NIƅXYØ'  ĠvLiHiZlȆePQI$l!ȱA{ܛXJLQhJlcŪʱ<"Z̼5r@&!N 9pɏuSґPC=3S1 ةĭj8 wCthAe@ `Q@ooTHC̘,RN$E/I%*-aEϰ&%C'B$SZ S P498T0 T7;Q<\yޱtXlA@ %̆d[L G{!ḶT;;Q%Y&%6M]mU~'|@l@E3 N7`֪c=V SV QVSSc8jid j9V3pl *NQdx+vT`t% UUHFUP[be\\aV0cLp@c3_'8廅i~ukڅ\P̃*ӑ?I?CK N^I/³EJF#0d@?HeO; + ^фV`e\I3e;A KPP^K0A3 MH$mZhUNHb;"1fj t19{o"JȚ&nZiu?;K&SYydc HFad%`&*ph* @'.p1 2*1Ah[fV0;"K`eQ(fNVc-HPHD}\nueM@XgݦgMZQ?cC9s[ޕ#y _]!~fhôE: d^뵖޸&Y1O`O 1x,Ih~ZV<`DZy@ mH_P@9=m=.@V06׆ֶَؖmצnخm66mf~NN[sF!nmy 8hԂ+13$ 91H3++J`0! ,^I$\Ӻ[ȰÇ IHQ"3^tحǏ =Bt䶓(SIˎJ~d溅l\ǮOvݶلOQFS8k9Ugǖ†VYb5+v[3f~oT4%ǯ߿ L/…ѢD 5Jb˘3gpϠAW*Dx e>[GMvmsW3sOw>jm lV v F[g #oc&/Sܮ{ ݓj͢&O~4z85ѴP6&܁mPE>eyӠגrkQksE@^1 ghiG"G}͋0#y.X7O}͇OTE!QĽGF ۊif%7@(aItaJi\tD؍ED&Z{\({R#escM-DSo$=eQF^u#`9}s4(D QEYY<(!zyɒKij~`6'IUff`F(3>b*n5z~g6Tkʘ;v71*N0CNv%gO^Z`}U7\O랩71V#!6pz_JuZkc<$lr}z^Xs􉞢+on䞫nh:]sBfwko_Tiv(OȋW-cܦ '. loZɠt z6g(ԥm\[]]qZ&r<-7w˫2x5W͏]9̌V˾FysHx Lo-;e_nNz>nݑ7[Gk=촶5ӾvrȆ>m|7uzئ{̰{=hvgl?LW+i{|S`7|WiWe|K|:`-ULjf}Z}u-Fwxve uyS~p|~CC2F Ā 0H P tzbM4/ W eC r怺{:H&EFmƷU J\>Rw@03WF a_k`(~r{ vx-P 38Ї5hhad@ @3$.BC'C&[veEqaVx'E{{W߷pn|Fh؋v؇0}X ~lGJ 번AOՃwOJyp}Zad]I7tȀIwpS馄h@g&ȁH /}苾(H6 vc4v\96P m Pt7qxd3ǀݷMXEaLajWXIv 8tF Gg 'gNX%Ekzy ~Hh0oLj11x -nEJg.fR瑫WlDFh\C,ubpMtjpWȄhx  ]X|>@9~ݣ~xII PizS Հ'E֘iYia4$ r֓϶)XlCKVgK\t[6 ȉQa 'bIChU9Yh @2A2:`9t  bp|v)n\l iy؏xEYGIYłzT 񙔷@ 0 y)QyzAvР=yXً KSMi]%_V>ʂ&Yxx'FvVVh5C J 9nÃAPU0ТQx Qhbii\G{n9bG[3]EOe{9BDs0$EC ڋSʀ$k~ Jp` d i  dnZUq0w jm8zmTzO&{{DW(?%`_ ~*r֬fV:Cp:a @ Ȕ :%zzcrJꋺ_JzhאݓAU WL| ځ;)fE~`3d ʮZeZAگ9 vʠ*ʭziźqe}DR@e\mͷCCH$ښ iGyd2JpU}d .aꪱ#`zʯ%P?Z1F&x^;= BAυϽlxn`H`B DJ蝗ξYy Ob.PHkic<S^ECN&gþxs,ýڍh HZ[`nOd~z^v Na,p^'QDtN"w,6_8Srop`);nOoe~ڨm\E>Kn}|:jK'zO}]-N;Vj]V/u{eUtUNDn躽r>hn_1ӮbȁyCkP HLR+U.Nn%S4(lEk!E$YdIsTdْ91ͤYwܙO>]Y͚FSTjiS0YvjUXF +ƪ"=i YheCZd0]x.-ZjʔD5F \p 7A16L|p"Aiԉ/m~T1Zر4*R8u hQeVj[RǛ:lȪ\^kX&X10r}w/_y 6 (Nqd6hAE bhdZnA#>$벳6VRf2pCͱD"I+FrEК;⒫ӫ/SBI3𫌃T!@% 4|0B0#P-eCN;'6GLq<.`qg;0bKǸx!<k!40$Tr$l2PB '4* mxQ0]SŒʌ D D7s)NOkqиv+kEY1B|H%R ?,S,г@[\ oUWt| ĜtN^5WXcF{HGguqjQ#>%AV2q,P v2աR^0-J-.%Z&tMT:njad]lXF Wd(&b2VmHoR' 2Jc Ji"XՊg ס,ͽp|AakZ벚*jŸ/VQ;I#2۠hݖxfU#kN&8=oG`9ǣz,W#4CE[|%[ &M\ (d7he=wx*]QWsC3o'= EVlPxfx݂{\I}|$#Ӥ?G nx( -^\C݄786nҼ T^) fPN]%^#^ Oh>2.X12 Y-0n׿BML) T4"hV0[dI0h.atMߊ dx ҘƏ:ʑ&YAG$0IL0D:Q dd#*B2pTZ%}z 'EHD򌊙)G6p~xv~%0 ~)ZĖzĥ.)DJUXf44Q5y$EÙʋD)0GTIH5xx1Z{>73@x VvV+lGYNqLnm idvDe/l]Ej?^6"Li޺c©fZ>u1\@mu+ d'ȑBNt /yWb6%jj+6Q1lv^w/{{ Ib k[&-وm+P**E\ʣ;e˨>uF(\1v/42 ,Of%, [V!~-RWcIz7Jضm Q_'G8n# 2*W94r1 v#GptT) k4E ~2soJ| _Đ1ȁȸ4VHٹiqw?J 0 0Y4;<9%Yi>MSҀkPA۲0Y*kX,aC]+&%$ u?),Oi`c1r@  ;%4 D ,0/*p.@Xc  33{ 2ܨEH3(k *U9SFUHf| L#d ֹyA>n#TItsm ?mRQ!Q rnoL˸LsEW1xkQ}3Uz*IX4#u;P čDuX6 Ӊ2% ja$Ɏ= MIW"Q+5ЏH ?]T?NpT+X(FʞXu=M@TR,̖ C! Q\G\s¼S כE,5BS-KɍuXfRpBH#-R;[= 5U mEZ]%/]xgm1-:%5Q^Ur!&i(sT*} h^OBDz3KĽYL$Uu؆%<)RFpDκ_P\6}H_گ `%`A\d`z@'% >^@`E͔~=fLrγm60牓Tu8fJ(=8%@1cx3$fS%.xH3_(F)X {y>,d0pԾޕV e3sCNsLe8\GUK>Y-cSRd+/ ȟ`:+ߦJVGvHjȇh%PQ6a5e ]g]g}XU`8ƅP+t)Q)wU.HHQFtD<>f[a'`f-<6jiXfHf`P0=Ѓ=:8,#@ Hj(e*UvdpU Ɖgaf|犩:bch^@sry[2.=Aj֝<'NBKrDHav-D@iX镖[PسIR0Si#(ՠMc3Mo0ΜkꊱmgӮ&Nt+Hnl۷fI`~``zk^cBcXwXicV=;81()'i#,S߂)om7 bnJL>  Vn^鴩xLm}7D WG,O R,q86;0 Ed ز-#S<\(Xo6DPb%"k&h ?\ԑk2ǜE FhTkE?  ea=1ö\`I B?V,rc<B&-m+'֖bmms27cG15ODJQ%QT)[FhC tn{u'q=RFOD9ȃ<($I E ȀS>yO>&}gSW+Ou]urW<1u܁g@[u4I'{2fh%:܏VfIHFnnuDFlvk~,?xvyk '@[g`im,rVouYg8T, &N2Mz4>|`Cj/(d@y')B ? (7P8/OgYj,ئ`⢀"/ z3IH4pxR foՙ'cgVžsC=tTtL,)Ir)?;%y0G W_ 8,ؽrHwz}WKFP4(z)ju$>,H@jPC ( I(G;~ A*OVC+gxʛ r1%j 'O0@$A 0WrH⁃\!Ǝ%[Ҥ;g.޼뗯l0Ċ3F-v'f2fk׮Y+L0aF˥K4ԣ={Xcq$cA ~EhxHC(!I3QV,2B3ss*'sH,} /W"B_:@뀁Z_uB\T^%֗J7Zh96لef}jńVbjШZk&1hrp-To%eE5wqʍq,4ǁ x@Gu D0v+%95ݤxJ a0y7uRNm2!;hŕZ\ l'rIS͂ rء`zAsbfN8fXf!~v"V̊,⋬| 9~Bcp /0R DyÑM%J5YPV^骖;a@@Q1&jW|D6য়vg Wz5`fi1Wc+b"bu3:jY8ヌMjfj\c˨H^>*`$L @+AdBMMnjԘE0 64ȭ~/L @0NzRwȥb<,h7UZL uApxڻ2` 0% Z`.;z0ׇ=I+b&LN5[\DRU*(!_Yfq3'd4kN Tu y μxG%D8Bv9I09ءD,r/B{ƂHG>Rۣ$>! j+/TCO/{)1r%$+gvҭ$~%MJ)btZIAmwQ MzM,6(1-3ᜪ8[ywHTzƠA2Vd&O"!X ʦ*b@J}r-Dֿ21JDH3j$MڕHZҕ+e)*@Wg*aQ4; zc2jv@'\BEvF0fԛ'lɘY̪Vx"vkXAIփ2N8hZP쬈0i=%&p Lq,=lӠWB`M ]^\`dzMB~14d Up*i; Omm%inu4-pC8Ƭ9@SV0׭-嗄R@v`N%2V+LEcij$ձM}r%. 2峪 6$Rz|i!vTc EkCQβyՃŰR"o?$^R#ȡxe^Q2I:+g8IHDPӨSI~@*&?ֽW 2@ xW3+/P9bj ^xA vh4F8ʑuoiIO8=tlp|۹^H:i%ax-*AzS'Z E ~mkyMQh%KuԘF+,23ܡsУMﶷӁt ڻ =pHnwǻ촸-[ ߥwĀH . @Pq:.c$*E{| lg [PϠ|5;|y0AD-Nώnvppt#ݵbӟyRF{Y5/[`7/nxgO{_#\Q2w7󸠀@9Bn- (hd3_I<)L]|`tEmY\/4^]%\'C;;$ȁd4n5_ =1Ɔ}aݾ0f8hDśY׋ܝQ \4 sLGIU1=8^ QlSTaܡ4F8|/L6޵ }`-4t6%A@h"q , Q%_% ]./[AF:x_cq%_txΈG_Nx_16N.J`'Ă/' 8-L5t=6'B8%µi T 6'"ԉbÐ`b*B+¢)Ѣ/hAAbsbD=@ZpTGA2Kt`cPa0Vڒ5E`P mnAVADAf@ A#AgA\=6lC+$P%.cm@9Ȅ}b=_ "FߌB"5$]g0Ba"EZ?C-p[@xԄpXM@cAY1& _Iv@.m3N tPJdPNvxVQ\K7bS6ef xD `AA"\B(;L1p.46&$B$\Z)"4,ecPA)]&$)b`}&/fa&-#6& AB._eflIe 8j5ALk&0IđDQ"eR6%S HvB+.e+C7? )??)o~Vy>eg #J{Vi^gե׭`D!,?6&& h._e&-hEz9䄂L/ɤh(k؈(HuD Zoޡo S@ A @H)6У#n;jXj,D\AWy2`UT)^f*n)b+i0ZrsO@ishuՌH 6ɇ> Δ{ME@ kqbHB)3|j7B,6* j@TA4Aj%j%|Ca΂ڂ-k$+A,kŚeJTZ܀2*(x4I#mTT4WdAZX'ۥ1f()/.~٩^qer,Nb vt0d(JJsHJ@zԱ0s3f \PA$0C-B"< @%AynXe\69\9&r]^2Ȣ]b> DzdEt@GQ-~W"˧WD@1šFot<S #&<6@@; @A#B6Xl:L#:`_д4O<?S!-\BFdV?GuЕUZ=IuԎKLяBi4㮶0pPAhi _;X-Xڃ=%aYc7c;G6)Gdef[nXcShWڪ+6r$tX`16VG0Qc(+6v @p1d_oBʖ\w_4hvx uWSlqq6{6,7|c&w&Ў5PIьJvBġ`?$7?rrs6Z_8`vsx7ڋ3:x/x SP E6N-IjЦaStJ1y99hKkwrgÎA`'ts"Ʊ F/6os8c؊ox o6McSx9j i䷗tuA3&W\06YgLz^+$s-p%Fv8CO:#.dž/9ލUz.hq nIp)kDZIx\zܮjWY<,"2d5l "%Ww=q] l XbXg(AF+ƈt|*L*cJTxNGf@ZwPAāgC6xQ" J - ]OHgʦ2hl$|f&֍C+TOVƥOP h. X:v5 m3_ThWîj` {ٟ}@m;ڳ=frsh̳̽$<<|>fZ!I=焉 &BSM0`-:ړcP[4PѴ( ,c)ˁвEr匭ws]xhbPaA.1`tґiڪ8m{@QWXQzs,\:z>9yC~pT;,D[4AhM4J\ .K`leq:я4:Zq qޜaXR6F o*X ƢDEfP9 tm4WXPڂ5c]MkzVUth[-˱b#0 RDXK z䙀jbC,r2ŊJ;QXZY*ܗ NhOHlAxO}𵘉-hj2  +kP Cn3*6:*` T01 )2uG]Pz3S 9G{ +Xd1ʣ, ncSSOӾPe \ׁeGL [ns{h"uNDV.zؘ OH<)>sYL]iܔy ٱGx$c)D)^`S5&e աja /@V_ Juea܀l33F˚! Gee(5lg],A|@3WЭZ2J;?:rW3ӻkXl<ґ4Mʄp;K>H9w/$J+o+\*8 FJreWrGL ߲ d N#p$Ph)!n&]]Z`cEʖjSbOO9\n'=p&m88 ҢPu-Gt$ <<˼x&k+ĩh%o>p FlhY6)x ? ^h liVah/z!GoڢЎ &n:LeLn/$旬FG#m҂Ǝ|$XPLIDkzp o,1SO]~Mc5lItӮ&(kG X"` $P$`n GNLJE)V1|)(mݎh,N78)0ֺ;j姾L]OD5"Osq5qt<qHk D=I qF쥠&'~LL, *nL`#փ& 7ivYB>.hy1`f66P#CqH$5\iRLPrTRXr\RaRUdD`TZXF1+ #w (I,G --4fI'f *Pl + 󘤲T,N.ZpB2R-#l@꧊8Q!i;0/tGM`2!(MQư1'83@h)tN)Z+k`Il` Ξ| pvOA-y-S3:*E/.BSӀqYkns: :qlr1F,8ɗb24EU38p(8 *%K+)TJLB?C2 ;?,ڄ@uS5r lA*$H2SBlbӣ:4fryj;q'q8n*^ʁ#(~ rD^'o.0w4(ʙh%-%?֔ I4pJ g̖KSKҫFK%/)LрV4הM 0B+OsD)m򴂔EKvZ֍<yҫ'lH= R+55h6˚@J-UaHUTAik#9U]U`9e՘h'l%su&XlJLFtP`TX@F].o27X#p1iYU(QoGo Բ6)WR/+?,#R" H} r]J3 ̪Dt`% l8C >+PGS$ #hSNyN}r؋*"44dJ\[ s`doy u-s|6ЯBh9vRWsU&yu+" avjrvoBP6t7veձ@ >'nb 5npNHoVS A;H/Eq6^ÖHg/a,V0 24 j\4(3G7ESLwėNOqoҴJ#d(w*=ڳWG4Osb-e!lN `7L4yD/j(Lz zg,wr{rWjVq'kxTNuW=@FX*dd{*sI6kQ>'2?+ `bMԁ `57]#4,LR<@Xzf$0[X^a58jr(}b;N?tv lVwFF +no|I]@x7ic"paɷүN8)yH6%7`W:i#FksTD)3UwTW 1KnX`e0YS[V @@gLNY)0XDrUxpe V} 3457jw';ocotTD(?P=Z v9>jQ nPKeXb- l\Uy/b:oIz %"Ԕ!Q +B 0wx~|L2V8Nwyo:1vH<#lU`YdO6hrZgh*͆Zp#!JACÜ  !N*"m!c&ralvo(ۏ 9B EjQŒ2$"Ҕz OZKO1'&GgzL&i ʀYd1@'d Tנ 9A)(IZfN:lBbfbb1VJPoA Eѭ Tl"9z@CLFyO=՚UIvɩ@ ʀdZ9Ԑn^׺←Z֨[g,a06p@ TJ.ňqFqXJt'r4OGfT2SE3qs':*-X*(*#T̯b}/d.^(fYأA؋2 $PoD60B ͖EWdGZhI)UÝ7N~xEnw9m/_+mBeх:*Tm_kd!y]v%0EK)&lRgJMyPذu(-!ZZHYЂ% p8MėJ1u/>A^4 }roOtKL׿O * UJ81h(EPFk'*[d(NO m`3U"!mԠia m58[Háw~`ir!gLNX}(1Qp2Dx.#KB,i,}j0KQ) тRfErcT@M&Zl$|3#wyb,"$If>,YR'>5Pls9RjL *Jxo)P&6'  rA~)`vK(aL5&3;Ȧ5=/j:fWcYN؜mblH5! ;"jV$!DtR}d-+6W@# CZ-$J+a>A8@ҝnK+)ϼ<1cLk19e);M4iAC9mZ[pUꖫvڔZMQՈqEKa7Z"?z,B:ؑg(Uƺ l?L7>,^4@͇LA*)Af{279_%B"\jb NKcm*;Q*ꕚۜu+$ .@@›Œ4bFb=_`DVNT j y-fxfKT̈́Z$Dn%"i F"%,- 7q89G8V&^e2UG&!eȝhzy+̈09蘿\c4O2|fmv(z y/(H]@6$!Y x7$(p4V<ĺI [ćx3*D'[[]4_lVrWSd__>MTeLOh*e0+X_s|YXrfhs"@إt-ңKG]o/Dopuf.Z&öHVdòk+Ǽ6&b Cf/XG*:Y7Y. hFn9)J`g'^pQE3MNz_903km4)l8/MVeq4v(.trtklTkubh&oWfŨWm*sG*9iy햩.P ` `As'( P+4$襹jDN5#]vJn>:?4=9`BFf"՝@'k)kzyqijlǁB8.ZuZyB:q0 %70 )' 1+ۡcKY\ ˚XSF ;]@s]DKy'br7fMIe/))S_!GхnYc觷@;{A;U_>[4vA+.l# 775g'A0p b @ Z'v Qz}[ ۥlۚQ3+r[V]Gn3QN˫[ )*M1/t9uA{u0HqJ*4Nd !дAA0 H++cgZʋl+3z!1(#J y/9?36 njRe+{6:k-ejd^ʢ)XB0 @Y0' ̰"|3$\˨5ry?ܒ{Kt) |z{Sz)@O6-‹KdW _RLT>q*\ !s8 \h| `LZ@ s,u\鸥!qMS~%s!G.JbAr'];La@Ց ħo+x٢dA0:Y@ C o57cQ) W fIZ{oUP\C|ϼ:Ld EșW$C! s³gj"v5eOMn/,j)Āk=dBf#pb_E5ѱZL YAY`y  ұx& GJ{uuv4<ΦF6=mݤGYHd^\F_ [Y7ʥ_8* v+NVuZˍ)+^C.^Ci 6Bdw;.8|]Q5КD~|#C$H"P 2ʢR$K65{ۅ HY =-yYP@z%dǐE g~`Jۮ@\H/e]y^$Q,ֱesݥk!8}A";a)Ipkl4T5ev+ZՠY? $Jj3==Z4ɻ8i kL @ l b̭+,l~z^Chݏ!y;^u5wUWG{2W_>У>z;!;KYLcRMpOdBw M P->l]= JQh 5k-1?S`,+/#F!yK(Lwn@V! kA%,VhQƌ?|ظ%GzܠAJ0vŒ7,("gȋ+3Q(PԬқFQDZ3TTKn2Hձwl!&T+w9u]6=V˒'.hbR`Ҥ۷=WNi͚1K%FM8 ]scHK7~[n)|N!1 ,xpD ol̝:VjM?Х6-dA;$ns 1fG/SvƧNoan +2H;8jywl! N Klq왹,3G/t/j"|gHa0CDlig243ʹR 7b (6pHxRրK!"ራ@ 3 k·8R"%mȺTr0ɡӀpˮT礬2(P f?sJ)R!s8:A&!PK:1iCHvH=;0 N-l4Gbsq>K% ,HbS%v(rB<K-uˁD bMkN=<ڈ9ӻmU%=L6T'owﺔ訤?($"*lpS;f!ƚb:[m9U_+g3kVR$ 㒷yy.cA1ΖM4ҠEi3`بvJ%v# .Shh A6r")9MJ`x`Ho`4?W" M2<*AQ0n[$xc[ZG4 ^A ]}51Vvڡ;聇Fc sxC{VHԊ-ʵyõz",8t;1+vSߊTxn༌Fڄ;Re3M;_XI$'Ѐ*I8-p 3Zc0UbZrurqwA‚fa#hÓF񐆼)EJۼµuH!}JXMkC60b!O<"}9DD>yRҳCpV"<΀o8"$ã ȉ`@Crы9hV:YcPv8'a*D! [lA#cҠijZ~TiKYh%K 8@ @$gL*?REu`TOF&!DZU7PTר0;'vrNBDF]F$;jPWJFc3GZqKZ q`[Bœ2<(GyMdECrFXN-'go^#6U\ХoXQObR'ty,>Bj:W͎';cNœf1ZnVܹX=̜@/H<3@H/`Gg,!`xc* N m':^QedՍ4_3LHxuү&65k*30ͲuJ 5#-bM|\uРCCˀIc)]B숴*q$1d0<1IE-H݈B!JW; `/* ׹Y(Tw[8RS8(4뗋#1*#vcc:4 mw#Zd'8"HG:We5$"Z\Upn#:qd$cԈa T[լfVPC!ArKΣ'Ò WeKMV`zxriY cѓ9R ڙۓ:3tx┣_yvw=]y4;Cj1;0-H!)d ;b[82c2Q8b6AK,&>S#>j؉)zmQ?H616`R&tPȼC pr-u?ks!{Q1;_ɝ#̆d#? !M@% K9'A(#AT} ;(+- 7ȨZx o:ҹH& A=B)@RQH+p\kS5:78LҠ{vß"&@tF8ӡj<<ɢC&F?<~IAǢ9@Ɔoj¤"R8'3S|T  th{KA,ЂKPዄ̆Y$[+>"saL,F01.D,א19¾h hC 6>D>A&mDD& ĒjDP xآG*TTVi7vi] H;h'd8Hd`j`P{ ? H43Pg4̞ܵ*!]DnSxCd '8';e&()-Q>Ĵ / 3PnB ]lI8GQQor*JR@$XR%V,P~hЙH`NPk߱P3H4]N!yèkP;uV-a M!x:,* (fc sQTTKMϴu $U|'@,pJ1P[i،f ,+\yUlMDcƀ2Ef`4Vbɻɼ(ٚf};6(L4-Kx W¨z|9V 9\F5 Z׊Oˌѐ4{E'pWM,ZWAK1p-Kh\i膴VB`àE`E{}zC`5ɒNMݛ:CP t\rD/T˕̙u ԣ5ڤ|IOD՝uUZ~EM1 KD1UDim9[\ +P1:c}_`+)<$\s=`˞2%Ǎ?$dTTΥr oo6!%]])))x)420;R_hi؆`nH9C;D.^^rcxaX'@)]B)ɝ;̔!-+-Cl2כbšռ! `scw50F)0)@`)96AcB UP:aS0MDIV؆i0VO DЄSVn]2(790d1x`(,h8*^<"V?1bh|e4 ,^k\ĒQ7Yee JU-RILc r'@`3ZO~Nf2<@dVmhn5[;`O IZϐ!vHVOH|pEȬyV&?,u.C)T%&eh <C'rOX` NJf#0},p|տ*Rm؆}pfnjV;PP` Zs^XO$h],.T.~XBV봆vVk&k~\B#FlŒkdžlkNʶˎa6볶 m!>r2 2Z* 11؂+'00Eؽf16*0n*(nvЀ ! ,^I$\Ӻ[p J讛Ew2jȱ#G d΢mTi˗0M{9M3aZ\ǓBx=IɣHZǴ鶧ۚ[ԠA2ʵ+E4s ٳfn9 ۲d16)<~ p+^X>q͛˘yrJڽc}S^zF~#fgԁnzľXqҥ_^Ewmk>mJ=A /(r(Q#Fӫ_o~";FcwixQIRUUIUO-`t;ՄSJeH`RO)Io\lםw %y'vd{4GqI$ߎ<(_2٧a7! r)E9;V %R6gZԡ"HvTl2S7,Qx9c|nt#|>2~T6)OS1y\OqTΕNhtNU#BXr[WcY} R6q Oi>Ռ/$aN-$xYzAE`f#资e)I&訒PFι皋.:ch%~:ݖIdgͫ k/!ukO!k  {;̶Cfjcda;g|ƴ.g|w[ 5XdhqD+鱣£aF5TSUW5\w"LuoMӌ\d`*.j<r#tǥE7ؼ,\s~7Mtб-y@Sø<zd#u|u]޺W7}Zkt63E@lpi>#o>x筷!/7g 1'ዃ8}I~ 67+簾zꫯOb[ŠHwR@</y&21 ~m͉F\K‚| ٧ڰ]WjsWGg{<` Z\y)tLj#Adq8st|2 &}mT8xCdBPF&cI-P"I,Ǔp g]dL%k _ PЅ*B vWS׮w1u@d!˘"$Vc$+@'I_kT Jڙb.h `T3>wy̯4GX2 "4)͆fAQ.pZQ`iwrS"M:p6D>E 3aVGs%=iZG%2 M'Cop(2q<A6 7j+没@V0a]tq_5\ 'NӜzDkZT }>M5SUvdHm$uak=RVu-%"]Bկi6ְX 66[b# Q,fYjԳͭNVnuiZb@HyZʪWZxuvDa\Zҫ"\B2WL-;Ung9Uլ^Z^i{?&qEz+_T`XӰLMl7~gL\k L4tHRufE"8{] ,D1Y+CAE/i g2U\WYA9α[95րY8r$lݼp5i3 f+g0}&/CҘ+oIZ3{i^iI Rxwgg>ZZ.-p*Ar [>}.,9d!+Lήti3 Lm8Sիn^Mn#iRH[lж3wlXT]1-X곰hAxLw34*qNF1xZUoZNj:m.o%fg6xakUYK+ qS|[_ko8Mx*Мe){=rHJ0%n 7괎`DY+CujOr/0QHw櫭3N,7O_; 6豟t@;HI>y)k~|L%V_@:}ZC~CdfwzWX?UssovtW{N$x{GMD<.0Iuhh}E/ | ˷fͶ[f>6V44VmswH:BV}Tw䇅P~sdH;x(8x  6@@ t38W?7}|xp=Ve?8/"u'i| FxZHvL[7cP(W55^Blj]~_\-exe~i{  >dp 0Xwx5XEL d|ҧ Bexzh`E ַVftjqfpD Ϡ^:h`k{TsjxDYdž| a胘W`ȈSB_>[Zİ  \Vq/qG5}Lj,) XP1P q41h)|HXeঐ0w|  ɈHgyDWeC9\ٕ^ٕHjea,j0ي@{ P T5/?mA)}  IEYUVHPgh}'S R>c:hTh Y)Ajat;覆15<U`P f^m9}YyJo;Xz&8lPXpH; ЙٜiPTvٞ/ٖ qi Q` ?gqTYI`WeXyXљz9.>gVy๡YtoPy+Qn  Yc)Y JDy8MՈygs4Vx ʙڡJeH Ȓʼn p -QPrJ6vsUHtp|Bdvax`GxPJGIzXyP`T~ h x^(ʷc=:bJk z 5pnjEs:v`0 `{G'B:} ~PIgJ֘p(JXmun٩W Jff9 e @ @ J wy {sZ[ptk|ڇ^{ժ=J z25)V ̦ԣ$eX֮ <۳ʰ @0C F{گ: 6۫ڕ~ ʔEɥՅFc' Uď++3Ecꍜ^'qZlK˖.{|,=(reiH `%5s{%,ZG755\\XyxgMG^Lʰ`ʳdlm"1P*PkWƶ @ .=Cz8 *7:"V&N*{]&4X\bp6 dc,i\L &s P9$ q\4F-]=!vo:/ٜzBi՝ObۜQ? M7Ş7 `쯨<9|N0EmԿ6:ЉTd$oMyWd9DP]팡:̌fȇT3( u}ۍy %P7ME-c[y V8-C-Z׍>ٝ_ O]&8e-FJn z|M:] #PB^S| 5j̝e H>bi &~әpeM+U4^ܯL㹭<^Jol NޒÆhZPVwxm^IFZc^Glio0 }x7^ 9mR@#I. Գp$JYڍ|ݏWBcK\ě~f)k]Ϊ,,mv߯.?`D]]XnHFi=e?;A5FО~X>׎L5*We߇LͲKx~JD ^0 ˽uu>CcĘ] /T Wf!eMX -Jnuw0%n;0.oK>7A^ [3/EP`nEWMޢ8S$z|Y; dnP^ؖ ^›د[h$m?C/\z3XLNHIt+o^ @^'$(jL@o .pe9owocc}CjKb?:ͥ/lߦB%1A (pA 4! >|ؠC?(B(\9QNhK,M7YiMbAi&YQH͡c;QNzsKY} =aF%KvYԮeVq΍[欙_^ /^<OtM e̙_{Z8u^K~2]#Vg$DNFAd捝X)ypMF;mܶq#mcvnJarwo Y_oBi xց!ʈ6@V*6ﴸb8;--R'N?;[gbWv=s޹pjILP^cibJ$71Tr(0"g]*d*ˤue{\\x=J!B )k+%+%Wx`ЈYbA]<_E(-,tGx2*TO.e q.Fՠ e" J-TOLQ  'q"@,bm+%X L )OԜڕE-'+8"Tb/v'\FX%>8^C/vP69C`Nbhk!;DdĀ@dFF%)YKRQծ{/ŝnIKߡiH'TacHC(rX9KF+[#$m'WZϲͮiMRp=O fe.pD$#;*sE0Qh kW,q[`j)V1l BBba UC܌nխnw'u@"N&Գ[3qZal!.c$hW#I/M|"3l;;()^M X'4 Y"(ۓP!b <:/ÅSIȡp&B"{؀X2?:?hS9/ؓ x6h3 s'3u`PX=,H4$Dd(\0}r@C&Abc8\Al桍h3 B( 3K _8+ b b @ˋACk;.p<. -pc7;ùñk)!iJy'eKj|DH6#4Y H3+3iB0SU-:-ʁE@IlȆt90$CS_=)H>H2E'bTcܑClByFɊ ar)}7FFDq< K), YE"x<$G a`8E 镍HDj [ 4p7H.`%$Hd># I`#+JFpII Q@aI/Dp!r,BJ+1J"K *|+Ј+:H;@NbK\@1d4EX>H%@bD@ST2C r, lDC,LV8ilZ̔)R?($:ţ-l8$J' M30ԼͫJuIR LX40Mȁa4Kc6Uz;৕FF/]и̑AFä9lz1w$ϊ04ETS,$x,\`ɊMXf(PXJX4%X$aDP "U*aAQܱ 5ѸeKN<plOI% Ѩ0-đ_'(M>ҽĎ:u;ihOLE.S,` NdQP7%!8˻$і SfE$}I(\Q)s,PTtl!($1;B+):0#\ 'u(-M1H\S@U_XULp>%0$NՄl_]@$Vq%1MAVahA`¤`V@VAQB5/mm̌ਠjHMBL=st]W1O P=8,Ȃy5;Y5-Uk)4lkT(Lp48!Mm'N$U|ĉme`[Xr TI,t͕c̐M YrY|`\rY&ԩ$(WQm݆fR)P]= [`:e8QxΚ[λ۽m$ )\V̐^5L1L9-}JM]K._1H>KwjX%q7_XgvNC=N#0Di0eTNUgEAXK/̔aR[-' cN 31a]":N\]-2ANWHf^\pp3gqFUK@D ;A04Ă'е$QsWVe6~6Qf.hx_>)=jV߼\ǒREv͕Q3i;cwIO;h8 `;!#( Dhc^S6okF8k$NgxF$IT[ ܾm%l5gM$s5TlKoM Z'Ȃo-0GD<0m޺_ӂFPSjmmVmYh`BMړfnGIVЄ^kčЀ#=iA&1[D4&,Rû̵ @2@& Bf ;H'Pu@p⹈p 0$) gg`G&nB-qh YHTฺ RS Z$YKlg~T\K$Hջ62t'UWx Q:ˀȀ HT}p"ys. ;_.pe7sqs[Юk76qLt rY1q%Rꭵ:mLa5to_`q(zGu@(v8Ls pup`gk6wkb/tFveD a ebOT9>GH01Z9-@Nxw @攂ۺJwr'2ȃ/Pwp0wPpwH,N[9rַbj+x.`a[ KV}x.ܘb7v8wd_&a@ "Obq YgFs_two!?I<`zw_{π  H1x9+NZToqZIIn٘wtsan9RIz^ݲDyo߶t|'xFxF2gec_">/Nk 藕@@ӟ 9(/a'ZtxQD.?*hv4\EA %2 `K -X3lv 9A b0$#2CHQ` 9RJS>Q܄hԭ9qh)>[޲%/Q'{M67f63f0_C{0 .`~(~`-Ml}`lU*8P6T.As$15͖HA r Upg\3YժW}@*6T8E/=x4;ER*XcN(l\~zѐ {sR![P3<`Kb;A`pJюvckFV9MWtLo!: 5_:m7M VwKdπ]|4_kvEQucw*|/B\2StmO/TZV~+r('p!(V2'TѣiP!Dh+gRIPza{0)#$II$2Y}%Lsant^wb^vyZc#}ǽqBHT]2%h ye.aM)$,WpYUefL!/.%؁hA!B(Du4B"A,U#`Y/흠=)GQuXɄm]1])Ž\u_~ـޥ__DHhZE9lMuaԀΖY9mm<#L$:H7a$@&B-R  CEė R_[w/t5~`~B!e.}yn ElZ@x4LGe@-hǩܫE< b AA8AA"6˵ dA&.$"'=)B=E9^%YFb2 .ر ɛ&♌Tq@"L\ D/)tںQEu\fNTAJ#\DA @ |%.l<1lC7&DB$h)&4"A4=cF?^=<"$% d>jJ`A"_-- -'-DN$a|`yG:X $9%b_@Kfh}dM6fyȞ@e93B#BP^gHASL9Fc+h<'?$q4B$ā*a[cC#C ͥe%-$/`aڂ|*<&NIdbVe*؀R.r.ihAHxӟ@ XlZE]\g  `)h;CCn%.U QeŁQ )e'$W*gӉrA0 C_'-D)aQ$D[d~¢X(@I`WT΅iaI^ zEi]ɍA1Cj2iB)-@9d'l^~)f9ڇ@ @)X)h]3EN P (AӟY@l쁢2Uj6k3W)h#<@@'ԨNR?Deybjք.*gFi1ȧXX$F"k0j 8k1g kJ~h 6jBIe@NP al&\ ,H$)%\:tXJJl4jM̪I'|BD,zMMj!uKxO]pc-`€YUI^w*ngQSduEMVFR'Ur[P ٪Z ɐ,{~n~cD${|̠77ASnEf:sdU{唺jv)4dKW4(/RlλlƹW1YW9U \}CODx G|C-8+P<L!c҇rz>ƂOX;^3Q^Ta,&5|W>1[0λ;H-XA`Rw.,ѿ%z%=›s4= _UC$ X#|lׇʺƅ0]UbE:_Ĺ}ċXFgf#ŹޛwU)Oۂ'X$%(K7}bL淗=[@3ĵ@f GxW&l\Nb8D D 6HP@`Æ 0a4#(#h1ELJ1v VZLk2QMg1}If `l)HA.F*F,V秆"#HBhq r0`Bh0*)xf0 g>!}320|}SDQ(&V\X))ImKqQi>t4 SQΈ5b#U02P2䅘!P>4ULbJ%p*["v F$K~ "TxfZ)&h0̭fs+(X"q 9iEHQ1&kP4! HJ) >AnxUl=HBP5mRJ! )΋S,F#uʧՐFI73)J~}Ö@@V q+FFNe/ @2[ .s%  H %iy¹t\P)">K!<3ɗU% ϫ:A\iXU i-'s)Rc4[]4pe U%Pv+(sX.3dɔ 84;FeS486&9ZvZ-<ϡu8pn[ucDK"խ6(RQޕ׌|Yt[]%`79خ}` 7ҒSo&%Coz%0F$'Y4@_!ߌhP 5@%M7\6i h   ѥBp]f4c,gX-Q7shxoa YQiKJ=2l%U~,)C ̕aPfka,2k(ԡ7W4@^ TubЬ1#'` Qb$Dh- 8ho{OZ\w~ӧ9jV=!m9f:8=5o~Hʺ*xȋQF\Rd m)䁴ڪJxQcDW)JiTY0ƹ"w;e'KoλA'Up3AMr*Wp@I-8azF$a{;&G%ow@% l^]> <@)H0{sI/t"-]̥(Mov@YFzVſ$6 ׃&<;,11.mw;f.I&"d] vU@LO'&X' ⚎#|- {{12D.$onz@T.،#V8.d`a%@exﴍ۸cnzۺdc=C,1"p֤ʞeOZ: njb$D$T 8oD>jp̲p,Xz.+8(Fq0d0z-ufMRn @fP Þ R Lou%0nPPD@jE$+٢3RR@P C# Wz$M솈*O AbO3o$#C^eNBrBJ4ji2&"r%s$[HljF8 Dqk pG d*nLYncbIa / jО.D} .x#EY/>w`X*" DgQFtH6HO([(ԨߨKĢ@cl%Z{H!Q@5?,z?)uI03Ҋޯ/"EK=0*#7 0:"(&WcDV6HRtCB1=`ْmX45 `lhѤ+&-5)b3 d HU+RՊ4,Y( ɂ6407S#1O6i %SlR8$TL&>f<m(Kdb4 .+oáo` gI:RU蜔,+LgYB+5_sU 1i-?2V\G4+`l 2#  (li-vtjr=6XdVpM|+mft&3l7L.) ˰@D ,q3sdMd3@I 3LQuWk+2_7v6 :a5]|wJVbwqy{Xa T3X3h5:pH@1Ut-s{eQ+TY6{/P^6 tn!:7a5&QhgduY/X vuf@;w!  o+y?8 Ǘ,߄z3]#R F6.{7Ps0S >%7c!qLO]2 ィW2a7` R8D<&kؒz(Nq8exj@!4xNwSe0iw@PSY sE fU"3rx>;LXfJ|˫\=֔On)Xj`\}yjY@1r.0#| Uks u uI0Qji'-EKl Rz CA晝), _y9;2DBI g]I :~0F@C4n%_-rR0YϪ5| !:ԪQtܐtjwj `@9wc:虦n8xc8NB4p4ue9褪z|9h|YX†*M<;oqB/8[Wg2\3)Ѭz3U[]eOכT#쌌D>o*jt#l|`; z cPm;>tKwj3X7_s)u |Q n|c+J0oŜ 1dg>X3(~-vYq]ՊZ|g˻pUV_^-dYH",,6]8μخ; ĔA(}9,(\vce/.?R=T)@}znsB۩keɡ 0JK^k6jzh1b=w/&ثܢn;ˣ~,I«51\O.Xif3>rvAV])44"@:81sKLB۷==JxKbo+JD=Ұ(TMkskg@T*`~>* @/!Q#^Ak$^%ґ_ˣuVÌR S37&P0+iE d sv&%G~Ԉ;*.?2}rϝB⏙9-!>T}si8BE 8D*3kE18siRkJ2JaZ,%j~ 3gNilo_o`B_(Jր9/ŀ?58r m3SLDX݀tʮ;WÈbS&h!Æ= V32 #I'Q?I=əlԩOB pS9dp`;zaC` `V\rՅ`kO%d 6X !b e,<Xe} a~qY 5[kmX(uUPAmpX uYhpfq$!3i\tRtJUYTxSPCz=U dՀW)T ]LX_UaU(ai!_dT"m #m/fV4v[8x)8AjA ADHS͓E)%teƖĝM^&yc^ShFAl^p\] mZU'\J#gn`Mva*@.rev@ˆWiیxZHU #PCCiXb4N2l ݱUD3w}x҄YF{0AW ߧdwn)vpa&{!m@f cUcY]TH jx۪7(CM\r2G. ka4Gs-R-#8-¾,XHYb2q\ tiYըFO*gTS@6UhXRQOl B( j{K cȓXBl A l6zr3J@/2 Tf,@#QvqH&Lr"Afd.K|1|M/ZOЄ&[Gc>O. tjbf#sѓLj hp@mLS)ez7ndkyMƾ緊Gd$}C#T&3dlH̱6DZcjVʶ"-iٖi..r+!$UfU5 ݖU ] a6I)xT`<Ϩ m` >55imØկ%HNHI#c12󪔍_r)L+H3`ܖ(7ਲ਼|V^%ڡ:ͨ{RmY2cSq' Km/NPeϿTÅizWe7 VV͑y]zU /܅bW|0$-`1Zi',&sCf/ܻ %)И|*Ì@Ѵ@@!5o$ OE{לK:Rc wnJs"V. PC `IW̔*|9JblT%@ 't+D,I"0Gyȉ$X89 7ecjrcEGi*D!s5<4k`Z3s9izDJr;=EWFݞW|>2^$B#jɺTL2EWy9F* X6: 잢-nARor7K?h+̝emÄ\~Qq#WEXGrآy#[MX\z,l82,2|Eu(|*Ք PaU蓁 9r&)_6lOPX3?_N^ c.YurI=ƖlY@̱Y˞ez{u|,BLp=^'i'3u{|g񥘂~Ե]x87^qHP R?n9t[趼@a %g 3Kv x'*AlR|n@qP`@}$}zWWNu}ts(r&s.D6WB'Nxxy0wb.7uOaWD":B'Z@`MTh LHzg( Ӡ@:g&v cBF4l70lwQV>GHZ\fc[ن/?&V`6,x;00W=u&xj*N5Kb1B>&JUi CaK\zQ(jgx将l fqOTI'vpwznww!qUqwtBxvI62 (h=\ev`*|"WBTDCQp< `ZhKYi9FdxP0 ʸOxsXtxvh:'YMw1##OCN'Je]3\ 6@!C/r!mȎ4V81YB$RQDC=$-% b'b+8&ؑ!)ʘ O (i*IkFgB-&Me";Χ'To`P2eE7I/v8mx=x(m#m7e 0R}N:DAPv0 @ Lp 'pA`R,xh|9 ɚ)v2uY xH AxNqfw/g"~8~\`aru=ԥ=I>lUАaf pXɡY7WЗة܉,(t~=Qd9AStp(1<)//l"P%6c%\ v UC<?m yS}*Yʜ@ !Aіoy `3,fzɆٹ 8ZrHt4M'H:N⤭VRk};T{Uy0:xwf zȦm*wJCvڐyp `4NpW2zj"SjUNT-MnMI# Z9 HT"B#J#b)6"b/;hP5"Ǝ0tx%pk mjrvZc]ɚwf Њ@ l))@v0ɭjo:*1lj-6m3J #'MuAqLzW҂S)h|V!xXi tGi,f(Om5ewۓ.:uc '0vN$!ATgZ9G<۳]KpvFdi4J|Nk 燋HYYU-NdmlU)&rxUemk˶`1Hmydk0 Yc8dʧ@ p vTz2ۨ[:>KH1Ү+WGȷAƯ vr;x݇rjĂdky 6˿`AOb(}-!XyUSgJ;ĉau[>PI:76;UoP{SYO\7~_{tvkJm:w _لMU6 X^%,c ofo<7:_<,:*UkW(CLě\XFW#6M,psXZ,0U&6=4P7U 6Lhai\ xIp|1Zwbo IV2}0 @xN`^ 7 dxC'G"sPL*൵1ƒadvi aP}*I-g.0Zq Ҡ Y =kϊ, YiM`y+^Hj=拂D:Oco xgm6牶!NӾڗ|2Gp 92L39H!d 00fvTG@@kA'+~(̫"΁ 8tAMiT!$ Kl MjL,5jl1̈*खn˭7%nj1!R C jS/^̑H.J7ѦF/,QEC. ʎa1 ;AkWʤ;b1ڂpY݈'EKzvjnFQt:$nqR$tN$ .4X"X8,.pC2pR˯*}ӲRhP1QHnSnȥ@Q;;(s'>1);9F('ϰ[UۿW鿼Kd:M+@K@:32_*s2@Q@;@clj BA5TD{A8A=a{'Ȃ'A ;ADJj9θN'΋*\@B(1-o1Cƚ4bъXٽڲo៖&8H1AGTF;+C8AS0{ ,$h6K'xLܾfUhJ"-D3HQ$:EDPy4ۈE++$jB#k8#ӈ{Q;4*C++-,=Djصk 2R>3C4qLX4KwGx̂xlLJ:_J_01pZd{B$O b 10,`pEZ.rFIF4IAIA@9b:)3,bKڛCI>89ǑĢLyDʤóUXzBinvi @ Ђp#J_!K㚝+ K,Tκ|̿ٗ13dl%)7V/c"0y,OQs 6h'$2 A9i8*3i_YOk'ځ$JNd/#Q˒Β:ۣNn4aU@pB]\[slTF=r`y|)O$'xJ1ԴD9HVfi J wJp1e%Ф[++#"o1Q;EQ= FBA nZ ,Ԏ؊I)AxDHT{;GĻq| !$NG:ԥdJ3Kp9jx4Ɍ Xnj9խȄ`6;91)a2AQQ ɳ A"&<~, F$е5SD&`W7 $HR)WOJ\Ui9#!rZHXS:\X$df؆OX$;I fXdEB5ZFB\Q4;]CD\AGǚVy$#\#J|uJA%GRKt(Z=ݳB%̕cJ[`C @۴-:ɲV:43[; @C!gd#R}Ϙ@r\#@%(.H4A0D_5B92BB][POP`KLOU6d9]90Aa;Ч1ވ,Xb<#dOYm 006 *C ?<\$& #)(Q ,`Kʀ:]N@xt-1x]|h_I'4(v[yal;-de 6F^c =%_.50B^̌^k.XHoyrflds>grVvNwxgyg;c>;!ecT@vf[Rn}&}wx.fnh;iyX hSZ*-Жv6,fi3*N`! ,^I$\Ӻ[ȐНG>IH"춭Jj0aI-bN6-&i@\G;xFM*uӧoǧN=cgu֯_Rgөh:Mϟ-wNźri4!@ hJذÈ+^̸1*3F\Fd˒ہ:rϟ93 ڝc$=/9zqM4lҠ NvKþ^MȮn3SN| ʥ OxÇ<9ӈҫ_~[BJcձV69~*7GQY>յKE!Z>5.B&-kzlsnQP @vz_P/߳G~7l"sW /Ƅ$1H' Hekk49D1"7kIRd%1I`ל[,k8iW91gq LF,Z2#tv9.y7 QZ2 f6 ڈnB1F&`cM GdlQd(ܙkTcF79cԲtɹ~BFIQMf3")-rJ6ԡb"fQ\`9i#jQ# IM=gJxwxf:Sd㮴%+tS^R}7X?zTm:PddjZ sW4d'NF`KJkXYTFT) 'טFvMʃ#*ږֳdOEYH26hR' ^RT8+=vC+ZvUXjU+K1k \ȶmЊvW5%!Nhθ/WjEkuv97fG\%; HhW=M/z \7KDmZ߿r1a茷Z茢d)!d Vu`#XH(Fcr=20`&c!шad՞Pmwl׭dsɴƋ1Hk$>LMVUk\-h%ٺTdr;琎}󝭌LmA(0x3E-\K_5X@Ùns3:,zN;@/lfK-w(z 6P驞vv^ )x YkiɛngFa 1e)y_wv46 ӡ81&"j)JZɢI/ 2ꑗX[9p;2t`uGJf-֛6]5n R *pNvYa:_ZW YH>p6mcKZ9ِِcÒ)3,Y|o69Yy0%:d(m?[{g\)_*z! y᧪QpUP[  04ګК=㘛|*ccvk|ݘI:G]zTPa &LWc}઩D X0K 8Ǯ zڪl:jo: `J 0[) 0بPf3HdaY*d  c1ȫz\խB `_58k~˷ ɴ?+z*JTpzZf`v Q[ITPjjLK v9 ; x1h˲7S؃$kh`+@ hwJ ~ERx$?>?y %[خ {p@Z9 <苟]ꐅgXZLD( nd8mNf٬#lkr[l /.H =܃ ;F=ˎ{Y2Yt[\k7SNU`#GY~ޙL_>6,. dfPd8 n>%!}nJέ`G݃^XL~޻Y;(>6->ߜ1ߡR ,?Hp71Ɔyg d ;čչӌA]Z۲\~3.kn /=*P|>hT;1#4I*gu~̶ H`^~Rѽc6+d\}Xp,Cɽzq6f2\,N1ن74/67 6l*p?urwy|ceHYt cN6~oޮJ"Ǡ9^g֍ vT4/ܽ;|`xiG™_ 7-SFa >|(q%T$^XŢ-qquXdɔ$,kʮdK1aY9̡ӧw \ў*wS|KNEU9U;{f%{٘-ծe-˒%uյK,XۗV-[Jcĉ`\P?p%N&VqFh}9Ik$ۢVVܹJE绦M*]ָ^}3G9ѱDqG ;o_|iњU|+V8Aǒ)W&XGIѮ(͢ (BYVc5)ƚhm³tP . sj3'9198ꬑp&bQ+<"o3 Lsl>$ `!.DHҶh\$@d(4 CސP8p#1zsOWJ@a:fF]rK1|˘'HBJp TL.?rUXAZ 8{:C:mE*=ϬuA[T6.+\xa4VtR*5LߋL>m2?z()J58d-Ym5X,% 6ܐJvuCsLNs`śӲN`h]Vnt 8si&\.Ï3@*8W_~n6k 5*a.ׇ*-.ޚp-Ni*dՖen e fx@OK 'H Ԍ8_wa67jJXk=A3lhxNVߕF0fOr{r5^*6m.YuPN |Α|)tB=IXc+E8vYY=n.H󝐆t5Hᔧ<Ҧׯb@c~̲mt=&akJWe|1Է',lOQh7\w2`bbV.w|{@(=J"Zjh-Ik_tճj7# *|+rCB]l:ddԲҝ 0AbI2K[|D4> Q"<)Aũ((x 4p{l\G,8:GD⠡%;%J[uL, 3)rS2W~I L S( 'S հJЅ0712ъ3K26%DBZ'+7?_ sYwaec2+d+T8\vv.> ѦFpғ4Sd9QeFj:+/:&d'OTWq f"&UI5R(B/"Cf5o.,Q $% >)@v4"\E\+5t[;NXS`\ Mf\lW.0{.UI%S縢#c<0UDݴ\ W/QC O| M6 $;/ ȣ<**JP4K _5 aKb]X<֨[1d[Yq,Vr6ІP2ZV|{-ljFVS2Mn7[)] dku3)WX@sHGd[:,l!W.WYrԑm}7"3pm0,9,ZULp=F5:Mrf [w(8)]{`z\X+_"vF2cv' vɺr_ c"rCx!cӘdNQ^l+Ke. FWŌQ0YgbI&CT9 TݤyR jJ .>l:aqCȡ SxB1P-ԓk;7M~Js 1#L^4ڐI\yU]oћ\}@.#h 8/ wS: KA S4A wD0D>/sqh{NyDz>"ycc">"C~>sě19+z<&܀Y7HipK1繢-?ˉ4\fIЃ084 . 6{2(; T*i9p@:, AC7*%XY`9@,(D&{ ؔ(87Q+ ?$$q҈{Q :eS)407p<)($ЂD14C $ 2{@7#e&$к#`c8?|R A3CCD̈qAMْs3&J #,!; 'FaVT4444[t&C[ ,CIC ҧ(;AF,flF@(<7@4eqÊl((c*A=9F>ccFc#ĚD[UEv\;N2b1ZD)ݍBE[4nt6UXIz6=(0(1D*/(U寠e&^.c0e!֟cSi)`A*fis|L3Uͷ̭T_!{=bӍESxU ;01Ȃ&p@ٻP]Q]Eh3^)R艮h+cF!dc7˰Y8fpֶ'mIVaeL^kcŝAD4CSaN*آ6bEzd8bi}P8Zo(@@E4DRLGiPC %PC} -fP-~[A A8ara!ة9yiZ45.*i p`Zx8\0h$pc4*q jqp?jjTMC"?R՝x2],8ՐJ+Q:PkUrADq!vA̅;̢'X-g;ѷC1p,f#lͽ9_Jw(mg cÈjjE(d[Œ~'~@*P@n#+L€.WU@4A|)YFIM:]K x)wv K'jz%@@PL'JHYvfb%[a:ֹ3` )!?@@\`pNB LziP0lKy<=vL.jq=#-/(R2f6UъZ>9,MFCT]GhLclb^5d*mҫ:D%);b^d O r1냉TzHP.$+YI'\V h;i"w@&D 4K'l/+)VDf(f>"ь4 X4R p"ެ$V!iYM);R` Y at 88k$jhv0Aw N 7T-4 (9Flz8>RqPĚl(e 01ҖSEZ|L[L,cx*HA.:)Q `D!1 uJ+,) 9{Vau3pV hbHP]SM3Dvt Cz<Ұ Ƽ( zз~/}ErTA|}4ft\}gc=-r cXB @~q;o0 @Izm@*EI1[5:>/~Ӭf{,/彏_r3.gpigYP} B)i/QI@־s i| u@ҝ6JՈ㪀AY aErP$ $!K+;QmlyЈVBF|1)moXIΕpd/y ^\+қ}jj G7KjvҭMP);J0rP%.H* w&йȪ.7ȳ`땯>x9;E3n(9."q9", lK +rP/ۂٙsHozS7J~M g4UlOd/$vD5GVoհNQ'!9@Gh"ݱs,)L Xt;SNgY ].vkEh_M-ښ:ӸFlV>$L4n0AEc}2v&'__/0 S<ω.B# |`Q]eabe[صTjB򱝢\]EPU<9\5UIDwdǧ-] 58AA^Z69@N,JIpM HQ JP_7L"+:!8 pHB&&<3l.B)hB)^)<LA (c @ BH}V_D}5ԝ660 0t.7/Y8`&h%Z: #6PdD\ŘdpX\t0,YfI8(ɥ@ Ѐ-A<'<)|')H  %dKAāL9$>|&h֤}Û Ɯee`61eQlʦ/8 Ne! Y܆he#r#XLa XnXtD;pqLv|ten"x]R$A @@Eh@<(+Ă~j!D@ȹ@A#̂Eg!h[t4&O5 k&llʦ6%h#1PT ^%qrV"O@$ԀEmG]u"?>@iĆe tyG)  (/H>BpH^f`a1(2h~[@hNbc`.aHknhv#W0(TR`XiJu>bUdssK%dwVQmrO̲|1N!V'$j'pi8( N2"'B2TƂ9)Nب5)i!S j._TK(ģ~ضPJXPxLd*:GJh ) ,FDрP#/$ B"dN4B'5ٰi}!d6n("SfqUꆍO:"J\, DIǝT-R P *dnX 8A"B)|B$PAe D 6(2ƹp,4Ȓ )iR+ Sjold ݬ#bM\Vnޮ.kjzg- FT (/o0|oOE/ ]LȨr/DuoMcn d$)h>Y5&``n=HGZ6 lYS:+eXb0j0 B"Jk:.n̲zpJ (Z@ ,k._is&mNWffm R1@pH wz#nM0:n c fH GX qQh+Wfւ7x ?h!Y"'":0D$/|bb2wjYnr3VQ +,@V%-hQ8A%E-1KM2i*b dbn4d QRg\5?6ˆLnns8W > V:k:Z&oNyβ,K>s>V-d0[hH ǭ!ei#[05oj/t[jXrȨ ɠ7}}[ִ)U)F hP%9@RӤ2SS?uijflPfJ4DAC%[2plt]kXNqᔴY(_ς0;M`X\Z7jp.ͯZt`D :ü9Ib44mi֙5Jd#d[e[egvovtl&\YBb1\LtPkLeZSP,'B3aUc^E|R4*M"b5ƙ4cf\2\ChdecVo9gws7ܽP uJAɥs6o~McxԜ4]Չ4ńB7x.pJ#_D CvW6ieTrtvn?*$8!=NxvLe lЎO _ w'!wىv;ˢC5tWys5t6w㙣6M؀ss|:􏚓AZv`1Np3 k[B0[ A (05,HDG.ACeth:g{tsBU$jOg]ɷI$ClmG :%|ME99L33OG;R;}śoQ﮷c}{owG.Ȑq#jSQ7ͻ\Ht\_xԀ(cEۂ'!(MAE+ēbcƇ}GT/FgHw9tN4=p̧c K7j_ Z%>)޷O'_C@=YTT~@] q/J؋}Ny=Kڣ]{ѨS <ݟu3őRu;Gɼ[AyBY$B K&!UD|ٛכfg?396 ~y~ 7<6Ng}4H(pA)F|cŠhࠢ%r$I*h1Cȓ-[&LD;e$KP1/- &\RsK6ujSqSJu*oXbj kał5{ٲbٶmZ5d {ЀC 7fq>hp@})W|sf|%0p)lAj>"v. ADH[F9I)W 5sGD Q5|2TGF5iWqWjU۲i}kM>[rūS! ,XX1"l%в ,42H!Tp 50!BˀB9@6z, ʳNM2DSsGC2~;m4In(;4Jjι-}})//Lл a܋0$ @TjkU6iăCqD^C({qpHKqAhD$OK|8(,q_}q@k҉tF;xFHQ*(85)ڦ_T (DIyo^n@AJB"gc7 ME ')EN[ 唕j2O+c?9j.sirChlZ097g,2/@#mD)f"po<0u^(p'_)%.UESS]i a](c ^9(4 \>dErݠc"vl\Hm* B禓a hny3LOzjEZM0bZɡeDc8sĬ-F<9Ѳ*bB:޴5wcsmZ_qK!$poء|<;ⶨGd-e/26^͚JQ!>1Ȉ +2$s^qhqgIYp@Ԭθ"!JW2f  5Noa# ; xs8ԩOAHE\B/U ҙ(3Pu*ͨyG1Xs JHFRo֮vjJ&]-%dS\&,a͙B[AToym e `:6NlsnZ!qE `F[$w zo'կs`#Ĭ9uPXzK &+c g4脎EaU䌾jʷm[NJ;ڼ@9W 3պ۟gʭCH/GخGpzԿyoV?g  ةBl;]œ[~T) 2 oK_Cqo#hx\-rAPhZ&1@ GU$[Tzת_]58>_o2Xثػ+A>!ڬ5NJ2\+N/AI4͘c7F#NP,5/j֚8 &S@N` !==DmϊpѦH7"&OPʵȪr`v\3Иjnj>ʷX0`hI;~P FvJ #܂pгH2g+Ѿ /Ɛ#0pN sN i`   `Ei^Njj vz`  B*RP)'&q>PZ Hkgk/Zl=g:m7 Gp" @`"J^"ʃ* pNr^h ofl \  B /->Ÿ qKfQm& Lq Ҟ/!2+#V[e"̤c͘a#?rz'|P%` Kf 2آ)1 RZ5:'DNt2)Uq)_)1۴2]b1Ҭc = OP,h7j& V>z 2%:adXr&"Lp971|Q)5T+2\`D b~/44o o X#m3,Jw`Դ c")aڒnx7m:~Mb9ɤ9άA)q; Fcn`^;wI? =GP=?0ܳ864K!c>SA":H2T,s.`n!(G&k99|xJ:5G;;KOet)Arq<( H4>>`D 8I)BPϚTPl 4Ô  !Ɛe*t,(/)-hMyJF tq4H#QN&rc+YlF"3}^PRN>r #V-0?.nM W")W{1XNC.TO.PO%(Zu1#gdA5[U &7j[PIZ Qڕ4U# ml T_Wu$W*NM˄(WamOb,6E3R52 NYUwLB 4IeQNpj}wEFb3L*"JmJ u 67gTU`j*4)l)bWVEQ<]^c nV Y^SmUXRF!H 3uQ[ä[ I07owRRG #EJ ݸ'|ʳK?MV7lM]vvuE5$@~P#\\tIwA$(4*[nެ rWȫ9ol .l'D$Nh&;v." 4s,[3̘jĆ絙Lu!dG#ܩd |HGx5fxPeRTRQ~X#(Zze@\r1`*d|Qhaj @~hێ;Zm*iAD[&"nni& 5hPdqB%)Q6Pt# uYhqM]~Y8bf-|`i9J5%S ՝^}u{Vm* _0$\Y5("fPDpiY)X82&jjV Y H#iz/D r5=Lבc'S-qxD zH 'T̉:l:@*V^((W=a |pº-Y@w[wvǡVwbL2X#g!r-,KX",ՠsGN:O1ݔӟ[V\qlcc]s a3 KNJ:i{15ljxfA'FOPR$C =@Ah,]k[.w3.u Y:5v5lKZ2+.@7D/maj76<} T GL,5|ojԙ] de#D Y ENP,4'A`Z^Z6t ,1|:DuՂC.w䗟2z *xuF mo3"2ܰV x)فTtթY8"CVcD%!qsEM(KB[dQ1'lʸ@5΁xeтeYK A.}WF ADLHL֊&Ivͮod%!N4@j<9PLZR.P\I4 3jKr/vU0pm#˺<)lj3M{Pf`ɼnm{Ӄ8` Qg"Ȩ^i= /} i*$b5D8 OB TAEd(*FL(uD%AѧױdBt&{IB e-7ӲMɰzbɂa 4q06C仛$Ra҂ aZF0TA)B3sK FI+[_O\9JL}[XJJ?zT)U0ÎpB&|c1Y̆mc21(85KCLjr498ʐ>$.]0i`-̐ݚġw P(sVѣ2+Nih](p<v#J'4RU"S쩼Ǝv&֘oERod9i(*H3&>E9D"S T|;T,PfXEtNr=\ uN\KL݌cab@_>,KmbB$[/l{;$iG".;/' 7Ju~rAL pF0NVD{Ø3 &.&-& wEظ֍xxQ3g5/֎;soMЉS"KGʱuTJhN~W9Y]4Ti7_;K+a)gW< PY T5gH aVK*9xg.0R34Y߃LV'<َ6@Y7#>b)u;Jh"!>"q#$Ss5d[DQu`ykqR'@@oYVTW JpKhErl) `RDD;' YKy+ C=b"5XuB/ $8C4e*G~dZGjih>8 0i?vl&KmlՌp }ءf K{`~i@9GR+9ɓ͔~ R#,VCjmX^R"T+zO211YGO@"$" pc3 `**m))pMq!@216G^5Lsia9A]α,A_=" E]#"VjVd79Rddz$;J%1^hApp fE ҩ\I1Fϸ(bEz) .awר}H5Y%Prwt %$1%;ZRCv=V!B7*.Uz*iQ#Arj2!/PR P`0UtEomH| E&P5Fb'ǝ:A*&Ư}"m 6 @CR(eZp'""izoDk;UNeJCood嫸bGcS=Swp ,KEpZ}sJ";- ĥu㊈4j2y5c/[G{}WG(#pO^AOj1 pBi)TZAl;e .j'ftK ( EX|Mrkf P)P;\rƸ\9R7) >y%TbJ5;Kebr8 rkffff+~kgKZ$[P@E\6۸k̲I81Xx|/h!f5MX}Fc8GD47d7eti<Ek ⲁlfq ar<; =/PսCwɨ  ;< 1Ū6@e򾠇Q;>gqpi*@dWWLA9zZ'[Y˭96U8@ƾ:J,EmpܢI)qSM{9}tp pcPQP 34K߻<ܝu";705gl9P*𺟼;)й(`\aiYT8Z7 lD1U>`#O)(u7i v!ɧ>̀q ݞP *.Ҁ q0 +?<坈bAjV 䓾tCt5+Ù|Tv>g+;úWp$cёil_>)P?=yɖ`!EҬ N3 -(lV a;~>NAK頌mh}YCH.uANJ; νz Qq#% {qK!^\Qi ,l]PUwGɀ ̮u-;&Ҡr;ϵ6\>k^K@'zb^$i?l%T\F3ָR%-^,AJ-W~ irĈ49R<9|`SCj`RiɊ%.jǥL¯A6ow/@=Î9gyGZ.1#.` zCc!(Y2㌘9'5Q\MX6y `Fvͷ6놐rر~T]u] 㾔i&bI*( `!isH$/넯#`NP*E͵TkO=0 1,%c8É ax̖|kT3N4O?51EQ^CTgd+/Jɐp^NAX{kSɧr".媄jY6Nl7/i9dǪv> ܴ`.데+䲁$u"mziԁ[ 1P.!ČOAKT!L#fbL$-ԋ3.cU%,,U:[oq}sk3٦{ߨwNR6d}$<3͵j7k4fy뇳T;56[< ;.K# zO8d3?K\qJs\T]Ts+EFtw)9Wu$3QO#:fԜ(E &A`w )ew+ٶ~]yl&9Oll6izfP BfЂ8!icxЄ>38ƍy6&A 2WgIp VP9RI'e -p\nds N)-!đyax0mZE 1b/g3|05 vi  DD~+5qQ D1T$$ꜣ$npd X@*JP:93&cR`ς*_a 679a<JQ^b!q;$q|2&ӎv shFkt* Ie)㱦9+%ʡ`oMnrZ9[ǁ4F=ʊ<$͕Đr$=*Il>>Pw@ ;&JCf(ħ`<hU9$_XA Ibs3pkh꘩LETd GJ>kͲ:Gs<<^ҭ"SfMШkF٥T^(<d^ v&OMT0*4"Ba܂u2;~;Oܭ=Eg DYf@W%͎uݿsx)&bQ7C,tl|slbaz*-l^b'0gBUZ wD+`Ya 1%a$V0O⎪x5,vWYB9zȹ8~Wǔ}_"e)\"(Π&II fH%&#eN7CxƓ)CK'hF`[;$bXCZO NAƀEb5rCjLUtI'AvJ,{:xRC~)9)yGAإ[e^v&'Pc#{bg@WcF0"a"&0 b /n(F9Wǖ&H=,Gf:(`q$T4:-jYLsldv™p揳B̞'fwL0ـc~ӽ;o@,/#KGwjuǧG SҢ$N g {W2d>$7>;`^%K.cZ8d?D݋-_>wl%=)`AHz"ӪcPr r`W[ C= ˽< (Y0dk>~"+O3V%NpA ?+?s;eJk1H)R#?Ɗ&Y@c5kS#ޱ:+4k@L*Lb7v5h $`0.;A1 @ ďrc`+PDHv BS2ҷ+4EDrqx#佲p@$ٚ7YҒ1x)x;'92 ("*b;hQflx+Ăj+c)XDcpD3HI䠠p:yD۷&1 /,.y]-72{2~{/3H޻LHZJGaN A* _\5Ð̾F\@ZYb@kno{js 7TJOeF q, :`pʧ|$GB{L*䁝 ؀ۀB,ԝ:ȴ,$dCEuqxcx&(6.ȿ$ך*2;@HQDQ̒ącА &Idcf́AIO0HdptMr H Mܼz"`/.PN.$,  )úPKv-$DdKEx!4L !x1<2K@GxD1RK0St"[i(9˼n^\% L*]( X\͉H-+0MTܤQMU$2QJvʁIQNʒO\$P 4&muq * R "K0HAFa;=H#^ZIY7iFlX S?S(LK@15o\DppdQ5G=J5 LEwZǓ`NU,N TMU K%Cb 'M3 KN dU-U@Vd (X异+/= lFdxjqe0W[>U DpHĈդWI5JڔFWw׀؁ ؞'i7S؅:vSGW qj75؎mcYc ْ5YuX LՏ ٞk|D>`X\JaIY-"*֣݂֥]Z3h-imr$! ,^I$_Ӻso‡s'[Dy2jܘQ"ď AJGR?(ƒ2m0MFM&͛4oɋW݂wdku+[~\Ià0 1([nWhӪ[an}K7봺xb;&$<(MͶ#ɸǐ#KLq&{\.(ǍBζl$[=Ѩk+1p햖͛w}m_[l*{YΥ)'>ddR[x7%Gʲ-7;okv0=TUUE DQ5`g q)gBre Lqe&O!6rXo QH8FjU5eSO#5Xe%`S t;B;X8Qt r(%z("!n^eNs"`+.vcxx^sz#|"ӠFG9Z2 2އӇ2ݵ$l:ߤ\* tmZ ΨEy`6`gdnQ\GczU/]f"EŘD^! :(7d2TkmXhhɄ6邤*a8κK+ ezF gq[sf[55L,yU-Eٻ =ǩOֈe-cmir/ʰ\2XsM65[S8G;*СkФjμL7jD(jd\I*2Uq7hvTLWin;ZPW7x7-z/l5_cO75U["-4FޮsӨ3͠S7Xuf-;״>Zΐ#9ncI5}q՝/o7g>;Z*3ưl148F#}+4饛_xǺ iv\3CWp$x 6 I2wET^{a>=Nڒ>CR岟:RݣBCǽAKk2"$fވ7"-uK&`E$|Axlm~&K%hmpx`8Ҍ44q8H 9*".$H0tb6'(.0e4 (@ ȸ HF :kdH%53J7uqy e|$p>uA"hj)v]3j'nm`Y5PJc,#aFM6_JMvaIT(>:Mv &aVU˘s Gn˙M$qVjT 5~nl5AiKE/8E*#yr4KwA4-8t U: LN.굎Y(%Fyt4g"M+ITuj< ,R)ej e9 1qAV<+PzR5 iRf SF a̪^7D>HH:oU*_&9YC5gVnz;N+` {X DW>}TAo6ᵲf;ݘE$vpִhUmH6|m"w=!|=ޚw;+Xºɰ5Z1KjRt>ާ (?vfmx#k^h+_64Dk74ཝ1dNEq]9w#}n'|j׼] .$FGkt]mzgXXok֥Wo^UwŦs;X$+99cd=tiF Z슴@^w5; j>|ǷjRYEn xb}`$?jh|V>-9ɿnrA]{i>TIj>[*~{i%t ɇ |#|'})-ByWmH},FDzB-=Ɩ ŀ(QiGtwcuweF^ '{6$F ؀70y` b:WuHRZoB4$'z(xl,HW?1H{C58w?"ŃrnsD(%g~ : M(7}v 8ņ$U3cc^TbޗWv cg.(Ævephsxv?z 8$X   6PhP xw(u]T5V- ؂l;։.bxex,&5chȊ8pxg8E0 ~oe@V[xe(:Xn#*xl6uu%xǍ`ȉg2Fw "[莸 cT.Pq Zoa`7Ѐn6QPip 0 6閄9D8 +xVRs ŗYwwY89~ Dx(:a(y)ff `Vkxc3Ըw^d3suy=9Rj؟\jŹF  ŐDɠ采։ڙ6 U`c`3y 69H[t)Z  [miyma;JWӞ Jg|WHڠ=ctxNR @j5ߩ[] p MsG-pzhtVI[]% U T@aym3Yʠ~ctVVڬ9Z ))i*`Qp]*_ 1xpg*ijJUu_vf'^Դ^YV- *jǪxʕU:}@yںfp G퉮 Z ɮ&ۮZB?բ ~k.5p Lzr P+  qIJ bv@jDf"re*''o P?(Mx:Yz8ŊfGhzE[[ ;$ @V{bꭳJH&c{2Q^ȚjQ_k_k3^q?TV&rՒ=A0Iǘ͹pyV{ { ˽ʴN"wPR[KKZ:幱숀0L e}Lo^һӌXYڠE+ ӫJɇ9: ![ܫ+#;˾Z[ Ŗ + +Ķ;1z릎coiM?mޠ `- {{lpȅie/Æ۽;k݋QZq P* XNPTp[v p sY ĮH|[>l{>xKbĻU|WŵŚpPm{L iD>.SpsL'| qR~9@N0Z0 ) l";8ZZ3E|Lq #:|D*DY 4ALƱ 8\C d_҈GUnL M̓{*{#P%?Sц|;qx [eſ;z]}&:ps uq ϱ|C}T- =ulK۴+ %p?`{Z*2I &-=|8*-% ccJɴ:+˔@=U@CK}FP3؍md2RMTy^`O@{ç@ D[.cD|Кlj몎}mbB=MY5;\8t AL*QMq\#Lǚ[^_Ѧ}Ȉ|1@Lx3m>הEH:ېfmY Tu }ʽ^:8tum}t H睩hC.\Ƶ}Q>}:wm z-Ryo N [-0;p)ۺqhjUCMng ӵ]CEߖR?j%Cnm~M]L kĺU{T^ν {_Nd75ܪkm$5v5y;ʓ[T첁.ՖmV䍃N A7*R~"l 坞d.,pLkڍ|н/m8y.ArzPgqiĮ Be[˻@.!վZ`>"Jx͍f]6Fy@@@bVF/5Y5Wz$rLN, 2+,Hzg+H |O4Xu 5޳l-\>[ _/"᣾!> Hkjl/U^shD<.|/e&oph.RHb,fi ߷^hP$Jlҵ*[iԉ֭bŒd֭Ȏuݻ{y{W_%WÅ'бcC^h^dY!݈+b0#hۦDҵk^1g֬u3N-;ҥMF**%Zz|˘8fMV"A˭.{$/xn3„#V5f|'Xs{HHN@Sh5^sp%]bp)7А4 * 8 +%X會ԪnKҶ K$ =K=<" tO3ACr5VzeL2DŽ啖PpBZ 6 *{Σ@\jD80p"`it9 #w'3%r"<2U(S)JT'ˌ:)%1˴̔d&7<(a7Գ)srHB ' 5QD@D[iѴ6@TA0ɾ8Ӽ:m7'{D0QN|*(HR@ sCA !O%VLij*Ś"6Enad(x)Wt1.؉X:#C~&!Rԙ/8`?4&2vEJMdM9J&?aA (K5+Wx:PZ3ԋg:Ǻ $3Myn} ^IDʠD[З6iҴ5i0Q\O*J5ish89 G^IgYpN@ňfW;ȇ5d\T/PROJL8 + 5E]"ے&rES+ɢoUh1N(?e݂(NG 2Wz #ч)+X 2pTLF5)b-QYVTRP 8MǛ-BZwk@3V,'EGͻW\ [Еn~*ܞ9.Pܠ1W@e&t5491|H`<)@H˹Ρxeq[ڡ ʦ8 aSc\1Wt<_<וu[#k3<6Pr-Ve5E( :a3o7N&V!vX*%X鱮|;f%,$QA)?OTm 2iJcy0a3}ѯž5ۺRe>3`@*$¨9ֻj5sFJRbwx/esƟ|QGťa B84#\m ݦU8H^n;6b6Nx}ޡ*< ~4 TPH9Wo7FXG3| KBr!, [xȡ7\)wiyFfNsNYss2|ЧoSmssU WUFT}b9kYĚ\"xWub&d Dܥq$:^ |@c+-m;R+!a<-y08v/-ơT= SynH3W,{Vj$ȅ_H;u0;P02)&B`8>ۡ#Y 狾m>¾o󶄸ؾb.9 3TK?XP@xs&Z 3z˘s48?N脬cQaSfHIЃ0,'&0⓻㙿A*93)C|dpԅCAA`KDy67<B()7@+4?{ubbC[JcR043H48D0ܧ[;;;zF, 48i`0S5 YxcGKG!D8B$|3!ǪjB =Sl?f2:D;\۫8۽0H7H.$HKƎ[=l?zIéƨpjĩL+0i' b`s$tTG[`7vT?xlJ؀N``H(#8%0%hHX%<J4F7@4h),@UrhH#8a{sIzxIK#DɕI c(C|tģGGKK,зA 6 0B&B!H2T= @Y0riK¨LC礃4#@I$BP$Ij% XLtLDILhȈ,GW GrLR;GT ͟x D" (CbYK -8PYb-qK2|l\Jp>H#!Ԙc#,'GC6rƄII[qlDO+Jɣ(] PMG.%¨LP x:QshBP5;@!u؆TR J(7#03몆:I.9L} 5!EO'Lc"$5+ɷ.Z.%BY=PN1mP<#>{9%9:`SlmP(L>p,%@ '@Z@Hť=Ûaz R'c6b2" ,yZHGV͠89$=MUWhS4S!A)VS: MYXj8Vڝۛ_VlE(Vo$Fjh="LuUi!us){]f-7{~WY|۹͞MXZ/EP2Չ?| XBH1buSKΜ ;露i`R(IV#؁ -cTt>QꝠ%;ZAJ9kzZ0Z(U7_(J_A5]|]Lt0ӥ" %&8߽MSXEa}S͂Zr[<5aր,Z)vʟT}ijYk!= 4"L)U7р(]" }n8-&8.X12F0TifGXAhQN Hj*6XDĿ>P c00椊=hkP69q(`]5PCtJTyljey}]FXSW/:chnk*OPCaL)p@0Ά2u!#%@Eic]`i戋i ؚwG&`ΩL.ofėI^oKt잸lR.B#vX xD{ҮPʪ],πe.X=벅UIH2h8 0h?~T@{0>a/ o.'Yn<} yFʬ<$H-תP(d2g @A'm6;ID ~ s=H KpԻk4%gqFq+o'oօaYd6& "oKM&5dyJ#,˨P>X=W-0c2( @-dπt;[n^6sYmWFvNqoJl7lr$r,7%_#-`l+Uj}6E f!BI],`EN4E xπdg @jklgxv*1, oO^yv'wN"_z'-w,2}7MN7x6S \fw8mL=+7.@ eG0w H x2Y5ה @jty,L w S Ouňg眎w u{K wtzZ*x2's}է+ +6^)2I@%7w{ H{g  vi0y@|v@g\g`|yyt OC (J`q!D*J|A6>h H7z, VȁG ':Lٓ /y 0ULXy!;Tv+wbߡ+k]0[Tْ&vѥvl^bլ/́:0/NXܵȑQf2f6GТE'KVt1bjƅD>Fm8?lؠb Laፇ!N8rE 3jYc*G|C/yqsgO*0%:<$]:5 UL]pBf-ǼU;5a5HZ\% '^|9_`Wh"dPVYf=~hAVZi L0Zl>pm gr1gP- aJ4]LwIۅI"'^wSL*pg 5%4Dv [,M-RԔʎ;l$d$CcZb^ 2XD6t:36k9 c4Y[ FsTMptAZ/߀-Kf]J)۶m/tMD:@T0dTQ찠ZEr81ntTx6;8dQH(fQNH/*b]d6;8xG)F2g4{5.'L> So7=-Y/\q b%]iSS7\;b6`"bF $A JЉF` ß D v@K4R44Wmcx`3>q1P wD+DYc\'єp֨ BX/D Då0PnxP"ѳPD#&9ts {%ƶhn̈́ٗ2:! i\`.@ P$ bȂ4LcsG7 ٌy>0&4a l* L>FM'MIPNY)MB2H'p5Xke؄K"Z :#H)T ylZ"o3ఽg ݧ.C)9V`r#J 0D"@%anuWtl,}5AIZk֮w-;2/`* Akh ]5Bː6N9BODok^Fc# L3Ђ]|6x ,<œXx0CB` & 1)<-ugY0r#[ˬǹ֝2Lc|4'Jb[Lizqyڀct n T>RfF<)Il`4[SMAt0>`(CUťxĨB bIq4>AL^Y^dPil}y h@C[꩞^1[Yq|@^dBy(@ tĚUUOU=@t!dM p&%-a}\=<^ޚ&2,0p~A`K[YR5p~`G  ޗ RjQ` hEyD2p4١)EA$`AAOށ&L Zu-$Cd `!Ǚ:Ᏽ05`!ga/d` NIb b"FdK8X($ w &[lQ$FPUBb$D A$&(UA-H2_3\B#4\#M+6n)=!18# 9 Gc}DJ<@V= asLv4XRKD@JMd)nBE$5\,0WaB_IQpB-$7x1HJJKF#LNVMNf.ܤeN>3gPB Qj 4@4-m q::eI4=MtLDneXbv\"?IpVIDT)V59  A$ܓ)\!<`0Za\bdJybXA^dg.M\.hyufg8@ qt!Vkd1@# aLDopdb JI@ K O5_PU]A8< # A>_Bٟ_(#>y&)>竽'$ 0 Cfda~$j'1gQn`iɀipHOv(}%F](i#i)oyD܈DYYMZVAPi$ s!@' cx2i2J 6b'zhnM, hSht LVDڐhuFeU+حQA dA޽ AŐ51TǍj2*hV..d>Ҫ{ @ 殞iꯎ?2m!ӅDXjVxDuTzeMANR$B$sA  B@\\I:ƫXރym*4)v+f ^#P5OADJ,;JP !V&wc9 Ⱥz ϿD>BhC,b(оC-*⫬hIbC4TN) Dm)\h,9Bё-@U׉zd%[XƺɭV+Il9e||nB-P4AJF.> mҦ很fd||DugBI/pQRp=@n0V ngiJ&׼Do(ߢY$`F-E' h7$iރšbk됐 K0аZ)@ippmGTnķm"0#n]G Iiot )^ph, .g`c m 0B1( 0Kg|ҜkYfq"kU(&pJQfdQ+߂1Vű|87qq& N f|Fj0 a^-uO"G"1-$$_Vn%#sD@k!2Ξy_'`2Ӻc/Ѳ._F@n1kR֔,l[^Tn?41ڑpB0h̘s_2:وs Hֳe.{>C ?wn@;%T"0$gwȠC/ $F;AdG{[A&9i:brJHuetfMs~)N 0wYO#-]L@ EE%'v'8u jZZ4UgVw,HaÑI2\ʭ_'%t p5 gȵ.ӵm+!5a_u"VA u (6\tU[5VkV^`F_pvxgv:`쨶\0NeTt\5/mu^@iylXGqW])xIdJKWNnDZ6buӫX9t74XW|E+ FZc0b;+ {S{FwEl{FMp]rmtn?-!"1'k_V4u1qbؙf#MUNSU9v?N0X⁷4؂%XtEXÒ:b kόd F8>kMNo?. 'oϹ)J`Iq llK06@Mė_uVsXq{" C9ȊI?)288)ACG{/ nɠ/ּ.%X Q'[RϭBP0Fe蘇xag*i:ʙdήߊzf:F2xmuT,%;o5T*zɴ =d" zCo*۝@4yzu\¨WH'L%B;sEx`0:gIas ƿF1<۶mW5M^+FNRBh/b%8sk jpg'DC$KfÖMkN9$Wmb.;^|67ɎR@=R UnS̔X;`=Y%U_]@_;]Vcy+UAP]kc789gC4y_Ώ.s 3,-QhEkeCaٗB%o0NO 2Qt~Ӛ]H|R,*Ð.ۆopxH 7o.Ȣ|Դpn.؂,m+$$JI5G^PC)x-0 dFM('Fٚ0TJ`C,IXHڨ P7wUV7IҐ``gHLaZ/<g阮KLӚN\nP(G?*'2). l`)>XH ` \6<,xUn! Ģ o^0.Lp~( /e)BdJBnQ4 4 R !b7"U -%9fzsq >fkqR$sX")s-v*E*ʬ&J[,FH2 `_] ` ڄ#:"dHD`lhkoR Lz;ͩO!߇!mr"RB"@  qd#N"iB$lQR2A TD,  `i\1]2)T2bnaam*ns=+rrp5;Jp[vmO0Br-1ÚP>l1h*2cH4s3EsG©"4qq*7)ƒ85R66K-7 KR-W3?YK>@.!qKL[%*5: xgv>QL<+b7Ӝ1{nlbn" MS}h&LP6 r( Ԭu1pM4RsI35bi{KATpLu,QU6B%0gFCn2(gx:U E71 @a%4V*=K9="46JSH90,쳫p> cf0\(7u#WEU9 tKK ^SB/ȀN@__0Xd'ݬ`l v 8֒2$Bs3Yf80TnQ>!`E7Զ.N?S=V60S]j(kgDVUSh7]hCiri9h ` 'gV{,'6#d,6lpj x`~ d-#[ޭNLv])S.TTu;j$Hniu"VvٞS4NjAw @Ӈ.s(oyM4i8`Wnkvrww+$_?P[xQwO'N^v7;r0gsTIz]R^$ ⌰,Ɨ.@̷S,윐} )(1RHtTkb6bwLAawN[8` s.(북eo!s$-͂zu^7{{#197z48T۷b 8x:3lOWV(e@wJd->5>>9ގ׋7׫-xxApN]aȮPحҔR9%!N·,jƀx٦U64)ْ1 ֕. (@SRy6WLALKayB npw2֌K.ඕ}9(PR1K-UIn8o" Y/Ftm)›q/9G u)h;9 j@ o邉ڬxdB JNr+/y.0%P $PIMZҶymndř eXsF뺳F_ @,S%; M2Ioq@h5/(WP]d EK00 UL?ڬM[;g#GͯU>\e/x/ Rv)1"D5[(BzDNڬn0Ƶ[;o%m7k;doBUwrlfd2moS=ĸApKl]aДҘ&RSͩM~֪:Xi'n:x퐬+aцAѠխߺ囥ۥQJf,.`>j>Eu5o 7_[Ÿ)ZXXUU?:h(30'Jې2#!3g\ldPR6 iϋ}~G>~Zlf}d\㹽ͨ)o>8H ;Մpx$ÏdVl}襋76$Ѱa.tP'BZ^yLs\R~f'Z;zK-LǘX z'4Y槚[y&;Rj}~AGp ^._)(qG>B&FKLٿF80… 8+6x9x,ć*J8yEɐ,K`&4cl!dN*t$DYА1ҥL^|P! #N&Y*ƌ;v." ڷoĹ} 7.ktb|ɍ`pn>jդM1UYxPX%;{V1ңѤEg 5\:"EӨ,ؐn<`EJ9v34&QPٓK7dIbCH2L4DAplZ!ԩVO*G']%kZ@caV] `p F8Ia9dYYq0g0ySi&Fƀ&[ #djBEMh@ ΍w(TQKD$E O$HdPPNUA>TM|܇Z5WuVZٖr-BaVx4NVe eY%]I%)m+b1Ζc 8PO1re g4HK:S $K,w?mQxx4H_z ,qHEnV\Tpf5ga'<'*)>ftV^L)]kAlyVj czAzV^ZjAk+v\%Q90H G3SRE TQ5B qC]1S~n":[MJMoi |A pZfÿI4k2^ '1 OS$)Uk[*4KlqDZhQEY mN6xӪ/mR7 vfS[K' =ddcp;Ftvz,]P(Ikz CUCgl&ί_sy )@AmYTcLS /뤡jF$$w (IFԌ*[ lF@M7rNRqlp+\(a)p@aIg k{.IM7Gr FȔ8O}ׄY e9 MA 'IZ$E= %L*՘`)PA lG-`bH 畉##Qx>cÕذ1 W=Ebn#*⼋̓QUx,*Y QeQ\ ?1P`4J%J6 naIRpG< n#A<5L)7#GnfRCd,4*vɐ8._,H|6U@}czJY t*V O MAUH1I[E/u,.yKzq%/ ahSc#i-*X 9Ml1 7oζxLāK/_!˴v%Rap'>HRf9s_(r4؋N'DtҰ%Ÿ^-}7QR{`g5ɥ=M`W0Xh\y e@Ј`E~W1]EH&&~Km]2[$`| ˪DHC$1 |eqlNd5`R0> b8s&Z}b3^y#=!#,+QS9L;@FCY7D,IN[}e(R2aɘM%odF|P.uA1 k1x:QÕ3qZzЈ9$_fwzD>-}(kizʎx;*P'eԣrhj%n PYC8')Ue;صyiĥM'<5 ;=0CDB ɲq pG3G_L6[dJV5 4ݬPƄAWç/o/үov֥Cqe?h{_`d=JRh\N \y6LVKa)p;+Ƅ _KswSwovuI;q{^t**[ӬH` KCna斧D%VIiȲm$ {)A1 4TAʜkkҼv[NpHX4Bn7l.MQԀrGF`"Ir[upymZ2ŷk&&M|a'x P W4!0r`r'w v)?"?W:QgӠ;;Ti{3[yEYfWiE&<M'Q>g7ue17Ay² 7OU=Z 5ng|9,XwaU !(S&8r&P  "T48Xg0 <ȃw/Y\' 3W nw6#9*6UR.$^8 `W)1"?2rk?cB\)Z .Ug6(r'ׇZ@x5:yQ]uH͐ OT")wjXdhDvw>_r"qB X)ieT, $gw+_8.Sq?o8-M)`` Pg(cq Ђ,W xwPB A%aC‰XY,BI8#ǐ搲ϡHE+勯uU[.!U[($v'T+.'A3I[yp퀂p 'WtǠ(FIsHy.1VM̐ R :RvաC|tK-rr_O&vq$#$!]G>I1c~55P7CqJJ3 5lY}M.7:DyElIKi S 1F镐5iGSْdC#r>'v0H{X(@b[`+s1`(Q>uQpv v P9}P uM  ϰy YgɃ1R`f7T H&3 tCrRY膧աArF JC-Xz+ =FPun"R=`H΁=@wQQX y<4x98ۗ BYVjisn E@1d{'G)R03IƊ^oʏ}\$[׫1v{Z1Ao 8\E0QP @ VfpbP 8y#(g}Zpx +(8~Ly]_(FH<I"SȚxKWʊyxUaʭ`G22%Sb{H*"SD>1s.pxwX &\` *wJb C9Ql#Prtxxj {b5h4yYq2+>3/#k#M5wYj?6/V`'ߊ%1awHZvk:SfQ>)W `@v\Vv WlrA'PJ,cp1KaɶzM!is;Ld] aRzk"29m*t%epB,pe: ~Z$k, C*'a\Z\W'J`4YYg$G\{ h?xz2s1r1;:ihF{,~CѦ?ڒT?7F+@ZGLChZi[%{A\ g!$7 R+WTF |gjUeWUr) \znW3"B/){YʺG}A5o{IhH1h>F+Z8KL>If'!a\6=[GŨKhAps|ƵT˥kMs`\L|%M<ȥ^!PQ\p8R($"`S%p$ &E Ll(.0&7P[\5=Ny%f\lhˌxWJi;iuAut.%gy[! r_CSFEcuf*rx44A4Ƿq{\.fE WwF rSЦQ0[P4З'Q},\dGKTȆ쉂<-{y&+<- @v;C$g=s\I"eI!4lP#ƻy%ݺ FzNVQ`q4bh+K c|+/m*6'^!-A uD{L\'"rgq"iΘGkyZ$,Iܠ MYm 9I wg[ݔfN0 T]yE ]ȭaVBy,L(,LAŠY"6X1!܍/c(85OI2NLb)95  oa=y` .7 Yྜྷxo1syĜ au=sҫ%GXQG'jX#D3UY5f, xuvAΎZFnxM =K0 bUWZPW]^ml=e>nvܾ:Llz"l "32/$HR,/蚬ӓhzPiéb@E"> O.b0 nEAzPG M] k~na KÜ Fl.no} .FUuSQF5ތU,O=!KվbCF9 S+YDz"6 f 6Hb uV -SP P[9A~=3uG̏mqO @Z g"eXw qS6=u+qL-#dP\A3IX,'Wq5B1 >?@}XFjbad,_mQ7le#UyFO.uHnLU #O=@?R5 YbNmk%^~U$b@` S9U׫n#.$#!3s'X?[ % ,b;X…wDdqa {&=A*r޽ ilj"ĉ,lI*4[54gϾ3UTq]֬]V;Ti͚1K%Fm8uW/A}[… &`#70ʕ3FرDAq_ͫ Y["صŘ -Q`pkذR9&>LcIeٽvX"-'.\`c@Su4ҦOZV^ :,3fx뭸4KlԐ2KA6 ,(@:08tM5M4 Ba#KG~,"N\8rX9'O~r ^&;Dp z̰Ė`Lj*>9/++,P0 h@rP/&bA-4RHA3 ?PRDkL90.m0 " PLɈ.JUhlqjƃP5GwHUߌ؅a8bЇ }I[I|(DB;AĎ,x) )U!רj|Ӫ9ߥΡ,f-hPR0D@K@ IMY+TBUxttHVMōSփnm "͢`#~S6fp C2Wv`u%QZi%K(BMvs^9:\>0@:i fKyF;fi>`)bTꤕ]ÈFfEn`xΈ@tt ͏f1RW>!0kQ\" ]3Y`ǁL0yĦc?vZBq142h#qq j@2@L*i 1''{P WLyuo(Kl%rD(gfCGi/4 F%KsDBQy\cnfRPi4G  x&6EΌ&)'1 E Tt,%PzFVJRIBբз03@kZ:Epԁ[$3/Bn=XŘh!Ǩ4Fi(S/ a)M bMPA#kx ̥IP@B $aZ[s{G3)UnC󒆽J}5Q4DښN 4uVMڋNJ^ LUjum8`9~׊V7 O3ibV CwjQAP1ġ Ɵ=lswSJk$LG;Oww%Q$p0.&mc'w,pF|&4z, ⭻/Au>@^THygL聶-YyoE +ct3xܭpc 0LO/Υ)1!{LޱZHl.H/3`~ IwϲNY[P5| _x'v*';9r >'ҺKb>33ZYN hxw?ϻ??s.XG@Y+Y@;{<ʁ T€(*Xb5;[ɕ õSx(;, ۾d3lт3d$L[й>cBqȰ') =?aXr.Gk(:xT  ѐ0@Ȧg//P( : B,lBD+B+ %DCR<lk$[(b#*E=p0;Bۗ+Eԫ(*F+b1) <"WF*:;Z)\1 g|AYۜ(c>k,ķ3m"5 G2>1 G-:vBvlFGnG'G9߲?~GJYHL ƺ JCZJY2>[ x0i88P84IKQ)55Cψ(ě4CC<$Q5AʤԼ9|L}~$1z4J.,s/8Bɬ!,r( 0t˺@,$c;̕:R²Kh ǡ>C̜T+0+I?wO-PJQǠ8K) :XTBҴJM=jMO໣ŒK$24(F$P;L&SŴ3N;%2kj;ЄVu ZBUGdPWbZdHdhW"$B[`DH h MTXQc:TMR;Ruͷ*&" 4Ճ ?^ S$;F UՒU^XۀuY XJٌ]P0C1$;D%D0ڣMS]'+rZ0WQx6<Zn&W~}E`Ф,l7~MH|P)mQs; #eAY%V7YES #p\ǽ2hi d]Vg̍'IljVԝ8MpWj(ڵ]nH[ܝ\M $WX[X\^۴-زH^ҋ WA-+ (ݤ?d[)ܿY)k֟Ҏ} dee߱ق$Ђj5FmV!T]e ^EUB`͆젆M B36#u ^W ] aqm6uSa>b[.h')eb6E)b(V+b'V- 2.Z$4EX: cdq(-c+=>61k-19pJ(! ,^I$_Ӻso;~之H1"nqȱFy+8Ó(S\r!ubʤFMjmN纟إ\hK*Uҥ2“ իUcǵח?c +ٳhL4p%s[3f~oS45È+^x<Ɛ#KVLѰȈ &mʹ#Ư -ZhGO9ėE /۱s_ʻk'7J\lV ~ :4>Ĺ_޽Pwwɮܜ5Z+[n8o ,ѭ\/M=}cV~^ Q\*N7gV h38]t 8!a]wnmIwED\R7 {6|:S`} 曐3ܑHW?:U .i'|4<&šsEňݔ]vbDuuTf.padz駟:&5TcN)WI$qT?1#2؎SX֤'^6.Ԍ/$WN c"D٫JAi\Wю5:.35F*5^h`k钑S~-WOI!k*:ThnTe fP)I0_rjqJ}U,޲tq(#2ה|MОsθ m˖jmWkrZ=@}D7R37{([tNOZ5K^evLPQ^$*5ً.<&,os6[ʌA#. Ug7m5t+yP'a Mdv6Ki_\cŕ{LKǺo<w֨zECWO\Cm9Pcӿ[Ms44J$?anhiJtMvaod;7aXY; {AiqI|ߤ2K]%Lbf<'ڑdFf\DIN=X)AaR u/\q9582Čh;wFə/W4h=&栵EZ6qcb ΄35Sbw5QOBGٵerog bnh+$LBZ#)B+5*.iDҍ^\7Җ fZSrn4-Oz#ZUy7VSJЩ5\V}ȊzZ Mf4ZެE3QVhl7V-m)L*I>Jܑ?<5ޕlX[Xag,&h=i񂶴ʵC7ղk BV FOJY4: .Wr*vMXuYV@mERu!*dbheoiFV4j5W+9b8 ~2uA7x|V`6ÿ@-9`S|30.Nj9ٮ?t]d}vIJ'Q=F}#=iCеr/ihBvTP3{dŒbv [_?K4i;`n+T'_6#ؼ+p\B#,S+NGcܾƹ5w MZuY(&sVQc+8- _ၞ8Y׎\QC4)Hs3vd!]rq/nR9ZĽ|y͎%.:1Zoz'pup qV9.h`ϴB-Ǽn}36o/sTư8-{_a~4V-ue+v>Pzo^s׵D_FU̦_So+Cٻςlc>{@&;pFAhdq#`|ɧ|ffzзowEV5T^Հ}׳](GF(U@Lo5l8~qg~緂CW~~yQQPLP7dW]4giG 8u ZXGZ!2Ua{x^(z(Cv H4v-ww*gzDz$&Moh7Vuc@Ch@ T4x8BNGnbQ|^H'{΄F°EV}a8˦cXj(7wvn~pc֗{'Wp>g{hP 86Vrj/jXs(OPUbm|}in䣀VxE-gWnX2,~"SBA0 P P=E8XX֌hXjgK)P٘(鍩&(({h{hŀ 6ȇQ` : yn y]Pa|h^dzF^!8H%g(7V׎)'t)Q0qp @w5i8vQ|qE9YٕJI"v~5v?IXZs`ԕ~BK-gc9fQPU` mُ<ٓ3t2vYfS-Ey]|ِ7naUQ=8"֘V u FN NZPZ`` 0 :o%fMIy}ÚItף.[lI4rS%6[\I .cP)f`p ) FxUI\։4eq.jR)bH醆"){™Y`y6ʙf0vp J-ݕ9Đ5)QΧS֌ؕ#)t3swF Pa-0 /J3JВ r  x*٧U bٝ#hL Fr}&XiXɂ8 Iy` c*)M`m(2ExϨ٦p pq v#|[c`  CZF{Ҵ F*r֡HuK^'

NS kJ ;iK"È"6/l2W~i<cx# P'\C* F6M8M1 21{챳<7O0DLF,Ȁ9paeC9^zz8˩iCcp yTUmL[w3Oǰ ʌ{5Lc%P**HS| 79 é ^y˃A|_\ʳ7UaKU~-Gٝ݀D \۱`;WFlse AǮ}@h]&p>~ʙy-ܼպ a#YiViSݪ+yaԳeGH݈C4%Gא$޵r޵剑}OuDb1 k FNH^%9,ΙĊɣ ^t].?jbЍ!~Km넕 sjwn'{Τ&} a cM-Mۑn}@T:ӜKySTƨ.}8ijSٗ%CԱ6۶>Dz7B>7,j=mמ+}jjy ޟT,ջLB)LI}s,ˑG2 v2% UЫX e-ߺK%= @,EU@x`\ /f-avᨀ9+Ht/5sm^9rhyW|EgOJ'.i]^m%!O7I 7]tX,_yb Xb!Pv4)D8/G%vn2/_jŞ*,ǿ7b ?/~֖o~ ȋ`?$|.xxyB/^BV#N8Ki$(AZcbńM81bdfУdהVH({b W1eΜ\nݪճg+OzP 4hiQFիTDI*aRv˘Ff,1۔1jX56D_>Wذw-ww%O|-[Ɯaͅ0⥘ouM$ܑ-yٻ&N:yT(ѣ $mBSRVj7~ QD+U̎ӨZ XLN7})Of۬303mA b5>s-> 0/хy7ۉB9.9RJ*bn j8`n*+dpU,L 6i/NQBS`7h铸? tP5EiZ4F?~H0k ;̣-FBI\k^ZT1OU*5C@2-YtH.l(΋rW%ӝhXbuaX&^pm읆%cUMc=[?ԪTA & orH3"EH衝22,դײ'f{BUQ.:HU1,K Cfb<|y\ywUU+? |sz25hzCuGԏew+}Hi;QCΝ)+-c`1+t{^qCUe2FU 7 qh=tb~E쇿AѮvBR `;*Yj$8! `WYB'k`<} J}1Ϳ.+ehaag-) k2y_ :Pm8d在qP#~QЂT.Ul ]سPy],At%2a5.4Yt3.XsI8<ڐ|ThA2U*#RZhdY;o-% W3(iI[ 'ֽ}q{X(K*WMW&]- ;-X]BcAȄC: (E<0+O'h:k.!f2s?%vF4^ !Yty.6 kd2{ɑi-WJz7KTā/X1[( @UBj- Emr[#Έ]AB:w7Ѝ@bإuGYл! VWf==L;|w=i{i"ЖU-`m̨@Hb+&a#]qWaf'|JyO > 4Z1$9qjb'-|x5m>ŭUd -Eс|4L)hB8_2T 4`3B*+xefm>Ӭ5ýmjw>n˔PE֊QT䆲qu<4bԌ!zGG{KCCJC@emq:]}GLQ/ͧ&F0XjǷ Xpo q/lk=z͝h`LXf vHpKr`3{|U9.V*ժ{ɿPq0'. n%sƺ0{T>$w R>797>Cm+*:*@ \@s(33W{zj ҳK2'h&;U@3@C@ث=`@Oz@d0-x \89;ABĵmr,4+H0Ɉ kȆl0$whG_HAG:4H,ɜ~ -\ tzB:T$FldK1ƻ|dK<8C4 :|;j Y-T}1@rj\SLj* ٨*-03KjH/Z0ڏeR54.uvZXX Ks(-HچU `$ی"תXp[hCj=29꫐qB"j$əYUŽTmR(G@)0N8!\Ѡj g^MI=1]k]]!M>ݑ]B]K0^bPY𥾂Tt^U"\B[8ĕ 5DuU_(LXդ ;5r|`Pj0L\Zx2 Xm E۴e@IX`@>@GY n n ^@ Ĕյ`_"(C`+"FL›i~Tmfa> "Bv= s*$eb%Ը 9¥AQL2NAB=45rٽU^ C]FMfʂ,~suf`Xze$ 2I.#QRUZPY?O0z"e FdKbeU@0cSYB;#B"cnMeVfVEQbvu`V(Lx$ +h\@jK3`iKMKsY\TwN7Sxb`[fUb;LAjX1 > D ^~ `\[kRA=h-3@iikufOxCmF];'[ EFmL`k{nmccXcq#2c'@f9VPx/Cx90 xU/={33oS ;MPF*sOv:W-<߅aZ&Q3 egDw}`ہ:@<tQbR cCuKhQee'9<(`\πO^EXi>CSShK@<(R/ѰuԆavookp%!wZjWN{9]Gyԡ Z69KL5>o>`nb;6"ohQ΂)ho#HD\* p{7聼?-9vwpnm0H1("igWHvyprgfEwvz渑$.zDz.{C:{~dP''4# {{?{8B@Y@y$?mS;q N@\|wa ,&„ 1$KWnݪeW+U|4|(QJ&M冘##4hΜ԰BU!B5,ZPJ &qb^)n]-FnqIgfE#Lo麹."'-LÞ|W':.thN;5P¤D)UmʩU*Rq#|CHL0*6w+ Tia$5I "f&J \L}1“R̗-[ ڰ k/$] ].sf̾rI0y! pX(̂jqM<=Tp0 #2 ,z!.:D]' ;b?LQz$464NQl)19԰ujP~ނ\ n7sG@ryK0|$ խ^5զ9 1w+ w0%q e7A $ >.Axns^!D@Pe'lBw;LB7Rp 09E!@$e{c@'nJ_9a1kۢEa#׼ Ql+aQG#(@"5`@pR1dH 2es@E dQpI%A6:;*BВ@PIH-R31a#a#RDtF:LsO͌5>͌Ωǐd0_8V񐄜 ŵ;H'(IȴcO* Uc)Yr+P6XDo ؠ,DBY~  X@ RÑ~0ɴ vMqӲT|ڢ17Ic"`bj2=oTG?..Rή~$1EO4g|?n*^d1+ ~h')J:BaD  MVi6&E2!괵d#-R/}߰0vFƚlK.ꖷkoؿᚓT\F, *\QecUԙl+oDH%(! {WE"r +pfA p,aHDͺZ2⍞Vl2aryϮ2AbjGz_UcN=2x>@u9G rLJ^VBWe *r,A @ 4/^`8X7` K^(@F>6[lR0+6cAyӻ]ͨN<5\ 4l; TTTQ.)DztR@G ~6Bke, T_=bAxnl f>)  ; +_zF8}rfZꍺѦ0nL7rtK1pi)?ApQU<`Bdf9e=cQgRGݜSBTʉI<1]V~" ܀@!$YL eZ~LQdW]NS0y_X$x&fTN /$C682t20ѵ1gv2!eY|&iC$Be=Jj&lk&V .?Οfz% f: gZxd@&@J1۽ߥU=llkml>%)vS>Wa9Sj=#IW^ZSR v! EdA"B0HkT1>k'▖.@SƣjVA2ߘ`2H qCu-@QPazW\yT@ˤܖx,Q « 9bfoګ&NE>5Q2/`*暓\:em;o@R)@zhdUUmx}@XUȉjgf~:p}"pևkVBC8@Po'fp*o$ŭ)DjkŔ"z)r `k^ꥴ-/NW0@WY >X-*i&q2 +4iN"DSyc1n&wZ~ZaAVpY@%kq?3n"rۆ9~*050##$Oo$/A%%?(EA((WOuiʁ怌+op#yUZmV%B5PѦCm!3" !3BL.Z$C3%{S`56טj~38)4PZ h7N\z?=I-;=..!F2= A{KBAC?s4KtR8agz8'QG{O)"I3[Ma" 3E錎햫%n5VN+P5$FACB;GjaC/4WSUT+lUU_5HHmn')p6Za5D10Z̮jn03]^5_ Y`va'Q!%;Tooպ[%d0eHx0+e;Hd i7Aj[õx,3;GLNQl/Ygzn9@l7e1j)ĺQw z=Ɨ@ ~ 7kYI8:QǾ;ta[FPzXUD$0x6pa'2Ƞˆ*ThQG'NԢE-fek$q&MJ$tɖj5ziRKt5פVvkV׸^kת^VMV mZŐ!;._p8( fć"թͮO-ˮX(n`0㠱", 5ܰ8́ DK NCT〣amn3jLȷ2AR(&+ ;p‡3N#r)%, <=m1#4B>XZQ:"w47wls(ԁDEQrXWe3UJᇋ3.ɉ)[<!)ī9̥ܙ+gt<[ӗ ,Z\#ruQ11RȀ|_m[ W1DH" [Sh)Z .B s(#R,曣~Y14R'n2Fkaƺr9M`PlxY D<@"p_b㹭y{Bv)FrcHFjF>/}ws~2`bQXE_QǬ,OZ~! jns\V bT |!=mDuk\ =k1w(܈F<  Xۚ<dT:CjK{NJH> 5 D0 +er3s*ZqbbrcXL4pZZ)3GisҬcWVda ;5A=b+SP_Ӄ^é`K&s∧ ^5 ~p>Ur2PoVm`a>eK3iי(d\`c@뺞1Uxwr=_F&S"Id2/E`3U+J TpBr2M X=Hl@[~%z|94ð^9MïsA] LnXbŴPzw> u11B̆zX"rC")"oǔNpriV+Q0[cFn{ٹzcl`b8Q*^cmAXBJ(&FEȧ[OK()&ekc.: dIXi bvEj8I%+y]uJkW~26bb(Ź3O)C71-on<\Em/{6xn=Ӟ+[pOuc,锕˿=#$0353 nCer!m)$.G|3 =*35S~S2*4,9P+g5Q5FFBt678TaxS16?AHB#kPP@vTLSתD1ܨ x2 ` 2A@j6G? 32HGa)qM/4HS`+%I%7&R4Q289_K+h04]cRd5GP;↕@ ߱ ^4F5Q )"p-YjQ免 UH?@ɥF8@1rS,u֒<"+TmVu9K9Vo&CEj50Y3 "G,` Ơ 2+E~ޭY )a-HBjQZ2lJ ]@l:5µƒ;S4Lnh0th_Cr]VATX Qa="d@ b1=sC*a װH-HdoD?~ewA`E~RlG_J#mg55|JhWu؊vY_j1y"RH@."V 2rFQNʢmifMnmFbm4PS5"$+rFn-#T" ]32q5.R«JKCг6Rp;&a# b(/>A>CwsNzŅ5ԩH4+UpT4(v@?47k&һVz9φwKI`?.zjm@\jGO :}% # ?GmNvÌ}bI*"vQ,k_Dԉ w81G#͚1?dϞnP{uQP@^a*]˛>+cXzz0}kN@FPn;Y>mLX:DGX}g.DdS2p/X|gkx)rkH c_SC0Zzpnxϱ8z@'lb}Wά bwȨǙ5>p(S5ORYZK({.t]D?Õa9'~6oCZJ5wB~o5) 1A|sD]:VNBga0f#>U֥7lHM+KՏ" Y=*؂OL^D;kE"V貏ф!_y=5xb`7&eMEcQ&V't)ftn[_{Mbי%5"fmΦ%EcK# +=:ؤ@`꒨ҥq E`1f-IclhImF!Y' D hu(xx*% 3 VF!ü܏AƝ[*i5QDFԷN %xݖM(\uNKTL{UCRwċ8s׃0sBn2kM uEǁ ((^by%[u,Sh4} M^y#P %eJ9FbA;SA_~6w#gBcH#i CBM_%ӷw2<s%2x sUKK8P޻bbÆ ,ō7T Ĉ"4hƉ*T1H'FÇ,ZĘ1cΥYĞAӜ9qt3kD)ѡׂ 5jΟTU&mfǎƒc8 ۲`!Bڽ{7޽0`` zS԰aF 9X":{ ]kK[B4BoY@ %X3tZ.ټ \…k!J"#HH=j8) NZ´3;{Lʴ۷*?]ͺװ͢MV[7U[}W `1a)X ,v@cI!fUfEDq $ZAW\HjVlkƛ)@)v[q\=]!u$RՉpI)D.I0TUy@YQi^J]c|mdVXqXbAc1n x b @Z>ue `fu+i 08[裬EP[5M.'lKe ]e =tcJ0Rtޚj)89yߝ~rj?b *PjbP 2f|zje gkݐC ukʻj䎺 r m![,;re9 _67n#:E4")5AnF',d0jD W!+[th@MLtbu# r& hqs<- DAt^4hsLJy^w:j3?4=?q$!eHAI lp/91!Dz9'7 (׃VJ" p-2 ByQ咪(za.QC XFgNbLt贞(ڬ)ɕGpNf]`n]׼WF.q:y T5YOT# ]/ʍmnsJTZe-AmI3 [5.B#hb 訆-yM  7X6NCYj}8*!ٸ^`_(EZN K}FWKm-rqʯV.eWbNlE_f_N$sɤX 곚y4w:i#κxҡ#n{ҖRM_΄I^}[SHWo Ml's{ߣ\d3.wQMǺʽ]6&~] ::˔WTA!d<|6d>tL,:vISX96%XKlŨ4a Og(D`M s]2ۭ|LF&kh{XޗCU/͝ 1>GSȴC6jC {өP{DY9/}3E5jxH%mA80k1ϰgmm7f:E^X[4mXv劓 `gmz!cX[7#W;̢cnjޜTZ؛t@;Ui~9f䲬ːb_Q׬]*ǡPLU[aݰ{1V2Ϝ81)pg@f:5vc̀jLKgyF~Kvb{^׉Y<9Sy";UQ}Cn/n;|@sP1j ZH׼AI#vp`ULVM4ȣ7' G/g1pq|`vqg 7T`{b{$_5>koCEAQ—g>5 uYf+?UF+&>h٧}3*6uN^yEH ɴW) ՂF5diNQz6z0 ͠.a5zQ&d&x{_RB06wӁuK4(,CEN$5huP1y67h}QpF^@MEhx ~=v`Fu$Sآ-WV XZ ]V.F1LuA3hqvo(vq*tX=p+KPqwC#$4H_6V,R_xlܦz{ ?Xi( (HQGP `Mld0 O\7lNaaX152/AАFYs#e z?^ĉ%#yҠ ! =p6lvw%W.i'BqAV96CbrU?Ž#Cr$=#68@+6=@6:bhtoEg9 ny0] 64s9UꐗZ})Bi({- { '0`f3c0 3JE9ʝЃV<+=W{کx_0 3rSN z¯U7/S=& ׷~KΗ;܋ߤ jXA8쐐vPQ)%@KTX&Ux +?zNIZ Ye6'_͘0*AZ!g5l[0q'k|+LZ6wP-^;nnyJ jQEP[S&C8г0J52Rj!1\%vQ Xŷ6ce\tUEr1¿2uTr{·ZLZ@ @ `]5+ v"ޫ$~AQPUHwRp 5XSLSurqoUxfy'4TSn*`zjqQ?5Цw"kwԗcz+ ,P2/ M9yh4&=PJR~Zq`q P <Sd@J+$Y"ÁR[C3X{ٵ:{|K$ ̇H*ͧQ˲Caj;~;bD|) Ĥ\1k zʧQ$ MD mѱٓ5y2lPmK˓.yj'8)2CS+2 FpԦ&KKaD]2 2 (L=: %%ՕƢp Xiw ۈpi:&Mʫ=U5fX|˲ x}H`Skh\`v uop1 Þ([/ypFowE0 EYPRY@:7 6MvF[Sn{إT8 9U(A ޙ;㝷r IPM%@FMchU8ڽ'\^:.Z3L inkMmMױ|uZ4zƊlKV"%1=w~H B-8ɗC߃L䷬oJLڤݐb 㛊F=t FۄF.ˑaeIZ<弯Qq/|/æn"lg5g E{a̙u1%~8v,_( èC!X4!(n#,#E~Fh^ދZ0vHtu".PsI˥!{Gǎ3)0/Y^ҿIV` }f[a+8Kݧ;f2l: Z w`5 N<SPt[wTbh[҇.Z%<%‡8bá6 >o>nٜJ H: XLVRXx  ,^B)2^ȱG%MP1E9@f yEK(dO-Q>4ARK?LC8s+ /^HF emZmɖsuϩË5Uą ]i5k,! A*jH8ШAT?i AcJ&9^nqI#?2DhZB,B op{hU3wA~x,!o, "$r- ē`)ü\;'0D‡NhBKlQED,,3s̚B҆, 5Xs 6h-v  h88嶬r|H%〤23s84i&85@x=*D?mI95 Ի!Dˇ A,= +8l0 '>'z KTCilA$& _X$1 ;,Dj 2ukP 2Y!4X{f>'qN7~b%."@ MuLW h8{6kp (hѧ!H#Rz, C 5 -C}/VxPpfl! zC;VgɑXԠQ6!o>Z%AykupGj~L>(LNݖصݨ|i>, (nz͗ttb',-1\LI@Vp9fjKhb>5U1poN}2{9E`ʈ6xfӨJB- %gs;aށO?!?Ղ U12qjaw؉\V<-4|34e҉4w=N *bF*F hځtÊP4T!"`3!љG>F,Lu d <\ǻc+h/*vT)'Rcִ3 hGI BQ=d sN'8VzK5!h7$|F}b5aiMA gOsFtIQ)IN"=`մEmK+xH yʢ^6pK%S%+Ye(4=D) *p(J8UB$*Q*L! J\&acH;q O AAY sdfΈt@h5MmD$ @iIG95.u6Mjrg蹨3Mds>4X`:) .CscE C#j g,L3*) x XDgh/ Mmj#D?NAUUJ A"h@E'lԤ kNCP@DF(؁`,3X )KUkR}:K 8@\IuwD"߄9)ãÜD)SʣO8rk5Pv0J)pPVh])s[W roOpuX|;T!u\D"(HMCo34LS& z~FPT ,xfITT6$חV/}sZaB>D(XlCjQV J(9 +>[<8 Jfk$N0$C$#K,oytvp\fH6i;].߫y;϶*uFzI`rD@7}KR8-K5 ps9ɽ貫Qゃ+U8]R>Ab ǂ>ʙSXskKbډ ʉࡪpo Ikc?A'S@ ;42:jȆlkN` ;46,3s cE<%ى[1 0x1 ! ڋID@+󱨾:B3b@ψ>` ]>->h; FwNP?o)ͻr}B0*q!'dD.HI ˰HX1w L4wl!(}~B"J;J;`3$4jE#)FHH˹-3;tI,H/Fk Lv+JR'4ylġ$O͌ǰ~ M4 $2B33*ռ͑j8ټڴ9N>0 4:7*qyDN-I NO4kD DOHʯ;h K$O DPZ8$%jerR Lʍ@e\s-4+IL* %'MޒIC1 Hĉ&DŽLwɝӯ(OS$**է3Л=83(53 I T;I$O]Ѝs(U)&>H::2Uld4y$L475*:͉#|G`ODCbӞ@B=4%D M;OQ.KJ+TYM]R okh/YQMRSMl7-zaUt/EUaȱBx CIɑR\NݶmӚMI3ffnܩ$J< ڭh7?S"-3)P7dٲ껫RVݪ.˯_mK,eӪ*&i9aLф L0%ǘō2rd37ƼʗFn{ͧsΜQå#S)۲;Mͻ>LR-?ʶt2_5t׺wCny֜Ix?}q*WMk}h"YiDY<t 86%vywsH v߉ z,΄M5|W~4SWӀ-Rz1bYDXt4@Ņȣavye=<;*RCz"1ʈ{5xc|9ig9zEiW/9胅$T`F(3-Yd5H[VN7f9MɢLB#q 5諰;}:Sk hKp5s^`*3Sۗ'yH睜;VSMJ#4ނ;MڪuVukʓ:buUVӄ3ݫz SN$^\`8[ۛTZ 2s12zquP&,RZ:|4 04\4@i vbò-U#P>=Ւ#CdMV40ǼU@r(SWf_usrK3u]Md5-x5/M@~-.2$?- ,~nEpw+V_m mu=Jm{Or`jsoÍrӭ7X3.ϊ|fQ#L*Kl5j;W Sz1D_ ݬV߳; Gڵ׶ɋσf?mE0fb=Y/P7q7ʵ"_B>|CC oײ Vyd@ "H?h<qZdj@Z* VE.2pen^.(A jA6No=!F6L7` ͗FbuW ~߲0X hXIZ'KGIOQ}õs2^ +^( ?F ]ymtU%1mle|u~o3gv^kF @z^sոI?Ty֍-X޴^}̑[U%:oH;vX8:[iOsr m>wz߲w=8|ZMTS=|V}|}VU<Χy}o`wvb~T P X@{FT7yU{gx6gtcX#%ruS:x}7K|H}6y<ggHSInF*~``P #D 4;S*S<5SSF688xA xwĀ@(t([)6W>x4Ic'Kewc%r=BsN0 6__ԀOCy̧ji>}lxw҇msȀ} n؃r{keK8r4Zi A0p 01CWS}&7CE$Ksq`d׊8hAXӈO|x8D<_5n B)cp8bԌ|h$Erm,Ew88x[XKEEOnE ~y(Af` 7cpYUĂx|i8qHKPבA'Z&GI[g 5Nb @R E鐸(q 96SvuyONi HIRŐHrSvcnk7 `NZ7S^W)%HwijA18h'J@Zj)7I81F :RJI9aրwٮИu3%] ԯ>XOIkDg(/yʨK*֊~+6QW@T0$j]spJ?മ PwBuLijD[ؗ}STuʴ7y*IIP+:Ds5i (P P05aK1!*apu{ZTtvtYWݩiuu5WH=kL-HD X 0#ݪN0b@*g uJtvjg[Zv {+ \Wa[V[OQBP (<["*? Sk@\FLtO75$Y*8sp|g`Z5c{OY #LAt jƷn\ *bP%7@H8 Ae!5 W5{ `GD,l9Oܢ$ _a|FHL:DK?hLlo+-\wx|<H9ßÑx +cţ|a{6"wd{ˬɋ<  fܼ?%AWGIMDܴʭʯl  ( xxIǽ81/I6xĠW Ͱ)ٔ׫?ʶ噢Rk9λa݄-D`kL 4Ư =0 p l˷˼ Uू\ f%ʺ0h&UeH:({)b짫A][m$$Ҽ v4Mӯ8@ |-$>/S%Nj+pZn \ cd^-j|lZo#N$^׽ ȟɥ< $܄nmY ȏ>uWQd陾a0 }+O͐/ ]Һ`׮>R]o<?/l#P*Tթ]Jّ캠욽CMOsxޥ}?a[}A S:QS)h  yt5?>MN.6~J Nr.v\JʥU?ٜ n t"{O%b? |OX~;?$B/_?7о\U` p5Xֿ(j-x_(/%]o߲}|  ,V`1h YcaÁMhEzձ+!a#/)S޺U˥VMTO># PԨ*rI$J*WLeK<:4 WLYh14N\:qͥ[wyߝ=&\p\n-f9s Gv1C^kPaYˊ ִ8~$IkY)&Nt BG&])*TZQ'ZdK(ٵl{^%/y境+νkNek;{Vp,iNKm |dk%\bIrډɷN89j <8b " 3ѡK<"oGl;=q3H̺ϱI!)S|hET#@LKZTI$"i pB&L@"*FNDQGp̑at#'kH$35Rr2h|rJ*{!Ӗ"-V;K^tc$0@MM '6>PyN)Nؐ-h!U"L$Xq?2.h|%NL풅p$CCTꋨj)i`dS&dcN+ 'YSHYRH< e-T| Em%+Ӂ"+ 1$j,- :ƶ9aQ@7N@'6:|dj"jH,djSJQ婨4r~UgZR`#`63vEg&1Z>0%b8 9H$zz@%&QyT.r[TNd@eu%*I-N ٮ X9 6(>`Fa;BS*VP΢54Uؚ7@..1IUvFVv=oڤ# o7(Uk'@(%\OI+9xx^ `x2`sk `!\\9!rCq a\u]bp` 1F<^,@Ay7&`ڲη(#S@79ժ,:gP.d|2|6$a:VƲf8uVM]Tz; lXųKmӦ6FRz]`{DXko5|L#2V2qmk{$Ak)?x{#'Pi8i(bCDj?6ѿoI꽽SD+g&2I‡9R|>-t35J- @6 6k:CGp:Ci[KxFO`dɑCoGDa#$9%l$uxP&"ESTX6B~6[XkK'5^"7<{\P *Fc4GiUA1x1BUFEF(DClIđjÐFp sPL -yދy )\G8ŵ)-JG{5# $iiFZ\ȳȆEP4|'h-DPcp؞9(krYIr6|s0xkcSB CʤR:4<=A٩+2bwUQ_J JX=)0$=UtmD@5U bx%{XNLơH\Zܧ5ܔM\5Pr:iY >ͩ$𼹊% r ]fR JX>@,$ DPSAun\؆XK;@mޓ;,  ;_ ]-"5Y3D\U_)j_Vz+_x2-FF)eR8`G7)x ` npnm0M*x/ƴFaN=+ުa]$ VaӼe 6_zu:8u#t⾜bSt($&I4 p >X=AcmZGnK&IL@XPlYokOw9/q.9pu>më́mμ ^{m5g\9_m>?1KU&f`f_ EP`]ڽ\C3:ҋi8KȂPkYL8>[@EU%LK.2,mrѶ12/3/ꥎs$2}7\"mP9-'&q@7;`@yBfMG90`'HV?W_NzWu;0rW`<('Pov r"1 oxwrw|`]p^\`G.FfT:;iK`:_ɵLnji^vlh?ς@?Zjl dFVXOI0)Pxo\FREi0W_H9HXT<`TV0u#N@k"Yx_Uf,2[]䑚(ijGgaY[hEf[y4S -8 s%ܛv_Rv?kDAchK tvSOFƮikf(G)c򖶧h{_{-ZEJ\]ɦ1y`>3oETxg *R,/fm2|/L >(XA #Fph8Ə$IPt%,Z 'MQP2xP%'2d`3E&bB4ֱiBխ[׬3_|1k !9\QcDA DZtƒ-kVjײm-ܸr^kW2at.G[/b{j+W.Znݪ.\"A<jU.زcoؠAC }`@ J(Bl"<|wo;^ ʄĊ-jc<#4bb JD)/G(PB)HSLD7`5Vc I-֌%Y5!89Xc5-_5`Ȉbb/AFYeeYgFikGζdm IY@ iq*|mU$t1gs p&B| wI 1^EQ4EKd@!pU$1! % FzR";Zi&W_$nAmaKi% 7 6ݘGP֕v 5x/O d28 _B4(|80 [ND"kDT~r' Qq,ȸz5L3l} 4д͈<w:tF'Kt-<5 SS]XKִنak/đ[i]BC1NK&%w >D4_PD"$^x"GX+Ť sC`t[AD겑ou^eAcv,bÜgkx+hxK 8 ̊ IZGBjsdO{)@!!i߻70 zG "~sG)P LЁ@@r+l TL Us.! Z0GX kp,# [N RCU8Lڋv2Y4D$/3G,D$R S4XlK['mnpS0>#(A ~ձuGiO% i9Br-HpBƫ.¶n/'aىr:a@}+U(E)1,U-kaKrjKZw6R؜8'| X`>qJVd|tBF r4`t;@]g Z0-TY,\K'eЅ.tlb׽c0|]*XJIHT(c} 3e>39fO ԟLQ9 ՊTPzn86U6$0pPQ"t[o ZJBʵda(qv\+4VXS OE+bWg􉾴dYli旷ϦmK^Δ>L]Am4 IB((ASrUh% @@``A`HpntKf\!CO.ve^Ϋd{_\؂Il2UEJV6-&myDTъ \[5]rЙ0EP+Y AD,yI3b)S>qq \P7q m[ert4PWXKVrzFS%ck/`zʯ_*(@S\eMny6묂8+ 3 @V@u !;ADDtK㘄ӵ9IBԟ$K"Z/.FUmJ\E$P%zOLi{/xVhv{dpH˶4]a* 3+TATUܠ(ؠ6]"Zc{Cs[|n@78[:+^;E +1qc`-~qӝ8C#6'|8 kv9@-&)xl [[;9?L(?d3A ;9nG6utot ooFaa޴oI7g{މ٭v;>;75̜֒? `k J < xAB#nA WE}?:ܐH=`u{ /䂎9^ڱfŝը A0ǮE߳ q@ܚ%Vu QCY<Mr-ի5C V^y`_˱f^*D  ]IK٘ ._ >@ ʠ[4Dl}aSA Q9XDZ*VÂ9T)!YX!8 !d!_!a` NMŝƸ T l$"2o\$n(4{<;>SK5~*|]""&`8DC> #1 A2.#1 C33fP ! d"z7گ MlIhKi=_n>a"')YRV!-Ŭ!Ձ`eEZBCZrd[H@i0SjI $j%^JpFnd_Z`%܀a4 bj"4fЋs|$ʄDg>a)j{~p,CBVfBfle)9YlXE%1hੁ 1|[TpW dƵ ܆rNQ8sBg \٠%[nhohK5x @,D${^{j)~g9CBA~Y'D]X рK(} 3eH pfd@H脢F4mn(tzh$ao$"hP!B9%fTPW3<'Q v!i[8Y,6)d(F).(>p:,K_0t))j_]R_~f xš"jFkXڊM0 Tbj9c8(c@6ꘪ*MZB7"!OKAzVg~hA@(1(kiWnڳbŴN0Tfle1pk&$To =Vndi3Os \GY*e\S:Rji 옭<~՞ĆH^dv+zl,ɚܥdUOel@rl*X˚y`$UG-E<"B-0X֊Ԫ&҃^-fm4ʵ0 d؆-/Ȗ:Vlٙz#@mۉ^@ɗ\@ܜ'wLlX,>-&5n>-FRn\]\^d~n$ٞ5r+Ʈ7F_K.;ggnEX(͜Vob\( ~no.jn7/#}@N4~ bUR-U W4,}B.*=<Zg/`\0OB銰ʮjpOnۢ0#vh Mb譢-5j)D xm A>p6[QkX u&4[)ߗ?Q~Y7v˸a&:=@iYNBUGB0}&3H4'jt)3~͆A0BG߷m6+X!~MΨdE|4HIJ`<[\]ԅ`$=wz'%k54O4}u~j,rPR' ү53;%UGvh4C"/yxrj)5TZ; [dKko4\L'4TáN/t2C' $@ںv'Gb(:>GT;vj{PX 4AFtu*-}޲y4LÊ͔©\=Z1&j5ضyuo"]+ݥ*r'#xVc*c%ДuȭLP E\DWr#r~rh;H!'xd-g[w&!'556l˕0\߷'wNneIPk\l|x_.xҪOR,l+ܱv{GCRWt3Ģx0%t0H,^oN(djx]}8#w3~^ ӥql3 /IS95M2NClbuǨQXsfw-vNۂ*x"@!y\&@ßx!#Xçx7Blu~wnnOhN]l$N9+ [q@sDoKs٬3v9$]-,É'X°'+;@{{{a[4 =:n#IV;]Iz jyѰ٬Qhɩs&0K',o9kv'زhG<ˁy-yfFv%jIɏL:Bʳ˃;kGq^zIoI{tRQBⰙo4=pFOO"EAy!\|'CEWmTx M{{ˇ7`Ѯ Gn Ϡ1ܼ&~-A#/ޢ$% vߺ_<$LB"؁$@N\DVö}þ:Fh׮AX b 6LZ5iS8vX%Hdq +s丁R8<8P @N;y(Ϝ ` )Rd*T#UTAà ukĖˀA]ѶP!2R葤'YN"KG#*I, y &9s9wo:3}ujզ7Cس=5__fM! JhF>A,$+Wdsf֮Cv}gNE^ӨR9jZzU, fӖe-Kj+X0&C9C)~H!ŰU.&M4V[qn-lk8*(z*砃N^:h+jx [H$IĜ+ڪĊz9lK,:OA! bp)('x* \T2Ė`4;F+NOkM}kFs\hLJz94C!ljN*R<ڲ.S:뜊hͭk  + ur)b $.lIH2Of k2lM=47Q_fFQM57kXm⌋5Hj*^UW^K=Rcm.٣H*Ҽͨ* 7+9BΠ`]}l/yZhP~P 3@`Ą#t5}.7 .!V{FdX\R%e2fé؛=itRgVX׬s=qui3xHi idA.EF[N?ckffټǍuDhqGNm*b+rJPמZPLX+Vhgi%(Ox*^ '@dƉz֓ᦲ7쭨{!a͈qLPr'fR_ M.3K,Y8*`%*PN %²D<6D" oXCC& o }cʒȂ#5 k&c*E0tnUYm TZ @@,JW5V4  rָ8Q`+Vs+ 4l,Np UTSq8y#tIsDzZG%Mʳ6ΞF SiU CUo2ISe4UNp%(w:56kn{FưpWb.cwH^Ù6;HO O[/)짤fVZ}٦S2|ğM$" ,*[v+eT 3%6t/l4 rq|1v[qtW3qlaLqz#ɪ@esfcrd8x&bOMЂ^fFlCF4:b$۠3@TbI&O[ɰ&@}o$*eծGn@2~^9L((x^;@8nh59 kcF6qS e[? h6*d L[T+nh ,2'ST[\ӿs.87;lܫ{4[:t'GU}(o63=PjvC\/mHNuP2QdZ狀v5τL7AJ`W!;Kp=hqZznq=L$mĊ9sZ1}f~/X#k^eo>\OԮ,bآdBȭ*R0bK\e=Q2L: fk| \  hAB7~` BFzJ+\+ @:trBXNPL̞%Y^02oNj-oiO.@ʭ6.F,\p F5p ڂ/b ="'N*#ZHc 'b ؃z~T@iNBTW^fD(HjJ-Q"x|@l!q Ƈ-  wC5>qTLnL1P*~->8w.0,~sjPCYb=FJ|Ǔ >q$` \ͶmĶ.,HjolBl fAq:q|IZ6c8"0; N#Z=L&8'!oQ u("1Z~f%LPr#A$XF2Zm>Pj\*ùl Ol l` !K'1c41R6nj3to2DzP)O%G.2'O*Né !#p+igh<24,-7H :qےWr#L2*@Nlg,tmü1 @` A*bv2#4Db 2oS@c Gbp)csǀ0 Ih`S`!6/0nyFL9H -s-H\dvj?->O:`:*` xAlJF"S2mJ=IEpO:?>cĴN-Lс4?h1T6M355`8EMC`q%^:J4:j.*44Np>3@,ZR@_Ԋ^r0t1WKF?/~<]'F)/@4Mn Ps*&UpIaIMĪ6UQ(:LK9 K7,*H\5*Dh,:l ana!\F7 OY=$(j94h v $+'1i!hJOocdKdUPdTvHث$-4ܲCO)l lr־vvz?ȪPP6h5$tUCXU6O"- kYrk v y2FQUxl9m?Uup64VGAn-[;9(7+*`jfm6R`XpM qc",uLq9UC%6$s6jt`%겖k\ٲbґ Dn6vkv_wAdz}6<@BRG7(f:ZB_*-z=zNRWr)We^|SW׷}ND\%rФZaXaa7v/hӖ. OnunH(T-`-<:Xp3\I ݦPDM7Ўaxd;=/@t+̹"/t)®uvFV'swڼ'}w{(#&t $I)jp)BxX ,#X!$|n&l_-R1L7+m, :l)YV/y{Z=8BBK-=nf4á- 0YM7L@!j}̘9TC@ꮀc$SlcYK?sAnH\[\)fm ʌ,tz)SsU h_S Mu(HVE\@Qj먠+nRTab/E bᤷtle+:̔$= by)\}(zCE|ї$:b~]_x9C(l]+ 8䱬 dbvțLy;yԜJ~%zZ)()#S9fJ &]79\Җs9:Xo<#~.]z>85[< d3y|暓wsCv_(U)N)n1@g9R7y2$|2]ve~;ގ$ Lv,)dOR*YМc-|n4!oQ䊸7ҕh 8|l\<vXyMe_&Ⱦ-r+lVi9`+vM+SGnmDadm98? &[ˡ9E=ļG;7 hW{W|sBﶨ͏i۷,6k転'Iadc0=gpPEm5M#ϔc|Y~ ͟ѕ͛3,NT] x$.;%0|9#AU| ҉n1t"{8 6] f:z9b+6!aS7 C::)V<2՝p(ض$_JMR#%ƗȀeU7A۝&-",]`ѹlKRZJI&,{VfjƕmRlSe2o$poSzr35G~c^KZō:ܝ<Ќ?e}@\-VAU(m#e&|{DBE?KMJv*/ z&n"Xj vM…:T0‰+V4-#yd`դv,''<| F3N,zdqСr)up@0˺4` \ݰ|P:TōUA^(x<4r ƒ+/_xKn7*n  2Ma#-fع4+Hk+F|(ar̛Q!A )I*Y9N;U0n,MbZjV8Spx hײujviY{`;eE ^|Y6RلVP,fR7QQrY3v'tJ-%M5]HeN=A\RVL`}~1[8׀PvajׄU%g}jVPxyX]$gjWi0DP~z Q,.fkO+qiꫢl' M"3HzpFz[P7g1PBzlҬ}Mu%-e1ǧdUO=sϲV[WbDgFh_!2>5m 8q-ٜ咛{ #Եr"~oZX'vvx3 ~mѪD^IͨE0jS`AA^嶒iT}.g9pYbP(0->S\XW(4;B E[ܝ;$&6%bMG6@ [@y/|F>oL4 iO~i$ a2K”- *^Zfqc8 fZg!mJ Ը:>dT{P*Tx+𥢹QALuHf pg<|[%N!c"~"!ѥEn'a$E3Z%[A *T P.:\d;Ah KJj2)/ɅtA1 iC(=a Dd!'!!> b RiU"RsZ顮̇Vix`UkA`x,q|r%n6zH $1zWzR  iuMG7a!A{Xu+#/c{!5gL>FE?DC\5>~Ǡ OY@>HG!cf l7V_oȀۂre)Q'xuhYdvK:W't`e`rQ"EZTyW2x'^%P,ws `>f` ~uesP9q h\O#a(Ba Ȉ،#%&(P%x%5(,:Kg2@聆x_jozxwQZ>G"VV""@irNS_i5'PVP Sb@sIyyIVm $&)ƨ,t.a15%gؘ:ɍh)"{`jE9T'~uGt|8TxM.":B35.ORiy [` p)AODi pѣht \Y898̸fјiAZRqeuڨyR1@d3`_tEߕ9?TTau"Ms(/Z(Y8':4 ` 6%hJY!y6cyAK+g/Q/ZN KBѓ%׈9#GT'!"gTiwY4#LJQG/7ܗeK3l7di0[POi @]7{J <$yd0=56ʝ'n;:fqc8u:ՀJ:u8X5u.rj\+3nQGGo2[RWQ5 8o7Mc?Æ`t CjT {J@\0 sXZUȐZApmDž1xCaÁ>ڒ>z$;uc%Fd,)8!Fc:a@LjQ -c؇lGEe鈃d:l*aӡU"&Y){ ֪QPE #9zZ<ن1Ydޥ{ %uu%l&a9EMSbʬL3HۄG&Z%qA8LT#k%f+[v6'&Q[{: U0<**Bk?:o5aL}{< *Nj.bNT,riQ}?]TO;T!6q>c ^Aą;߆ bn6Y;"FZv?aȕ\Ak޹D+zJUčXfb4`Lr?PI ŕ)W[גw @zDebt·طx†ń)O]@s1,\Vb?$;thaq;>FrGnH{*PayMOx}Xlx8 \lv*L'iWE|pdzeKw\y ) * Ѕ~% <Խ1{ 'ahbZ`V \ɖ1M SR{v"%K_ʼn1,T)H`"1d)4Qc+{\(lu zL,1—5bq)a 7#< 1΍b7Rl%JL7ʡ Aa֬(|uY!9Ձ6 @9hZ@74@&jw}{#0=w V֌ ` ){ \Ӕ7-;- ?}/8Ji=;kZq+MLj3++2@Hqj%*Y;< Bp{|%60M_\w қbYh @8bؖ7-aX> ԣ+/QROzqY &5\Yߨ-H\wG\4v@=,Ӫ v2) (H)S0P9VL}q.9ֈVgԲLSo EvvH;9G?o*iEh}#T.?OYo+OI_Ib Y2r0tNC,!#ɘ SS|*?l -wVP:OBYra57.Bqx}_OT~(G/liIO@.tI ) j punt/1ZzN%BEKTbj pu١-]LE!灞"dnwKͷGˊi#H'4ƒ!ɖ-iIE(K;T:v̗%1OP/l*hl1 ЪPl ;+͵H[DZ{maL+j+(7wM A,1x;.uQ.H':h&)<"ZJ=ܓ GoC>=`M.,HLQ6B5E4RKs( TaBs#ؑ(w^}v$NHaINfqr&rJ+ϚD3JO d?GE6$>:E( . 0vAoEI3Q[nך,5I-5JKFۨUF AT iiQŌz5Kx ( ֧a=.Wy ̨+7[\ K<]4Mh|5T G<  f1# rSЄʼn" />U9,I(WC>j]eh`Ya*KLϩeYz+h Mt:wV<<@ y^-V-mKnKˢZ#m \C&䐩 ;lY\bLK6+]t,2iZnӀT+TxWE+]E./+ Bi@yKuDs2(-{k v@|&:l(?HgSAD"+G D8rN,jAnsGLtS P1 `XA> @!DxaZ{%@؜וJi,7T JḮ ;i^@>%bZ`AMA Zp_b#KxF-A9bod FmX$]Lj.oI`QcAʓ^tĤSiDٕG:2Teq)e2d0Ă -'v#6̑Y+EPr}ĥP5Rί~0@*-5d% 3׸i x@"G]+XH%=p--1"Wծb&*5]H+y? 7]z!B&IQnML`g`!07*a[b:3Ti*HPMn*#=rl) ґ$<[4ʼ<,*( }@nA$ؖ%nDQJГ^ǃ25,I}e!Z%8vBM:'3+D閖”TAg mhRMR.dsы@4Lp1l1lr=aLB}2&n@DIah0޵,^ ]~װӍZ{q_'@#Oߣ) %nLӟ|\H\ÁlR}{t[u۴i3L [ajOzјݚ^s `IJ o7C2Q##%8R̸3'/S,"חL|Scb-n62(ɖ9FcSxFS:4\KJ(4ԣ-HznK#H#c,n ;OɔZEi3[\(0c<.%1d#rV:*٥Wfҹ7vjVfNz4牢+閈4x\Z}>Fn)}#ȵ(wEb#J5N䅻ھ2%^[s9M@7^{Qg LM<][]a9+/_<9иtQWF}rrYI`)*xjؕɽh=)ck4>(82ȸc4> -k*!$4яJ i9UzF#?$? K ’:-p18h /ދё \d1#AK~ +>-Pڡ0Џ<"!EC4zңƈy%1(1P=+Pl)`̙T1d1@C{AY*\|CKz@Cӈ ݲ(AnùC\Ah(n˹A蹱%p  ,22`G3ŖZ7d;U\VG | ̲a x>3]E^Ea:lcd$<|+A FіA6ljd6GHlFƒd H(I$qW1ulG3*Ђ*=" zG{ &:g/)/[:ۋ JE7H<diA:bMAA۶AD\NDɿdv G2ŔOO|Dhy<|L߳Gc`ϐhV( s*#u3Hפ`z BRCdPI, c pK6ƲP#I3 GWʖ;)  *(vdG;;D\~+TQO[7b(ZPN$VH$ [ ;R0ЩX0p5l*,.4$d9cƹzPdή` ="i45BQʞl5N."""$2;DITIP0GiQj@[(B8QƊHUЀ զ, h>RL͡sH@QT e-TbNN;՗T+Nd1@5Wـ P9sq(hLpy%8"lGPl[ [ln nHXnjHh Tһ[0ϲ&q* P(bVҼΠU2 a?S4dNU#K9W&%Gmר51-,l ^HeІeZmЉJlXl0۳5[ol*W,Nۥ#\Վo õJb\{*tƭX0AU9l8\%-+{NEp]ו (}UHg}pu(+$-05PD^eP'! ,^I$_Ӻso‡#BGE޼]8ď Cn(T%J-]Iv#!6sϟ>_ I"͙Bv۶MӧPl2*5j/rzSM^6 ٳLnk  `f˷߿ L6r8ʛ"ߴ86Krg˕1oLoDM?LZ_̠#{JƋyn'&مK6+L»P[m񡼅sZwX$; CV[Ow y,OT8 RTM+Ye}[T`7]%g܄0IsAǀwxg8w]GFQz Ƥ{5t|g݊HLUB =uVXԐeeeasEE #vbp3+f֐~(zMD )$PxMAgd (#[9ML֩V v NCYr!\]V`F(dUfQevTHbYp+UZgU1':zԚ&&HڄγD+m:ޤ* )`]p·5bh.H櫯N<V;"j6 l;Fq JbuQLoW-2J<8g5d7\œRirJ8U6iKe5xee3Ͱ ]p\sՑ٪Q'OАXRQRjJ'}曛4POcZm:s:_36ٲwvKQ{ٷn-Ry%[L ^2$Ez1>xdkѿƴys~9 ::N _\[MîfN{ݻ39]ci]h2od68U^HMzXVޠw=co:9#/T·s~v49@  Ͱ[D ɦ%c 7f"Zᩙֱޛ$5181{'Zn k$xc 59h\6 $k C?oC[Fˑ͉&V%*Zo}ȞttF壔]ӞXr&$^QYHL}f96Q5fIAWҮs(JWң|2`mS*yK&7fQ C-QUČp:Qk#FjMy5s` XJLբX[A7[#4&swF^׋M-rYXrO{ʲƺ=^TK\vi[ e׽X|u! g=5,zC5 mV^Zܕr ܘsi5q^wG{xguPWzj~ kQنV+nJ~0:jZ77ΰ9aP 5G1U;wX>FI;(-VlW݅wc(s9d"f>Ɣe(? }(N / `Kj\+bX̕8*g9z4Q߇Y}3GN[GVq-`suE! "ۆ)4w1Ep|U朄)M+}L6C T@XfWbe1e|^ٻ.WMi2cilS:32sl&wW&_읅- iOސ3={rt~ wnWX^̉n _ᖳv#.m-o$oɕ+I0_Uկywob=0!-`%3Nx'r6kHciW/muݽ%G`u!=؎]z\^m9[[9.{KܾƤhY{dזv\=qO;2kϰ$靷&<˺ OaTW_&};$Md;<_wkWey(%Nlg|kf}GrX5an]7~w 7~T35YC6[ww4US sw'hL4meCՐWW |ExX}h v}JcD55vHnŁmE `fPt&?gh5[IwVtH_8d1ulSw?&BhdֆdHHp^ 5 4Xgz ]c(ʣ P sU=S?L|eMhTԗ\nvtgƐseGɤ'ՇT`$ hGiKodDWv}3 NXv䳉_؉b]3P ǀ0r=o;m5xx7'>kʠz}dזhsi[vѥp M|QhH7NxWfS}HI5eQ荘`8sZP 0POmkw1H\w3XIm'uTM X2%ۤi>ɈR؄`y`w(S"ȉ!hA` p j5ZqtP(k hxi'uZfGFL|•|YyɈb'6}H'^؈4h>c4\H` APw` ` V5oPq&ytTgN}5ր9K'ywyVy虖Dd %iN`l 7(3viUN͇SI6xĐGI>ɵZIIK7 IX!i֩\ܙ9i]5QZ`` y -HtǞRH>iٔTGU?3}& *X rآ:0G=b  `{oe|)j̩w$7uxȢDٔx)whixMFEʁ`IO IВoh)>8:ZzwCeLyI9ʢzFtQ n :vJepވ h ^ZjҰ~W`RG[ vI?9LڃV_ǨMRu#ziEZNyd|jTJQpWPJw( sX^\'m謔Z讘%-5Uyʩz9[dc]ip0BI:S@Q@T z +9F hZJ):(HðK{_z(gWdF`+ڱ {&NB IP3[8U`b@  >KS:ZҖ [;jdű)ibPd+f h%>07Ub0v` 8P{HL}Y[{dG۟];iX%GѺG 5[rP%>{6Kly  uXr\g>؟݆(}bJCllXil[v+uI'Q`eg2pPJ l 7s[GǀI&+L GֿfZۥnຍbŭtM+ZĠak%>YIi ;\24l6ڂs |A݈P k`J>> N ~PVnX.ZH|q;p c* Qi&%vD ֱhTَ BZ - M݅ ͑.p龝> U ࡎҡZ` #< ^džUzFI3ĮH2BcV=QKcWPm_~La@&gHJO`E~ްc} `?cej_ !%o0ܜ*/O7\}5@ ׾oދ{Ȍ趾vϦ-mw \׭闍_ 8b n U/VÇ $HЀ"1F!CI*ThҲ;8KM7ܩL1eOUZkžJŒn}.":3QH Yeِ?+o{ ЎLf%H+t^\ƁFK~U󫤞s*ӂ:JWGV+rh+x!VŁv1΄Fds2乬O}jB iDY> z)B 39rw8u+Z'JhW䗹̯UcCRǗE/0nDC Oo2HA ^_l@6X qz% WBn8 b;iVc584YQisb#y/mG`%7c9rKe82 4Z<סb#l&XF3#  ՛7q@D',PE1cRX?5WYHR-c|Hl%S^,@'>*L'1IP M ȚTZpbkʸQ#D i\$:u/)&OALeG pL[9-ѰѸǩ4~ó qIUlgF|6"G+ohU)Ɏf2 KKT@,K|^yY_'a^ KaNhG)GXYւylb\Qsku$U b59vMhfCTA+ e`5X- ˈ*wKpn3p1i ›>1>b wq9۩fYCӑ$vz4[ k?8ض;M%0.陞 % ebiB@KXY947Em^ 9$bgxcdC8 fgAiԺFXE|쫎F2#8:ǞQvTwt(4˻B-L>9I'DXE :<"JCbC Dƌ=HĸJ{Fh$:k,DF >6oȐ¹$ɚ\ZN,28ʜ!bRġd@bV3SPNL2 |$±#5 3)K[,[2|2(22FN;D4NsI p1H9x\ # ) Y&u;ͫJ` Mڬg<͏D AKCtH%JNڣIΛPwźcwP@UI[ /c0MS0KhDPMG*O1&"H}+"^1MHFi|)O-'Ѝq;,s@ĉDML@RiUB1;(9:R/tPnSH1Ђ(p-@(͝OXLa ÐҴ"S=34PaPaDj%:o ӌZC'5GF9=_2lȆ/i0C'x&&V%e OumS;$u8hgj@X]]d h8 / 0P$H_͖C277e)CBV:uĎjn5@)֢znتEUuh_A)'xWO@N- /܆iR5*@ HH  WE<[~1fPA=8% DZi`99HTR@(C^aPdedM%b9"dٙOdP-_0Xb[ub7UegeD:bkk>pc3305NW"} RRG.X8$0KЛjz3^[H;ȃDT 6a ȁ ΙazdtUU KD0!6 z&{|}h)hu0z,-Kj=rCs b9.cMcLJX=@,mJF>!TfX?NRLHW.*x.jt8j(^vcaI֪j+Ȯ0*dEh6hr[fY ;N<0DX6B(9;ZW LX4#JF<~s@8`f&<('@u 8bӹRoXM윘KDD#?_SnpTh0M _ M }Y78,(瞉ޚhmL(>+hgN;>fh`&C r#H"XY t)GgeԆ-O\pϢvBp 6osU~s0 ze޲*vvss[`;Y!D/J4CQ?tt8`X=$ KVmXKQvG[2uuQ5vUvfuvn[SfwphN4K$v@/710wruWm6lވ@PKG}t?wDA/x ˚g-ub3_vG_怍WDҍŸдFP׫3,IK y@tA?Y_tvf`m.2#'Kpft_4)x&FIաdJ K[M^CV^Gwp)WK* p_{~ F[0|Ku`w]J 47*JLOJHw!| jIGoȆq+h`Ak 2l!ĈZfQ2a6rq"a,9Rǎk̘&AΜhP'П(-jh4Dx A"DZP ƒ:jDiFЭ[$NՒDQˆ05jӤ5k W)CyLArZ34iޝ=ZuӶ5K5)'cO#NYfQn8 -^̘r.A$"Sr%1Ӵ3灪AE!}J?l`T , ]-ՔZW d=AoUdA >_ia%42%rrPr`!J`WZq%SҊ&df#t)0p <ۄX2 +>첮RP 1Briez8c9i0k^\IّK.¬x0 &I:z:!(c! Sxi[ .[i]# ?$ĩj!rHPA:@7g z "Diaȱ( qƒC`K5dpж};vr]h0D#&].ݝ1ݛӿxV< 8 9r*DUC~oiT 9ua{MJ\P;@a >iO!Fw11 MB^ Qے=}⢝l}vImoݼBKΤ& x U&8HS\u594 `,*b 6ְ0- r CS@@"_P < P'hlb() P!0y4 nEk;"C}:۸׽)V-hq^@ < )  7~I @#TSĢNje,QIDLr#PH R ipC"؀ !- x ! 3|@U28!70yH47Q\撈9¶uE]SHe+f׽.F~!Z"G~[ND:TR}tP1h`dJ'TJ%  a Bx L@JSaI\Z@l,}[߀4Ln-$^HXcsKgN1*F~(D.br-W xZn*TBVZJ(7JDtӺ+^c$! 5s~+=Isvlr)LG4 IY|?M_P )(HuR: a|îNtNrKǫ_E:$sgM@s Lv[a [XPҮӊ Z!!?FaKر%Ui{F4}ܪjLj4#TrL D l=*ˁ " ~P\ Lr_\ tI>+(mɨSl q1^njM.b}/M㻦h֙i~C+@eV XX;`s*)88›?@4 L\%A*A@?Ȁ | ^ ^%\E75t;3W'!HQKK/xU?athY^Ỳ2a;F3˼uY6LAjp*0X( `Jn$~.(pEV@ >Qs\Ӛ>oc;ޡ5aYX5ͧ(qOiHDlRY5sGȅ׵8Oa;%,eTu: :! i@gj#`0<` $2 .Yh\pܐGav!6o, PEiz ԟSꋁ5'XV2uYvSs8m.jwUSdTr%6ZQ.://G\iIosI;1YȎs1.MѓLF7-Ͻ^bT PL9)ruOy_`e*`YX=|mrΔ !/Q.nqkMk/_I,鹍T4TA^H]깞CŞ1 2\N8ΣGaY _ݢ0=!h;am_yߥ@_(APH 4^D-ԟRiXDU@ z )>^Q1UX àQ&\!|L@  :W ‘:AL@ Hq8E8`\l'À((64,iM5ۺM9Xab8/ACbBZe5 //cJWhXEʼnYa oU<%QyX")"(VA-+bѢ8 s /H|/,SNݍ2{E fDIX4J#5&QEi#77b|P :W" S9@8ȃᇃT$>Yd'$W"B~et14B<$DA4M3FH^w$A2յ1T9E (Pl}0ĥ]SbUxN;LT0%=S ` `\EATp|W*$'lM[5&Z%9LpҷC\e]Zw!_ž-]9K!Lr C Jj}ly'yB&\I&ee2fQXgThf̦PE]"k l'f1J=33xD&[ `G4HeEg!` }N5Qd6ހ'(Oge %X @ g[ 2%:JWg\ &&l(Wh؁>Ib^/~ "B`Z"`rv^/@%j`vV&w!xy(~'  4S. n2O?MilVf`(3vi9؄[)ȗi䛆(N#v$({|'$Ej@}̠`XC@ ] *m }[S<-Z([P mƦ'AD^T"▦*Mt΢R@xd4zh2%*^K ka{LQ{tERh]c0!݅ր!ՖSt+^n4EEk^@pBګkD&AG^0|i.$A3&BlѿHF <ul U@ԅ m,[ܠNU`?kDA+ZЪ%,NѾ0<.FG5g.V`^k _95z)L5VURg~=Q h*`h:AD\,޽Z&.As`C4@n6mNha^O 熑 S >] Qk&:Yjʝ-Y.](k_g! B4D@/=N/ 6`1`n/X~oÓDIPƏ-\Nok&PUHR̮}>XYH ,l1d BpDHQ_0qF30?` ˲k я:5EO ' VLS~hv̚LAcJ/x!Gjq%sqe&ľ1ϰpGlVRPV}I}~˶]XHo"$-%oC+&&vZ-2Uvat[}Kߨr+qL,wO=_Vʓ0͌"5'h*ޗz` 4Wp&k5sH@nxDdu3~3ҋ)s*:+ I@X\ų7i5Ƀ{ ,@ 6D YDdp"4(P @!I,xeJ+U< *Rp`AB U,AC5ZARK0fFTZG&NtU1<:fK'Oin,bX%9s{8 6|x5v-Z KFe̙-G.ghG;6ݸZjҤsS)&xĉoTpG E yr#xA 5)Pxp=>9ThQLF5 VeHG'\jkU&YJ䶆R|K < Fs ¿ :lH26,4H#4TKb !*m@3Τ$cjʀII:/bˡCrS҇䣯+Đ8 d>˖`P,Pq0Ӱ:;q A,aJ Q[c`M%1Blı(1,c!]UH`Z tipR)rʼʽoꉹʐ$ P |>DlP@]tYwB,DeQ7IOԚWsM;iF2XTQoTS(K [}╚5*j:u'>h'\0:#ʩ[J=20hj)x֪o4.Uy\sJvu{pCzO^_3Ɠ ӄՆȢΈǓ+N)$瞃ɐIupM>YeR)t%X0+0pI)@\ƾe 8bdhn>N8 diNiϨc̱@3끸5 յ q2 )(EJI y  [!FW>GǡlxLe;6d#JaSƣD)LE)tB-NO "ؘ3jZ9^wT kXQJ~\2uDȞJ IX9$2`d%˒Pzr<@O"*SYE$ hÜ&'%,v@DCe Q ݾ.5L lTN܀ 7>! UUEzRBƩ}/;:_0PQ,T\}zFQFکv GPB4f0%TnVhȇ@PZF2$vNu![jTe0Y>:nr8UR *' z b T h`UjZ  Bjv,+1֓Є=Q!3Kud;i]y14d nH༄!foǾȊ*^ BgJ44AhDM` GN4w{Vc$@G:sz{@I\kIoНÒI]w#v%,(U80I:zR  l>5\m.QB2hБ^Smp1ad^ ׺X뀯u TIr: :XF5?Qz' Ap̀87KnTd.(Pk ZY&b|Yt'xUO;H v"ȆӼ5HCyJ|ۈ; "Bvdψ= c"G$4tcRBP lFIW- 1ۨM{})-Uxʦ&ͪV]k(7IɄ_9n1_o}*Uy&[Dy֤ T*5}(hJj}pnt#Pm![PJQWuoFFkcT`Y Nb;<pDo'XllӥXz"uO"{ޮg:-%Y.= ն{4"V7eM'7k` i&C@ +{e]\Js#pXSɬD{' HH%` تj`6@ %*)U1@[rl@ oqwvsꍄqpͬ %*j<E7+(1m0)$KK{&/qb "efrYVnz * 2`x #R"!rRMX#m8rְa6 HRR% " 7##&@ʺfvғD rrң9/'K@)@h<<3?s3&D8Ck0!8q/18'bf$Xu @ ^@Q7Spڑ#N S rCk(m + D3DM2N1!K4EFeE #54p$Jj Af'$d먉%Gr/B7U # hKgL'~i1 ` Rx4P43k4O_E=g FUhzcQW3&N<zM?UJ I/Iwo 7H pQ^#p(K]u؇9/tk hA4D0CE#M0+݈;)l dEhRobf&, m&4Us: "LŴ *``EH1C%F#<(*.vl>yfY Lu@C`2 J k?0[v 51unr }Ո0(d,U gM BiuXS̘QX4bP/ɳ fW5RHM:kKY+g6&lD ù._2,y?hiʃZ* L3Zpy,ybIgT)(@ur~:Pә!t^?*Y uNyڃ!DZYx!ec]cc-UITa+9:;d{@,&⢑􇉤9j)@K,šI$ʗ"~7Iu3nXB'xK@&Q]2!2C;_!dy8W##&*e#ɳM{N;cH;@R  ) yK 18*)[GS>gX$p  !1^[ۺ ȄOI50jֽRy n̆7nB=DqmQ+w'}y;pA[3g#_{@$Jaj##ū׍>-]& 5E$Ix4οO%" D>o'&e[5+wtI4Z{*[.{0]n 赤k7lD1 k~0"Gx#gQNӭJJ(HuԨ^ϯ7U̅M_^I^-U9&rv)IDwwHMws}fLt*iZVSD/_A)j~#=@+ND/U0RAL"AqB@ PBB%޸#C ?|` ;z# 3QÈ9h̙3ؼfzdA#Hѣ-N4TY55q\z 6,WhdZ36ZFFct |pQ! N0G9dx\:!o_Р 86X*`bu $mߠ[`;o6f:+=:"&=z8DU[Ś?^fպ7׵5q=f^j"'/چ[GsO"tٔQ' {Bf<,[%fCfYf WmYÞ\twW^svwY؁{" * *Qvhf zSUkR oAm3,ѭ<-sK6Bf%v\3T7Y;&{l{WXLV5n. U*,U`16B )̰ q>4rq7FjH.k1+Na%H4 Ɩ :G`HݭSƱ*0R)ŧ$ d 9> ŭ\ɧ T2ND= +m$&UwaŰQ7aDΞ91F9Suj_4a vla;P7c.`B9`6}CŴy"^2׾OqݜET yA7: [T/HGY^.s=hֳ0seXTB2uwEzsթC$"aQ^`[fgCz 3Kx2YP%-ت>~0C=gBq6GrnXp(~Rq!|7{E{t=U i("OUS(iN$ugv4hԮз<'is5j n`i, قcTMD)y O8Yyk)~=/Cc#!] atV1u=u*7{WuhpFc,= ` T j b?uvSw"lgff ahDD7]P,'OֱmIAa%giJE==6aV|0Q$"P*&guQDT'6@s(g`0Ff !@YubpF#Me!-bfwtw^}}R@'ࢆy" O[98RA8WXQ'Q0J];@]C*F*Nfՠ@I P M`RNPT`|xPQPc?ZdBD@A*/$9[6 k$ Qxw"%"+p5CbBnD#e#32#}sw Rv Â!NKZAQ LQ'4G @}qY){AZ9.ښʖj٭GZZE0 vW.A$r'sX=ֆ 5$A*2AkEeR u bDFTT?9Nc0D p4b*Zt@  lV4OtF1zъ*%z8K2Q2'Kn~CQzUP;Uc7%239jTYFr,eSNӠ@S:q;mѱ`i:N{vA (K-I]Ɓ'WP$3\:!Qdgy5ع[K4U'*濄Sn8:s+M_SNpmZr-a1!+د۽P6vIaIا&J抳&zB:+0~QiM˘chNQ/t edbS9i,}h P3f,`4 :ab"+,\Nۉ w׸&& ٧'K>+V#׀7PRɓ|8Z#(vQf? `b P jLZqcZYoOVl2 4yRBHuEo~)\d;A8/֩+Q}\.TahMٛ`G9d32Q mXo#_\bm֛-B Z ~ \G;[jEHE!W"uĥ[4 rr%a]QR,g>ٛ,M j79xtp #9vAऍ`",F @c$ۆ#kHAe9/ [dg 1d26ci)9WV#Dؓ=?ޚM SěfQ S X _f]&g&: ~, d2z:~%ʐ  Bm7;>"8!O KYXxc:NI䓮 {w lwpM3PqD:} _:Gc4`HsH Wm˼*0t5PM;1㜜?0f>I@O`-ʰT?Ҁf ffV~d$a_ً#.ȼ.m~  EarAHLǺ j NY1ЇS9Y ?WvvIfϱT OQ xRgs,tqXf2Bz ]O}H7m" _{qn4ƘF8mj:>-T@uE=D^c0c ,owfheoO5›*#2Opeyg;mm.jH&K;QB@YP0aA7"TPD,ހc=~ň#4hPG'NPѢ%̙b4iSL3lIwEMuI)4+gЬZv-5]FV,ث֬Y5{6gդ6YTYxoF$tC0Ʌ4<@4p(Pa%_.IΝ=1DhMka*n\80ڛ?pA(\È~ؐ|D3AKaD-BS TJiT(OB@>.'|R#C73+OҌHPv(x㝕H^ѱYVzȸ"w _4'j d+`Ub ~{]Ses\ꞧv6:/ q @{== =6mӶ2" j>3p*>{K9ۚ@!@+X%D9LSdHkAD M<|$KB2Bb1BΩ%$=L|$}8#,Ԓ0$IhC-0E4;B94 @@43x +Ђ1@~AdtA"24ĈA%F<΋DI!H::|'/ٵiE,,1 UtW㳺k )xCC3ƨ9eCedsB4 F(5FmFI/2<'3~j=fV8xGڊI|t}srlȀLcQj>E2 !7U0 e|AH!{FhHȉ0pD !@;Md8KIh ( Ƞ[KJ|l23E ( )`'!36@3`jhbbpb dMt!CS8D~cԌ|ĵD:Ri$l8ص҈.񈞤:36+cNULfDN| kCkb$S2jkm&}Aq( (R>/?:3Tm[GcHN|g{` "E?+P*`R%E.u/V2Meh3bHPd@c=Q؄H %cD8˚ZWv}[ۚ* R]yM 9  6gWT3I%R!5,.4mK/çTH&jQe%%^\R4=VSƈ)`\pD BS%!ݷ h\T][\%)s]Ƽ]E]4 Wʙu%W^)u (-+0E%%m* *+P ! ,^I$_ӺsoÇ#J|ȍŋ2jȱď CFƒ(Sdǒ]0clRem8sĹHy@qIѣEs"%IӧNmХݠ6mWfoS45殣۷pʝK⾻xޭ7nР KpP^,rKkba˓EvC2ޭdu1kd%.gEWn9ђgNUO-:֢[]0h+ڸ4mݑSʔN>~ƒ>7ZziᠧXsSKV~\^+ݴOSԥ_֮igk߿{rGMvٸA~jrbHF2^r[M6&9Bpis#a;ʡY-UHGDYJd"i(Nsb7>-zHx]$^`Ttn,l(,gY _= &.G/yGeUyL FٳH]ؔ$E)ƍ6dBO D>%DeX)a%/q=aM|>bCM1st3 Mr-a5Q:]z6DQc7N;hP}vT򆧊N١up2xAԢBH0fK4XhʡĬ(VFMDa6*Jf]Rym`ߜfҕ/^ҡ^:|w꼠Ͱ=c^U,dZ }SA ^FVh jĄ2Ece+Cntj|HCjVjk:ŬrUGK僰3cyv%KZbSWV(ՈBפXǪ֖mn͑DNRKdS1TC=ށJ_}- 1R*>jP`YS/{5u87? F5C 5 B[9R0 aǜY~)eL^58F/`-ơ1wn~EMMcG yM"s"ۘG!.,Y5az-Kjdw1f-Z \5hM[9js/9Џ& 10:ev܌f< p쒠x5 Gwcž]0UCvw[|ʚַa%7/>jk\'Ŷ l 3ь"hfo+վ Nn(#;Pr& nZ#T6W%=}/^268p3aT06F6%Zk Q3m#dVdG9r,,׍ߤCpyRqY@oXoxfwgK(x5>=ҨH>>'+׀_}xd|1ҁdhֶ8A` Xhq%FP(^V[0X KЌhV0x谈=GXH8LUdkuHyhGh0見} N`` fns/{t~yƇ… nHo6㈏"k&d ssrh1iBlpb992;=i` A)n8g/(ZT<ׂ=5 إ]ʀZgYYfYĄ>&9 ؖ5|5Sv9'x G PNZf@ Ai)GKiOB':}iصhlxfJ)lsiPYa賚kOA_pFY)ms89)Z  B鏈ٜщHEiZMQXY˧vx6wçկI׷QH>NɻOx&YڥjZyL5ȃ$K;Q[ Ղ&B5Ȑ(GK˭5sШZPgițW˱ZŘj]G۟'[7JN0v@0M1RMGä3yg[&Z忻 jU9'ma,(9HS뺄uKć5hX ÛT3̻5lÀ$༹ifkڣk ∁bɬ[3]t7YKEG4ȱĹmj#l Me S=VZ] / MZ0chw0k]Ɛ:>=ԿlsmʷӼ=lB ~Ӏ^܅mؔkhLʷ͹TTcOڽO׵ddhǀُɌWY\`Yv؆xwwPZݱΐãYM$|k pǭɻFXEu @ p p ;L,P Xޡ-Hacwg:ׁLT no8B0i̚ˡ8@^ π:@]Q KK ~hṐ D"#~)^m -^/ 68$ؼ6eOEEK+ʻ䕗N:Y*^zh&~C>UiXjN qn)^ p y}4Y8<Yb^  f  #]f[fmM#垎-g, =k΍Hz2> 0W}IpN@. m@0ˍVXu-LTH}Ç] F@q. o]>ڳ?7dfK55$ ] })$v kb^iϱ_m_@(.&^O^F摕 ok | ~Y5`j}c0K"6E]$X d-СÁX95nwЍDg"A)SK[Å1 Yf1h9uol2,b ;I$GC-  rH>qLz1tCg]|OptHaAL-֔)"Te촻; H[jIm-I97(7p8CeQvű]tH 4:aU'!M;T]ƪB+-\j2-+v?_{@0UX(ݢ +d6JO]HZiE(D۸vHq)DQ[vOxcI_|{i`j4`y`Wb!ȸW6a Y-␐RY&to%ifu+C\tgUH|O(R-69&i:^JOX`NXV&mpkm8LBd+Jnd΂;&RrhExCr&\%?Ӥ]s ]tztuA=U u[,i7[/zh0ĉ +DYɒ'hKv%.рkRa6p@,xdmBvsQy:$BN pCE}I]Î:0>= QzyXҐ[5Mw7IJH@|YtQXSGrI2EGpI{ jy+'LfϟGuMrF]Kx <&((ZѾeGali-&lڥ#PB*ҿJm$]3Ħ-aB0 2.گ A 8p2@U.zTp{q=UV1̪^JVH"zXU0#֌gmӋ <2c-=VYl,` 2OLg'/ie7QjT㍓c2Tu_ځymY5 b jX;*8,H%E[6 ^XH e-!3r! 6w&nŠD稆4f%gƾ,_ӻ^~GD|P)Z+5k_/]`;pbh{.Xmma TdRt,`F3-@|g;E$N>mJBR Y"6*T1x1X}6, E(G7P'8H縲5lLtCEtV<꼁N65SV30%kfzk_h ~<62 ;{VΦjtgp)V񶹭]og`;rC iģ!i5S cVL"NzRX&M4UW? np vrWxaژo W򭑜+ɫ9i|y|SE+Tq DD@F8M;)E?B+;_]Y>@pzYlv1+8j LP6")峭3̥\5 _W+a;Dp0c޼陥3b)=ocZ\O2[;0Y[*PHK;eN<{'KӾB>L=3SPAkA{; ;PKAD+/˛4jl*9/S=jB\yň#-ƒB k3.ԋ€$8%0*B1H;17,A8CZA:,vwp4H-'-@t $k\6.D 9Xɦx7~{2tI'\EDwzQ,336-\_+Hʄ"1;ZHvXxfpJ'(YPX[p_Δl\|a.g/b]EsߡLHۚ ڨ b 2^uɍ%CHv7a34aY%SPdR4^ PHddؓaM)l\?_$VbUs(⡍b-#ŀ\e ҵ"sYTE6 5908,)᳃K<^W">VPuEH)0OcM[Ȇp!Hᣡd!dd_'^TG%hnI(j6 FƖ gը䚘_tNgH(+KYNLg*|ygb]+_yYָ4FHha}uxhJ JpEp,f H;Yyh^AxpSMV旆i] s>g J|黈^Wug׃SԅxF^aj2889 j 1ħBɍMRmIX=, Uֱ7!6h*9xSSz _!{юM4N5hx]NL$TLzoJm;HKAqmd\JX>FPeCfuxAOxX'H\ @g~jճ2LM*PCQBƕ?_ʮESegDE̱oV+1"M>'8 b֍i(0́ N/:r1 ҝC;xsdd ^pS+q9\q_LqU8.`-[3+"/0T܅cFB.K-R8Gp/(HU@jȆu1*Uг]K ;KHq{I>HFj\CD&0-˜W(K[S~. ]~L$#u',H)+Զr4pUC7:>v;d<&q|4 vJ߶@tEos?wG_LWy x yڥt\ݥµ٦à |oKUeUuc4$9/7. TiUG9f~IISVPcp:oWOElzy],tCM,8 rϞtgzީ /Ou SތL D抏,[a ])9k@vҫ.&&N5wqy_|{N Gݥy-K8WJխM_3RT'GӂhjX՚v5+,?~c&xΜEs+fH"FYYkiM#̘2c̜:wР.(R]v!hRUj1^N:K_Q% ٱj}r(@Ç%P/ |C4Dx dhPBō,TXР!.=h&B5|$IvܹdԦolZ3_䔙ƈT,:&z7Z|R/[yLyRb-bϪyQp3'5:hRG-QN" UDMU2 *#V\WQ%UYgV[?]rم^hba)-2Xx@ P ,Yg5&Zd k5TfmQf"b*$!Ip "A#a%W]vGށd$b<wJ{3gS}(0sͣBY Q*5 Ra0\ !WzXcǰ\!xW^%xka+؋1X2&ezpXi8h  YLbDHH)wtyG>ZHN&!Y' EtNGv(4d$D7R /8 0ET4{Jhjv#n̂W ⲍZ@=Pj#:vg>&쳬 yIK,Up} .Tp *pe@*0BH^Cr2opfQ I%r'7#\F ;8~FY0 26Er&S̪h2!1:s_6̫waYќi`5)d6YeMbJ|SPAFw)d@`C l'xʛw{ORM4A%Nh/8'8qQ.@u rB Cfi`:Hh{2,1\p dˑ7%lTrf[-WK %gp,08 @%f& gT@B":),,hy+Gƒw'i]K"@-+T EЁV . OBI@" \lF!jI'EN O.Qs:#B+Us8]u1+%"8Zo6a5#I4J,+;P nÓH ƴn8Y({Z%ώo.\3=, rV41Q#$r /*g`#X r-Gnw+qd .p|wS+X[0DĢ(Zs@/o~Ѯ%o5bjt誣 0 )F^[[ OuhH` a}OD _(A 4_=_  P܁)4V=}F.U9^H_1@E!Yރ)_ZE% )N@1Zi vWTF) Am$5!A <@  @ݹm&T2uFԃ9(NPEbͥ́ xEE%-9IEj1pqP_a}Pƈ{a. ]_( Twc8Kw95&bl;:" C0L/xa]DnZ8Ubj.NԌ0 lTnjTӮ*pZnN(mU6DqTSXJ[,YUtJRFL.͘pgBm O@@jVTRpiŎ m.A3(>$q(*~ku^6.T p b`aYʆB\!J$JÞq@Oҕ|6P,(b II'z@#"=Ts5#$ i3eHX^(f)r[4*#R +_W۲USl !V m!W\,Ġ qփ5_3f4$bñS8VY.9;ZpBCYC*c;;EejH=7=sJ\lv4%kFCWHt0$2C#"G?rFcsx$DZDĘt)PJ/PKn'40&% ۸Dt3C\#lGc3VSI[B9!.^?|T*Ms`Ų 31c+`?gF^MF.z#_ikYU;4!2szXFZlVm?mov p_ޒ`/w<7G@tO"ЈeXds e]v n*{k{7.XSݷQm۵y5@C\Z*aaƳ,;8 CsxT @ 0iA"](g3t8zBttjsmNjDZsHǖ*g`܎o_xdq  e^9? qx5Z) Yh @n9z4l o5˛ĝ'x7hGOE2JaK,&"?5OzWz аR.p8,ikQ@c{ ^048|h|/HӺ643;4*XBBi_G%(lx˚5 S=eN3|Cy?ȏ/쵱PMe}D< 8I/g7tD;x29HC |h}Vbufu 7|@GD\W׏1#&LKOxɟ0d}$ƹ|C_4$$p A #XzDaQ=nGw$o{++z 㳇_0ԟf~%>L*FD0 TQbĈ>iI,Z8cIU0i#UydZTh+9s3iִygNq;ZP׈- ibKAKS¨R]ZVЄvZ l# kٮ-PAsQԭ[n^,X/k(X @7vĉ (|8?YE4A4JN;`5 05Jbt(d073񄕓A.":|pऩn.;ƻ,wJҚ>棯+",j @ !0P@L1]- 0 !2P 90BTJ<1rgcTF1z! wMA'|F8a|.NʙJRK.jl/R3=6r8pu Xq0Ce_E%4҅pC . mD0 (TfsQ}xb *ʘb26aKVes'hy6j} 5u+׾rӱ:5P=+0yC,߬#,R``=C>P3O>~.M!,Hhhx T~myek6(9(vڞ2j*:(r<\ObA/NAu_l~8 .6/em h`]O7"&5# $HÍ~X>-hd}jM ,QL*Bc&tYkXQH‹Z1 Alj4YX\%4W>_ 9l*ӭ n$Eo ;eX)~Tdr`)XAt@ #>c>nRZV~OJh=^-X:[0kmeb;AJchxeH ``$8j</sـ\*EU!~w7`*DIAfמ#Fz_WJ%T /t(p~PlW"Y}"~&rKl:W%T u+LE t^ TB| {qG\Un&UVo)ݲʗ{ksN8pϡlSnA$BQ^1|;'l*3tIhR/ vW0Ӱ=~Wלrf|ʐz+C۹UgխʗYZR|pzf\ҜvР{{lы^ 2yЋWn fh)Tf&;\+tO+P./b8@Of̤O@xFn/t.D̦4Bԏ,cn@"Pf" # 0 F DO N . n&`/$PMKn1pM#*CpbL"@hP4O^0b6PvNݺklxLT*-P: 2&\ Cg ,ढ<( j@6k`PBF$Yp @Fњ/w亏^,^j/x.U\(n8DU@.M gWl\_ ʞJD'dgn1)rQr :Ɗ͈/o3N 1eHCˏ"j E$1ѯ -@皧T 6z2:>Q6 *"`^gb iJ!;*"w1'7y6g ~#30FNR* 0Ur0#e3۞ll-;6@0s=?ި 6-` :!A7R??gӫj/gt7R 8}A3DRJN 9 QB:KtDtDuP2.20bEC`!6j8b4 @xa)@!H+(7_)szQ+t͂s.NS3`P: 4gx,K/tھ iCLgg1oM#S62 #EUm`z`tcr3*OfRѽ2R^ӐV_O&]6I6J`6E `0RL}WT^%#o=)p V O +<7??+['dtISA2o3j,R@ܕgv!x138@irOj74N7Tzk1nOʶ!>RuJnnnǕe#`OPf3f2Z3Kqq3{Vΐr^1-Hz?so!8FuҴ v g{u[wtn7e-ꐥE0l5Bȁ_p8= CRv7srv@،G mұb<4Xx`4`s}}. bb3-,ϲwXNv+WWL >p-X^D$֥y_*hp/$twa'u(x{I@j |.(y2zkyg3SeOk}TAzXxbh"=>E>f'(+nƒ180Bx9gSXqQFhVxa"mYj6Ux|YDtoCo.ᾲbY#)YwYù;2^1Soŝ+NmpuF2ٕ[UzfփTM@vWs֘C2 )OmIp:†ehr8ZوB˴LDKm'vUƥOќ5/k]q:jx7_*gK(_١P9'(GLLpi.W&͑3XEz^q'I"-Yz[}aTY=ڭ9'ZM~N6[Zi@ 4_B&Cg苑U BâreB/zxUXo:ƚ}g( u3%N)B 2cg.xqRа{ٍ{%Qu͑DV;'ٙPMBP}b6R`:#B"< 7: (JuCG[B~S".^4Rk$C]!ݐ\n%e6.&M{9fbܼ譾ٲSJsEe{%}se"@q "4hPb+R$ر}}zȑxC *J| 3L^2g<"Ã0pA@fpp@$IN CvUd9h7X|!\.70ܹv?K7˵lo4 Ŋ:~ L{qEDfch͡Ѥ9T-jk~ {5fxD3j#Ȫ"MvږW23zO?9dN.db'Wu+#~J,`e^5aOW [9`b1Y%CM4DZp63Yck kņblЇ 8DF4G!g\JѱL(L9-Kx^S)]' uR?ـ%DEUUW {eɁ`6Xa7 Vf 9^[ ҁlA `^ATepBf\2K5܆i7ߤcxڇ&c4c#F8#܎^w$E&Y=5$DiuMf A9 Uy硧zWd xU"u q5x'sf&,{`=И]Ud!z5=aIPl VjЈ3☪y=\sxzld.MUg@T* ?=0eWa1+o4le$~DnGٕ.hVe' ":AHg8nvڑP&J 3yU$ˁL`t}< %Q,׼MR%7usֳ=?+C;G]st&sfY@ /X[M()ш-H- Mx=K0<"74}!Tjz?÷*PVId8fl3R3q:ҝ/MNr%\2JNj; g O! "T`@vɐSiB7o8<"`bbgDOK;ױǝ*\2\T@?`2p%1l­´DZCB Q@OvhB ꍅ;Xb8Hv*spE PԻ b\S&l SLsC)R\Ph GKыH Z%Ry\nR*p拋2`?PSN(%.B#Q0!}5f1Y  YARS ۔VA3IU[YG׭hTRB' RPOYX_9pbU$́l#tt]T]JPSvOBg|EjZtdF0BMVS = o$Y2`$}㞢(r6CZNNE&$(뱸l%4~5(zC'a {" f4g~Vӛ0EԇA'yS0Q>1%}p4oRH#JmQxpnkbX%.AA 򅪐f{tl/"7NqNf+tszH{f%(8:b&jbPd͘~PuuV%Q? o-up1~Q1PSSJXq qlP<8[ Gh| f!l<12RX1X}Xi,&TDx8'neqc2cި)2?cJqKQ^VP6cBuq-ѸEMR"kd " <(Guvi|p NPl!|"Uh"%8}_ҏؒ2ƈJ4!@!,L5YcL‚2@2CL-$JFP”DҨRiyFA2fp wbNdyTޤ l9Uͳalw#}x#\1G&1lkя5FvҒ3nɈUiP$U@FPS lLV3FWjm"{]tlMTU p f0c) fyTD Ɩf0c!y!r˙w.C#} ƥIkL~yC*'~(v d6e-F'zb4-RAvXk%%6 c^k t<ZGUB9[PcP"аl'wVU=(O9 p%94.xG:^9Z$8iޘ+C@ZGdka-}&C$&orK5Y%-+ aN``0|$c$ PNv0vӺc)("r BcǘJ{9ER@5@4mz>h:&#EF*-eRxD)z-\4g:a2(E  qMЦM@4 gY) J-w҃7z{S\:GSnJFR%0ZnRLH@*gCJ$X)z@"+-+q+%fvbӦf` ngf0q`h09!v|V"!B;>E+,Jȏ,垫s(n?>ƍ?:8d!X1Mm-Z*[*q/Ր*Dz{f fp vb`2 3K #= qҳ#z7Љ1Cے@4PR8HՍet{wJͱV-(hE7s+fPvg9f kp'5R&zf[Nm&_ ;0;-(;5`gl!`Yv6H5Np!ObkZZ0`Lh/qb!?LJ/(:Bؘ,7YW:Mr@f@Ŧ-nAY@uPm - p<Т/<==Hȅ*I ~Ì dqzF(z*">zHe(A3QBH8:i $CIJ-B>E^H O>q ǰv* ÀZ7`Αb[h~O뙖|xz~jW+*QsxFMiğŶwmWMr@ `l5o/rBs<0="|815&ia)Ȯ5i5AS,'F0q\g#Fq㎮#MCnM0Ab{r b`Ԑ ZjY',T"|8-~^AXHEPX+Jh"3 ݌B- 2$ /XپM% )OhTЋhuUDf,Zd1RJh]~4H'9% R(2r'ի} `i9 Zȅ}̑w')?!I_ 1?9C,Ȟ,'1jɔ*Ή[(_a'5!XјQB@?a ,F@~\F=zD!PC%O#SS&YĘ!)s|GXKvhbӬZĞAMj4U]zVѲj5d+FXZm۶uxPUWF>@|@p|A8|;=HxhҥM_FsaOC<0 4A 8F;A ܈Əի\R 5|4y唘4AŌ%[S*DfWZbPv*@kBK[z++ R+A4 1c̱ @Q4ʹ 4Ks3 F Mj 7K@8(2N;D! <ΫI BؓFi @* fQK*p0+ +ZЬՂ䢰 gE$1  4 55FelJ yLMfMĤI'$ e%8`F-YfD3ώ1ư7O,;3O@=+-B!^f &xHijQ,FXnF59! ro]s'FذIh7P6|K&YaU$V¥&19  NhT@UF4Ђ%HwTSK0!+^)h! ׈ мEcHt#ٯ5sg8SDr@)opH pl8Tg,Yu,U%)A ~  39J ;\@F-8N̢U٠QESܡkPkY5X1@7ސ$"ҡz& c MpE<$Rzà8{yR#@2r "C>e)`IF)afwb ЖR4hPi $)IsT6OP)ƀTrhKQn(D-9́s ITP /EM"U2hNs< XȑI8hF‚`rP$)yO哅P?eh ʳb CBz$v,nނ3 b#G'U";o8OI!I8X.L2r<@pZ3=j?k Nh(NE$)RRNlj&UC L({MF1[AJ!*{ ,?vַᒉMd0:Gvd8J2_vYPD̏9 Z< I*gOЎV5-STMpkAPl=ny[hs#Hr{'Hch@LQ ]j}$)9*&G* c:IUBsRDX} g\HIA /6V[{soh6sJeNbX$V's1e cNeTF5"$'j{5Vʡ &PL]p{u_K:RrXA2ׁx8Uϸ~coNofxdv+A233&4#;#TSx5D+^ IcR/yYN eɀenuI!A<% +n=B%l0A$[mk&6CzKx;thzdIIjeZq \BM`-]^ (.Jv/ 5ZcB{Jr :=cDndv FCd~^mb`X608EX:V?Lh5\9(Kb#8=a&+63 6lQs>!Ѫ껾c 5%H B,}S?ZG,+Z, [.J$MX,%溺+4=t<{ŵX (@njDk z$bBeȊddHL=/xCMʤӼ2JPA\8]*+?«ɿ<#=)3VQ;X4*5)eq;ػ8>N L{77BT:tMC;=٫"8?K8XԛE EL)aLADȔc \Dx9aH9dkPeP O ) zSO-D-`+\] 46.%ݍL,Ԏ@'5#^! QB 9*%4LG0&,C>@j(E*ULYMӫCs7!( YO<<~Mam(K#0IOl Ls$TPJKU@WMUV `y,R1@hsJjPX兀X5Se([@؄ݏPDHN"ሷءSͤSֱB;@UACJvTQ$P:,. {M…ojUZZe腭ڮeNh8 ۲-[Dk+HA0wˏM &uL ,Lme(?&L X}u+}N3HZMф4ؕ]XILsM)b[r[mO4ݺр Rk(M\I8O3”>H\ !ŀA}Lʋ*hN]eZ^͆=H}Ԣl@{S^eݪ`% `!pŰ&aS 9ېfa-Qa^-U%0 ƭr"P-(-*+,bF*bFs$ ! ,^I$\ӺsoÇ#Jŋ.jȱƉ?v\GrH\ɲubT9Ɋ۶y:#}IQfVZӧP̹R ؀iK߾ʪ]v,ڷpQݻh³亻Kx"F1ߢ#WDIpmrjޜF/73ց \Jm\6aU]qLv i;/)_μyɧ5r>%uNi &l_Ϟݏy-pumwܼNSh`mv}Tl2Yr?ᆆUGDprUL'xřFx`3hShv/Vy@PLCBsUdaFYK840˖br-.G%_9h/XԉLc3)̀w%{y圀Ω Ec^`*WsVb-]9&奕ܞun/,%%-Egfs&smߕvjg9髜ΉR,$qw\`8E%c׎9{2Wi̕~gʚf3Qf'+/*M5rx%5l'I50{,z +n~;p9Ăo*Ѻw˳鱮,M̷,u3J$?]%ai-in0g_׹jӜߢ;(ڹVMAѧ*I+(bXPdYM/ub-v0Y&]y׳lޔy:XܤיݺL,}41~898ƍ;Qc;yK3C3wg~i-<:75㧯>9#.~5`J;-k:Ї/<󞧫m|gPftM\3k} ,x+/w (E:{#La k@ ։z'{Xu]l9+띣̢Xϡz5֧E5Om5F6|vBp+c4\|iPG9Wa2 b␿ b=(rcI|sLQ]U]@pqЋhǘ2r%l:ꅑt쑏}#8U0 1%%e쒲dװIFTLY:i2~H+ʼ(q3lG-o >a _ @F632c(P5j)lv&*f+teZِ{}/|!#>I Z ^IŪ9,GN &g.M9NQ6*d7ʇN`(*6^%F8<} UH%?\)K]zbt.S%J׺Ւ+n&T nh-",CT|Ӏb\UYutѯdmjj &[a=}]l7v1Ws;eIuDpJڼ1VgT&f< wZ gC+Zu(e.tVD5㩚7[ն)6͛{X*M>U+ 8h!< [%ah:WX$PLU`hVhTV#:׉'jzoMi@׆c pjXvEgW0jvQWjFXiz7k6D' 8{F`UHwPh8Ӏ~j7}(Fɀ":8ygvиnW5Iؘ("gdp3h8]xEO 6C玪(  =qxo/<}P+8 );xl09ȑ6#)s%).pȵ7ʰA'pbqhju Ѕ/jȋw8W'EipVLxX *JЄh9rp ud薠txdYfk4T8W}dhs֐y>ba%XƗ qfDIyezӔȘp0 @0ɑlmpJו o_xh0s!陞iЁFq^tPKʜ )HhX\rG p/Xj骟pCTpt?gyȧY8;8 計t'zך Njz` %Hsǝ*\HɣZ\wqJpu1 =*;ɕ] 柲yJW7ejp 'yY$Pp蚮%p9J{a{u⹰릶xzi+k}~ukg>kI;۱i?giGT:媲+h{nO{x z Y'FZK& ՕK`{6ttxVKkڠD8Z ˥?Np;wOX%ZK툱ut頷;i[h۴t7E z}$:y " %[$P嚺Yʺh[%P?O P{ P *:J˯Lx+1ֽCG~=D.)\i~:}tLExb#?d+[ h#0-KK< Ġ%lMji}뷄Y;Ko 6ȃWf@DjzeIc a/\&[N;?CLGLˍMX(Őgk {e )CIƾTLETj !?xǃ˃[ qlv MrƆ: ˺K%@7{bďTKʌu(ʛe ze\l)h1@Ζ<K쨭9ͅep˸: k':\~>l%04H0TU0qNl^I*̦+^ &릁kGjD;hzGϖaHLԷ_V t7m۾Z m7жS@dc0K \diiuC똤=` 柑x vk$['χΊ\MԶL T0=?8l7>9.\ivLCLeTjP>| 0~\ы Kn$N;mɚǽZk&  -LwM >L0z1n @ځރN<^m[ Nkn퉊>| iYꑋ:{Zl˕^a S_ W O#MK@c|^Џ %_~)+@ /* ۞;A&[؁Z)͹  .R? V\_aH`m:m+{ߥ9PT@Vc p} *T[]$Zoslʐo3~` sa / @_{2[<<"<A 9ȑD(Lܲ%NxQ2 $d2(dٲ5B|yZ:8ͥCO? %Z=IcSAN ]Nqln+טa%SVd1i&IJ&*QWyk_UK0`jL @ lg `B9Q*-6jǹgd+֥M&{ܚ5'OUe~ҥLYufWk`ے5;ya*cR܎ ;x- h@"4AK0"Tk+2h1 ^z)7M,s:k:r4G$"ƬxKfr#/S @h ƒ&0Shp xA ?(֢5ضNneIJn|=wѩwdd*隲J9Ҡi.<#C-R.@'rʼtU۲KJ( \3 vM6<dK;_B10ϴJ Ř-QH- Hq*K1UΜNkQDk$%y[KktKUY6+CKĸ P[h`X`AW^7Mς @:4؀dT *Rي<?|Zlwۘ cT\|騤$sMn]us:zT+ O[+)v Oa/~od-Ƹ3a9nt3dPp*Yhᥘ&CoStF{jh"**fjv5EzL)<$/`48K-9Zmm`raF nL (ApN%yrBQυh?O H_tFP_B})êV L0)Тo1Y؀+$oy˃  4 Vf2Ӝ{1 C_>%8G?p9S >WU9鉢ajD1A 4A/en$Fفq8l˿@(pL$(< zɂS XAJ̃JPBs&@XBTBvl6T"T#`4 P|z)C "G 5sD1e0HE$}Ilkv9 @qmLI1 6F`2xy0Đ¹i X )Hꑆo5-8r"Q"w Ð KrÝ݃CQ)M9@9T+R) OP%44Hb2zwȅZ 4$;!0আ +:!+:_VquCZ„-Ulc~IJ>伍fb0fCXIs)ްyGbT-ơcxSȘyC* ՛Z$z@$Xw: [؛KB)bѩ}yuSH1 1;>$!ۋ(9c'4L?$klr:٣K*{3#ӛѲ;11 =Qaؓr< 6S#{v`m؆X0.HHȀ;೗ A+AÝ|{GCҒ$NBz-Ax&&DReQu@E,x2U7L $cP`ScMF>@NE4B=ڕ't TY-;d ɓ=NxbpBۚ7(RBU_HCH0^BҰ|T̓' H<bxzHV ",z+CÅal$k΂ۺ`P_bfHR+eխan&\]`+y4;%]> *'6hx8,ɰH1kHaE^2!W-vTi$fk3 O Vu`W I,0Cx" 1y⥮*͖ϮHNӾƙXm@v׶`kCDV\޶8nֿ&p18RZvSXN(Zl.9jod$yjW9GbFLo]o $ V\ :ltmfphh/n-Ru&EH)@p9<%" jqqht qgrrA5BrI$z\ўFUWPvJȤ`pkKpf2du ADb*@#+qth5sE$qAt˒_\t!tgDUt;2afa"=x* T~'p1 iE[E.m_X=v|g\^uΑ`sb7udeuK CO@4DvkooHmc]rU[5*ˌcQY'Bc#nЕ($wVm>n;OJChz1)x(Vg" ppx(/I;MKhgvKhkotlw"H]H@5ٻitу0(xr/p)1x:Ȝf {7{#Y{\ afrVĤXT^F t=s| w/)~'pU `xЇ8+[0G}8uMW?]hѮ]SfPBa2ԕ+~QXEb;G_0AʔD2gҬYΜ:?p=@C JP3PϩUr* JQǓN2,@S٠Eb(9(4BӨ!Z0K@{ tBvp1}łMᆣ8pr&2 Jr޹c`XSr(x1ֶ̺#&:fe#*fb۬AkVoϯsH М9F n DzRwF)<nM'$?@BZ~ nCzCC$VgʌH}E$^yn,SaZ&Jyq'KӍȮ/4&Kʘ8`<QWod/x ̒DŀЯ^6P[kOc|pp';㙷xGT>A'եL:h`&>׫ jR ph?qO}iI3Ud? ' H@A@b/{^Qũi|qU_y̶]ީELU_s 4@qupE-{xTD˴WԑMdi@  ` @$B- óAAALY^1 `m)`hLݝ,!_׷MLPt( i9ɁJx\锠W$tZiA_L )eY]E!X݋8!L H!%CUa\aNJPh >@ D}_ `x|LNMR8Rl fC6 fM md\E0V%Nc&&DX`'"d(&ޓ0Tb{  P }.!`f0R11@$2V(a..4#M+M#%HT7>0q T>:& ~ bɣ=SܣjD?ȋ $ASbdU00CREDN$EZ5b$Y0~˄Hj|K`ut嵣{#ЈN=ND(-ce@d(#10S`0U$RtLq`r&z7ؒK"Qve|2}E%L]5U,O⼐Pa&%5,&c`0TdNDjDG\dh"Stg~bXhjRZKK&v%V4u`C "aZapS2DȍJd.g>F\VtD}uj(fHBHx_ԉQU4N Pf!$ofݧACjj\$" j,&N WrM0_b @:hyԆFK(։Nф md}-ҟJ mfo&#R*k4 $Bb(F(HeHfBUǑ&.)Iu[P]V fKɥ<'^ޣ$ AfeFrF }*#44 BĜTY 48sz.&4Xh Tg~!\"$Uxx&K!\"@9Mjt\- dHe'febF D٬N^WjFY+D "j*j͸G PΌ`dhQ*a1PX+[z;V6!kܫ嫾O+*EM58pΌfrD j-hBĒba`\NPkR94؂h!alcfqp(ĐξW,ĭKЎrDl^!7sܒXAlj'I՚vJJ;~,xi*[  ¬% ('؂4Pehfԭl*ۈƛiPmnz",yF2崌PXJk0@Vқ"UXhMXNZb(5L̆ځB.Vb`O1/ȉ*@.0o-(6Z jdroQ|o4r.viz,IK0Uc"%l2=P"4LCۆ"܁ȁ,k +% 0p' }l,iH-Y6R&*R@bH] nH`TN픭0H-CY'ɦ/ jW'ݧbk,hj'kpy?1S.±ql0<@xgE1O|aN9//V r$[SF'd57At*kb`b'{2ͦt5r*۲2rb+q[/..\`u}1eU3!b_p^5_0%P @49})ܐ%U4 =+YRqZ>Ņ֕A0Z1L)su׍UE ֬`LE=ot4I 8%.L7 .ݍ+(!Q*.*} u/ QpZbIFQS^P@!y*b^ Ww;zUd袢@@}ý'w׽==[޳Waļ ^Ix k0(cv).[xwW`E#/SЄv-@9>Ii|ߤ~ 1>~8qF+ aB *$ZBa Ah cF,XȐVMjƒ+4f̘%>pD*XN7n!B4 jTSV hCΘ T)SΛ7ՙM IMpw*TC J4ʗ/W(#G#8I1v\R5#1E@i׎ A MX7#1"Re ^&M8u 4NDVٷwVE{ڵmz# ڋl `j! %1*1%,ˬ9K& ;d=b5p!l-x-Ljk8ㄴ&99VK)ri 4n/T <{2>)|J JZpA09! 0 CBL3" Udj`(!p͜Ro"PwThr#Ȣ"<%Ob'eb!̈JK-O?d:O1|bAl?b\Np>/tRF"IkR`5ZEkH5TQu\uWyHYC[q'kd"JXi|+hb<橤]0ve&w=&]]j̰c^ !M4@N!aI{zq֢j2֘cyavf嘙Zȝ +M  OtWhVzi*Bu^G,THk܃"ENjeG!Jm;א[7玞"UHoxR ӧ \p8tڈ0qT3Jv9GAIÈibIo|<giK'-XJ#yU@Z]LSK6[6AD  /`^rgXE2S]"dy5y6:T2I79ҡĘ9Ĥ٢gN"I~+*8YN~Okعx(9pq c4F'`R[h-n k`QV(D&n $UUy#Ie(<= '&KT&gh9@zx3dP>R]₴X sBe%d!j(F\?U=~Fk{Yȭ}\c7Bi|x"Tb;9OfRS b㿝o<⊬uʖKբOv`P1lEn3}fKvsNXYk ܳ] ܢ88\M X Ĵ%qsiqt2eW՝x71x 8 -A'"YW sy=CZ*k*#kZK'  DyA%-h0uj۝gL0|Ҕ1VFҥȶJ7R;ײhV0˹B/$md&Swuhܦ^ӮDk$SqHBiո*E ܡ|q\d `TA3&aeZR:(E9V|kH[,}@رcB!Ms 5BB/#ƈ9NNSƲcY{Y:`Vpn tqW8x4ℜф({v,w:h3n3ck4?yU C8U;MWzϛHfR[{`< ;@/xg VSe#,z/`~k^6MsՖ!}n*L!vw} =E/kڤP*.k3Xxa9$쓬 y.~~ΝaI"Uj WÆ*m4Rx˗F|4yV˰>1//?%ϙqͲ8d?8lX^fuڤȉpςĥMsfD N OESgBކ7϶/޺zO{ $" O*@`% w2@Nr? 0|Hn܎qP@Wn?Bǀ4XFPngJ$j쭘Kz?^#c ,@`0,2~M-8҃j+r·n YZ1ɥĐk>c.H#4#F$$O?adi { #Ok,N@0pᶂl ЇC<ϞnnVlKn>QvqzqS6x/QK%U2V#xbmn*jHd!L\E"]Fב3͈('ﯸeٌJPl(kD Q P!x,*mQ7 xd#VS#OϲVVlA$G|Th^&'%T`p /dDRY(b(ːF -jl@!R +,9jm,VLi##W-JZrB$1./ snK2,T11 q٪kgzO3ū0UD &!lFSbgsLC'r:nr` ~V4I,s(L-ДRt#@SGë023@ 48>I'bi#NnJa0KA{pA{$gnr?F9Vє/0= 2wtH*1OuFO"Y*"bДrQ\bPg̳4^QSE4RbvT\=GT"JuKtUq[^5z0Nt_qr # 4C'Xc,q$ZdXYƺ6}TfeU71-r5<]Ud'q2hwD@|&sPJGO<ƒde3cEeDduGId+Ϧ! vf"f6]=f"tV˸4^1h%[.U@~5<`i2_` K+tO( 22^Bь)o_(4Pn\n]zlf]VcRKwpł&`E(qW&113(qW+Msڔu r FM$cZow0z `HBF &mB0(wMMwixMo|y699KPN~Ur'j# t끫ڗ^M}IwpH&l.j1~SQOHRz3xlGwao!5v7? w&-q4wW3n0`jK؄qukZ_bX;fmj&IŠ z[/B?y\k5JqgQN-*h6MNW+8:4`h+2x[;d3K(}ۗ&N+Cs | laJ yrKa!6xIjhpxPy;15+Op.~6sIX~ PYn8ώrL73ehs=G_t$<` @GH/"oqKw#7ih!x'8c꼹/i5Ne&ey$@b0< .KW1U+8 `MAl~M`B hWu'*jJUW&&xV/ wBfetEdYw-(VPh1#%8_ :!$ onިxsSCv:Blb¬1 3XW&T@t1(5z`…˗8C`İK8h ~3;\njĠmS8ndU7Ñmƫ<q}5TҴ,&Zjl _"$NT-\&( MzeFme_[CXR,E:mgD?Λ6U"xKy)$y̚e"/>;yEfBD0J[^8{$}Tv|iābyVZ̠~z:q.ev35;O% jP9O6];XW8Xv&˺8@Rb^؛"4_5C7¾V}^% @jd|@YO&Z*U>=O [yJڹۢ+6$|_d8o%!A3F*U7X9\}%d@K|Ō;F *C\c9ѤGxʆ@-2!krᒶn)\f id+iPaP}{"J7eQRo }/qBdGlI{fb墫4 bFN,PB~Gг1' V~訛W|]g.;S "Z_ۧ5/vZ:GUeAש 8%i mAD2FTEL#YQV0p@j۶%DzӺr++kyӍR7`G m9_ V~K4uƧEYT+o~j zm!.ZL=i W$5sĪJ7Gl)/gSUMfZS7CUNut6𒵂H/NBp5QxF5΁w8{0*ZlA< ^ȣ/by28@=X*@ŠҽLx#F׷FiU>M*Vi(7$"qS[kDiY;"r33\հ95tT#R)-91t Ոadh2Z s5O0LVD, qoxKFČ|2HY9rPź/S wyiG2vio%xtF? ٨*4 7Moc@vC9j5 ރT{=1d1CX47<ɩUD e69ʈx`C!*5"FUJd^rttA*ӓt\Fҧ{Y 3SpaOGkڡ <7NoK#F"}lbQBrkZԶ6O8ά$ %=9="6]u=WLIURhE[ *6v=Ȃ,1Vz=}Z-Dfhqj<da-, E: 2t|AoW*wd sN!31|AOϔYWhD4T&4A ZÏ;AbKX'4_N(QYkj[Xk1O m U8φpͻl g9~3qқ"ч;,cc“rAƉtS5N,J-eT+RՐDa]e#1reh [+@BBz-glb/ۉVjj# ]5lS`m\n^J*!hG;q 3Ό ND9C5\Ջ)hZآ xi"^6 SsƯt""h݊nbh%ɰ(VVSK[ sh^Ӝ>Pux} >z~MϬzc׋ +|e7N9孱klFv?cLlNdI)bG?(&Oy>]| ZZe2Ýz¾`eP]Z/uBap{a'vdpkc=&LSlgrGzq2"vQqTXlwRXĴ0dTS!yws,~'b G@Tv YiPzB_Rvj&HU5xא^ vf-WVg|f%31lumvx$K&Xmv,sd]fs+rKܷR,A@N;iJAN@N_@Tfgd}!Zeeom5e  ]x ׀v)aPg#m%$1Fg$ua8 (hbH!mXa>Yy p .MN5X72XUbU^hk hvQ!XBQe8+m3lGA!>">)~_xxRv"sݘO'=h` PT@ P @ f =L_RBBjWxB{{pQzK"FAbm$苃w.Q~VXEk<$#)Pب2)5كbFk?i..d4H<}`hx(w(0[ɕTbiD–}*9$aF1!77x}E%bїVϑ+'1i؈YV b셈ZoLE{YpVI0QlV?gTryٚqE #gRP)mgux6lfs'R+Be}JB>1ިihGi ^bX  5YpbxU (vw'0!28YMڤY8?ly=sJ3men "țĠEMFL Lz\*3<K"2\'Ź[V-S2ixrYwJܰ6ŬةJaK_ `iy|ƍ ut+ǠXpL·kx :| 7 3L5l7,x|+,w0X9.WcZU+qI칦*/:N\\ <SŪ >Ng+]vpGPy;nIӭjv<̵ǂ+BR0+mkͅLMȓ{/(#hإ]|3 ٹ6% @!rw7cϬ<7#ITrЇ OZ(Ƌ41[Y隕$GD #M%D~Wʿ(@$1C>WtL6&?_{,*̻_,B%Tˇ Ue$I_} cm6$–(Y!҄AkQ*26́OϡSP }e%{ܩ؝(!ѩgCHuH UƲ"M &ӫڬ cldpќ"t۲h*}!v$*V\xv5D6+XMDؽHWM(^`jmُe } X6.voW=8!iW"]m&F3WbH>35CLѺCDY1}Ԭ ]W)cP P)Y- 6~8a-.Ǘ qGv*LN>)P^PT)+x }a0}K5]]bc[LwqލJ(nNr İقn`YӎWڍ<@ 2[_',~vW֩Z)nfm67τVEc}iط~؁xzEr.4,z{d_r@n_.Iھnz3.08îg)3D<_IMLkLTvە"\S>`{_YQfe_#O%(+_"m> K*`)8??<^f諾M|NRWYϡ_b/Q}H. 8CqoO'驭bp O_Wb"Q0Zp(:$y8#ɪb_@UmOA>u@oدŊWŶkվ W D 1/<.!̰ÄQ,F2͈ fjMJ`kH,hhC6JŔTj @t8J:9CA:f t/<%:Ϩ8Xs>KA, ӿhSp&B9-0 1 m84A`(ECg@I*m4/+풬4JX@(NNS2+&4IM+!.j,jjKۋ?l0ل3Y朳N;?$ ==OQ (PoE*\| GeHR))%nSTɪx#*?4U)Izz$JZu-\lk>.A2-5UkcMji7.2og ]U5J!`~]Nsλ]E9JB?an`AY΀mb[5#<{dbA,2 "멫&S# *mR.GpJ$6 [K -(i%$ UBfA=qm"@+"oZl 3 .$I.RфI O.\q(ll$*)%cj(ʯE W3+AӢh2-4BB: &"JAY VDa_KHE!F%L:k!@?YْMRQDE~Aj \U~9 ں βx'(?Eb0qAhWx!?eRPSNv{}y`Ω :[Ob(qV84@'‰0kIOO*z^";oא35Ks_0bgN#.S|S.H=Uw߭V/ a(&oyGlk\H8pyHm-T\-SWf3^==/+*$T>ĸ[ |`*.-Ȅ#LAQUe J2"tЅ.F :י+>2!<{.5cxB GAZ%P |cQ9ܞoi kOԡ򨪼[b2Xgռ 0YbB : ^$ν@̱ eɀvmb^܂׾-6s '.$h6@JhG p=:m(!ce,ey ʖK(|% \\,P8ױl#q?@LQ= kCyeNgKD#sx@i)FP݂2 l-2DvtO!۫rهx_#HNBT^ju[C#nl7{2ͯMVS#ŋE,"$EG(!aAz!RM,C{UxpW@UqxF5ڦg6w v'$ru d,F :8|<pq .O˖hf ?}lsW{hYh* $ȟLp!$p?+ \ + |h&1@0 +("dt܂-*%(! ,^I$\ӺsoÇ!ǏEn2jǏ#IȐm֭K/t:ܭ۹˟@ y3C nӧvFZTjz괯XlسX]˶iۦ1ӄ6i LÈǸqcŐ g|v#fngBX}M(ϰ?_|ymrۢK\okfʉDE9<8kw3P@ٹI<[<҅a/3{YŪ_ϟwhY5[7MKx75Jh]_ ucHG]uׅ(v$[;1UPZ;Wh=g-b7wkhVVha BRde WNE8a3:v[YvuWUyc[VzOT3օeb|UN5]wc:Z*IFHSchA c|^QJd%*$Ze-Eع̷*Y-m ]"w㗫d*j& y2'5ʍ&k<l9:n\cƕNǺ$o[m$LWŢI7THH3:[>@Ѥ>XΠnv6,Ѣ`0;hl;[p t{*[vVWͰI5Th fs 91׸"Ү.Tԋ^}0U56׿;67q1B֭w^} +@;389/ZYv+x^”:+i髻m˾u箭ǣ~ vxZ <9kΦPأ[X11tUfY}0$B 2fBM[3r#@!pdk5>0/8Tь`.>ٞ>F4u(O:uSF2Lho39fűX9Zl0Bqc"Y cn›'9I%*hEW :a'H\͊j%/NHKը9!^)o ; ~ C-tT$4-" TYվ q]&8=x01ǟZI1K[vtU+/t~ꂖ',sK_m<.ϱdžs )#4*d͎N^&ÙM/N౓tsߋ(סCv8-~U)|Lj?:>hl-A{PFVˆFH7֌ѣh%CmR3'-Rp $@\ wHp`rA%OIԲ,HUdIBPT{jʪʡ\=$Vlȍ:|F7*&ã_H V.j4P7 xի@ZKUOҙ]RVgǟ2T%6z7ŸS5Y"ԳjhE;ǟ rZT-ժW`XiT`M|ĻM@*l!r$Zs'ůU/)wjۧw' ^n,DBS|Xwy%D%&}cz8oZ?& ,\@ ĂAeжO'A!DlJ!4jOZhF3|iஜt - |{\rt kZG[ޭ,x{P9zs GXϾlڈ8l{jyk#}m[i8ւ y3*C:vvV4?n ծS\Evq?b'R;&6R/wg. 8Bt.14.y2nYl`SJƠt>Pgޫa}zk-xv_2cvM̷srkw_H814n$AZB}xUmK?[ϯj ]p.NY(~$.vjhq{ E|GkU̴Z;rĄ H8zii{A|QPU`h  V}&!,}*{Y.tl:€~'u :$y ku{1+UGSpl϶L('%r&v9$5BVw$wKĀsi`(x؁Ʌ4}J';Yg;ZW5@ð Ճu3&C85qa#[X8MHkUhm#cՀ Љ`(4'Pgf@j X.LsKXaBV5F8( @NUDŽCXq(Z؈,_c hmx(vl\EPx 0g Ii5|wjXցpjv6_v:wRUQE{GP6[Ld @( Ѓ~fDwEx{M6r V֨ō8n(yigHj 7fUǏ ՌzDVQ:99F(tTpYѠKg 8u8Ɍ>5 5]CmYm%Hdwu+9@|ѧsrxVc 8W QՓxOQWhpU pau ~r؄XX`I\lwbeY!Q P%К#P5I}k(e3h19y5Q>{A:PmT51F]d{+GY{7+GF`TCGƙ_^׍it 0#Pl9xsnx`P> ,J2xF=5HVIBnFgqKr&UutLpg h?+r5> H9Pfy `9oYxǛӝbP :mV zyzWHLW qT ZS11V&X^Y]%mc9p9<ښl)9;Gj Ue0v8d2PtXZ(ҲP'MuQǹU䙀'sUIy~>*W}pUZ|٨zqi JqXy+za^̚+>gz"Gr id&Fv8vZ @)I(eקK{Y?ZU|tqʠ^BȚʤuY{xLmi JuZl]ȉJyWP 9ڳyYj PISZ >pfɝ@:7>u,lQPNJ{٬w z_GKۙs 1{3[6>뫱9ND;x P gs"?^ 9?YW MEחw(y`Uȵ\Քfwh jKEK*v]؍2*w[ zУ*p? CKǁG; @P>OmkD0x PڗLc;꺮uY-;mn%rt{v[!˻8۳Z#P*pvuzPpebhÞhFZ}ô D[F̳ YƛS@T U0 l}鵼YØK]LU+l 炗ꥇy{YYPŔ0ۢ' ^lǤgukA̻ܳٚ%`9ɕ}|xH[g'rCuZv^ WB啭{5NU 09l |˳\%f ̻6=\@ܿ |̀z@LWZ0ugdFm 笵M@eX5π)y=&K=C- [y ݚMє<|p Cx\luWwKK׿@tϘ Rk< r>M- %DԂf~nv|wNEMUs+Ţ -܄-)] ]sw Yl\Ǫg˽ܸoFqZihxl=ftܲ$Ϸkăҝ Lͻ ܣJ@< @ f. |MuԦ[ߧ'veZi=-:LS, #D Vn@ ]ݨ؅!<@\xO սLޗ˦Vʈpf\rFTS> HBy]%iQ>J^WZM[ <3[ pٝ:Ϝh@@$bHTZhXh;gEK;J~Ԗۯ%ԙnfI5ԹN^Nӥ~ꦐ%B ڭī/i-Yi$甾1ަ[*Ƈk̎J[S55n{CB"ff?Jf0#OV.&_B][3p,|)NRTg~yG9WkNuTPo Oy~*vo#'OEk95ܺ,Z] iFp (O `MP 1C7ԑ%u'V&ꈺ&d6o*Do+>`O( bZf*fJq l!s' ;'CP2 +LLPpCұNuD8**H7ފϿ˱ H ))A$,rȡ'K`*W,A 2 EM,H3#E2Cͱв?'ɺ]Q\TsLКw]hRJBѠzOOiG|!.:5U#YMRYaI6@<^{** _> 0DVٵ%M L޿@C>#ѹ5wġx*,--~MW=NS &UTEU]X`RbCJn *H@:=XL-Z(/Q[:Nhjtyg̀Ј&h+LA 0:@|1 ?~ekiHty"[.kn[ń1d[d@ ʭIjips}01uNF^'yH;k\ (6cxh[Wmk4@LEoӫ7+{P1{[8,g9ORkά?]_6OˣqAndDш"w-p5E*6~mn6=[yÁG 8Bvha|1QIQ%n~M$X)O@¢C^#md;q9l؄GUQlS HHLJ|L!T+%.|ް%X$Y TA>%[_$A)P~2"CT3d UkD,%`͌:LԴs`xc0E M4jʬ}T;twJ'`demQ"/@A HhIJiJswV@pB7UkZ1hͯ1Ż3gF+Ta Nxcn=+$2׷ }]!kf|KF6YbTL_.sr}9M~F$" Kd(X ε/l۳s/}GB(5i jdبO 3P$Wsb6[Y6)>,f{+ʾ6cAfqB 24 *ĊSjڟw}}؆fM@0F/C o J) 3PQ %8 0 d>^# j8b{:db6˛_&g 9J!AAr@qΠ#w@oa}؇}hRH2p YBB/$vC2=5pëʈ8  $+leێd*h*ȁ$/@+ĵZ D(0'PSBF#ŸQuX(lWLH8;ج{:E:(C]aܝMl(# Y0_:tF_AbCF'Flf6C)g+(8tTGd4yďl̹|ԓ('+ichf`L'D[<\sCxaȳI l8h A?88 6p> 陫 S«>u1hxL6h?#oQiOPO0V$ dHNPƜas/)x= K]*H!,b,@;;`[ID;퐳i̓|kڳD$!YGZNz Gru̱ŸO7KIxNZNpQi;)LT& O6DZ`Lө; QiK1Ȃ&h,jJϑRfJȃ2RYMpBQ52͐_}*u]d K%ى+Qij hDLN(jB D53{e ?q L>wPU2)xZ1[%Q"ϑR_x2$ 1%*èX庪s'6rC;d>8}YԞT@UkiY %6 Z"a=/SfI47 u3c,{7tAU\B `FdWM^`HI@E֏(ۅ)\ uܞIqmߝ՛E$Q*3K*C!XvWP00H=7(.9[ O*S*xc} p0Y8^UM%9'͈h\ _ Al$ߞF m[ *\o[Hy3B]_090)'p1H>ωTMO' ͬl`歲 ṝ@ zIaCKܚV-m\;Ea=˶mܗ,?#ƨMEE(:#uDh/bRۭ D 80[NP9 #HYcmӏ%xӚBBYRB>QC5MLN=Dn*4:dˀbP R?p.$H3Zbjx,Qe N02ltc`{IJ^Bfދg^hΎt_Tt$d%Ը8l nܙݣW.qG mC,v,-$ jLm@XGX4XDVf?:*O"M| 敽;K߉ՠ J]hkl#d\;b`aA H`½kiaCdr FfD <E)6~Ȟ.*TuXfHR(30$N +dGxtymkٞ`F4kRbh y昈9!n T6(1i paak )uLQ.J&$WȎl6BQ}wofpR3@+cojjvS F̋v"b1hp OLdV\Ƹk ?D^x9Ҏ gHn_[lo lF=XH\rRЃQXgrN:!S*0"AF9b=yY ghE8…"5sۭ(iqFdz(dQE_EgDʀlZlmtL,l#raf`yN|+H\oLpA}"YuUyVL$ GJu&g8߻s5hg/_i3kD^aZq'& r˶x"9Tmnevwu`~,_Ma> Ɔ/ 㴋 w/ax_P]hBvIqvht(B4Sn#qqu.JO$y'DHB A|BP).,@ЂDPF;gچz47x!%@z K8dxL( ܴbsW;; l h0R r}o|u'Z(H @uY7t,h 2lDk%^XQF6r+ׯb"E(LJesȒbfNv";/_~ (R "|P0`܀j*V!Zm+ 08 >hk%44kAw>؛k1D ?0~)eqF6s!DǪ&}4ԪϽkCc^x6MhZFa7* 0%-T9\xɤ9Mv TF])/nH YK|ρC_h(An\rG՗^{5^1h` xI4M~9@pA'*#ixk6[nn=~#oYSQs 0' I]ܑ+ċu4awywLPCG^yL9ez7z'S~eT$jwؖbE|) j#(Z!f1r9#ԐBd㌥#䣏sFdF$e4HV^YNYn%xEP`h j^% * '~@Rm 4S˲B hvEwX p:TP@| "LiլvĪZ79i D9 hʀ\Q2)K-_k exgP / U  ~E<²Z.nu9X̓I8/6PP08q%@LZBFᘛ8[ } +JnHD)ѵ+ %_U=TmytK=@`uaՕW>6"FCgݯJ-Z SDxűiV8dp] 5珹D"p9&1ѓyfIzNM }f,;]c@iJҮ@y$(ljF gzKb'9QAL%Dqi7?%'#ӟ&-I.Iv0@K <,# G8Jn@_5%C\urDg5{u R  \RT86E,},dC? j#-5OAig\*38 [;put1fUWI"$\r2fQDj.M+_W+ /bO:;(Lobq@rx/ƣ=JA=u .=P"ĀtA> /p'g:J\涵z.t{#[7G.ݑ"pbY;oFoH59]ڊ]2d`^)ol.[a _rl4Ooc3lP86ZxѲ]:\$9-[hzƃo5f$ 1?X @$P*^^> MϩQ=áVAr PҖ# UՂ9Îܹa5P\VZ&wŵ61e\oN!7zRs?@C %8!z:015PSG~'9ʝ;J+ F^QbPqdx:AWx9a*lby 6@ r0C`f.RR?RD|tfC>XsD6}úC8̱GُԒ[~c4pw|ۺ~et,@] @58~ ">\شN-KO Q-"wp-DJjQr; #sҬ%I[V\I< Ր9MYСZ iV8A5 F JF҆Xi؁`aNm|}]Ǐ5Q Dդ Y`ce>H1O9iJVhA 6A@@'̂ Š: ..𡔌Y`O0`*X,MR8Jx) d8SA#1ْI(IqL o"1U"~T,"#c$& 9R} XKq!']4_ b<4A'iAA#8 ,ʢ;Ċ)WFbJ/ R!͋ c_#` # tʅM3. 4FcTS@VE6NYE8SՋb @4A@9fhT-߼;0}>#u?fI0l-L@f@:A$^BH@M3FUc[\ͺ 1 qׄ\a bJ>FK"B2ddM#D$INFI˼,]X?wQZRRVaTCZezE=@bӃx,!S]M(*aIcKFY?%9$,%0(Nbf/ &Qdc6Uc 26#KAe5SL T|lV]&_@$Jel Τ9\m&n2(n&p eq&rcƎWP[HJUuFcVe`fgM{v%'Ĝ'Mت⊁̒pXbWhaicFfG}DuNR|P` K 'fa,Xٸ ?)*BhL4ܨopag&0fɅ@UJD |<ᆆ'ny""dh!x‹HUU))Aě6WOHܩO鏊2)2*5.JhjgZ 0=y@ \rjmTL:BdԊmLoٱw&6[k2"mP$jf&U ~% 9^X@h a9:+TyBQk'jU0Lk5d+.Q跖OZtm?aƫ4`(Jje+Eq5iCvb^xHgLV؁ VFz"XB(?ǮMBQ1+q bNN"[N,11؂a&dƅTg*3lDFY՘a ,H}uZ'kbMi4L-"z-H)֖jmB[Z,4ce-1(1͞KѮ-dܧk`j JV hbҺf>]I4H-x%ZD-RXBtڝDoiپ-< ^`3]am*)~$Ÿ!`Mc!nZrc!%m VLk$!Oܕ-"ȁ^])ѰŴ.bQ6U-ՉbHb.WhU:ߡ![ ܇nfTX`㓅r'@ kfk B۪$d@TVl g T`0'[.1}-Onޫ om1; Z@@KDLaFbp6Gb Ȑc@$L+|$$ 3 ?" "/٢ZC1 v3c2'1'|_ W;`Zfe2~&T4Ҋͤ{>J< -EhJb*YTA|^ߛJI[u\(+atu_:M1F}vVjPGc70 Y8If}X2%2g~ hMiC $\XCuG?0VA\5 \cх^/_+- 7`IS-tɎ֭bNbv>OwhW\lh." {#P8|w~Fv*U8CZcU49Ix 5D8c48_LKHVK LfST \1Rw&͸%>M8ݛf7ЅU"P|'}cfhXgTm,o$g:8=ITCCw2|6bsr l>y{7zʥܸWi9 ڤ8~Ӏ$Z^NՌ"98lm]Oy?y^SzCsw[ pwZ#*)ѭ*y{ xXJa ^MLAGWk @ Vi}׀ k9FĔ;8@C=o8/, egQ@_$\tF'{@?C&=^OQ=[Vԧx5lF^|1HE 9jH1bD NӬ`ĠY $4geJ+W4G3iִMHx Y (МC z4(1Ă5mjkդ&<z5*T,zlX*J%톲(ز-\9pxp@Wp` 8AJpuɓ}[\,Fh9AH'hsjժqD ID!x>6|8"'ZX8cQh$Y:LnV&k>!UܣHH:}ujվY5X7ʖ8vڶ'7v\ ԫ0{@8L0`nXh8D#V;15P T6/v!N衉ጻD䊚 茌%q[r'Rʡ;J)J*b8² 42 )@2:BPA*h174- RQPLQE+6PȡN㢌*N:Hah"j+b<.R0klHT lCEl b MX04P=7C4%@5"UcK1FH`KI=N)TqRu(VaJRbnVt&()ĕע~RX/+dB,dK,0t4xg40LB =9)lE+w}t^lH 'f_%x.1hg盶 6VZqxJn]Ub =ӽ q9oPQsv&Ԁ˜feТDP7%Gp!Q&Fmh~{"WΉɎNz;)<wUTpˌHmOL3|s::Z+st T(=Q&( nt;ߤxUhrH2Yt< iI[FuW7L1#8ʵ&rKr\n~٪6P in`:pZBPC3[x pjD.ˆ'':sXCIyL*\pH,f+ b" I@2LhV"`xi@*PD$XC*DA { 4:;S Q썐!wBE~g)3<ƖX=bpc2.u-[`#R!eπXTjQZ!-s4  Mkr Դ,#.Eb IBPD( itnHf.2pk3_" BWY ] < T+CUĩ+pp7dnt -n<$I. IT6UHpSrajjI2@/3xE*(=r^WPVS2 0(!C1U׺VحȳhE/ڵ"!!Gkłp6~3|lOB2ШE-%1 UhM{l0O{B;m)Uʲ#Pd% ?Dԇz[F*q.(T+K ^# uaDb Ny ;5=JVq'[ zY`!*"|z? K,8?l:Z ?ev1la c@D'Bz*EpN<\02!C;$(45Ȁ%/  Ѭ52̟ Ek*BtO"Mbh > 0[d.sO-%5kwm˛ޓy5fϡ):x{c@^ 'WYMFb82f`*iXf2"݄\Ƭj{1{Z\!;uPap@9A&[g_ P^J3ƥ./̫ޮAΨ*vC#v( 'φyX VcOyX*x z5D*NUp@gdګ}cE-m!km&Rp]rq˜(4ms ׊;J69TƬ,F/Ls,"*;'U@ {غ  |}"x6{rL ?\.#dHݗA+ I4\bӿ$NM抅:ayd&8^*QS.(\/ >A]:+ #,R %s"sdِ>$V(n۞O^BPd#k\HTVU9&' {%U. xpo~A!'c E^.jl Bq0 @gP|HgFu:P\fb$~0'ā$ nhbŏUP$xp q}//ߒOh612ҫ .EΫNVΪxhi1H}h\EZ4 * 5RXNTN"Je=q&fT nz ı$ /BZO,k(V;KNEؔXsrą,R* .n!oভ|]e%" l")r," -Tf&BH2 qVRI%%}!&)v'+WfžO(ESIF0/r.jOdefҷ(NN8)#"x$/A"n +qo01[?(x2_j60g24N @@:'03, CR"հ{vZ9!#abcVh8kb%#83%&A:s ב:Q!FF(0N@ll@<.=e&=[ +b׎5mb6(}qjSR {v2.3k/O77E t [H7 -F%cWH\uEVH澸 r Q.@: U! FMzԹgJL1848_9x(_NDxŞn?W21\dj1WUX_7o4Rh^\Nw,s uZ5+Qg+CVw^xς`xmAAA ("ŠJ`:PnJyEw=A/ȫ-q2XX;7@"77Ypԇ1.:.!#MpZ'G֧SAl̠ f9KywUnV2^xOs6qkͨṅBuZBvhvIs饐gOߘ\ 5꒼9e;IjQm쀣L&m^P^/Lq&\Rڍ`e8T1X23Wė(`y^*]@ѶJ` ӈ]WC]T6 0 :AD9]?j#m\#:s"Ue]d?_'ߔB"|hg2)2/`QRlB7.ϤBu@6%v0ÒJƹ9. FgsIC{:6#V)([شC(k|s[ϪW5'_KMBg\vHq4\` %!D9`x5bUZSmX-xɛ0h!VWJ{'B5Y4ٴSYŅhPblڶK<%|,y '̡ Le$AmT J/G̻&m\3V{JCۇH[绾MX4U@YZ+񷇬₝Qq6<(` !-a9$aqmyNU;z0bpkbz%?BtP}џгⱳ #/6 Zzu:P ڵ[9^LI!aT%P; OYa/s)h<'m=۵M bPF]]TdhO ac5sԑdcHsu+OS F|Aa΁A pᐄ3b$&wI(>[г7}%f,DG>=υD)4:A*6#bq%zK5[3FO!UsK&`%R`"D5m>8Ō{ 9_*|ק'<x CS&*J ULVZvCVx`4aɞ Y,4]⼉(΃AԾ456U)2j5֬gY/Wu3-CN, McFsMNؘdCeg}&ZȶiI@-AA54@5Ԡi[5ZuU9FAJ=1'ʨQtxyםEǔyb0z1'G9́< !&sN]*4CY 2EbZXأLadB2Yg#FZTkIi;ZlWmωTOuW[+VXw@4d-fK-Ψ,^D+mPƾؒy-i|W5s4^v\2K079`]% 4L(bbBvie~# 9\|y.Sٚ\ 7]o yrqrC5@4lR:PX~@-夬:-Ǫ̞H̺=jǝ Mߠۧ:f@c5o+p1>!f c1K c M:(G-#T:3@ 3B>S*Q4DtF:g, .aνpz"HYSN9%v] Vw[nCvC0 ]Lߙu 8S˜CbB'Udzjncn(S㈚ 85#KD+,g3tpZکH8ƴO55Y`Ȇ5N]uD!p4ODڦg1A[{ !@ } rr F$fL&3Q+!M/["4;|UwYfcVF]qZbH] Vp3& ]w"2qa}^(oaW#.ZV$zqI]9 ,pcS#IcʌXr8(]NOж] AOB܀ ͋AvVy J +]yMVvwx >Ro)c"Q>P3)>2jM g U<91vyAъʚsn%t$̲KWz^+6Թ^_SvӓzǥFʕp`= R^kXNF6 D`ԷCYF{fra(\mR8ZZyV\G~~o>iAW{9e}h!4{cGvK|vLJ|Nvv4H_1fCUߗ0GP V~6y~27]&dkT3@+~jU:YBH3kTXaw-6dHMִu6{:b>xvY  }}q P0Z}7/1 dUs(yg"hs:~@Y$:"PuC *XԀKHo;y:%{E8z4urtϲXx-2օVE#AWYar@Շb}^(V  0 8 ( H`9D'DSY$AW,ʼn?+DUvWXpuAW;KXcZ ( 8 Nö 0eaX(׈à؍\yX-"X@d,űFqt-BpM+AWۡd6-7Qe̴,_[u]1h$'p5`QxbI.VgN f ˘gBa) yɥ 0sY#abB<,7ze28{Ca8δ#_ُHSsZɕSȄL' rfh)jYlnY dS =i=6yї~9 i d]-CTI Kbvis%FziXq-&4 cǔI9*GYjTw"f0D I=ZCCi&) ةٝV่ qJ曱b铞%* 1+X2uG%){ 08cQ2bXxHǃ!C" jUP$g{& *٢.>ktc]WG#>?HFK:@ X p3 t4)j_# ZiqpѓC(6۷(a~ +J领)MDI7zYa%Aij#9M9@TQ; 7Rv$AYy#@ iۋ@CODOatӰz:|0ׄ*Xך鞀㓘QfzNǟQs tZX(pWu^Y%Ѫv?J;J[dU = : ~`"Lo Fr;&;LW^bIHX4Sw+GBq:[~4{~jќQ͆#lÌpZ:ԹP pAL!#YP@V >צ5(VUio+$L2+G tO-28n,4ce$G1FI70 f`q qC,Etm!gBh{,YFRŸ+EmG=KH]{ǥ=ϡ_߽|,YHڭڍkHi @lɖ|x!{=΂=ʥ<ئЇ8aC-FMӞQQ򳉖"CvZQ0M[XђLm=xVӥ"vVz6N4x: 8 a${ÒFVbNKwB*Z혾d(p^Ҍ/ެ)l7ZWpmWc@O/}m#f0SJpglܘӅ MGG屬rfNQNCv18jWeD#sA Qљ:_&T0nMn]6<ҥ3y \ E!8=$Ai +FꞧQ{hhvoSje/GA3^ۯ>p{=c:蕞DN\'ҋ!wMb]M%? Z{FcZJQ^S0B-3eA͓a'Fʹ םPGt'׿MbxNTi>219śٙ2o{uUh/  2_Iڢ})|ˬuw<|~Og؆mgUV%OVɅQLXZDHYr3\vD%@BC!&BտG#\nIk yąm!gQVKI|@8(BDK\XqC8 B%MD @d 4hD5mڜB;YHETaҥ67hꆛ*tbtQ]a%zCаF{&堡ƒ } pߺˠAV+bD$NPbF;f"V0(_Ɯد_={hh͚UK5 M2t8Bl55#S<9|@TyN<}lҵ|TmW/'Wfj-k燴ƕ ;‡a*A[$̲l4LAJ;-.i$m6*a:-$߂#DX b/"l; kiHxʂ @ HLjǮhF(Mޓˀꂫj+*Ƞ"13Yh!2l,4b\+B2hÈ$C@DGCrŊ1=X`jʞ3ǠH 6T2O+(_u:?--A2%.{/Lau@?'1p&u8_ٳ?-iPBiP5nѐeRRTK)j.'h,OS^,A*!D2S =M5CK-\r WVL(Y"+AjuZlsnUB@It\p]m]MV<'0=xFv.a:(α)*929ᇟ0, cKu{ N"1n7&9Wn9OnՙܚzDF7Ɣi0.f| _>PᲨ?ՔPK]mk[7;n2]1ioi\4dF| /B rbI/Uל^i&c7=>es;.Cv%8W d Af(Cӊg=a[6Ww|烡BXkGy#HaSG) [ժcu@^m sV /-t҉%<\0Bq{+A ǨŰr3<`5Mm~QĠ; Vd$Ύ_9j")* |Ka2&9ȳBh1rgƚ$n\":-$t)"vqe.~AJSeSe0XY }j$B^Җ$t}切5c]f#X[",$K\\sQ.(BqӜ'x \ <4D9N5Rډ^4|EWsn *4 8*ä,RVC'r陵[).pd81b5$ךǮ ^ah41my0m^$%Eܛ ǩzn .$v \\#jӪZT%2eAbLĶn˭nWrETAb*߾hCV7x(d K]X7.fダ+B] BXi r,C)իN׭| ?6pڢOJiM"RΌtD)%_ 5 ӈ)a$;޴$Zl=SIMA8-n-I^xҘ#6SjnV O0Jrְ/(]h+* XE #atOJ據]3,%rn~!H5Od/CB/WZ,RV1|x\r#.= f #ә6g1#m)`S̢=-fllI)U2ɘL11yTf غj^\(iQݒnGrq>߯N... ~*/:MeDpS|VQ>FkX@4y\H}l=!wJR|&EĄNw ۃ.T :ҋ  .2ɛkQi6ḗi>SUZ YQK@H u?Z!@VŠؓ5 2$AB%콶( PyK)B%*(+DQ)-a@e˸2,C@ȓ @D8CDh;-(( '$(@08&HGĎI'!  0Y7́A79cY E[%tďؒ)2Gb4FMIc܄Mdxk jF3jC<1 :*oG>B؏$AD;}!َ%\[;$*#X|][)H?0iwDFE{ ":y*h ja6cI2eIl@ɔN;ј< HÅ[Pn0;@G !D)H0X KG5 /阩ʪGJŮlGD쩬ʩ0! \˰DB\J߰Dp"˾˽ L+؂4DTL,oB*p! ,^I$\Ӻso‡ 止7n3rƱ#ď Cɓ(I|(nbnQ̎ع[s:@ Hѣ4}\^L6ȎѫFjZtWXb̖ Y_=m|iJP4< LÈ7\xqǂ]~l)aNʒ'?̹Ϡ=SnH0gER3װ)k vF1v([5.^Q.%\#J:%ƖЛ}*ޑZjUs׉}{w/q84!!HAl! U4mT[:~U1nG~2_'wF)Y$USFDɉO$K2pE:J1?̓w, B8F3Jc=fqe#[ƴƠq,נ2RȋbB*\aqsR4 8MtcpXVEڮf'ljMjxLg ;q~-*= [⟭|eN$4Pa^i#Ԩ& ]XT.>hXdYM(fm Jk'Nw:w*LʦJR+gK'uKf7hhlgkŽ+w]_s?8 =#"~t>"54dd|q4H`XQgA0PǦP_ݫ#%'\F=}f>v0kj56/}[jirW8,gK%DSkyҍ1fTw{Mpdo8ff/o58.[8qex]mS&r8*A ]ˬ.%^];*ta]ຠsэlk~cS7Jc q+:}MTKy2i69^y pVNBnCeeQ$RE,}:85b'NՋ͋#}o(q lx&N:;{2_ӆc{o W]uevpJ%|;s_vodSl 8;z$t6pGpgH07c]y}wygC"8H^Àrl?~qd0/4SG^mw <&.)PH8qnFwX2jo;ƀ]CSf1MGyifM)p'XCi:+h:H?~نd/~5؅wSI? AX# .PW*w؄uXO_EfUxMUht}(5x4hApg 冡}Y8yl~hhZx0o[u؇xHF `X ЈHx9PjJ@wfJ0H,uc`v4GoSBoƊȔ](upmp:dpX?[цd;38Nq9<((.h%ۨJ`5:ȎLZ2|5ee]ez*HZ60x~7M7N7ƃ`ȑո7'%LwA"_ g鄏'mpS؏pFyuhx4[uFh7XAvxؕTRbوeY5keQar:)0 7~/C'XuyĘL)>3H'45:lX\ bHw@Nee!5kS#+~tYGؗyt3Ii蠜Ɯ dqMI~yؙ9pe7PjYК[&sxGc_sCY6u6)L~B<lp}H)F֜L >x0S9귝)ّ ۨ{(*Ye8 RJfHEi5J" >Fg?V5VCyLuVMd7Bs ǥ)b:d: Y7H()eKu ^LGʂIghsB1ا::}>:ݗc@hę9DjU?i#N~yȀ ʙbhxkj0 @l :V/(hh鰇īxR%8l0o(I¹HM@Ԡ6U:hd 9pɪXceu*HBbH:VCux}j >ۯF7H;Yk [Av8J:r46T{ ^ʱaYeY?u7Jl6`Z;˳HЫ'^;_&uwմ>P4G kSWںw^di%S{T{f 4 /$5 5'~ՊI:psp)8 ʸ丕h; qrXxѡ!;%$I[е; [h/'/x'˻;xܥx)< +jc*GǽX0#;;kb eT q*ՠ٠y6Fy>ꧼ 6*e}G[y 7)dQk+4sUIu\ @iH>$eTк~B9>6ƸQ; <[5[}ZU1[G;_Q/JSLX\d0 c̹bjg@j5Ɣk ?J^;G̰<>G[3C {F3 \L˗lVlLƴQSL{蜟L \{ʩpƥTr*s Pw9|UNltO>ʼѻ,~_e׌U ✭`9 H?ЍN{j;ǵ dʌ B*>7{ +ĸJ =o#G M ,M<3[?BQ=sD ּz%DӸҞ[ayk, Ǽ7˵/jђK}V|`ǯMM3 [];?Z=S;;>g ҙ\Q,VڷYYj$}p -``HH8.{춲sĐ՟e%ɺʯRByI=$GW ́Rڡ]ڡ ޹ ;Y <0ۼ dIug񋵧MZ`Ϣ>Cj'N'V~jhQ(.HTtmceL M}7a]  @.hkN=ύ PLgӷL{F?͟HVQǔ:\1)ޤ * ||.V: 9իY A~ ` @,HXKHx{q{[c0 8 ^Ɂ,+z'c.k'5- ~tl}?ݷX[  ۮ% 07P7(Pbn`'Mnܝ b` { ݬbw;h~|nZS.7×Ȅ^>^Վʜ@P ‰~(PMWnq V;Y€7~{Kf] `Sx [bobb!#?%_<>L y$Z0.9;ҷN>{C_ % U34رs #0Gm?} gZo[^toq/sb]<EC!6`yn?{O  ~Sͤ'vs*0/ p hЊ $XР0.\CzUW5nxѣNJy$I]C&4xX``* ,X ((PذaQBP05UY#h჊?(Bʕ*UXӬ[&%s,YAy_|Egpas>cȑ\yeÙ+Yq6ټ}עf ص@2]0!R?$Ҥȑ +,Hy̘4YM!Cgϝ?KQ(J40uR_}uKB+[Өӭ[ˠ t3̑lB* l3= -tIkJc d# mƉ,88KJ ؜#`:j;(R)BLl +rP¾گY P@ l0AU\pN43 tB,\=;3EGoR4:U gRf1$XpXz$ cl&<*-H2Lb9(!ʢ"N"fOH#uN0B=O@}gP;{:LT 1GRzK5ݴO=-QcEbnpQ$U(YguJ*s~ͲK/*L;(U3Nri.jv%̳w\ 7gtyWkShJ7 >xZlQ%IYl(/TX7~d2T(hv8MbRZR[t.;3 u\Ȇr=:b(b0fԔQ.3j`kiV>I~5(> 4,8ŻYh$Y}F[IKL\'r|}7㏳pTB=W= %^ls]lmvrװ wh7,AW`PLЀ S2 (k,E ¹q[\fĔB{ lxG5 $_I'Nx ;ݑw > <` c-Yw3YIX F6JVE1raWCqCsԌHHs!5Xk]Fd`N(E d!؀]\h,M z[%GT=8Q£}IGR$&1㧠c$KdErE&1/z# <`$M zA;Np10eJJUVa v@fϓ4Rm AK922YC_O2F5X[዆T+$Ϥ\jjVs@z$gѵm0eCA lj4JyA9vN md';y1 ȈOrlN꾄\*e G)kcq4Dke+[=jS#f@Rԑj^*d)PZU|XIVUTNЂuQLsP`Tg?۟FLueA9SZڈ/2*dc>C;ʢà`4ˬJ 4+ +w[(*)ucX0-?|:Upt=ha,;#' h6)@ d;>C!*S1 SL {KPDEfA0Dk,@aFdC$DD:=i3'dGQiHF pBcŹCX;A9>OEL*^9 $P S NCFIِrFEl0i_-uAkFPDK2H8p;`X YGvDDx*,;> H>2ܩlK90H5C0 ]60I {!$FԽd`TfI $KPC A쯕4O *3Kߓ˟bң&lG,?H,JǩǬŮ>|M"*AiȲdK FXJ;u1s,1hBPc2T:Ƒ-Kȃ,xLȀxo8OL<$a?2kJGUDM t?|Z4ۄ724rJIgTfXC`)xK锆O{yk ɑ\A,H<`IO4v=xaR@$2 }{.oXOlMƛR*3t%.b'2*5iPғMfI4H9H }1@N4 fh弃,p ;Q\ѣ!͗Y$ 19'(%C*RTEU>${øٕR1bɬh& `ih6 4p7 2()V8cx?`2=m(h_PN9xBMȀLD _ԅKөT!5RT*$%ckQeRT4$UmUc7 hȡC`LP'hd 8{4fC7)@#@&ȂrtVwc zh:F@2pZr- Tu$pW);yTz1QW}]AuM,mM .MuAUZ%+]>ٯ?RJp>%pt}ʸAA_XA mL̀ ʴYJ@ RMM_!˔wפ=ڡ܉z3UMUDK,QM4 ۱zr'-D=jIXJu.$0Kc ěծ[Kh Uo5ܜYd\_rܟ ͥ S T'B=R[TCJPd`zm[ݵ,dU.~ + ʶ*կ H/U]'`E[FfpRpS#؁F;^Y(! qI[@1*hU%)[ &e:"̻ܦ̋@ j)*_Wb>2_ӽ;keTuC̘ ;e(7 cݱHc<jfTuRp(%m&xj`3 OfPfRH,(JQ0'#saTShXk gQxOֵ"\ILU凾bۀXI,X_Z9UvUwRȌ*/)֒ jf΂:ivfiPX9E x^أj?^DΈl(f4 g0`(ZYj|&V+Bkjax嗪_;V e;ӭk|%4 w*8X/^HضeWFӀN$pl'(YkOP0ڢ-VxA) DXti *ȹa. S텸mmP+4i'e;\n*}hofw0 &o h6Z2 @'hr`19;1H)2 I\(& IpĐ%dF~8%N`qh>U8fc< @.%0ҳLv r VrƳ+^1א4 4- e`GNWPFeX :C4%bgWhs(tBJ'`ʀnFUBH ORfP>anr)(9 * Teo H/W/Q[4_F={^s^D`׵M C^sggT]qS,ƺ%U{n0hFcra&ЙK/&)xz./$pπp*^XS )3ه}!m` b/ve1\xy/ؐ$wNس{RgVyHy FcP`,6"zUCzԧWzg.=np0\]7G}r!E퇰{>h`W/|҅ڏSt~|z]%'LCLgC.x১H*`}wV`8/N=;.ܹjΝ{!Ds'Rh"ƌ6"GfR#Ek*d)K]2u*VL͜9_TSYB*j4ׯ_:Ek&ΥnժZՖI>xk>([ڴjaU,>@K,s9Dxp@_ *TmqȑS2 FpQG'P)M% 28kk zAdKڳg̩d7ƒs $MԸK2y\3Yΐ*.Q:9m TWn% Vn糭|s?Л^4`gX )ȁ4eU6c]VfhPgiT(D)k Mء Ahg E w#1r=sAULYg4`'J>QSyѐgNzXiՁbuV~kpCjյߘaUeR8e6a`aJ8EE1 '`Cf\2 12L#H>dF*Y5 ӑ>6dL@QJ*LN-e{F]f,lzכq8  zi Ap 0`D6z( `@ :)4<*rF*Gꤜt3يkQ/%U]&a D@ Ъe22 ,Ys9Wpp-p {2Rvnr05)WT P.1v4 1"Ymg -dOI]r*0PVY V.EB3\ 8sY<|A@ipb,DjK khdPuX3Ea {N'=Qn+_ܦ]-aHT1t.:xl(&VE5U9__p`p@xphhuP:.*PhnG*TEqb17*-y$AUâ٤(SZ Iדl{ b`Y%+&}Z#/~?lSNG}id@]wkx]- XA mv(^rh74"JeBh0~|[ wh$Y0Q`@"R+fC \þL7V^60:%NS? 7 !s2 Ԧb*QXh-XЂQc0G>pDXœ#I b4ǨgJ2i$ѸFSb+/8Dr,gTU 0hhiQW7O|~4:A!&xj)P#P(PPr`Am걛¹qrn / t"%)1K.OiɟȁDb#t +:%>{S3tzl%ˆj-QyqLMH*쐬*1*[ԬɊnӔ$ŢҨp?w]֠* LdYrFWhZF! ZBЦM9y#r;K-rLL,Iճ l_2VKOEpq,VE tLE9k8ՂЃh픪bYhmz'W05M{=$){m~cdȿeq_ken.zrkl9,gY6.eeqRם _$x7>PY{_ I+Lt[VMl99%XXDK̸:u :v2W 4,j awmC6ē4[:Ņ^LAƒNrc"ɵiM/16Ѫ|sS@1_tBRs YBm8 3$8'nHizCqiOS{T sm҄Aya*?27@_c907h2Э2J,j@D0 NgP1LձnlSPîqzO{eI~Ia=@0,Ƽf`uqi"̂)9|E7u5<  DMߑ1~\_%UY+Vƽ hg$AX^i4BB0 589O0Q[H;ȏW*`aޓ ܥź!D$ƙ0JF lhd @čSJi՘` ?uR ^dn9 ۘt"#[`MTMa&fd p4)ֱ"bL C|Lv (Z?`$a3:cYL#a5``\7' n(Bc9*Gb ГRu|!aݣ/{c[@Q %3R,fD|у`D@gl`0FFA!CHDB#*):JITrW$d RL >bmYi  %NRn 2O:"P3..qK)Q`hR%A-x%#XnePXCYvr%jj1.ޠ[$0f%^%A $%0c`fSODIbr |FdzDU]5Pfz"!tmEDff!j=yƐY!lb>f\m@]%A ?d`gFaY sMA_TD^gXӝ؉%8J4L4l%wxNyy.90I(fĤ| <^lgmlo 2H!]ƑR\M^aI(,' DVN 1( z$Ag)VĎҨZ[䨎ƊY:$NܝδRLM`.5, 5&F%U@\W(~ᇶ-x*)"i7"* 3#П8+ QA§:2ZT:_{h AoI*4fƺ ,9dDc ~Hv#Z+|$YdLf#]VZʲ>N UDJ-^+e{E\`^ Avh f`+8daU^IɫڪtKe$$AȀ 4Az6DxM;b1u$U.!ܑXi+a\QIؔl|Zlf]βK^aQY*u*UAA84g@NeuZ,(ؚضMic۞m`v'mQmޭ-BFR\N}:i~0i n#.+\Pn)uE zpFv!L@r5X pFnT G.쾨̮֮ }EHX- oZa*Hz˖˄Y XRet/) M QUBf:$ `m2mml$6)يBY:p_l/mF5A2v#1R$S4tF\LYsz0iP*AATpg^hVDkPP,7Gq ֌2u *ieL/^Ί!ߖj1 ل: D'h ưp @&AU'eni!S 12&qx|l2191{'Xf nᶬYpg)3k\orypmYRBIU!Kg'>LAA ' @gl*<"pXKT )LM-?EZ0psy6@)* bs^$aҒenyp?IјE}Qs"^TfkH_\S8IH)# `_ 6cu!M.c;amo Աipzvh'ufJy6Y9x}->m3mS njںu%s +=jH.F%27F!ru_7YLa>3axdOHip|z 4ՊUg|G7DXE_ۗqtx bJ'W (^w PX ;;LP{8P4wKt7(guHfƈ|6("BerJ% U]DjUI@{\k^s6af 50H+ v9aq0 ]@Ѱbt@TRI }Sf[ujI PYSlmCrvBR(ꢲkK3Z5Or2Ɂaxc3ߌt+25)˽:H$68񟟀7ārXq{ӶS,[WWTh\`yZEM{rAX6R:q;&ȃ+L9LEQSFO^y;< i?x\Nj<8>9 q%]MKZs^|Pmq2;2JΗ{[L>[c&R߆!h@;u×x ,AfTEUIx*n]= /4WX 804H'I?#v;(^OQGoA:GdH .qx 5S-Fl6^l}y0Š4X` ;&<x %T$ _% Kt29p' hPCZK4a`r cZoT!%U`| Akٶu˶B#dĉ(QrJ%I|aÅ U͚0!4ɓ'fsf͔%k,ZhHC#}5B^}slZ[m!3 DB5rDiRdr?tK5LU'ϣEw3 >AԫWsTQױ!ڴm!}ˀFp~k/%( 1dZ #f6h,Lq<3'EQhd+M2H{2|ph7zc&(p:cP bkR 8', JZo=B+OL4Џ˳?k@/h dZnI5P0S1I44EYlm5 M4sFp|Hzȸi9H2ˠ vZʧ+u *KtK0:%@bةLP(j*NK ;: H-FRF_SRz/M9-@G`KUՓmV#3:*(Zmxrv8KtKa`r8)ٰ٩v܌*@:vAK qj@;3(\b 5U$1P:5ZjI%_n_:TZo %| R80rn,{)]XLSj>~ز"@|Jֺ/Ž"tFQK13lqͤJǙըFc]^篭J`JMeWCmR 0yx'zÒJUCm{ ~w}+t$< cC(.ehm'ZR"U?blaI.4 fyd%#A:a#*yσ[#lHb g&,Z]C]ftP$QLt ^mq28]s;T'Oقg FhYsƂ u^A%/4UhTWI@Cm`kc`LW@L##NzWjmKYWDIxXJ>zWQ&T0&$}@TQf( #j;3Y^+@ Rp|sRE*H5%2eW{:ꋡm(}%Lu:whG0qU,2+1{5^2fa2c8/1}*C񹳾HU g ɀ5S<`aJS投j K#ZClډЍ=1bXx][Rb\'eڡ)%>fI+琳~ζ@\ ը²dthS{@AQM^ n.d97R2HoQԘ0)6x^זWЦ@IܸkXxp,JU5,`6ɉY\NYіRc}H׃q3Z:6WČkN)˵F-\;`UITD>b2&. s @;)z:H3J'&YTsgX5]F-,J-{# @0Q8OQ<%9'E9Ñ4 :jR"pe: ;Һs<raR݊>fILa ??32@m6S@ lA (6Z#<HB(CoC .S:0qADS'GSEۧE]$`E@r@@3H#~#MP8m,HJ'P-H8BAiL2M^PMMJ_:yB;+P=v2P2. =% yCo/H,:PtY[pe p,efbJfQ@ ` |7%Vg%SMJg$`7~8U'+P3OO}*n<˳ vjcӓɚ& 5! dS!Yl|I'2]E), J9AEZM,2FDRRd%`rjmDasDyqK?d>b>+eH.+)r$4E?Hv ݀e_IBIO6-,eR ƠjN /@sAiV^i9MI "6DVq >l-clcU m&&.(v lO&odmH+Sddv`+p EPA4$W|+^|36t&s-sS\%u _NuH. ( Fvql1v(mPe=KvMR&!zvfJxh|!A .6%ԯ}!~1Wj{/WN sm8',v/UbJZSPbRo٢xg_b^. ?v XxXWX"XYō?b{K.1bu-}*QWi9Ç%C~x_u/'@Vc+CU@ٜG͔ywZ coBxFnEGXegz0c&P(퍡ŗGR TM12Xr s)^1ꔶ(#JswaU-Y1:YLJPٝop҆Q%5xHW`:Lf"5YwfTblqqVzI7Aie՘TS5ҷK/QB8G XYZFOwt,Z @pڰi-v<$:=jg>dڛؽPвICy<Ȅ3@Z-2#^QUKz(у@I 3B}=K_h5%jL8W"ϧd7U&36[g F*qC3`@Q-$040} A//sZ+%߫Sɼv~$lWy=1؂١ōZݲԂT<+;l׻{qeaHzW^=A{nBN1,P^{Ֆ;4iUQ?:%,۹Ģ}p#>@ p'a1>WUIIx ngY[3P; jB` `E)M:=̮欰&k}CsNI^Grb@ J,"…* `qĉQhƍ z$ȃI kHBF'v< &Ys<{6Nl)-i+Dv zڨQ'ZŊAv-VXq5d=l /"7ر{}}zC URHHRpDb6&NѠCkZ?Prٴ9#)UXE˗2d8Ѥ-iyZ5U4iR1lQ)TbUюM6,ٲۛu uu7/߾&\kdoJtCD%tPB. WB aFfy6i%6jqpZgeU0^d"j+6c}A5HbJ8\L2p <$ Nd9P6R4S^bAҼHD: =.,#Y DsP2B0;lD!|ɖHc|)LB4gbAE# cd|6eQHBG 6BZGE>` #ڗ#!|F.Mj@@esTHJIR64f8|v2P=AQoq0t Dp#<ɬ%Qk2!]02ֽb#-c.`wF5N\W4-yH:ʍ^  Z#65dYӐ2HԹN&G:F">;V83 N@8U4(hDz '" hSgthW8 H{*HjR6M,4T3\ÀHv xx4p u.:gP]Ұs!z.n tXiǐ;a (^h:E 嫷P ][f,+VR6D%^L+D(ySE87rj Tb!jMO2)PA 9['rX"U/};q 3,c,A "ף%0w!N7i'2xª̓-@~cC~'#,GFBu1:adHLAob`ӫ^Vg`W* b(b􂲕9QtTC;1KRcR/nE @2/g3t@.[Y l F4x#.%5wj`y9ue B=ZȶQH zj WZPD]Ĩ/lbabUqiL&UaCp+Lb@bp Vr<Nj$+Gۀ|"blYe0'(1T]2z*j/UlHCRX]YN}(s&Yw>OBD>r*E,w.>"6# aL#?Q}Lp7)'od05T.nS%vSVTd )602}v'8Ff}0x=U7rmurm&` QA?zfmyb% sm'2  gZ"DGGo\Wq'9UwGii|F<0A=Ae%<֧1H*i75`xSZ0'`vpMAQpeQtE&?ByEHA']I @ 0K'w#Y׀w @gjbGei Bp5Hb7tH'(h}aS) {' >`$7m&# ,;s Csm6;sm>. p0R{Xg( Ȁ .% |ON/B# #wa0f#Uw7!= (^@茮D)$G XTQ[ԍRQ-n}h8thUr"Xh(>V^ NN'1Dvv%b] O6Y=`#qh(*s9sCvM $ɗ|i$'fV30 \445ɘ6&8y2Uɤ'4b_oQ Ȕօє70bGv#7Е9ߔvYZB(,=q,@I}zɜ{ɗ,8>8T )EWȕj L9)R#œvHx" YX.T<;bZXi #y}YWלy x~)i+IEv0K`s$ -msiIRٓ渞LIIv7VxuXhviJ*<#10c/3bà*Ù(*/w)z}#mU@uus+U0:, R~u8k&VwuCE(d F&S(/egZ5]Ӑ&6Y:?bYsB"gjʂ5ЦY`U 3 xAsfrW*FmAdt4 SvkV{X*dv$%A#r)ʥ)O "Q==1U*;*8g}){5 [3fW cU x!rJجEÞ5tS݊ߪpiMJ'*٩#=ӕtH*3`Mfi Xl8YPD+KmKꬕIA 3h(*;5Y^A'v%H9*~+/ @۳C)x)iH[Ɍ9c9HzZ4q:L; o4ѺEt3)p+i^rx 5(C7+!M A:= ok ɴ{x'GW`[$JQU{[;nQn⬫K@IT= !oDzyi'j3E/ [gM>%% f "8 "7eH[͹m۫:qWy+ RZ4jDM(EC'ju/utJ«i94?W,lҫI̹9 R'J{mtk 9p{q>kwD(#\ҥ%n q##_ܜp}"PId W@c@?km C:IxRqLE|$Ac} dHjGԑ CoɪR7ط8ɚSN [(LK0 ]e X}̒L >ٍBuH/ٳIY=Yjzަn*}U.๧m-bN h t|KK .v;KUq)p>{,!1} /O (ujGGy*.6~㱝<^>rE)u A ԤFP)LM8]nQ_*zG-*N:rqQP1:zd~--f?wG(u͚{-QpjAĢ2^=*1V,g.(ы".G:Y^F Aq[ y>ߺ.nm] *] PtgNu}ͲPYke󛓬sgЎWL~C )n].Aq U GL-ctksj ;W 5>{햭B#X W.0O,Gۄ E6Sb`HoIqbq8DZE2-BZo݊ZѝP;':=w٨1< \` 4 $7a"@7NwӓRBn%6SM@)F][%&/ANE^DA˹1=lb母Blb'8kĘ*T VB"ho!ؓ1dYsЀ0Ajmfze5E tU!0vl?@Bڕ)Pn@l c|װ5mX,2 7' ]C@8 󞏃-P! I >6]گɺ3RHp{I k'\BcBDtB0q.ܔе6+ D4LF*PSfkhJ)Lndkڕ w*/5 ktޚ;;۵lbpʲA}tmUku- Α<#Ȗj S>ړb.j=:bg ngrbjkϫ4M0 @b'Ohx:G "C3!iKdrIߧn3ͱެ948ϗ9[y6}tҭV42M'-X[5)4v|m/ Mv#kɗO>ic; 쀌ݛ4\x^o~?/θ觿3Oy,FBnWvٍ ]-i/:Y vAp y(1dc™>WE1Jǧ$xEу^0`"2d{0|~Fj|*_q>0~@Y/͇H?TcЀHAEYO(4qcBc "cvvJWK/A b1zts4"Hr"C|IT_5"8 10JW:Jtŕ Wt](tы`:fTVc( <☹-»=Å!H԰/r\5țB '=9I Z*}G'99NǁE|,)ҊUb X@-T f%bY{Ζ1&i殄"NN=h.Q @>SSєC+URIw3G3g;hI:.[LG03p")FSJWv %T¾qimCWwh6rZ ^E..Gp!"S>pb*Si5tgB<ǥƳWtdxr]t ="zm!R[v߄[aAn6r\n%(vH-O TLM+k\c1v:.-OƦwp.[6/ C`YqJ6R.\H#>p]j`0srٰ\ cb.+3^DgY9Hl?7IUnVF$a mic%"Yi+c&2 uwxn\sqI=jn` jE"\4@@ ^*a\4߫l1{ˏ^1-GSyTȱMrJn!r)]5Nnt{\+kx+w [n'`/|@Ý@?vpC~q S2]rKYȢ\@ ܅g>rr>h#[>4>G☁ zǏE7tߚp:ԣ4K[B"Z1xW].~YQwpNXcޛt5.pӭnƣ| vA?IF{_:BG6g] J@Zv0P`GMM:d9S'E}iW>@|g|ȗ|n2rC&>vMt81em8w s`IXX]vLu HyFGz0 9@\gf@ hܤX} >(5>Mcm.s_R6`!xnvgWs_8x'8nS~CnIʰN9׃KupGFI9OOZ@`l=!F}ehY8]xji؀hgv>d|LJ|fH~1`%e}r?`gfаXɠp{t HY8p٨Wp w?Ohl]K>{h>HI#D9we؏VWCjF&9\;g~2Xm~hXqvQ 9 #IpV (Fx W. YƆc+TTncs ɑgat)M}m89p燕8Ũ4eiYǖn sI/yg;+taߴCI~h"7S@R(;ن{KəW)4~iՌ yl (@ syvIP(UTe)? i }59D z3{I|wRZfe&_Ity6b`頩 О zII;Zr9-OJ 3 @Hisv+8ȜJix^X :alDžDN#$ڇ&j_lx+d)Lt$ym У;ʍ~z@J<Cڛfq @ F Xbx>T +~A)wWsfsl4@H ]9)fȘrzL2z uJ6:8}d5p c:4] fCzz 6 [D15>ʪNҊ҈tZ57P5M V (]HtvzؚmcHibuERMaZf&N窪n/꜕Y*cSLwx~:0v^Me앰6I XgwΩH$K8Uf؋D7Qi%Ib*ʲA7HZ i`)Tw*m:;? wJNUchb[ ׀ /ZiXVW\ۤcf:;VQE"Img&b)r3m*uq+s˯uzx\Ovp /8LCޠ˸"?T6Y/WgXؘۡ\3-+k8xbk7zbCYBث jWGTC0IGX˘.8N {|Dp84׋V6M˂$DYyա3XՋh[Lv2aDȑPMVĶǾ&Djq9zz@O5v DdWD\ iM 3$8I~8\ile`GѓG~.],usfz @EhhǴ[,y|fc{eAGmܑĿZKpW\ z%^Pݎ> X̌bI l$]ҷ@ i7ٮ+fsA,z:v5{5<  - ׆|ˢхٱ{7Y',^)vLr/]cdcLl x˵P 3 ?a7P؅(0zh pWlH `V@`RU [9)w[jdIV̲1 €ʺ$QkFL& }ׁMz @ؖ :؍vzH0cI =[٠- d8~V,GڭUN<}؀ dȏ\{~N"w bKrmmVf Q]ҥ\H'=MӀ-܆70ԭ׍]g-Wޙ `m,+=Wߊ{܆[>j ~uV`0l bhܶ$h*&ӎIpWPن s'rXߥ=hEգxY ‼Q>~+m`li|0nedlUo3-礗P(p煭0(sف3~5^mq ㏾&CmS͙*hXZ]>H봙8+U:~袏6r巾~Ve6IFrNe%{ uȾp9/*=?m,N[ ˼l޹鏴R-.bzrUM jE`L[|r^ܞO?Ǟʮ:njs ܸ+u2xh{ޔ.heDCm EJ?SN(LU/W`ԍܜgU1j 9] jmO':+pLzW ޙ{D%ι7m|vybە!뤏p޺W \_wm| c,N5M/ (-`Q B`PAC $&xР#h#9(B+[ii-^Ŕ͜M6yO7w&Ӝ9tI.MSQJeZQYf׵WaB+Vlٜ7%;kZzҥ Wy//rKać!n`ȀoMUٲ-V|s>`@U#,0D$*H@bƌud1(W ӈ˘qӢjΟ@ *ӭm]K'UyԷ-6goҤ[^̫p2+h"2*E8̳F+RmrdͶ0ȢR$rP$n%^2kJ˚sx:;;$K<&4ԓRJpj>]K /J 2|0dBH+- <6HD&JDB UD) *db1jtɹk ڪH#NIt4u<(y2kJhlǯLSa /0uQLduuVtU sNpjVC=MSaA 5P>Pa(U)N^Rc.:=*B;%n_~;<(*_9 Kf:[sM00^[MR[tp8DB;1|6Ñ4Ojڴ(3n"(•q ;sbz>R%}S#:}Rݔ:X`jE ̂i=b5O:FBYK6iSv喵S䠄u^嬋-h(=;驠&Oj:)z,~a_)L$.k\UۮeYjy;nsB7?I)u[Zÿ{E3@aPʀ[r7[ЂX!Ux 2(TyB8ĕ(l WA oCQ'D Ni@ K&*(a59`t\""!D/u Y* >@5~Pp06 Ip O2Hd#{w(_cp>$X,ga}HB14N.ɼt^GTd\iJ\ȕienl˖T^T!1+a)Icf39HӤfvRuM =ݴ7N$۔slR*⶙Q'O|法|md6rf r'(RcT92?(J4?@{wPcjOXf 7cv4>@zQ- B2IILe[[ ~U;g.EZ-ebDC^יj'VD I\A8&[Z.(ņ\8}!q*\<gWG[!)M.K0!(9Ih7W`uU D9]|P9rjHCദ89Pcu#[Yeu&7ͥŝ9s#EȟL>'u7#@Hn\r`΂Ͱ4z\}J9"-Fa51 UpEqK85YI~ږi[pr'ͻk!*Ʈ JNjIYrp+21WݴC`9XISz6m BhE D ^&rMӪ |{Ջ:3lsҨ@븤3*†0hjA퍃 ɣgG:. [=:`.)V'a3ADSdtbެӟJNlVxki3yg񱋷a*F`*T8lh#*yPݛq KXbPEϽ=O4Pa1a NXfpg+_tmoWGg!+]e8" _Zz˪ʈ*(!F1=Z6+9p-!>I A9;U8M4K{ u`i`VD8<;'; ?KkɣSs<⢼Pd!۵S=RK*3؍!ٽp|uP_XB1Ȃ&O$t#CAu_hKA ( YhAKH?+m2& %&tB)ª«K:Y9+( C1l>pdGFZ[fK22x1H[AJm{a-CDDSH>-GDbx<dLDM q?uI# )&)߁BU?Wܵłŏ ?\[:+!Cc@ܛH yѡsh`u =3D?brs ;SD3hDGr!bo8o :xG{GnR*{G#&j~?s.yuhTX=7r-@p.$HAV8d̡p?*i8VH2@8ZHGu,9ͦ|Ǩt?3"\5\Eq.Mѵ%QZ=y\pĜ1pLtOCADLRC i^Rx>'1@܉4L_HB$.'\'YJ$\IY^JKAr\cй\͝.nϕł ]m,x]%H٥ݩ=]]',P<|e>U˻DLX<0$@NjRaHfNPxDNDJlDj\J0a-6"m_}__x\dqU_dY 5V6:/i2f$ ]* $11@,H;یd(2;Ui*FR6qY0\qAwtSaV^P ]Y-9e,__(8IbڵLef拀fTZDu!:mnݒ,.RX$UXxNB9 p O) h@!jJ:^\8S (c4ĀXf]_ܑ6԰ }]i> ؀bZו1`ܨ=(No)i.$U[̀;x̵ըCD("@ǻAPY$h[ JduMe b^E&kx63 `t8Be5 haeڧQ~N"Nh.2p$hdnm{Ӕ.^3q6CqC(5~q_b]f@C(*!F#G``lB~jȤlYO'/DF2*#:5e)iki9b;'T`<˳K:4vqCqddž<@To%MGO7vgH ?g?J2K>oU >s1u&2]_o phbݹ cg*,KvX,4j6,s[,CoȮ4 + %uuWwpgA$#R!Z_Gk8uZIU6Ya81bY', {$?i?sd :i[p+t yH#Qb(9({_-R1oz R3Z7_b-bz %{mSuI` \ KeWb  e{`:ȁȁKj|8(R$ЁLy |Gg|&Hwъz(rop'\khѮTfPB] 2\K!ˆƌ1##.[&$ʓ 4@ ,PЬiӦ 7as&B`4Bl괩T" 9Tp!Bb-kl#F$ +rDb^3l ΥYϠAfbĆ'n1ȉ5gbÄ glȞ .fqbFa $]Р gC Ӫ71$M>])T&r7ȩKȉ5LroZ@i Q_NJm=l&y+UE_N* Feu& :x8dghviAn@VчYka"o p•JK1AWshzI]wMw}y\i@{Y^YqB[A$\QEWN,>3a:x'A(bV؅f5xhjX50cF3H?H$L?$sPK69IPJyYmUXaa@&%dɧ '[XaDUI̜srfg6e4hC&¦if؆vu1N)wDV 㪬*v:J=VZ`X Xv6^,hŶm~z-ϑf g VD 04״vMQ2Go\cTTX ;MԥJ2yqwO U[9q ^M!LV,&i6xUas0̟ |9+4hfу[ -Ҽ mX1umFYk&W ~;J UoGRsz鴡/C֒:zidBZz㑰`kMxDϕ1y2@xFoY"Y&,yI-B׸*h! DĘ9o1lk5#2 bd{ F :l \))` Tl@ Tp#Y@/9Sf'NͪQƇ D)qMEš0Z2h0[L KID%吱j3U*6"oI QhSIG*W-o@8[z't1S@gQEoʿykT'7j|D@*>O1e.ob t]"#+i}m4 Ip\Zk+K |%ncÿ>(La좺d'QXݘq,ojH¬R6AТQF!m/zZ hDIئu*l\H&4#8[p̸6c ,\usK E,v^QrW+L, ^^BۤF`@ZQ[Pv@ $` 'r#t@ V51:LꏀR$<bu$IRe,<-%߄b#[oW>Nq9iMˈsM_`̦e15`n,6c9߄(pm2RpbFiU} `cz(Վ( 1,QiS㴲s _T,Hq_V#7iȝjU^l`]/opΛ ݣPefO-ёl3lէM` T$D+ƈRO$a$]Cnsog9F lF*+FaSMxx]a)tDV7a G%]", xn75u DڅWlʭB9^mYMl@ @,r-Jᑅl)^[íԊ*x"AéEĽօfd@ߩm0V*x9E, TKENIGLLud^̓DY Ue bT@V,n1`pĠ- !؁  F8F MdeD m/LU6ŭr֬QrŤ!Tl dPߑ ZU@Hn-\ -S41!PEb@ $uX @dRA& 1=@yx̡E Fv ]!LB"$B!jAԀ \"x£_bd98L!M#A_YFDOb ;%N RPܫ9eBA@U^{ZŦWv-0p*$Y"[Z5{ FnȁPԖ`ʀ @J9Pcpcj+v+$@ x6䕆lHgO Vbibk'\%+ڑFa<b)ڂ>%Q.lL쑈BV^zloj'uY.ZN 8@wv_lSIRf@C -bm|*2M֫8ün+$Vbfhm>D%ĀD_JL.Y],ƖnlmQfa\YlEޖrk*@, _HaDW gc`9ȫ'f.и0DQ<UƮSn.J^E؀V | k-0YYA Y|nF:"*L+Z8ky}ncL".쩎lkh2D(fJ130*0(BCWV\AVRl1U$hu,o|Ua KP k/$* V n INJflj-qnf0q8qٯPM]'pSmDJekV1&. ]ܾHuTpEp}0| n @ *t#?2ckg&a9#1'wopH !mf`>VE ,7 1P$(ٍR?7oʦ,͹&D(Η@y܇~HSy<5@6'52LTZ饝9k&vOqPq/3wcĺ+,%/ sf|hP 4BWݑtDG,*f2m)4It ͌4ct4tMw&ۤFxPJ(obiy 5Q.Kh\ʮZj5TW J2/.L@@1"W#X5~ PA] ! 7{u^+Lu_sԙHjN P_!*l3LH 3/Csfsah[iG4vEEYɸ ˮ0~DAPn2GoLp{NR.qΆMs@0pt'xYwcv+ w{!1Mif2۪wFW 3j;0k[mm_ dSO[f] Z.3`K8N`a8b.cwJ׊8O)A teNuz6h/ހ$s N3b/ǀy P@NG)u|d|M}]N wT)(2c9J9r噏x*ADJlj@p.tF8MV6vp}cpLmSAc6G"47nƝFjfTsk aDfRutr8 lƢnz T:UU6#nŰE8 GrTA,R~Ѻ~%f'μf9FhF!ebjZv#S}/l,Jib5k$-4&lPa4BTX E6ldb~2I2$1* 2-[ gN*( /@!hR@.jԧVxB*T԰aCŃymڴepA"h {K(K<qs7 #($(T8_% "?8@K`2R$6p ;崅t ?71P AK&Q7KjcȸdFz1騳'VdAʁ0+-+c+˷./*(e DLM 3NrȬ ǰNf35A7h EHx TDF[x}J+ 2Ehu9G}:QV%v:@j *p*lײ}M.z6LcJ/DsM6Զ 8l|p D89uQD5x$tk\Ӻbla0KsŁ VS[w H2`c` 2H!+uOeЀeajߗesd"bBǘ2zbEC\N d V5zamwʚoA}"dlEBgm"L(Tf@Q [D{ʢ%ՅE:bTe fZMt Ş@2HA3PE0Ah0Ha(cj4R`Džkb H5JX&0G^vUfDd#iehK$'+Ar">YE $1hbHJTtVJ3\w.qv4i3HRL튍EbRu%$$Ӭ5gt xyqZe=˧R:GLxzXϵs>k>]\ 'r@Ϊ^@beVZISQq%GG$Reo|cn@y$x%D%@-pMnzכY*@2<c h`@,EUrTj: `ӗ hu Vo}eQWy눰 c\7Wl^c؃T#mgL~pFsrBLۤnud^) <EҴka"ڜ`CPfOd"gsI(zLcl(]WDt?JkTmٻ fȉt^套FetNnRv/J '*5-V~`XLp>"[~:xŸE p%_@\\Ȁm]#@s|yG.clD IG$!s޼2XA l-HH&*А=*x8|8iO%(r_ 5M">eOs1q(͕v'a@|z勫[?C64@hEєԢT*9o'8.%b,;ݶCu rQF2E 3tսn??5xH|lVwV1kzoq>[)C b" x1 V?'Ai6ч Aݙ[ۚ~wұ_{ -h1Z嶈9s:5{x**-XQjOߎiY?k ʓV9s~ݵV NȂ5xƾvFxOUa2>9g{cpNK+F`|41>.ȯ80BV;̏T)g@^)iPݎǖPNN -xcdFoТbX@?(~qK*DzBþZ1q0 nF1J*fdI,)u匚w 5d!v/j @ j,>4 V̎@?jq{*o[|#.6o~p }ˠQz-:'~pplA( "fUb0љf&A1Ey4^/@ 䈤Yj 0O9s1;Iʏf20@r]Eô '\ m^B&6*qzI6̑DF$&nѾ l'0&pI`b?IR)2\ $$O28.\%%81&hڰ qm'k'{'oL1&x`-Vor.2uݠLB*+lP+ꐸ;bed!bo,{nn`r `*('q.-G AA.̀$鬘DZ]m1{$2e2i 3^slK)Hp)Ns *k( i.^Vaonopעc24X%V21\ /!:EQAd;%"p6ˍ%`Z$>@>]2B1АYSӨPX(CB2t?h7kfڏ/T,L*2C-@>cDE!-P%N$SCIFGBBQPHOHYa|H-9vT0ԶJ'RJoȧVg @c`ccn8H"sLx87UJ9 dr f!MOW4<@ bI26h#QhQUJGqQ!UR#Hg,Ss6Ef@Ls?kt0NUJPIvU"L*: 3yv?#bX8[ؤR jIY5VTѕٜM![w'C5hjGxG#5S&]3q@+l]<54Gu^gu*2<,'F(4`_]fafWA1'hbiPb+f a[qcd˱d5S=eeaRe3^U)'4w~pCkh`B2*K ,#i 6ƒY㸢 m .*ᎾΤk΁:c=v1d 0Ǣ)\Geuoׂ@4; ,^FVg= '-^i#!mFj4꤮L|JHZW3 '.`iP]ftP7l6c_I/kw05[}G1KEȕRQxյW3Ͱ)$fTsnz;Ib6*lr #w|$)U+$` sT5 `~mab Yޡ2;UZ&4"vd ؀{wsXo,j833H6rRr(axoO{2gz{_7Ú5X0Bz2:꺴QV d.3[, OgibYjA!\nv.G)_z{8<;A{-&$wLdGV[ľfzagc1¡s٘V?7oxĸW $[Qu.m}i BW.ӛ[\Mׂ<4W[B=h^ op{oYU|i:ʅ~BD 5✳ۻ]<^Pviglj|U_;o`?@7pV%H/yS Ā<AQ!Y/af:;#&3Sϛeܽ[`vq/ HV5{k-I/J۩I\qz˝鲹O4EOŠ zX8ZK}ClLQn`w|{a5"f)^i7=I ( 4 /V%EܽL14tEXEI5@F}w= ~,HZx/8FӨOLΒC^ +zXL\S e2N\^8S-8G̔r1ޭ`-4~x?ZfW| |M]/-!Su{~s"Ufq:8b:|-ĉ+Z(,ƍty*HKz̅ʂZj) 4k,@zٳС%TUT( 8m5*#Z;z 6د~AnE 7ѭtn{P(X{# ؆ A7y KlI{ڱc.!Ȏ-.[qI]#k42w.fee̙6khO7%+SZ*]%{ЫK#PWS%H}}|0pō@Yb 4K0̢i%vh! H#jaEfmn+\+0TqtO1Es>Z5pX `OJyUX>a^bGTOA[C{xP@]i_ 1jf`dtA\1aqp2 1!k͇(H"Q&b-*bq-gY@m@#HYi1#z`NI֗:XtNY \Ie{DW}dRDj:&aDŽt4#MY@"(C"zRH"bIRK*Niv*'jt^^5V| Te:VWʕQKT7,R)\Vkm| t A)4 A@")TnĮ2ʼV_MR%o"p^utZu2M Z@8ArKY+|,ylst%[2~10N#PQhs"TNH "Ua jM[MkqxD(Ñ.S3zuަn\W +jjd_g{d@AN)pML-I#O~Ci1w1k9U~͸^?yݟ #AԴ9-psЪA j.M(BAcv˝EnךkPѸd; o^Y;-_΋KhN nZT"PtE= 4kZ7P9DSpd` *PY8Qm ":hp 5ЄK.V8<…9^ C ^χb7 ]BPeNܟŷ,~YKBai1^ xf%ez5gk N4 Gkd>d>x/,TIyT_tE1Ň'JQ҈ RUR,|Ng.Y)-q]>_ D23MjT&"dK"iIM 8$#t( #"u Da$׀v&t<!me(HRN'@>VEPNg)" _"*Q(N2Z !$ &5WMmzӃl4qBa4'US"e+ 5pO}VQUʞ%cT9B&]TeҪاfv st]Ɲt':uю4,7)" 2 _e>NqS v ZD.YYkz^(4Se`u|2Ud.Xj¹~%/T1͈vzx0xV8XWJR}TŗCV%U\IV)'`9rםPZ2ʚ/% \MXs+TcC# (ZYCc3⡊l6~țD))ݩ`E8RmW~N{nޏJVZDuH?~{x~Irk|OF šл /齝U@_P^LKUOԩ,&> \J~e嚌x;NzP` "r?+v1"=r7>疤{nbI7 [SCF H}z{Aa'n(DxZ{V{gqW7\vG'G|GxǁwHh5Wn7}6aocB73)yYA~~7F$_8~tzeIz!vV '`V&Fk whV\%|R!GP ,Ł,5#^P5qby t*03/C=~BV6htŃPj p FlR8T؀U&R!_ga(RX(jmHoH6(uux.H{H}(I*B"\#FQ"{sYarx?(ÌJ8͸UKql)ЉYflAHȊw}!lxhAF!eTv׋(JMg_K`?'v8PIjFB$P?U{JHHiܨf4\ARZ8Ž-%N.}snXcsCyyY>O*^!0`bzò"<[KYIkUyn X!Ya5S6.+8}fHSX&XcUApbi.;='%=RcUKIyWuL2#S{ѣ[c_5lzWX a ,ցo95/:54 yxwj*(3瓀9nqu`g ijOFP2O6vYC^ə5&f9r2 mI}X.'3)s8IۃOJviNquӹuu*6hFVۘ\"Y?qZ rI 穖ɚǞ cICbc~nNɟsTJqj[PzLQ?nd٠FͨQiC"w4Wl\y"+Im9}}뙢;0:K‚Oa8ڇyq8 {E)2zcSvU1qZw饪88"J}fzɞjY,6mDUt*vt#N!5Zv67YɄڔұ#_AF5p@j]_ s9dȩ]<,2V=ƪ7QpL$y NjM*xQ*-|Viaj,A*I!yi#o*YC֟JZ{fugh?w/0 <&@.EPc: !ʁ簰BZS)2 <hW"Ps9ziCGi?J,K0ȯd5+KI<[[p B;x&LKVtIXZ{o]˧y)Z7BAUү5[{Vtqh<qW кŎ;j/ѴAJ8W{[o7]畜Gk%`OdQ;xq3A]ȰC}[$pB2j{ț6*I}EzfGX `jj.sQ:罢tRtI=PEl뫚{ YtYp,@jFyde 0Gaucjqטu,YJ+Jq{**T0l ۾ +XYrs5jكd:â*HF6,` ܐ9?LeJ ŇLc6VpS+Iq[kka^CxJB1&/ .0qpSmχ =:д> J'h=b^,qYIn0>L$GuyQ SmQJy[+0=cdݳh 4}ьAtM y"/q I d0m؇=ʦGX8', @*ZMϾ{ ֟T#Ms h%Q}(l :]/"<#}5y {NgʍD[*TYR{9lޝ-PŹm5 ^Cmn?8)]}3+ Θ(2gw*-]" @|0e(  "(&~▒*sYP~p!35.Z0 [W<>8A!ʱBF0Jd$=nTb=韎%iV*a]^.#L 湊4T^GՖ"$;ȐX5FN=lC6l=j fE(Pmc %J}pܦ( 8;;@5o ;P r C8cQquZ00,ٝu|{wT? )!6so#!}dy{p::wr ? N-y?w߱c06(ϨK UinjYVA-8ao Mpv?^.]~?4 `OY<J?*?*?'ejC:o~AUo_^gOpFQl ng,taܹrDxק&<,QcF =jR$RJ-] e4( r$U`TFB@TRB$5* A_XE zVuAà @V8p *q8uW4aX` 6\Lbh7&F ڳժ hNfΝ,(mZ3 8lIqȒ5gĝ[5|sgOO/Z8S̓xk,2#,~%kvڶnRpȘFh)&_~|ċ?vL~1g?1uF+bI5{md$PRI7 77߀N$38;9:q:BRS .( b /I%h+/RQfLC)(> -<6-%f0DW)\ d;p$IGzˁ RH/1Ⱦ̱l 0pNUIJ .Lp,s(> W c 郐jNaqnBzO?g T)3 ݈*bu,v.( b8tSN;}/Pu95Ta5K^5̕ѨsMFj7%9 0 9Og?6ZvBC6(mokx4=% ?x=^|R@UfUTPz(2Y#]wHa8 #& N1Hco;FgEVn:8Do0*:8 ֪9 Z)P?1=:饙iXjU孲֚4;zˎ,i$@nC[z]d֨:7I$MRD8e1 gy+w3VFzsU. }VXoŵ+ T(a'>bq&(9nsu<&5j2٬.18큊r7Os9H-"JֈWy~cX%Hn;Y-e&r_ƂpxhI<@Chza4~|H; Sυ!؈pbHI\BLNd(D"#F.%[[hĚ*Rf.pø샏c(E=3X @S31n/ VfbW;=x6@YdB\| 1WyUJ;" 81F M8F[\ٰ ?  'pBWBAeBZU`C@n/|!_/1(qMjR(Dt2lxdzr6 Ns:!.jQ \B DВT@M\=@ H@emɫqIbdǖL)WAA =mi_EEf.| &p@Hau# )Y1F5&]c4єi |%=of;49L Bf-Ta D4 O0*\ I Qp5FGָu \w\;>. kuS. m=*Ԧ6 |<$](EM!HDx%a Swˀ/zietʸ7 Dw=gwrBt|]jxi[(\ Yh?x OB⩠4%pum-;2%6@b,y%O{ScLַ^%!,z^Յ?_و _7~bJFmta;-,w5&Q>$&?k0H`t[s89̿[ / =0Sȁ"P%$4<-Pt1XA3xA3B2(P ! ,^H!H%J TkZuFC2Fȱnj +)#D\yrcHbʔIMjӶ춍O#UJϣ+IlǴҕ;o|GURkBɵk׭\>JΜ?M[˶mV⮭WH>J'8Qu"-Vbށ: D81km:o[f F[N&#*/$jR=iD6jq]&+SCqpN8+qgSLl+,u&;ѝyM%B@ vUtk2:@y:jm)Gk7-L/o/*5Ts½Z!ʔV젳p[q)%wTѲ'.0r<*7v+y#4Ӽ}D铜;_OT?)p2g]*\(ݎU1JcL],Ǟ]c7ogvS| >@`6_䓟lj-ǐsEQdM t:լ[}*Yur]{T7Ħk {9t<{ŧ5q7w/oZO7J^,]ulwP@|x9QDEf3k鹾>q]HӡnK 6bX45h8mAL;^ S Wj؃ geP|p2fB| "0 sG?!"\:HV08Lp)h p]bwta{!pɎuaPGP4XL8@P_LŇxD$ Hd+ @KKÚEpQXlձ6ȘЌg%nkw29ػc1BGCFӄ| L$%IJF  Y3'ȏ#!E^ԐFJVh EV77`k\}'_Ti{ xnz&cMV(}d:WIue6ֻ#V!'Qxűhq `'6`\w&[Uf3>Րr3 ٿ,w[-򒧷e77\zwc}Cv 8kUZnِa']Ǯ 9bܹv+=_OiEԤ PRnvd;yܙV)#y|T:[89[þ޻Y՜z>-OHVlSGx{1ͫ܆9~rKe9Mϝtgڧ#2'e4pV]-)i׼k= #{&r+wXVG9G^8w~ |G~v&ftQveFy6~&~2~7k(&xa2[`w`': ΖqH :zɀ #Gr{XPc'|@|?Dnz=R Ő 7~ QKWgׂ蠂+p-'RU"K8HP8Bs$b?(h2UB8{K]Pc|U>{7^69Nis}ut7#c(*=G}Іn`pxs<腽Gh'\v`8 ،CȇIȈ'DurXQiS N4J`  ;X$<8htK' `'eyPYg~ukհFOj"flUll_ׇe,$fUw،ЇҨbvHcؘr(7Qx,]w7c|v` @dsV:a8y Ѐ`ȏz(fYӔx3ae'v/xzW 95(ن{ {&÷*Sq|Z e]bH*;xuL IMA9xGyIO) etnw as**QBɌ7h-v$W #ؖ*r^{Si?YxX ЗslGAY㗘V1QdY3D<U rFyǙ0[$9)HGHiD Wn$ɖy8v{c:EYh9Hxd(*V~ GbyF9wX֐7zy9U*ՙ[Ɋ&tBǞƚ6U@ٖ8|; <5 1 @@ ɐҌ zusdzAt&:'P&k<ةC!#È0PʼnBԆ枪b)cpS(]={S* ^6T`2q0 {)Ai8y㝟9PE%aFn<}ٗ٦TcBJq~8*"By\xg~ X7,@(K]4C{ N| pC茮(#| !_&9c, um5 zNU6=R<=kzA@L?PR~~ W[a/cӪM~I$ s-vkzg(1:=׀- ųh֠ߌ N_̕OMr|՝OԪyIlڼ܂(t? ZP Ŋ%#XA -4C2tE#VĈnDžDrH ^;x%41DXfSO% Q?{ SӦɔtڋW/]`mUWV^aoUˬY\XE![oh(Qy_0X!B ,f̸AGad 8ȑI-W!Q®:6i*6Htuިē$Anl.iD~Ӡ͝K-ԺRPOu>5U[+Yjٺ wnݻ(۷}(|A l3rB(10\2j-mҍO* ŐRs& yf:ꆺi;f<+* Zk= /++2ӌJ%pp4'L2m&ÇL" E[&ќ!HRƣr$( 6ʩP<4kEhR.)JZ+K-.P1ͤ@pB+F:<--O>4AE2܌-wQE Rxs4å0 JSue\tf*/ߤX]WcVr+>zX8 VLdT l=:Mm_ٌn܇FPt{Y]QZQG* _}a%\S` UNa&uֈ1J0xD,ŐUHs2PVbe%V5"_ː9FZg|v<ݠ#W]9Db\jZ:6Ũ:ϫe˺ꪺXE/|ʲVJf81dMf;?h낓I#[m >ɻoʏԚܧ? ]+ӥR^R_#f`wZVg`|v7n&#A |Q+Bv2 T=Dd{#Ad"K䑐E+X1 M򈖼&mǂZ DEF‹@fք5H4E\05 _HDwKi Pc 7LBxᤅ.K 2 d6>(r 2 rs? b\(Ks0R@L"ͨB rB[& j薶D"VB 81̸юroP G4^r^[1TAՓn&R'! S֢$:+s<iO- Jue?˄Ք1( U"쬻=+5 irh]1׏ , +w>5VYF#(UULyٟvCfuG`<+AI TYUl@[>Lj ַ&z;qOr(2׮ˈG*5%Ѯ)Vuk}0̪W-nE_W2%Ze 9AB0lՂn9zF7& ; NpBkj5)fC?RKf_|i24|έ!GWsU Zr.1 gXx)$.LW'lD.8qw5}$wgqUrT,oa^À=6+Ҥ/~ȝDg,. E2O@>8U1Mx@8wΠт̰\JCR4X\@OrƃcTEg ߽}K_.ONӴWhmiEcZ3Y jV{%Tk ׳: :3BUc;j lf>@0XAj8[K(;>>L?cѐJ {s?:ȏ/802i#A4 H׫l%_C+_TG$-Mx" {>LswZPO@B۫A[ @D +_`9ƛh E\]lE#3)4(=0hQK:i, Pb|n,غڻIJLI7#$ت-@Q0 dA!D>HiX8UD3Ђ p;# EҾQL1^ .t< t(Ns@9#ˁfkd2ϖ6lsČƜOO*q;FL*m*yB@ќǀ ~ =*PpMP9P*(N p0x1;Ҹu H <'B3 H K!f"M!@$$DRRH̅*՟Bt4ѺBiP x(XM7}7* t9=)p7̀pDZAeG,Ea&XFCa]&QtBJ^Iъ7q\LV?c P77 Y(P (`rE16pK>i ǀT9C{`Vu^eeb,aW-4-Ŵn 󲔝t"f*%"$%? yB(~ Ȩ;T-*Nc1. (@ٝgH 8Tc 'We(^`8X gXa`,U6cZeX.K"]d) 2 O6:M%.='le%&(($F7|5OE[P~l(d]Pdnf;~=*O1)o>}KzqfRK[JHvaw9$_ i7 0.ghe- S b\h8MF*"bx"ff EcX<~Ö.1@N XiƎc9iu13gbϿ5[jڡ h啪aĘ̩^*wQ0|a΁5kmܛo*P ۛ`kz lPei~Gqi϶5gVh좶\|Շg%F zjίcƬ(Ӯz[%?6kBks 20>kDK $8zI$ "OT} 睥yr֗>9O9VxT}jg+O$Z$3exe$-7Vֲ".:rNȍ<-o~uz_ p?ZTz!g|?9ǯzfƪť*sj--$W?pۿ}(4AÈ ?hA PC r8"A$) J-Q8iV0bfάf͜v<'Р@Egl޼])ΜRZ3֬Fz2ab*k6ڴ~k6\n +6v[hغxC+l0g҂1>v̗IMxlr3ПG46DX@˲gӮ]Y`7nbE/Jc6LA *&j6B*P+zj _͞Mkۿp2\#~M,cIFmmVZg@!k`!V=*,PCU4U5r$9YdR 1iwN=QI)%\' `cG}jW XX~)昆% c >dIX^!mZk &"!faC%Qvq4PC>$?ja2KؔS5>cL6):SOXT_}[%Z_ay lj&fpB8ۜ'{z-m@*p }CB(F=t5`:ŦqD G&ΫRzkV._;,^@ fʩgzaj|Rk-%_@ hbv.iq6{)V1F#̨RUS,0@D0zX /UO\14Ufc!t~c28QG̍n$0 )͑F4v͡U?ud#FWt!:өN."' a !ςD-8a Kxx? AqY4+4CB8-Ĭ@ D!(GQYD'شԛqdr>"NyWKBѝB%>w U "1T}*ϋQEgrEXͪ-ս̪+UKU#Z'0;4Qf3(ҳ_ pB#"VQ2+p "0T7-Ʋ*YS~V'UNF;=^ojZ6LځLۦ["fhj-!msCr$wԄ~ x2ǛlY-laO\ d *T0_j-BSJ;/ki h9ֶ(t> o Y6j4)^b@p[uFtMYSIQ zP^lR AD(> K bB& ^S9P*6OfaWV29i * 3 l么:T ,\ ?3CU9%2@]>E.+ p@>2 w,!`-$5P4&$Tp ~E=꽜y!Ӂa {z / ^tүmk&\2怯C-pTΌVjməٻ̀v3a|hb!p; Id 4AU${qvU^5a$W@'$ U}Z32{;m >~ٶYr~(F{3>yaFjnh /]BƝ)Ma N ΃ȀMȂ0{++T/(/epQvrt;~2k ݛg @l·/vs89_\(E :>t r Gё.w4+Y]aݨžU[^]|١h)% Fܱ8]ǰY9tq_}FGdj:_ (Ļ8Pu(A GěyU5T|h]CB ~0a5BŮLt]`I*eth ǽjߩ =@ \tX&4)< a  uDJE`k..}*"Άɸlm  ZQ ֠"*b## 0Q[ d"l_)ڼt'00)bXhQ1P+bj-E.2ƚ/bơԊH߆8c@ L :a"R̥_Ȁc8zM%Q9jČ، KcrA;";* zv#|Ca@*T,cAAR_𙐁,%m%(#Z͡1]E2 o34:H IB07nBBh@VDՍnL%#pvⷔ@P@K'E|W <->e(8eUZ|zdąeeNv] 8I%#<@\%eIo#a_nI`ܜ(c>&(@(a e1 R7'|l1_8iZe-uP%-Eq/b0&XJ0j@hpJ⠟}'!m^n2.#BHFLF`^ @pc~('$Gz>QÖuf?F|)YUݧA,bQj&邀6RƑb/|mm FQ]c5ҐrޠI $@F\:dhwh'Bc $G {Ja)_P%i"b!~\d#D\l1š iip!̍qZ#]>!o!J֩"6 S#|t y͋%| 8$NţF*)k @VFҢW Fʖ.>VX*3VY:ql6b(A5n nsvjDka&ֺ\W 4&j(^f,zhJk0.a ,aMxEi2 1!rV5(bJfEjW9tțn0,*"Jliy $;^DAD *:xJL,||+ƧW-"i̾,ΧlV,gBvxe5ΖFЮ%hyJGdWu=Dq6mK=̰k 5bAx)FU.$\얭@"j (z{:$-ka n.B*T QfGd 2O]F-|n*n7"fte d"/xn rKu+ ~RnSoɎ $ńNxelN/m,nԡn ㍰i"b^nHY:lޙF̤ېc\W\ؒ-mol| -×mݚ,40"0Gq'rb 2nZ ,] 'Tn."!~3-$boD,p`0*0G7W`!;J%&UIk7!lg8 #j,NqMo_p . o2\s%hFqhVOl11tl,@`WYuCuͪ\Ʊ_.mHbH(118Í43*^3Tr{C ~<Ȯ7  (9hdXU;ψ$[L; $)++6x1Q`@> <%Lm|H" -X(l0DsF,DySS($-4{Q H#NM>&+ТJPRJ /3+&L#| Ϻ3jM۳j(( 1BC)DG}+\ (tوuQ$y/=^2TP *RK=Md2(k Zm-,I;K2S%MP-[ّ!hT B<=X#H Bih\e#w6#v#{oW{qj}t钐ᾁEUȃ>9$36MVKj}b\c/.;kKdK0ʬ8*XiL <+mly!D8yZhS<㕗Ew:WYfe&뮋l9 {4~ /;Qr a>-$,J#S|o'Al7Uj{XOr;id^i?s\̜R]ou(ִ#;gM#;GmU[$Dy,Xq$a,aWm1wr;8Լ<.pBAoq"(Nr ^M׿uXN8 `]Jv8P\v0FK[ HB&iqxtBP4 C @"lhr> VEHh4C(FG5E>TiETƃ t;ȅlO|T=@!{WA9QoZ yKXB E")ďi'$w_F:uD(cgRd ۪VVVPmcLK]nޛH@_rp2-4&4@ P-ar -<"b04Fs伟9ωzSt;(ϿГ4P>U!4l2%$PƲӼ]J @CIt;fA b#Ҩ4TaR;ArKDEH70L'Y_)yVQ,lU HV<&6 bjʙZmCկVe;t e ch!yĵvP8Hsu4Xa IbuN³ =TL4Z ڄ7ebO{dف ߒt,0t"|^j%*U!ܶ(J.R`p® )BtKccy#üb^ɠ{ ihکZ8WMm\TҨuۓs2!|KsWK?.[XTfcY"*XP|D夶ėXf+޲8X/Z;N2XF88u`U )%ny%>S8;F9f!f h6%?/09ts@Y)|sCUFMfqAAGlV07*#44C(3*,D̓zfh*YTN?S |),gh44iFxt- -yH騋Hpv@jp@/ AJخ3ⰊD@ @LDqz "Uc Y 0C&U:u0q,9K USМM,t۬fMV! X5j"Y? a 1UQQI`'/ X~S8J-Kӕ9-T3?5(U^Ur_4 )`4#=+MJa7X;FlwtbP$rc5UdmZo/~1RC̵Sa]e٨f4"Ţgv6^gC6BV{-iV3i+53VjX;EBkk?7K7dZyTZQ:SA줟40 w%a[,fTn3iҘT&pyVvǫ Lkh r!wPT!}TWuFaa,375WslY78DbGZMctH[IPcR pW @:B.Jt1.t2gcp:'Szg-i[@={0YCWi}nod~OW~Qw4RN$Dٻk6LG+K2bJ?r;y(yP<¦䋾[*#5N`[.$ O95PEDM\Q۽ŭCF(skQ/ @oIwu?eUp,l\LDAX0[j58 /tjX s\ͼ\O{;ƺB?z2-lyw{ 2ePdDHұ" [ʫ 5cO"ٟө@%[ Ŝ^]H\ݜ~L /]Sn|\U)pX qs[zM\]%==1H{ֿwriioj5K0SseL\dPUDɥH^e5j.Jۯ 2D6-]λB)S\^cIwS._3V_V"nHyS ww'{40+^Y~M_]bd[5c_[,!bu?xaݴB@ z@Y z1!-ZjYlj:zx*J<2#R   g<{ h8zʥ&?,5TYX+ LzMyX[={صlo䠡B ڭ{!\#jpB%رh1bŠA-*9͙yf Æ KN+TOXF}YkW.{@&Μ>h 6T%*ZvYoE Zmo]y^{oT 3"Nd Hfhd@)ZvlwI-śo3tGO@P@iK#8%tQGuՆۅ ,ꭇW{Up >$E_b0`1ȥvBRZI*QTaF&:JRL>p@#G:̍4R}5I4VUybTln~$xBP\z1d|O:!唃7 -X e` &hf+ I,XX[O6'J*p 4ZBoF˫^}=E`WTQmճz&L]n+QYRx~!#'P'TB^|1uCxcz#ոVVA}A}9{UғPef-n:g{:쯌Mv?9v7/O{dlAE)ளMpeQrkYd6y TC·>8! Q҂ ӈNb~VW *';Nm mF9Esk-Qu<$D ΘrG[44.^JPWoszuȱA,bE<ͨSI(,B-P"$ Sx)/pVQ#ƶrs\^ȗ$UZ~s ^K iEB~͈ iH@2n+AW4/Wt7,dJJSeWy^$5E "\fM S`Ŵ1 s&#{s%ME(zcE#eDI:2vnjzTWO$ 3\>o* IB?^=ʿ\1S;ۛxQt;N`+fn ޴0I'Nq4h'\ȷ.p!IP HABDMQT,WLC'NuCV]VUdgX7VB'g=JZY)6u2+V պ5Т^3 Da x-r1T}f@Elbۿf:vDNZͬ6k2uj+#$2:Jc)ˡh/lkZVm-ġ}H{NlrDDUNdLzYfrd҄i]Q-Z1%U2^LK[ս(l ߼կ`s_XZkƤT6ĝnFikNÑ0&5lyT#$(WJH}Xy&uZյ,nojc86(~[si<, јCnnd(sKWm e)oVJ jNw)])1ue~[f3/r؉WKta2u1lB.l\@/Ow(U1zཊpwirҔ -Re>s@6o qmsL\mCA eB"gwG3:9@4U7%Iyjou9p$d$uMӝ6` @ >u'R`~wW"wgu'RI#ekgނ"Jҭly!W L/6qG>%W.0n`+& U) 4}|x?OZA襴GZɢWWwng~|5!nayP\s+2,;ROg{۰gIj衕J Fqz]axW uC~o7k_S>z엾L Eu$vk}8X?v{eU F1Ky=|u1ugfwq} y !r"' x}=}wgz!v 7~pR~*^%e4~~|r"${aafig9hp3F^a|!4V2Tggc y9j_`WDuvvqf 0Q 7sk7]a]w+H":iD(3ϳ(;xba$nİp  !!x~`:ȊJR,P6h|AƇtC3{8JP: I+2 8Y a ]y yyz YǠ p'+7LQxa2 U4As8ENؓ?iG<@eXiסBWufxL~%}9 R9])` c0diZY im!h,ȒtWy~T 0 \Yٞ] ac 0 j pd^83a©yiaN:|CBn3e>o&η6@g Y+zҢ]5j9P p:^q.jPlz}؜Jl!Z#KsYqL[[#,)0(+9pwz6Z\y `9 B:#(zDLʤNZY;{h YWF[jl*| *,Flv lJo /ZRixzzʫ] AZqʨo DQ*ZĩJ&$Z:dJ`y颱:W3zZ: `o),)hjFvZ<`% =`Ƙaڊ2#6Sx/ Z =d5)Jj*{A: hH:sV1Q*ʊ(跓iiܺۺA[3mᴃyXqϗ[E0"+; Y ڲ-2K6ۯ5ǤhQ@K|爴ͣ(x78\pE x1TFI֔Q% (; ij۶mp0Pz3#3t3PM5闞e:b^b='lBLgbD#0E>Y5;;,[۶+˺s uK:pz¬ׂ[`5O.Q{֡Ei#D9GS[Q_1 K݋@w yt7)us#19Ue!NtRKB@($l7(jI8}f5: 뺮н@lW|93$,:\aTy';D[aÕSKx,a]k}tG4.p@+MPʢ!kX,ś"L%LA=p>qs /|m,sy~#˱ek(=y܋*~GOIxq~UU* b;\\^캡J훬#ܻlb1]3 #uI)7l |B($ eA,r l,L-x\ ,N%elΦUj }N m= ݞVM ƞVi췢]2Χo ⳽{ǵOwMn1 ̪C ){Vl9No N[ (>{N[)SSN!,=atR*"5.~6ƪ=4^ qm~rݖA~xdVFΊ e.))T^V>,3,%kτ#ӵ=~9IX9*L|l0N6wJA:0Y4{=>w/"l9|ZqÃS@\_ Oi^M}^%p~ *~; lO.2N~s=BC_0Kّؾ[ R?,%ۜ" >aْ댺c~8OMȓiF$>@[LQ+*5ܞR@??eg?7yyO9qk»(6 i@P*_qO@ |NB ߙ;wDz]QcrI6OYx@8p 5m~SgL( JEEzTh0hԩ4\*Μ%~UuXdUWmYBuހq޽wh )2YĊ-dh-OYdΝ=lY44k/bHP= k}DQoرǐ#S./ c[rBs"A M m QÍ<iJ%U8x  FEZ)jǬzQ' ;O"=R.k2TQ2"Zxj2:-XL3&p4zG_MT;!**N;m<}D?UkдTx% ҖӪRtE2Q Šc^W7uF2nu0WXUS:vbcqkH S-bdD]T \KW] K+Է^)hV(`,0s5{u aWC6#mY<7޳c>.A޺6Г9Ȁ*cYFo*Hifݝk硛/ SQnAvҾ!xj4&\7; h$H۹릪Z9ooHyTܙxzv%k Pb,ߵ5 <.ϗ 8!vHi'Vi/U@ՊP4@AT|,x“g<%S(A P >x~ֻB#fq`M`ٰڗĥʄs!qii6 Y ǡw ~ FH'/%Z3'ĉO e@XQYERg՘xу~ (G1U"lU+3yXFyMKE.JՐ:JIF7 \ / V0 0M[ψFíHmdcEBc4%(! UҬjt}zH5 Y FCLA< IVܠ0)cCPE(Ba S#1`R Y |fxk,bC=b/H QR4fw$)sy?C3CŗHܰӝ?CPQ yF7MCzKXfTϧ *W5$" G.]B_}M;<Ʌ㞕D+Zvì',50'8! ARRR  +ϳ#Dؑ4lfy(SpԷj+/T7Ե鼠)`m)[JE4 k\8'7x\X4׹2z1]V7W2I oCoS;ӫEoz \BUЂ'Dp%P AJxjĊaۀfDʷ`vGsXtHJ`ӕi-똃Č˜Uϙu{Q.c?nF䦖7ㅲ!{擛I5J.sy`"ʆ JPf6Yktv)yYg9t ohtռhC'or*pAiNsz [BNo[(50V!՛>jZ:uuk^zfv=+\A *,! ,^8 *L"J"FBJ<: 6M;u A~\Gɓ(MsKxɜ)Kxr̹Ymͤ 豣G6k6mΧPYRjʜ4ojV6%ٱ\|ɶm۝nҌYHuQ.U˗N >XX~ODpa;vX"QQ{N:}QkIr׮]žM;V.4jU4dIv^μ7mמ>wwM;u#{2{H3oN9 $h0 +Oh"QG r-uGlWK]v[NNzMZ STd/Mm&r^X(Y0#y9x[We!Jz-Ք^ zoE/|PP)~Jtfx40swfa~'6n n.Yd֘3*nG8RM%fZ|ɥf]K$|1)RN_Vv'U9cfGi Gg!:͢m2c3<'9'^}e+tמaIfL&JfVdT3!d]}RP^ jUTaƽvX40(턮 Hv諆vŊl4>|Akq1j1% nbLF0kn3[jK0` *0=l%TqJ\RW0 \wqqH!K S4ul$3h2e۩ Fb3ܕHA'8іC Kw'IlĹ:`~C[Y"424:a2r@7N7ޔ A$7A!0?4i#+>2L[?ᛗaƨĴ~]wOA !ĩ 6'Rys=hK`蠞p72kX{w5Cq{( 4Lg ʦk>e-0j4َnHt]PA ix#-l>hSZW@txHa-hj`&8YT6GSڲ!M2F kxC:ͅ#D/v+) IpBe!O_"!w=UCq*%X2d?>QZ$Ly{czC!^B6RD[4q(d &.r|#خH҈ @@3k]cUҖ b,Mqa~PuU-AK} iF #|F2eNT8c=>j*G̦60l@?߄Y8WN (! #"`犬cҤAnԆH9TisFL?ā@ŝ妡Kg,>hC+JcU?G?[L&>4ě)͔RF O;9jP (GMz1qa!AV)j׆&թR#נ٣{|XJѲbYBn FJ,pa>W9uIMj^c,U`(}(F 'wsh }-K^"mqO٪֌i<ѵ2e)[2i%Z9fo-o}{d+~;xq*9&D{KDnu]n׬ Md{Y~6hE,4a/})HKc귢Ҁ_!cЌD\[N0o`k8q#euJ.V~x#".1J]JIeRabWqA;V3C\ +-Ȉr{lI5aPέGnAq%@jm\Nu>%P_a @u9^c3y=q m`m=2~h >#F'l-;ifҽ4R{ PFpR0|u;PA VHU Gg1 Z"u=|QX42[l>ڰĶ[F7!i02KvD7]Qo}@^-%Uw* |0e!D󈻛*gqn|7A.6'Cw1'3dւ]'y0!7\X\ p 1pGkU[0 @ PbsVxrtT^X 4_ZE+A`8^O'dOfrP( lkp؁vxU P |H#y fEuuRR~w9ppI]7 ll-zF9Tih9٣aٟgn0Qv {)pR̹t fpu3%L8shVYu .*_E raX'pʋ=zi9sZz٠ڛ:z嗥7,\ bJ*u3Еu G 5$*ʢLMɧXƬ Ơ ) kحN؋S) J54V*Z*,'e %@07ړp>uH3y0J `T*׬rڣj + oɐZ S xۺZ*U 9Ɖ8!p12ʯ*#P>IiZ/u k 39v ٱ !;c) iw գ)l fm>p *0#eCX*(`!ڪ FDJ 7X8 WYhA]AH)yYkʊ|hd`׋=*gm;X *mنwȩǷڔx,GjΙ)PRjy\fM]7l)1ȔKrTr֎;Y{`Ye;i|{ë[Ax5˼+\[ٛ 7Puyc0 dh پz.*+rgs m  Kܿ^y+zz w %x[!iLk>`:9t܉庆5lsv0,ëh5lɊ(vYļ˻f+&۱Bꚽ TX%`)Ukƙdw 3^[[dԚj| ٱفںޜ LpY W\Ue;N",= &܆-7v| Ȃ̏97*l5}lx L͌ p gΉGʦ ʣz(.B(ϮZՉKHzxn Gݙ#zбК nȐݣڼo8rؖ̄w[ pŦ$ݷ&Η!j .ҙe+ k*p鈉ۉ ̈ntʙ_tœPuK_\+gGYR-_{C[m*۲ f}ֽi Xևo-t]/itܸj7 k<щܬ6G=Õ<b-Lg)ݢ At*S p ]l}ۧnW)ҌGp4=ܩAxH8ǜxØYFCIyn7j,y,Sy q޼}{l dg -R}(>Lu]ө׮ʩ~\KIDCS P BaKeܖ  /Խ64ζL 7.ְvaN|cߵ 0펾M Y,x 0.d5~^ئ_{£}V/ R ĜMJ/w 9%6w_bx_}˸{p?kM$O\tmW(]+Bk:bZA`Kd ֐ ODw@N@w 2<_+OdR?n 5=!k1wg jmkq/Lsuo{~D IJJpTc$?2턐d  gSU߿prց_|b -dpa1ĈբXD>hؑcJ@ɓ$UdE!TD tF`g6s"= 8PH%QlѬZ=V9tiծEڴftK'Z…w&\aĉ-^1bƑ%3;ٲek5oPgaD&] U54W'ICD(J1URMuӂr`Q"S}IuVuYkmI&LPr6x8 J&Ԁ[*z^fc`j! 'B 3ēZ ˵\\b)/ڋ"MƜ}޴=_ ]_`$E4Gma&\|%޻dlTc5cW?&"dO&)'wmeСj;cqE1SHp̰[T}ƅL!=%C@1yE{_ '7__nko^67u9_~b.SbLmDZ*rAR*'`Ns\Tv 8@40:ҕ *,[3Yb] ~$4aib!a KLB2$ MD"yBa4ez17 1{@|ѥojv)^F!8y4-')sQ L  PFǛWGdJaT9…HD"a9Aw iUye29-t$~_ÛGE+R )oad,4Grݸ҉'b@RGl@8ـ9u|I'T;!wBpO3 > Dae WEBd\n-o)L/^|bԟBˊmX0P)spT K܈j%~ul {<.G$! ")Nno[n4qwފPd>z^[1%@?-w42+hTB g`:v@ \$,WkQj% XReD4bQ }h8 }9B4vdT 4@c!ó(C|O%  vlm ch*²NLX˷ ;33] E[\ M[zCtx^ {%ޖG6@3LӟR)Wn^M$ ~[!Of61FlėdOh\Ct.n/vEX rЄXu|2(9NsX;aM thVJy((!* Bm"[ﳽuB"ŮqGJ4`;* CӎTOn/-n5[3? _s`9'=2{1W9`AmH*l3 Q (@SCS mk % 'f8E#miCy8ٕ^Ud Ó,c5wfł}ʦ-87@/Bl`VogAp ;{%3<;`[XYh`6'=;h6(-b>+rᘾIpq9YS{9 У_$P|;(3*P8ؿ"0? 9`j8YPN@2TB f6 P;Ӑ"A!A2,/UHA <ssēAAβ Yȁ/rB4ѻ+x&ԁ+ )5d"@ ;.ni(ZK@D;0ݹ6Ys' ;;9\Ƅ DXrkÃC\SD )GDxKРR㕟aNc5O+؂*8E}<4g 9k:Ӟ#ncVD`'-06@CFj3C:4i @A$ĕF +ʣ ȁ~jR w|G!l  i{ |,?I:yS)AIhҡIt7*Ҡj"5BBJ)hA{GGʬ50K(rPr9Tѡ;  е ؂B㽺T;HlK8@LL!/&HL`ntHw lʢJ$:#\? N{JjAMDM; sMhhJ\"uHP;YD28H*U[K@δKi:6d6Nh L+,c[N?$ O|/d?T&` 0hZ)KӄլֻHfhU 1pNbX9΍K`kL8Se$, & L #&(e& ! J(T*4t/%%.JPuhV@$6uS9 cNi[(X/, ׼% =D^n&arM@OG#lJڡ۱-5ZXgЌl` lOE "(_bHQ]dѶ5`k=^}ĺ`2~D N+96hc7n/nצp tk\`x96ԗ`1U_HFŪ"H-K+]MdQsXv SJ5#_ P yLY]Ğ,e Nӹ_Ĥ8W`5c֝ {@7a3Ub "(iZ1JMnfo@ag&=gt6^SngcrG5&gYw- N4USģP:V6CmYCUZuUFd 9EiPa`閦\јƔ.uFT>U>`&zgv/j~&j hh.:_`R}z[aNslPl< Q 1<#ZsJЂn!b@x߄, tx(fZ U'83U[amn;`vZ2ٿF#Þgn^Ll@(ͦ P:F lnj0߁@iP`i-iDZ]'m9]-9\-~ w _C UH7h!Q ?JNש *"/! 4@rU_OH/#?JEdSh0aa.k߆;S/>9sp[47#!fspNK8asi kl#8::q_kioՖcPO@IK't3 C ɆlQGwWG9wZ2O`P(9]px{j`ɾsbjY=>gfD&-/(R#_1*/okOEj2w9H RmL ȨfƿwXz7&L)hxh+L ̉NE aVv#?*p(XP>0P0'RhQ#Rc-f!tG-Y|h#;T &MZjv4Ӛ5-j(RЖ2)TaR {JUjZjm鵰b)k_5)\9I v1V /` F(ŋ/n0f1 7\2f,6`.m48@ KdXa톞9D 9dG&N# 1N|Q&bYRE&s>{*4.zk^]5UYrбcv/2Z\ݗ^QX3WNuu-{e`݀b5Y yA PX *Bg%0@"֚k!n-cC=o \E5PF5 sYL!N$Au5(لH}gS5B7{PǕ|gQşYY H`meu`z I]x _~րՀabްJFbf x#k iШ< !YB 7, TLYy]vZl,5IsNNEd* 4jjżXքYH)-{~:b2 %Rzif@kXةYjEdDM11OD?`<4Df I0lʪ SZ{d߶[5.5=g;//l$|8op"o뢿Y6ck $1ܪ:qG5 vڙA%¥(#2FK_2[f2'NӹS4 L7*2J (ji BtLu?M!2P `X麬jj(*QS`L+C 9ɌkŔ ̫F+R[jY)R>cRrHddV|k UL hO0-/jSې HRQl[Ihq"j[^miŢ\96pAl\ac c) crDZlRS F̦?N:^5/}Oɫ~ULSeeU*jp&l ?!7J`?@B+v*(7;"/\d,c,RqcIeW\d%\rV Y}IS7t7_L".!inj2S$!7lܒ^bp# 7FJ {O>VOLhFdbl5y CC ukq:0Xj,2A=ްN̺~VU"6O\"{ͦE@bt81hSaَщlEA`2fSx})͋des+-MjSz/ XpGM#'&@1|_+pN8ݶ`,E(A P_AA ҢŃd7ۄcHznĬ}+8JV$dbޓwMu~[]['ǁ}w/+ 5-ުY%l6iӈ[P*\U</xWgܻ΂mЋ^ż}K٨^?CZe;D# 1p]ٯ۰R=D eTpUaWtY[&AEbQ d!]|mTyUBy^\#h˶U]ahݱHn|Y0<@Ax@1]aH DDH^`oS^༹[   P9`|HŶܠV$ E _h_=Uaĝ?)QAVdp50%P o֩_)B@,p[!G|UA̍cM~ Q^|]abn N!:i V#<%fcpHt"ؽ DXaAPQDaؘRdb` WA/FmA#_{IaqS2^0l$bť4eݍM!:FUGDtDlUmFN^H[~pǙN#v^1iϛNgœ)ai&ធ}(ybAg(>&{NgBYMƲAUV^jFpjJWJ6# E*vr^nZej[$ݥI)Z'ifhN 'n,cFeX.*'>}BFbl,BdD~†kFFh*Qj[NcL͇ ڪ$l(u(iNn0gAQpq0ll2:TBȒ<^%!qD+~@ ۽j$ϾLTm;IB jhk=-~x P"Pfl"uEB ٚZ XEڑIAl6GςIBB El`lܑb.JmCHPNajlvf~}@įo.Jؖ@<_ @D"B#1mǼJv F8F ZR/`xM5-9ڤJ~o:(.e`@**&D~lmt@eowU^0aIݭ\J%cw9p/@ҀdK^pN.aoPPMdR hcF=X aٺ ob1c:DV.6+\ aj ɘfᒏIq~DH OQ<,qH&lI!A–qW-d 1bePFerhRAP7rm^qHpu̴G-h&X\1'OP?'`)+-ixM, LTU02Adͱk8@sDlRid"3L333SA"\BIK^3/VH_iX4s8sE#34 :r; < + >?211IC36NqG ˜D@3GI f XSiũ\wZo4ZZp K:s_at?SԘT3vO34Pα@kIFB}STOE]/ cZ5ܪa I}*XsB'4VTZcJ!]m#-i5_u}J%,ם~,ϨJ&P.6A i 1eSCdmd#%x AȄF' B,p6a5DbZ*J \{ \~ nt`6_'p| *DPg@lt&19VCOXMQv覀nAFswwXzol+ZO5\x\~t7o 884x?'57=К|t<^3clvg6 e@X2T'4BAl"(\Ʒ|w8Frm\*H7;8Mf܀h3T3?#nr/L_%?jyxaد;kAB05odY1bEF~s j`$zM OJGrs-S!ad:B:ݩٕWTՁ]Py4B#B-E:1:"愾6/tTŰH8w0Mo3FO{OS::9m@fn\ѳgOJ s{芷B _uސN||khb$FyKyt6Il}cFNȵ'lgFT{|fH̫C\Pzh+8| ~l,\Xezlq&}|W+0|'(5VTo6zܑtFٰ G<~q9_SQhD"[ƌ҈YAsZDkՂR KT-$FC$Fvd4+YtRXLhִYS0];y3WO=_} i\EҢ5 jZBM!@V4AEco KEZa٪(m[m-k^{A`"|p6vɓ)O\Ef9 $U ڱ`h͜lmltIUG EB#Yep*EfC{d>.G.5,TMJuk2|m}ڹvo|}7h ذblj/\@H PH3\!1A$!DĒH,ijΩFU2S\[Bj8mFq|+G2 zK\'zO> TtJTSjL-K0Ŵd!M4hdYysK&Q!Z+g(#;2Jhď EDsR'FeGu|eW*!V@L=u?-[7W0jr,LyMkI_ѫD!&<k)Te5i7lc:Ddٍ7ne\|\q%\qK4+@ l '/K`UUrNx1JX8װjb7[VmAĎh6Bbpv\T|FѠZF+ $}ڿ=`λkw^V\c@j sБfe,Ėդ5-Fo[~Eiyq{t%r&ۺT~wKt>\23H.K8.2S}3RH(w6xˍ%XE-Tш8F!(ͺ̈́y7"LGֻ l/?Sc$G>{>Y@(dի3cyԣ s 12[@'" Z_>G,&%k N!pE(3bKl*E':NH4CW$ẙ-?H\Rۥ {)>=SNJ [ Bi!5/sMlni&ŎN-7ކ'KdDd Lis^`<1I;ɢ/oqB`AYH1}(E0v (ۏbtvd:6T.:jbLm8ɯ}c('̞Z:jjJK"կ#iMov]P2(1h g= ӮeQrYd/BBD*͞\L[ fox(/0 E\E(- OtRP-Xk^,@oH 2)!T#,~DR88`FnǨĪk/ݴp "/bK6 tCZQ&1*J˷ .-M_PFIUiBkDjay1'M#q帍΄.|4"OG L1CP11hQbֈlptޢzq.@!/dGzMd*"X Qc>Reekq7 *el> 9'.(lM/iP.cDZD2j1  vqr!O |Foڏ2V#;2$$o%SҠVraHˊK(AQz2<~8^HœD)ժ2n*)!R+/`N|`0" Dkk8n-=#gs(.!E^2&r&GR48"P0ۑ0 `:*UP)@,2-/-탶@!} =QlВ%55h6#ybs9Fr.Y%ۀOCB6.-U"1IExa' Ԡ4 yT*B=8LyRL1: RM[5&ޔȴ-V봑NBO4AsHS PQSolE5I#uR.JOz5tA48EA!TT'JT-0 C5UwUEt܀)H4nEN7@WĤ8NS XsXX PKCmQuZ!-]'(U[c5SSAuTӕu UCU1^`nDT-.?_4uN/ K!aiFzY b &Pl/$ZݮcD(@7ud]@]U]5f=tfInv*)ՇnV4=h!?i Upqjq[[\;u\)lOlw9ߵ{|= WcVاW$z37Fo>@/F bp7$IqBh7JSt0Ls7WX6mi|Jl d)#Tt)L43Bnu-ZfO+ P+ ig0 `w4Bjg0V(Wc;6ېq$q$7d-r91wlW9s> axUL{@phl}[w}ݐjZ~IM@ a~wu&cMRsĀk #3K"&e/d(I8{"<.62WnY,PWUFͯ+iW>Ahv'؏x:X{se;;K2Po{}O<*00,}'wz]\*&ܭeA5|{$F3hTTWs;E8e1mױ\2 oBh]zȝ#ȋ\ץZ!H~^eJ){96T^4IR{ 7gd}ʼlnظurͣ'<#)m25xcП4]׉h;}|ś'}9jVCsu_@nÜ2gY\I]O *1=`B:!kX:.w#4҄k"٥ &گSEW7n{w_\Y jI]sݏa]9׍X߇࿂~],VO~Dt=<<+`XMO]C\},]o}j ^뽺݀$ f.9{sKuڧWu7]+\ࡾ? FP:6^mY+,;u-.+9 aJ9գC_گbj֟ٿ^Ngu?=ڗ[ݦ{JFP:kװ {1ĉ'"1F]^2$HT$KEkJ[XMJ̙x!g<{!臝rrxf:} 5ԩR`AwPW?}={D l۶}7.to@ޟxl b=PŌ/ur [v#…::čK'%2uǎVʅR,.aBy^zhNJR=Vx3\qByNޑ'V EYܐ)udUQe<RNYeBt0uiF_amVd zYR$kT5Pwzu't%[pzh.(O"P)PDe4BK.RFC~6jEo Q~ +2mPyފL5T,ӟeI֢aznl_jZFKCɫ.0|n126_#/0GT/rK-B%I50F #l>,gV^ƢiNcXcFF UiC.'/8#N\s4Z\ 1@3е}ttRoJ=5VsFiә:X,Ecw:_=ЀRmGd +|7E@C}4 nx80~C#31~0BnM>\R^zk :; hd w ]ȳtGf)ЭZ=*Yְ io35qPq3!̗>h«Yq(L7:DA]D?n~Ir"ʁUD]|1| [O{ޓ(02j?(76.o a<# Gĕ2,$4'H=8#OPs99"[E(y:pڋJ\quwEz1acU Bp_ o3_A/q4\aAҐ{a `Hcۄ$y@URYe&Ax&Vi'2|JBV2Z,[EƖEex9-4/70t\CfШGHjJs(iI9fx 'vdNLcr QΓ]=Oڅ ]9j:{@:#-,)hM|W8{<=A֏zlm+GGZjՐ4RڌH42j:ٯ6MrFQ(>TJKviVX(Qdj +, ϐ_}#d<걵f=Zo{[ir+JOեJ')M46+; 0K YtKILCRʙD^[ 9j[=cŭXҵ pVN:,*ʲŮR7] h̑z)\e )dey<{!j[xxkK|!į9y`(0S Fr3Je/{Վ!eu |F|(| xGxxЧlҷ&M#QDFT7m}WDySzea~,qa=~9 *K) mFߐŔ{tQggF|@HExxɷxG'h7Ixm8?2eAgu O 0 hQ(G(X~~iR(%O2L0TVv_@CX|y&lyNxNPG$:TXYW$Y(\؅#X~"Dch9`*)`6E07 AunԴG8|xg|)ńK77!'?EFT}DE"H!: F~s@/r4;u1h)fU L_UHe8kvtЄHV:]G(,8[qvKxE3xtwt'QkF'x/Yx | (2qtWDQ$"yyi+)Bq 0&# #~xnaPrΰj'y{6/p6`I7 ǓuWAv:EibDNXA5II{!SJ(;4X6p2_I .7QGh*s-9kypysC9 ;Yxzy|ٗNݷFdJiyƂ,G%}zX4I sofo}XВ9xvPpͧutHAYdw\Xudy%1xgrXeU2=%uXRk,iE܉?xɚu3z2u4%uq'I];џ!t1(.63 ʠ ZZڝIw! vIm&(z*:~ _P)8é;xA樊46@JwB{,JH>9'\6lX$%IYĥ ~t`@Iڟ臩[p 2 b%sJՙz:*C1w!Y :($0j)I zچea꛲m? $؍A0 ` jCgRZ[BG}JСh:*'[jȚZ}i,pӚ&lUp WV᪪wʝ>Qڤ :y`t1KHfIF٢Xfh+g,=XW;=#%pbڪG0 k%PpgdI;R"X`h0w'ƢgYT'qWF?K(TQ*A w4HnQs뇁Wzeg๏}Ũs[z+VӐFr['m\6Tc~2Zx pn'9_GMfiV뱴>5ٵ%ۚaW d2;l۶(K^J1kY¹JihA`VE Q8w@z[g9lk[6y+XH k +w[뺯JA0[ %ԻbLrF׿ ǝG\d \-<5 )X^d>БKlxdR* /0,&U7d9<- %a'"vHQ~J67J>@ȄD%|ŠZ\HK?x`l;'eõ gƆ-!'uܯ̖ǚ`,WTȆL"Kpſ{{sgk]“LɤikIe8:ßܲ\Du,uTbRI\SU˽lɒLżbd${͜Ɲ ,͢7enpʛYD0 γEVD?PSP m< \2|5<^pe] =;'=-Iڶ ` ʰR]@?SKK^ Cʿ$mһhxҝL9! 6dbw, IOiwmG]= QU|M $]]QH+ 7/] ""l]s0:mwy'7޼h]ùLȂث`zTվhkЃƜg@598}.b!ٞ>-3C,NM3Z˃R=GUU{{9p}E[c{ :mqӍ׭ p0`%001)B+9:pޜ 3p}vcuŔh|*Q=LkqilR[νٜn Er]ڬK\fvgΈ "[$n&1kb|5~Qヺ*z;wF^$ nĈm$!$5é9`#,[a^yqQqycfv(emiK?0-AÑq?}|~e``>iθc8'^GsfR4t5R鞎ҡNdߘ=^r0*Ñβndibއ^z3NPBc8ໃ#8>8stpWtu ̫v[P=#?kr<hD=׆jlj© .ؓfpBDB7HdRPRqH u$O,iM'K |`WzvQ\#8>DŜȋ;юRQ۾ U_/SJa/fGikrq/V)W5*4UBUV]^[ٳHE,ڱν g\uVwZc>5@`QBb-]zbCV(E+'qJ0MDYhǧgy3Ο={z(RkK}Mmh׌_Uṛ +]thBCzر֨ڳjY#kW=y770aTzt ?w("8Jh B 4+ M4 Tɵ`&:駟С Dp͚jE FFFv銱G7;H2򻺚w@;@a@fHj) =ıJ&cFATRG&yxIUU]tFe8lb\˲]D:8‰ lx`.ä+P,VmӣrT_~mR4DxH$gkA|oal&eB8VeX2~ər2Ɣ1%\B_8aT؀Ѕ/ eІ7a `j=ma0!pMeE) W\ E.PLT gDcոF6Za oB8G:sG=я9HBRATB ! ,^0p(\a"b 3jB% 8bƎD8:FKjӶŋ׭us[@F[l/ʴӧAٱ*TiXYݺThѣ-u)ְfrlӞpa]6ܴq֬3fbaJB \HaÃ-ZHy3k#*ǘIU-Ըkheˎ鮶۸s+z]Wͯȓ޻W&;Iv[ݽyۻao:hSݻw:q5'OԀb|B1CdQFfG@W'FVTS2ԄUwԌTd'J0~tj-BJR*k/U EN#[koWuk1ӣ-yLEk\Yŗ_Y@ lV,ll C{^!%!r 'զ",fISF*𷯚k733R[$>s6ְ̲5.kCꐪ՗a&Bd&ek!"_Q2RMs5߀sys]w@22.1"c M\砃[2{72v~w-Ae .h0]݂`l (0AىfڞHZ,y1}U_^v88r=[F j‹EcY9pf䵖.w[+S0euhNIJ6Y-뺗&H#):C$ÞQ+b掘Dωt]h4*veZ)WR6sPFuE*tw9O!HB 9HP 1E'wHM<#?)BCd%sL 4l]<R**`+Fe⟱T -k?{E,.;Iw B`̌r>e6󙜘-D˖C`2nMf㜒,zdKwf͞R=Jԡ򓕰QW2HWS(CSˇr0Ú(EAbTPf!%"a hխJYz<{X>ru6S5(*/tX&㰦\d{z7H5 Tu(լ2WVMW(h@j^J*moF~ahW!vqئ9+X[ΰ?u2&e+r4d%{P#D2A^1pC,:ZFUe23Dc8k#gH_KԷL16-nA²Ȱ7 Wxx|buP kwG\V;v"|# _CTBtVzliki$j`;_5(\aC`.為Z1si4+7"&3Fl|<:~/9P&aB&2"_b$9ɧd2 `8Ei/gg>l]+2l3ɽ=f Jc}ܦ}q@Z@84 @'>9ʽŴ1}N[ QkRZ^;Lo^y؍5@}*e1 ՊDv b%bRC?C怡I*a mD':A,nGq>7STvݬw9T2* +CxtOq1p&y N/BuF xl! ԂW Ml'BL_x9|sxzrM6 ve09Lԟٟ:܉zz@{ 8Z<3܌3\7PV|Ƈ|3Wx\|ϗa]Eb H]7}_g-~7G~7,r~|47B7P5I0V ' RP{gvfWNFwD||hąY?Un'fs6΅t\HJTaCshrWU&8~(l’p&t!hu3q Yfg_IxKrPr2:?bxW(oZ\9],: X9ag ew3ާPmgo&G귇_77pԃ?vJSrCwP|wPfnOH4@ha\]t7J x6HʨDu (Hlu؏,^0|x,*@&2~LڦDw:dptҠ}W(nP`aߨ G:w\ )93iC[skG\c!`*u&(01GwH|Hh%Ph5!c5MpoJVRT XP` "`$yC:) ߈t-)KȗX4yfWH]u}w3pMY(P(GQuNI08Z "ו_)w g jl)eՈeqiC& (yȅXL@uXg`( ؘUi%Gi.љd%<'vRٕ E ` ` ǰٖIa)W$|Υiڥ1Ix U4\dqt9p)Y0ٔ  c52̴8:%[oj`Y>` `$c0Wgn]oƠG*Z\7bW  ik#( m%u޹cɢ{ p~ccŐ#ml MieC Ǡ @ b f٤NwRiVU t߸a`ffJT`jXu9sեiס>0Ax䗧zdc* Z2VIBrn9zRH`3 wMN` j8תg UID+\cp`EŠ:Ji@V(F}ht{D _FVDVԤH9R3 0 bA=3 w{{|+XU=  iE86Wt]b;I(?Y3F g'˧dgBJꭃx!ey/; ?r@TBWIkK۴NQS2\xIV8Fd$`KJ79+3M ԹY!pr; {D 45E6 /X @W8Cɚ̾V\1W{OkX֕LLʧdyk?Y o KIqE pPl%#lhv\j65*~| p ͚ z 0ɥc f ě\jG#<(E5X˵\Zʧ 9l Kb,d }:晼\ah,h3[7?0MY ֜4]4 4 Wg7'5\<~>Nheɭ*at e}pW: % ]Ѻ PrZ 7k]VZDMQ.E`϶k:]nfM]aX dn&5-[ sNMulܺl;B# _Y< =荞IPDNM8@JĴE~NXd7 p+ɕ8qdκw6HOTc?ñP ‡(`!C%N81"$8p @!E(UȑCdΤ M*1$I(EI :t@\2YĪYZWsĎkYdѢږدq[]֮)S&̯.rU8W1&f¼ONVXd| 90/ǑA&][Ă;vI?hY`…uW[a9Y۸(KȱH1iN`ɝ4q#EԆ R]R%Z:lٺi7˵sʫ@k4K^#pBa@1&K 30ӌ4H4KM5\M6D2t7#*!:j ঃ>@jtډ$(P ΛʅҪY"F>k- k?5Lsζ0[rE1 k P,+41\u4[{-j{dl(B@Pkz<΀xvjDnɛ̩8HAH"a.0i4k::Na`P? A)3Ce{^|TEJ[ R4uPEUTSQQU[}${8L©Ie;"J."%iy3o[6]뜴EpT&>CL] kA.C;/{iF1\lYH > 4`8vV|JЮb3f`UP5XM2Yjiof{/ .3AI#CѪKzZ޺h{SoH5?j5U 9 $b \]FŻK ||k -^l1>91 PM_CiM<`\:@v؈p'7*V<5I'޾üEpc(>>g:Nj=QMBg4 hN?$2EX@L&lUv`@,]PG51 ͫxYosdaVC,\p ~c !!:i|Zi.-,u$p"-Ф7R,fQ]`[Ygd`ѨBq WG1! d!BPIħF5dP-l$'c*P.L2@IdD#NgF5)y6-h N- dxz/}dN|\1L QC>gA*sfIilc L](NwRvM0ʻsR{bIjB7p{mR GQB 4>/KH*i~UK2M[Ć8e:Sٙ)'Vel p ePTD4N}*}b(̡ڙ9I'6]"G+9$\kk][q\]xn`P Z#svڀ,w< `1Vy@pP3,5>ֻ kw% 7/Fd>tI|m}5*}ŵ ީvTo'\-@_3ϥ#9IhRH˩*z*܊T{" 0xdhA?BQH};HcK!J393a/د|J"l W2IIJTp%Z ' b.e3:f8[ - p,(Kھ͸ƅ- xB#mk}\xB&qJ"c 4}J'UK%ЈK`q`99sUB$o_y-t>Y>-'-_2zC0V4j>ڵ)SB1fTmiN{,Ld2j=%aIXL&&$54x}68!^*{.n,0{vW }kiW1Ic5wMiNs|ހ5n#P&5_ [;<~7b'xn[aX4^DZDs+~g+DTمrWx]q$[ĸ/yP$<$ 08 $<>Ѕ]Gd,opw>< 1dץq6%f8#j'nY|N4l #ϓ YZg?uޜyo &>uQӀ˂Ӻ`:8c 3޳8zj0okDhD:``j>ı竜r> Ӿks+3?5K?Na9A@/BB4:wj; T"Rɲ&!2#2./L,=;÷4;QOHr i8VIHD;0'聩x{18DhP  >zHL9>K)2 d!Gй%{ —QU /AW$3,y먀 L@[-[K@\k@;*w@Chk$*p1h1b uH9i<(A}[њDM0!Ĉ9&G?x3;I7$Ic3u(s8LuhO̚ ;[?L i&CMb&R-(ETRjR =XҺ㾵\(^}(Y`Ja֊0' [,ȑdUVg LV01َ/'p)W*()(϶G-x%I9!#*Xmd|b0R劅EUUUjMb@4M-R/X2 UY-Y֒%p2 E0:ل4=˃Eٍَ币)ݢ)8bW12,c&h,G =մ]4$϶R@4=AĨj1(U^u<=Y-']h\?fӓSX,\+\Y+5Zu$0o66V 9T]x[XV^ )^Vߨ=mYaV1dU<_J  0i7nT@&fm-oVlTuv0vֽU@1pm*Y?e3.pa8pF`e dH p ;f *7P d &f攖a@^qYƆlXoɈit.UlT6d 0򩖐 UWp>ݞc:hs _G}U9/sδLq#>iXl^F׭X  wb@©Kh*owoQl ^TuYoO]*I %v%nvjӦrOkx37z|4)RSh{wHt$rk֒$X b2uV[J3P#"G4A$(J| f6`A(<8p @BJČaCˏ-~h*HZr+؛K,{34n$B ܸJ RYL&М@.l0bh֠1n|-ZƒK`cċMvsBb*< j퐖ĉ/*ܺw|ɡi fC <#PHTU^%ɝF E\pU,ؙg ́k&(в҉0€FgzF+7h&Q :.! o4R)tGJVS L1imK`c >5x1A(#V?mn(":o{)2kBRn'%iwdCIvY[rWAݪ`hØ0f Lv2}oZe/\r>v._S$'H8e@P`k>Q׾0y 3i M5MYѨ"2%$Qa_&5$UY9RT֩8 (*)3@X;= Dg*S!TOPdb)EXKE4 \ ג:S R4,mi6_ tg2)4GΡ RMOʒ+j)gݳ_OuXr5 S=VIਭvի촠hwGngDhQd#А+p=Z#mP^A6gxn Ko*V8)îLJ*.6;U@bSE, Ur;3m]\mmيy%pG3h ܸҕIi O/K(GcAvҷ,y$&L- 2)Syd0mHEVbٚe&]^Z lX ^ GȅfK\ ?pF(VXD uêW_K7Tؠe|lw˭ooqxѻU|yp4QBY gl܄t]` 0 <4x:ۙ*8䊰kQqbkeyo6')Ђ~U vX0pb"b\(Ɓ;d2a Mo gZԣWM# SZ!a5%}+[*/uoR9`T豑~%&z(0Dp>['r5E!.nqZkˍ\!}=fBV3AM{W[ * mbTH9d#^%`6C:O.mh=÷iB22CC 28f)TX/j}z/km:P_q[Iξƍ* y#'w{'7r\13^,l1.OGur萏ƥ\G%~M$O0'K:`Bwweb@L e؀_@9|2%vhpXiej7'lHmQ O`_SiU>crrq*^ % %t6  cǝxB7lgaTC0h%'<$y'hN&g7 |glƿ@=i (@w gr_eb*f" Tg "HD29L6 C-\B#g=dN4JbKm[y [k& *:-h-,ҩ]ҁ]&?8g~4"bgVBшFjoEdWnzj⚂Y|Q@䑀`ꩼ)vWnR|_$/bPv] @d:.#F`dC5 C0!viɧqf游i4p:^a )њ* N YJ[ (^/$-B'agad5T* Zl[Y뵆&ݝvk.tψk| Pk)%[Ke (  b "& B\.,6Y5th5Є(Vrl^jS6l(,"F, *+ڬ J*}:'&[$(,TY%oVNCWL'H%nPS.ˈp,,X'(+(O"*˺J(WIwZ+fCT1"g-`% sBDHe"7:b<"D6f+B%\Ӟ nFbq rX֜J~W+mWqg޾q///@0A07lg>/5T4Bx^22-8-b5W5"6/t$,x7*0l3Qs ײ~]esPw=bkʬ@A0ۑX,g>/J'え^453޲L˙[HK'tI4Ja9l;~5R\ ttנۢ!F^!PQt  !XT4ԪB#6lVKO.`ƷEFou4bII3[9@5Lj&QdXJ]_`nq,daU.,Pqbbt@]AdA'T1ՊA1s44}5i3i jjLJt'vmٍno _`G" -3wssbtse!X'؂4PD̂<8G"zېX_7Tk_k-}oQ7]Yro/Kq \Sl/;tcSwu\wS#JD܂#vδ6~njo0xJu~s]ΖOyF27+LW$v SyaPw4uc["vkyWh\jpZcXˏOnH$q' X+^Pv݄+wt;v?a,'- B|ځݝTk$fֹ|9p잘osRW+79?J;88H \:;0 ~tǃz[񊉹Kk~jVc;z|cj¬;f"+GoQKT!;N  % \S\ U {;Ղc)Ɓbg/˷<ۋ#͟tb͏ZJ)xzlrl:!{w. ۠ gDej昺摣WadۗFatŽܓ*LBtXK 3:M =퀅p8@ŴVE@C~K> \!2=s'4ɫzSڗo>׺Q~'*辠FĘ>D4p &\%TT#E'.PdHIXDI%QxfL11XaNI,YhcKrĩQNh+& ZVѢY* lXbr+VW]^m .\sҕ%n^q; pZTMJ"@bʼn <0bĖ-#G<!ƒ yqhы .pAj?4d}2bDk1r ! ..gDkt<F&]*SfUkV^5/yaiW] x`“| r,7.ӌ3@ALCm5HR6,MBRhD\i^*&n:.xzHBĠ8a**Nh=+΋F=r/.I ZlQ U 4lT4PdH k K@$3$NF8b:6c m:NdZKlS! Xf1ƈ#K;Wh.:ɣ_2Z:>k>ր ݼ-J|Sm֛ユ;zp4 c8w& -ǜ}7XV>[,{̀Ȯ1DBt7 3hRl9,@򖷼 +p5"7GT(w! /J+_Ԥu.}|Zd5IL_Za%w߲j Ks3`mf Tbt2NG 3ӸOip4B0i_j)c^5~~S&Ի ݑ>aH<"wA^X Ȁ#JPŊX ^&9 F̕Psr5c:@bN5 ݆rSh ?_n@}βe W'FZ*dJ咙 c'ˁf%JS !cbe+] 1edhC;nHeA~;aI s孌ЫQp١cI%%d779ƹSE+Brr"<Ș vXjjHpg{Ј@H~ 3s @5QAHBp+T Q2 cGhE=$Lf'2iIIPc)^v X`ӛn#)oB@5C}@QJ*<4EybhWVfRYK֑~¤n5Dr*-r`u]Ks<`TpyO7!OUGre!5'e:x x-*iK{ZJu)YUڧm%l)WUvi~%kt($IF"b5Uj]b[=ZwIь״bToYWj5gUʰ5|\ s,P4 dAJfs=$2yuriedhwJc2bjFTe!Kcp&GIL6?`'#pYhk1Wu O-Ѽui3f4w6VX; V9ȷݴkh* =lƀŕxE]fRnAu{f pJpk\7ZNAW@l-#j^lgkfk)B`=Laho^=sǘ4Vnw{~B2{npw -7<Ȁ2-Gy*.^h89UND>rZY$76]QnӉ尣SjyuJܽ~U~ DA-\,Ş$NT?ΎI^vQMy]\`!J&nġ @d&<;M, ougoK%5 Wu͛Zyg7^zidu=ž.t0<xa/<^նNPpKbZJ4FXEX/9^r gj0S,e#->PPbTI'bX"!8 r8^]#O4&k1coo.\@Il-r*`PZb"B ; E vZC߬&0x8T5n ObPb|o mbJ1$po2'9 "tp$厅q&_8l aoNp &ņ2 q.̚H(hVZ/^PGxAė1F\hde18fQP2 BTtTQOFb2f!"EcbwDDԆre :aؐ"+r-,%N#9d!rpJ~lgEklm?J6hJ%$o'{'F34Qb{"OЫN<:ްt2bb=<>q$mR2X\Z &{~&&)D.&9i@ KDnR0 LªĂH4S/2><X0~D3N5 !nX{1l-1fnKZP '*N!y .";.9:̦j ;ó,S<dz<͓r=C= S!Db>s6c klq$j%ZH$H@@.A!0.tB'th44| 0R,",Ҽr#Dɓs4fVmq LyxyW6ڭ.@g2O׌"+87}[}qXy!m,w y3ǀۄ8UW_dw,x9l-\.~$|FKy^"cSsyutcx~}pmx"i׵wYe+0SXHF\\BP+P T Q"13H7y!>UF7}mԛ3elԉ7? /!աwݱP\t`.9 ߻7={= z~~[9z>+G#>m@vu o谞7,1ݧ_^tCXe~+R7,7bg~ا_5YlZ 3>1i ڶԹ~ 7䁧u Ym~J?}i]gAsyaWw[Wc*p8Bn1D"8!̙>pa‰CfNB{D cP6xeފkJ:BL ERH%2whi1;敭#Cɘ<~8G̠Jۄ]+B}őQS](F,s2MZ8_j=rnGVriNxthPAg n7$8e 8Ggҏ ' 9!V9'l,˚x)_k;%Tjj"/Y<}9L[89(8ِ9 9E8o*:AwK#~c <=:( ~_x4ui\d h3&p3.Ac#$,]p&L8@ >h)#Tx:"eW'>W#0ؖ6P_zD q1:⒒.ωl%8^)I7%UFP(@ DA4pQL<s_ ɰ<$E?"͒dC]lɫΉtKAXl2e-0Nv#2cp023BHլ5INFH7NO])yJt,ZZf'?} E阅ЙCяY3vqEidI8)΃toAe:YUL%r<{Gtz.o&ti>GkED%Qr*S%Uvt]HgȤ^9pLӪֵq ?2Ř:2gA ˈ. k"*Q lL]]FͱM|"[˛,9e̢kd+}_JiMѵ9Z<6!B9+~ lRz)h4oJBZ3@3wsK,L) :H%BsN]y]* {˝npvPU(dS xRr!A (zbep4Is7Әdž2QlŦ7#aWFwq Oȍq'X%_ȑh !ّ#ɑؑhMۧ)(.XZ9hhfAؐ@Xb Zjq&[Vh~R @"I#H&v!LCsmɈ n)ZC4jZE ECNP'!IБ 6\yhHhI$<+щHVd8x`yw&Gf!}Ӈjy﵎, Q)#-_V'a\feSG9҆!$RYAygGÛi阕q7ą.9ZXҜ9+?IIgm 5@E,p .VY頂깞ٞLMF9h3xe鉠؟im2Z[jX1iC V8MxY9s(Rن.!w%V0jnz[k5jk!1ܱ IFh *ZǤt,Z#Mr2c! z:Z֣ysXmmzLʨtyWV]1-gC& / 8ꥨZ1JęI<ب6ڜrZbqЩUCa ʬ `jҚҊCV1*&ѦF f:jE9QwBAPP b iJ] fVrts.HAjmep >`nڬZJ}Xd> Aؤ:wh,[HW%)Fu7uJ @ K" $ " +Hw Va7W@^psa,AKAKڛ: k{Zh/ǴQsDd%SvGSg3gQ e[1<2z8tr [$F3x {k=Ŵ[ 1[Y'렋>i5+#+z{h[:y3Ye.Ok;Gk^SZS }yy)K*븪1\~<fpKi/̻B~[L:y ۽PI [Vr;t9Q+SNH*O>(xӛT g̽f5kP2WXػ%dV ۜh1$l%S(, s"yYPI5\RՄh5CƆ-# ˾N[ﻤ Wh@LwW(04׌Bl5xp YЎ'Q<0 {LʴZ+bL"E33L;H9,Sttń\WpɯI}5QCvѯM  OPokҼ%|%o̥ 3U;3CMG^0CuQӸP {yM ~‘9C'H-؃M؃Gx w-ĐSIz!z @ դ'R UM#  % 9\b(='Zkx|pM??[IHJ }zݑ~ϝ 0Ӎ `׍ =Im  m ޵  ա-ڼa x_Ǹ8a wl:S t=8!w40Hw~ p &~)~F5 /N=ݵ0 ` 0 <= ;K  \2E>}؜`MKnw`N#P[Y4eE+I`mcoq.sBFQ}>ۿ Jp` N 2>p ~ .@]ـ]ܭ^CӀ S` }& .[."kϽQx.nƮʫL"pJ0   .Pޭ N꒝G(n.ŭ R`^@nBo /e.#O%o'o*(_c09Z79W<_??COE:I_MOA_GFI/=Lf\TTE! ,^`C0`XȰÇ!ƘHEBD9v,d’ɗ/c4ji֬[Ȉ+g6j@ hQvޱ[tiҧPߩ[իXi:wCÊ֔iԧL]ը۷D:m]dxEv-\ U^yJVMҦIwn=EviBLӒC3KS BsHwQ,Td˝?"qU /feʖyƝ}˟/?}wϿ*+fI5syK|;9KYAFYBZbƋҔb\ k@Vk;E--6OQJ@%LxIŷ4@15% b*HNZt >8*~x k=:gi8b3,kJcf]5X$!\@lv ~ }+TW7#^BM$Iڢ@(\92jjXO`BpXf>f)}[:?:!$;.v. 䟤hE\+?'d")'D%vhE*`ܠ9`c3 đ!yTxɔd‰[d /vc{ +"ЇVɉbJS4qqe! /b.2&߃ѴdЍlܜvbf2*g& =c2rv0xCHن*E*8KJN")dņIF08;A Y0"B>N|l +])KXZtF7z\Ʊ-qX<&2A'9ᒘFIMDC6XL8d΢Rd OvӝLZ$VWp|ނz+c"dRCjE2ML9;hEeG,"P"e( 'fd`.@טplҫ^Z|J @"NH"Px9'ܯSkx ZM 4Mf:۹xs(/EaXЈ~NJYbCA0G;t! e/i$+9i1DÚGའ: %$ٲe@x TkflvL9۹ΡUK$ n Ch%>Aw*4k<";vԺд9 ;77/4{Ԭ˄Y WZ 5ugk,`#е P^HbSYlcv;H KpH%&NBT'QUs둻hSҭnk\GH-oM'ǟ6Mp=CpwvʿY8p{HŽjL MD NpB`R"ogE3a dH lUyY\W'>Ћ.+&]&u7]g=ߠwuA!CY. +c`gv_CAmiK($A6ہ1|! AA;).Y }s5Mixғ FuUu{dV{^'eD$'ZA;#qeY`fc|!Ul%>&BxN>0*P5!)7ǀFՓh~̅ygy@71ioH-u$SoxE)^wc{/a瀧5(000f vx|J2fZ`!X*@&y7GH9x*0))`U0ha ?n=-?7h,N4DFt4iqcVtJoQSHVMjZjW5 Xk^3 P^6`A$r8w *p90}&XUTL9@)0~4x+~CWtV8i2jHFM-OYL33zHFL&3Td_?R 7(Ћ0q(kf_(G`?@UT֨Ѝ)W٠ @y-i鏅v9鉣I \^1 م xTɐY q雿O3$ʹI ٌ@Th;x'\X҈i~=gs깞]ʼnI0]X 3D *Yw|AD5_a9 |yٛv_$)hV˨fgL@ 2QaӁ((䙓*s=js@f4gGʄti#IT X^T\*^z5UeykJ|csj Zhâ-i.ǝ2 x!-%!(R5y62&E*cIZ! i(Y^lQ 9#ԡ^YQ)< %jțjPqP,Lf`W %!OzʢE: Q5x(sN~.J*oZxtʟU.O `}1^ʚ'kڐzijjְ,fA`#esȜΉlyJPhEPvP('EQ-v i1ۮ;!EYUc@ J `UѴ*Op@+%|eff`![ %WPJPKm%ĠOՐD;ivzu}n9KcEu:#Q{Pk pɹUۋXGw&lˤ@i` `; ± ['VKq g¥;N 8ȓ<[jö㒭'R B\7D|yH\ٚъ,fޠq@6N }~jiqۊ Le6_[{g雮@mP5|xw|Wr?D2tUDȓ͉V6 ddMGZ X۟>>j_hYHQ鮥eZJڮ^toe"xzHr`J!.΃gxWN9 ?>>:qŴmXx38sv>@, n?!O-`%+ p|"X>PżC {1gyCh~B_`GoK( W!ToV"e  >V @$ZPq>#@IN={wc ̅-(޵^|ʼnر/Ғ:_(oIUnn_/ _IƭϮoAƉ- &D A ,|`0AT1 Nc!Kll*N<,XjdY;9s'LkA%5eI%cZ.PsMUY:e5VĈ-d>"ckVaq֭ZjS"" &<@P,nc%P9Də%D PiԩQ(0@!dϦ-{`6H!D)I1f8"*OMGtIլ`b4LO=4 <Y04(B$T 7ܰCAKOtW \0@7 R(GRw.! '[NIb;sNlN)rR.˫̲03ՔvZMsN:oSI= ,(!#Db,2#Nn툍)X`Ւg,eJ&)eZnƏ(b}S8Z d)O ٳA tW P2v׭z4mD|Q|[; ">`6Pq%|,I|NZIKܷ> D([z_7IeVǿ-K<E^,9  fFwl/}pxa x 6*Mh$?K7)8 7xPdH@'\DLkeF $ 5!,PuPbE`Q\D]Ⱥ¸Cx;-ft<(LT46 `2BH9!uJeR S̝,{%}dek+M ˮ=n'1RP%&S5_]'ǦL>%K1 ?n*Sc%?˖Gzf i.YViOo^cmO$̪hުR"WA"RGqZ@'%I/i7>Ѩb>wDۧX;,/ vq/0_|Pr&K ua DYK(Xm͌UBȖ&!9AJj!~1Ab"l(0tsg[l1yiF'6e1R7"v)B$3i]2i eMu*YM'"*= H#xk \8+A @,:{1?'F6|a4'ӄ +K,=MYx,|##tլv5 (Z;j͵}=Dl9lvTymm6mq]2#ݳn@k.K0 vn2"jޕ_]">'|pT+ܾ6'k`a g58 7 [ێ>AJ*#ȁܼR'6=g4}Z%|J@;FoO^累wĀ3. MuT '\[w.RN(("{+XM~򽿰zmk $U$k;>'(?8td1ԭ49Zޢ\,X~j;E,QoYs{j=#53=wa8C{r#):ZۻY cn>þ(FЋ)cqM"xBŊB *ܴ?e@OB%.7{0ԺlD=c@߻(FCc -{;A_<& a%n* Hn{Dii8UH@*p$6H{G-XDN )RE8kGK&|)Ċ4W@[E1ڳ=廌4 F6~87:g-=;FبFޢq3i+pA%Ⱦr4G)w_;GF+ȬHd4(GP;HHz{EHD81 #2@F;2cF<ɫI)!0ޚ o cɴD ' PHJ{*YpJ&$EJ&JyD ]K‚kQ0$C2\K Ayl!Y˽LfL̺+SY`nlQ>osӈ-ͬ;HNȫdʔȁI(/"JhԜT$ORݼvڊ(TW @VRWEPXYE[1 Q] &Xb-ޚBe5gVi[S n 1JpqMr=WuäTvBye7z}8|<Γ؁REHh[LCgڑݑ>,cY-IbIiݻTY<o;L @ HY𮡋4Zxz PEHׄ#ڨZV,Z/ծZ]5ӱME)H둶*Cy&һ +mEFhQD-rGIѼ ɝץE$uR\ҨV-Z0݆}0Ue] NMߋ ݰQ]]]a J^╈Sɾ*YDGȟF4J&զ]4-E2RZUZe$_U0`ʑĘ15)6b` η++-ӂB0D^D,\N8\ ԙ8_w{OnUR{U U\_!6S-0;c,j5ڸ n@{kU^m^y,\XND[UZ7O\Uc:zY d\ND6dH+a|d](NLMnxd#;틂 p>VveXTUw=&^a&4f?M({?N#vdYKP 22%+^uCfkA1ӁV~e`̀Ue4^v1coIXEh.E @ CV _^h#f$Zfӎhx(h6.Nxd'0Vve(r\d.ia)&hT0D@&@j*DjK6\5 S 5%i]Qӈ(3#-n~rM|ͩkc4ΕH~ÖPD6,fTklJy;ֵQr!+5TY$™$Nd(!7W4=ڮmۆIm.mb nn(V`U3unW ~ـMn`6dk?7e68=Ck(lk^qSI W@kBp6>pfv'*V X q lHJ\4. Z Oq.prh"g#opNQr-& ^*X*OWE&2cok]K7qqoPn_iN>w?BP"`*on^ќXK_/MeiљiNosPs:~ =Ou\u;rNur&^VF Xm3GR3\ӫœ~:Kw7bOw?RWu:oir<<́;•EI+4(vJ*,,!']ЈQͱ2 ɁA /@ nݙ='C6N=䯺*94XչT ,5ț_Mp?#ٳq#8 HXO\S޹x&z^R,Q.Հ cɌ`s$j -}'eL,X2N2cL 0 `@Ly܅2E f^% 1!`UÐmZ",-1T#hX0!e1ݢn!)>ћIP|HMYIXbbPՍqtTƈ̪,a`2y-$,87+\3ꔔހD)ArZֵɝ+bD4%7kpJҀЬ@Yh9YBM_\J51cK\P!SDE9Ŕ)Y0+Ӫj)0{BWJx6I> o2CSxS.V*N5\1O=:3Q0TĖ(.qSoP;atSkCRFo8ՙDY]-rAbgZ19z"s&{ʞTL _EFB`erЃ $Xlم/*Y%AB Kyu(X:{v cE,m՚[=nDo{ WU=vpI [ p4`otB]%.δۻM/= 4U(p̨;ؔ)@[KKE鰐6u؟iPpm-{ 4YHs<s=;T4%QF\3;)J;=-+ pWt&=*jy n!cWO!@HB-al({ nMqjtUEI]lg?Pf0$e4 H:]ϨN}npqUB7xی l{G WoN vڈ ?vm[DJvf,.ifF q@rJީq[+f]n*u9Djnƿ<@PBp'τhxOX֣)K2bymveko IdGܺLDʻ?d6`@V /KH.IW,*|ᐏ|ByZ]_(ycRJ 5cF˴T%"ƽ9;MUA5-_A_MUn}ŁMZ]Z}cx_i]eƑT`_`ZcH"`A- 1p8=I9e[I֍\d]er UKMa ̵ Ibыcx@ PtV"L%I8 asH*(@8a VJzL~Z]aO|Mu("PY_TIUbd*fYA%?a)`t X%ab"_!cT%M2mj!XN%w&bGBv1&nZRs>W5& )J$|h d>s$;-_ngWE:E]xyg]P'gYK ayI`q&q-'shs:g !HCB.P(ibtH_j(gk gĦ:Ie|(DM:e`u Y# VĀ iFeI6;c?=(iM~T,)6<)M[$ ╮gB #ly$SiqzؚC@ ĀzfH@ iXҐD)X.w*Z)i:$u ݌DdF&Zju)f$m42I 鋎+zY+q*<Dfe@)$vFD?PPBin(&^>*:I%6&WVaKy46'T`amdjEfGJ'BS/s0YYjү3ԺZcd2o {,N`* .plqN~>dkPmaqhSEY qǴJmz^l6n :;en1701$2 P@!@l ";.dEYU. o0q(Si0*Tq +P-7Q2r,'P#Zhqm1Z򒉱Ē&12$Z-gfl',qQNI OK3tDkLq0[]WĐ]GJAZ ^t 4++CE`cڊ*B`Fs>:{TG|HWI+r 4#vNNwBқ> HN7gRE7=5T3H5dpUVw5eXhB" J'=[vN+T0]]}_SٖN4alv-;u:@Gg pHcu0+e30tBfOH+$$gۍ[4jS8a}k Pv*"ܶ_ vo , pqkDr'd7ws?w֖XiVgv-B+|uho@ V@i6k|az6{nԇ,̢i*nSTh88w!Tq{DHB< xT x,0kM8eU9Gē_mzw+˺R(*io82UG18sɱ's2ΖO7[4 {DwO9>#\_l9xtD$ C y97)v~kqg lGlE0낮:020z{Sq if3*\w DʥTӰ5WK]:afSkzQjɔ:E 7M$ƺ TGLUH: ;{; :Wy.4{¼u?9wwvM'b m|oz(2$v$"@zW@׮})G.VϲkuQ>zdxiC#yL~v:0êϲo8cs7/}Wk vїU 3~?Gۜ: <i[gG#&a8ڃQi>>;.A/c羮7<.  a_6tȐ`mU5h#?,A%'Qn$28` @N;ysc60h3?aeTQUZVV Wɖ*mڴ_rHs8ȐE"VĉӨS'Z !0`7vXqdƺ||avtITa)Z#L$]ޠemfY gÉ PMF*[Y+ՂutQv_[Cq@Po-[ L+! %c~\0b So8 14c?ۥ4N[Zm`*墌z(h6@94-T\+斂Jk:ȱP+l*¯E *b,/L@,S$d(l 6lGOfӉB2䘓D8h$K11I/ ,|CHrc2Nr%2RX!{,hl&0[K ;c,?7߼uˆ*0COQ$n9B9&G"i$3KӔǩRBI - 5 LmPr`tUք%+[oFyW^ c42!gyah"gNнݲ tPἝp=*)r t%Ի7!±ݭTPԽTU`0>ᄍ=J8Clh"ۜˮZ1kH!Csn[KfB4)6 \ZMiEEƟս%~.,2< Lj6kR1aJs1x}5kk^b̻νw{AqsNjp-j7$_製s|]I+0UW0ds؛b6'v[8±=lsvZQ! ɞ-;#~CTD:FVX4`qHYU;@Z׾˭%s׹ǁ <.IA)P>ɚRC< ~ Xo0KFA z(aHF(bog3Q! .23t䮇Cxu~/\QtV*Ǹ2qqPGpANRi(o27[z3>WYRGrq\٥iʲmf͊`^WI"{C}:*zu"еVf^4-n mM&PFѤEiel&ģX/.Vhfũ*`eDaP冰Pm}c% o da$(@5HnF)̦ծ 0 '0040&eq,"k9LoL4 f0fè t_Hmyib. p< '60*Zg Щ*T8g9f cpUZ&*dMnaޭXlO){1 o6`p:>oB.h8c9 TApP%vcV,Yn\Dj rxzV1vͷ炂 # "[ RQ&2K$Y%Me121Q΂` 8ʎr퍌ޭ R 2ҢzD"-ůj9Rg f"}$$Q:mahsҷ'9x¡F q,P0**ì##) :`+$o;,_r-Hq..跖'(r!(/1$f&:ђ1?Q*#ڪk/+=)AgnhB>P. `jWrJޒQ35U'.hγ6k79`CS77S8qbBӴko2A> *SMO䮓jxo%' S<ݒπk5Rd6Qr>u2>/)3kF SQp,,Aͫ%d D.4C7t<@Ԯn6ODs(S/E3R 7{S*hl4@q85"'B9vԡg3H A;HS4I]L4(*=C;JJ-JSEwbJ7Laq2@iM1N(O> 4++O sA!f*;/tnCoRd i(7s:/TݰTTT?1*Nh7BIV1 }PE:;UXKSKaJp՞MStE3)-%U[r[-ĵU\]WbV꺃@P/4X_uuB;4[eT0%<^D, yV\566"dEBtp^wGX6Ja6fg %LmVL9wVTwa{S? ) M/200*֟ %bjsdbWSvXu^U.slYsf6Z SRr42V[ 8Ӓ8539w6)veR-^p5"jqq6W k\Q9&mll3mgBRJs?DtdbFo[\\7]4V;lC) irv| w&^udvBwxxQWX4ɷ"mz{H[tZMo`jQz]b}~-{צS%/a8`8bB怫B3SQs5>C sTSP{U{c"ڶ|uiB6>xRlׄ0~}~~urMD͆<􌗬X/˖˸r77Mȏx\F@T'"ՐVIQM6 z5",ɅX^M/7yQyK!EX]c]F&u}& ry;B 7s`.9nKYGyAkMY!!x iaagE$] Yu9}kCDhņtvʍCE'Ѹ@! K@#G⏄9#9~ux es69q3: [s ɡođxjsj( &C9ZS nV$N:#RYϧ7eW6kAľ'w@Se}z( 8zq}:~oJ]x 5(oe ezzփQڮ֡ZAFJ,;IpyfO !\;v,:*ۭ3Ni]u$CE*H%9a,v7-pnWc¶;[xgJ`S;>[Z۸{>@gZ,窳^an!{ȂD\ө!Z Z69ە{o{{ۗaT u:dkM|!YΛ -;E6ܤ?OԾě۹o I wc|i|;7(/`—yy3\ȷqɛnpoܥܪ|ŭ ų;ƹ\B|Ƽ0AAXۮΡ#<w3Mۄ%o}ϩd|o=YB%w^ҥL4*囈v}!Ytkea>X |Z^Cj9B 4 *|1ĉ5X1FLH$K$2ʕ+Mx 34ḱF U"V bѢ+*,RaE5jhJY5_\ kа`>6Zں] 7֪=W^iӤ  p& 2A$H+K܈sGJ8IYz3mi#H8n-tN{?ZU֮Y%;,ڳq}\-ݺw7`Ç+H;r9043hэKOyi֮_DtbWoIEUR=xmA3!\E!4Fgtub5`uxᝧF ^GB}g_~1Z‹S V"VarRu8]5 Zv%ngXލG"Y@hA{27A娣<⧚~M8xtB /0[A`bI WiI*\j8TcNcU^|݉j+f@6pѝ# ){.l $4 袌6:&ui.`4hYinbnmjbZN#fg"w"Bh뱕 ;k^5pE;m7X lҒ /x1Km"ZY.<# S[&m$E T4)N{Dh>-ڨŵe.wR S8.(W.[]9+4ͅb@]A,>'ҎtOOZk7µ_[ kTY.=s9/,wuKhs~xyEk;4/Ly$YS /7Y 10gI͝env8=nk <7π-h!`eT<=o (j`1E\+a\w6]o{9bG3ez#  p+} h yD#c% VMbW Y`l Lpd|F5/o/lhox `<;\>9"HϞ@'"!: Xk`E0 zQ7aZ2xFG76ΰ(((XdsA,\DIat-l)MI/۩n|"k-m&\Wq)$" f0#xd* 1 MɦC%uӛg`(U:[?w֒wjcC2!9.C2g6I⊃ UCیDSPzi#+oCN"5=h˻' S4z'})Ё46;EOqCM/6tl7M.Ռe TPES-Hp4iB3W*#7U/%kYgjjA}r݆"]=9a*YN} vY'U`&V_@ДG`r",#+ &%i`o>)3~ӵ;Qc9CcqМEvg<}>w?N,&~x# {#*9MX/u' ۀLAxf#Bkzr*ΰ;tTN=vm\=>OO^#|}0K`}qs$1C]~'!F~wME6#`uFKt{!Wx3p"XhU=a5G  ](v9=}Uj]CMRBV؁xdf>"qqhWat҂/|dou%08hD%n0wH==LH QRHYzi.zt^Z`uc{wS؋&Fԅ~gChxQ>#R7Ewȍy'}fRmRpZzߠ x*ȈBjThkm3r>(bC4@A@EPkvmy!qGƱQJIvFtgnLjN.jbr580"v}I=BEt%I树؈F%qdVkX{+3^ a"*97kxpp#)B {LIgy`bY wFZi A[Ȃ0ɘ1Xc@x,j mlS=Yڅ H` 駚ӹ" / 'nQ_y1r&@5ib*ViRC$)zn7 mV.KBD)aĜ&t)iؙ} Ͱ%8xKBG!}NF9)mUsS9$AP f qZsAh!k:=b;2n w4|8Ah?x*7Xl1,nbi Ar4m(eze ?j ImmZw p Ȁ ɠz* !bC @J76J=:6J\⣪) `*+j 0 r X0Lj-x'Ut8,XVڥ_?|ghV(hHdNJy  `wz jZ.tӡڎØ2jJvfMrVWG[rx[8<E0u4 #ʕ0i g? >"JpFB 0|$ ڭJ:5K>z ?  P7v G g`p"; oQj(O h ;R eX,%z@p_Pɠ ڲ|ڧt[6Zx]R x*v! @ ݀; ۸G ? (yP8Y4eXF\k+_+뺯 K [` ;;KNjɫ˼kk+ۼ[UӫkZT@bT0T0?"! ,^`C0`XȰa‡#J|p!&(P C @d (ˌ!0IeL9v Q“O&QlIֱǤIFڴͶIJuۺmjZkn`Ê 뵬ԧhڬ-ZmʝKٮ[[UKX-õ'ne*a˘3k3#ǎ%C)RnIS֮_dcWr=,L >|[+ꮹНN]uyѳg'ήܲ;; ?j,_c',ٖeۿ0I5VP-$AC uQx$h`j(Ll/TO(2AŊc'rT54:\sT7_7tcqD#XGXzMדPޔNFPşxdL2"bJ"@(%`=Crtv}HN1WDq"2k9U騩VywX)|ye2QZؒORNE{X:XSʪ%|Kc*J(h@iDFFhi)"k5'"j;\QGQSx^armyXI*$ߪNC b [)ح+9,!(E-rtBӒfkh nK$CN:Kw$r ScqMӌPG5T/XۍM ּ; 4ĵ"uLjzoeʀ BUQIs:(N@C*ȘQc4K:T[oyX#oܿ~4^M_;NGI{ wSkL7Tw"{xʅ/~cw6>F.lQ,M JDAE ҒwN)JQԁ:9XE Z]2^l^; |g*lKa61m~ARy-hQ|,{!)$1gҧj}Y}d,P ~͑]F[Rf(C4)LcTwpĠ8Ax!2AT4!PBK$6E/x̡{C,ۃ2$Q/[\ED([ܷhsH+. 5B\:8q7^By#!5 ͅ)# vCIP% t|BLHհ2PlV'U;HB0ZNG%RAъڂI/S4&4@ʟkXSY3L6Im̩a%:J28gk! bXK;Oõ)C Js? +1+}@%<[⭶h\݊n$Hlh*])KART`3)fJ09=l5_uf:3+CJ5A@*&TiV+U|`': Zu*Uͯ.ـ+o@YFck[ߪ O8 G55 kbOI֥LXxݮx &",g)9φ-mNծ`j >VOe\n thFG?RS@D$p\'auo4^-]__;YɦW#z|σ|5+_>-H*[ xxh`,80;@" Y4%"`[=AbDϟ c_0]cc8ˣ0& G0 cLdRvn*}o|;_φQow[VYkK0f(sμ49fl( bG*@6|\@qB9Ђ.u mADx4nt Inx0* RݬLW6xcf2\Z~WO˰hتnJ_5A6oA~Wwa9TΉ - v@'mz޶(cwVǼ4a;lfSwN=;S:MWۂz k.Żyymt.|نqWN-Av %"YPTy91s.̯>/`! <~ElKoLg|TNVHz׿ZW=c'{͎'S=k03"?}" XP$y߹{ 9扢*_e|q@yU9>HNp+Gim dlMwD :¹Mw j5UCC":)&:+z@y8~>*kE  r}D;Ôڴ^ڪ_acig B&$ wϤ5L#% >U^``8U(FDUdh U(H@. P09]Ĉ[zljt]xT(Mh3i+t p ˬͺ3*PY24)U ;H(WIU01  zz BH7sRdzC-Un@7@tŚ%*U۱Z9`Z(6l r9䂋8XIx zxjHX8yN뛓wS<1Lh_p;G:+08$6XKuۊm9f*"+:۳7;Mkۛ[ceM;u[K_kKV!? :h:8PRUOyy(r@Pc01M\Akp9 Id i۽˵]+Du辁:X8$k(lova5KɆ U \ Pܑ3<fn%?ETJP;;[7[Q<{ A^t j_jIi|rmp PjtJ` _k\ wV?AW`0 TjHj86<ɫ n<\šۄhRSL^7g`]Qv8 |V4 VcКB$N$VE5nhG]!E"YI)Mb9˥KUBEJRM7hO-%Zѡ@,QrZjUY?l5WaC,%tҰԮe+&̝8xʘ881ĉŸj>41aCRgAKi(Yz9+L6sٓ,RܹVWʕW_y/? 4-[ɥǥ[.)tDcQFf*._rxbBsduڿr)M$^R30Zj6m¢cʷpíꃰPp²9֚.9+ ""K[Z)KA8#K#+("*ʌ)T)^kPk7v# 8;DDDJNϷ:=b DrDBD2;:E̠,r˔P5T)ŸH !ͥlϲsN:BvN<$DgOPԧB.E+N:o\NC*H! \PM"V,Vd5`;i)L\LHg+~cUv;CجhCk< QtСo#uI\U.A*P" | jI&{9H!} 0OBx;kasL^Q͏SZٍ9㴥5Q丨dB&k+V P"#DpTH!28zhNn9i jէnU`]Pκ$c[&s6}BM1d>8ۓwBAwx*pzy-Q8U!ÉrAr/73l}2-믿a}Ž\=l(ڢ):K šeyy۴<A%z:PF|.D2 !r b@F6@'%.?_GPg?]-۟P$bl)@>PBf40cuXނ 2`DTf}0- @! 2pGaQǾxsh8Uz1'Qo?Ő4;$0iV(w-mzc opApLt{6GWzZ 9DEB@'y` x@ѩ\OЛC?_e+Sߗ(~Hc'p =ۻ= P[&ڈq(Sc>Os([ [xꋿ H0 ؾ>02 |}GZl̤> 8J+sjJ ȫ8dDlTJm;dRZrIRB˴KKGE<F̉SA ŌJ=D|LLst=tR˰M"M EBMZQMGŘhM)B C $:MTH2K(ބL.D,=k<6L%GABdT%NǿNT ϻż Ltn0r$tL{Ѭ1P%0r R؏ `9d = }>GuMED|;A/+Q}C[Q χtOьV Q 5?KB|ʸNӡlR'}Rl$${ȾW /K3'R4N6ʡ 8m9mNLؓ '1R #[RAj# @à $:یT]UF58A$45%ԪHC1R8UM4G RP$R@R@mR(B}NB`%5$֏E]Vf;mszا։VZ8 >Hm10NA [x4\T /5W~ 'm :DآCZK5S kͮY'\Vް8%J  BJ'xD1uDRyxYXTM-KD}\[^ `  2\H3Zȵ`)$IܯԂ[3&Ս$We] uؽ[$ ZMV1ֈ${3m͢"l%1X}<%?^l,3#iհT КT[9.-EʅUQK X @m,V}D-0H80>I{Ҹ-_ٽR@lH+ #FcQaXEm\a\P&.Ղ H3 ._@_ب%fE|t )EDm0fuVJu@_El Id9kaVfUf6< p3 d U$D|]e`?> _ LJ~N6m"h"e6XeV0}Ŀ^{_(Z!Ya~b bcN_d e.bfg-S"To^ n i)r^\6 F!&bcň@{C&~ibWDc9hqk Hd<fi\>ia5Uۜx+5`L+( SdvaOEߩfh..sU ٯD[X- EF sk'9hdnT gkS"j^VMl mT޾N lJm`mYEUOYtmF;mܶC1nUPl_ncRǶW?5g\H.Gpq6Znn{6d~k)Ex흞a N(Ƈd9NNnVeCiܣ Pb/ g o pbp hfZ4q޾kV r>oE<30lh} (g;A6r>Om(S%& 7餠Vo-b̼L&sV h_Hv<53F1qz䗞p>&?(@t('/+ ,EF?f0 t3%:Ov6 Bip=Uo"nXG~(D^ b)Gov)w/Gehgw0S (u+Sqm0duoO{sq'ÈrEs`dG6m{ wq0%uF70VȲ Qxnyvw2 o]oBrtxfj00`gdm_%5}߀g0OakW#Ty{$"JM}ZWR qzxT/zJcxvﭟp5w5?ay !?噃{W'gt%ĵxu--|RFy} bw|NybpjG*yWnO"zVtk&5 Edy}h|y$4w'g2X( |m]s ,h` jpW%h"ƌ1N㫌FTe'lٲ %>Ьi憜4ur,j(Ҥ.A1oj*֬(r+,-k,Yjמ!ƒ k.<Rd+VnDRĿ 'JXǍ7s|$h$KL)L7W̹a~6p UkY/",h3GV-Zpҽ{{gL&Z~Wː뢜r9YGTReRkl[n :To<TwU! /4aY)gtqe]9_E`q6B{94Yy!_G iQ|EIvTTSA AՄ!nU͉oXQ t_d-V0* :J| Ib YK6iZ1'A@ eT]Gu&Xi6f̽Vӝh_mAX {6Z &*6zyVPBgPZY 5cBҧ&pVy P6[czsjኦspIŮB1G- L/z{ږl2պK /W $7DWPNj{e\_00bt[)jl;xy,ǹ`$L(ۋuCL̛ޜC%2yב?ROYST"8[ tCMpTY$kucPEaى8[60$Ok2$moS.Fw;ˆ ,; L‰H⫹vpi99 :=ORiA#|}>WUY]`lΎ4^-n@]v]ˈW- xiw-=[,:1' Q z&< ekiˍ"OcU>`>O} @5T%~IJSc?e h1ky++YT8[H/REA ^͛'("! GΆ.- u=aT+!-%@@>Aqї2y;M|q6;.vc0Eoy|%,E$D/r'P`GN " =HmXR )AƐM"ˇo+X!8Iͪ,:k$.DdL,Bb,BFi+-'4`)Wޭa"m758bӎTNyD K-fn DHٴ"9IJ|3<cNEF8@~ 09qʞ#}%tY hteݡB SE4 q|rk›|\kmmDWkP.A_WL //O&հ0v+&ƬuTgӱ%ez"ZѸuW 1ӄ ]`a ik)͵sL`QTPNP*_7+1Il=/+b.*1/1lU_Tdk?fȯ5r*yɥ:m䵈$ʐY</UB`0awP_v7;rvCYusfY f 2w{ܒϭV?U `G?ڄ~՞F:ә2%ơOĵ:> PaXzֳFیk9WkT_9ا%៫֌:nvKɀh3Z6n-oWm(=\G]}/Z-g7(k|7ue]N;#YBl-\L]!"'fTG{D/:ؿRC$wD -]:jb\ijs]~盥S!'One]LskշP ۑ bʀ;X諬DXP.oȞ⛧p"Г?_ζ'_~Y B/ W;v}5XCf  (_@e,dEÙU&PYe} Y}E^OQб[ d=ݟ_Ś" A` hqUXNGe^EP]h!u摠m 했| ]R W@_vaؾ-C42ONEi@:^>NM] &^n !y pѓgaSY 1&"-0#=`N\*fbd`'z (^[\"a VZ,_l⠔Lt .vQ/~0 9 d2& 3J#:!4F4>%Xn:' 11c6"+# t\4ήc`S ?#A$1U<#1dA YaaFQdE&eF272]^Tyd"dd W$qG؁$DSJ_E]xbFfm8VW#Y# '1\@L!c1]Nڜ^a@N_ZW&`Ɵ \aaڑRR`2MX\EVEIfFP]`%G^&W&fnrvg؅&} @l= ͤ9C]fnX!Aog`" (q24&[jM$Tj_tvd>Hefm' w2ez:zyY'4CA>|ҧ}?~nVCoh U(E\0\E9vFimLdT`tGrg,٨gXY,A,N_¨h0^5iCD#>9)5jXY\md|fP1'$4"Jdsr>'AN \*jRbzjՠň,B>s Լ 1f\j]C4B+'L°:߬F2>mJbQ,GnȷfIUPA-@(ۇ}CkʫA TlV˽-B HO$&*0l&lQ,,Vbkj붊h z+Zblj¸.cǒF?!wݫ@+^0И-N,gM+bTz5$~rlq->Ú[m +`nSŬk2*m"S&[j_Jh.Q`u*~Հ%nZ6jz$Cfv9_b/+1.+"H -djBgNT2Y|`N.ڧ=o͍X0cZo򩛂z/ۂo-,. N ޯ6+R(4n^ /ф*2/㺧f-p/&p?$vA@b/܂ V]NB& G. /N#y]/V_R*(041ɦY;p~,un5LW-dq')D0z R+P!&lmFp0W"_p1::/q ivQ1r#;r0n*Hr+ב0&/Ns\p8Fbni"P JVAxa%Z1 15oUΥ"s2n7d 33wq4K4bivds8rj)8'9s:_=3-U<妴%p@ :ߣ?{4 3$3$% mi)*B O<4f*m})6֙U4 ޙ~t0XsAnV;4K5?<Pʹ1M^a6\p&3eeM4E+`tU8ityi_DApBUr&Ŧ"WrJgLG1Zc0@t$uk\weBPQ^5RZre)[`;5ל`hid,v- >0dTMlfYYv5'bgcgZ-NWA-jǶ\G]kv_Ա>"PN4` aZ*1@ =RvI,t)u˞V_fh5w3&lyGje\!km`SloG}j;_/{-Y8ڣu+A@uSJ8]ogg޵15W|kno"6x~31"ݪs'61Mk u!‘ m8Kq|s&d+ M'zY˜XM9r;8t&y% hx'5z#7ў9YTpƢz+%/hx窋s,NzWzim6x7 lz6Qa# 72#Mesiib>3x3?sGxmF2hn4@$":{=jS亀{;./8{fޯl,u/yF[h{n;2YL,֪4ŇPb|w{{ϲxO<7| j"ϧ(*i];RՖ6]G_kaQ8kG{ |kqxE \_g'ىhדسUӍٷy P<ڸ~~Q>6 ~sWZ=5 v8sEa7ɹcYD14 a„6tРU cFb9vcgYH"( f`ٲ9;y<@%5iRJ94uӦPVzj[R`Uԫ_j(m uA[kIԭkE^z_F6rS-bM6v@:4x2%T th;sytL3m©'ϟ}6-ڰd-VUyGZUܹm+7ܷv{/Fig?\,30/ys1K6v>4ˤF%դ)^m~jۘm7|+zۍ˺({K, +"Y-:=&>3ș$Nz? A@(KJ a =Qa[QHYR [s b2Kur/K14LF6|:Z< KbYfdO@qoPBIYE]_HRI-R)oʴJNi-J7>=NB{L0C5UlUˣ>Mڵ\]W> ּa+dPf-Y$\8Z. P@k1մ'm=e\0~* I5nMRUUm(x_u^[U z_ƆW"cPɀ  Z$OJ$K'Am?Pk?ŊPK7 zQNYxIluAf6O)Oz-S𧫭Zx*묑큮"[ܱ9jLS;mĕc[E{ 7g!%+Qc2v 3駝(iqɅʁy@X< hq/(X'mAB#XXF-wң)"Q8}1N"0E+jN{%"W0ji TtB]nkL1%Gް~bd飓rAb!B/Np,'J &Q3i,ҷ,.YYO#V#cIcWK"21٥!_>)t$T*W$&f"8MmԖTdӉb9KIN礈^ e$TUZ]VP55 '?i!ɴE P@:!6Vы_#@QN+IERxe{.g1wjEbaA5ԅN-N]vR0x(h 'oZUѾZ%e'YR2] %Bn}0Bx b&dTI2TjʚdU0 6V {#S" dþj1_g*rv4-PVk"ɼn+Lv1e*qKX&w&7\HYHЖn.xD@i +ӦcmS"֧q~yW f7&rഘ>^ٗk.wUh-\F!kԃeەkW3Z3 ӗYz J2k|W8RЁ4vF`0 y 8a70apnx!mL{y%`\Z'[|c [mxT/JM|s>Az)D+юŃ#-ISnt!#LOHDHм^SZ*-9Z\륆qqK`F F3Ht:A[4ѹhv}I"𣏕 ]qj+.hMZqU5YuWiSms Z*74C}׿A)pq_pD=|ï-i#er4VenDޯn\͇szY;o;vteFǝw^F[^3քf=βwP=Qv}| 2z,Y1T+/^@} 7%Xv]]v~u?}_?;C}ßYX?/? mqkE%g;/\;=(_1l#3 K'mro.b݀ZT]/O:(b -Fbc%l%RorpO+̾bs:,z?CPТHVPo(ad00>& ܍t^燎/p[0P.~ ' g@aІR [Oթ oeLΰ ;7r 0/?)8 R3$9j3? w44 @1R+;R?A)B%n*LB1t44%9EvN>)D,Ҳn2?ɔ~iVOZtFU4I4e(S//_rJ+3$tU*D5pOBT8DIT-nPP"Ku\ioQ W)o4XdXt0Gi5u72q=BZIZ?TlH`3YL4õ"ȕb b/`0"`q:u;D#tZy4\0uG/_vn۞AnVyW(.oPz^IAM*3vgKq۰|irB)*w206x8 E$N8[6auqdd3 }xv)Գl wQ/S7騸~L8iQ\vׅoojo،swٸkhvr78g_R8}|x[YikБǍ/wؒ<*xr?y8IᦔC3Qؕ]ϏvUB$ԑ굗GrYh;owby5+mt"2pRgse9wO2,lM'T9Y@;I -tŞ9chn @Zj )pҡ DBr%ЎG0๔Yk'8ם9ǬbNѥ5@etvy:ӎS5Ozo㨡!9:&y.2S`uŶ𧂪Boz`9̧-kPzyɮ1ڳy{sxc-H3dusYrl6xԺ,{vXvkZK:xY7*[\SZmuYWv[<غKMb{vGZvPS.ZP V.lbCU`asͼ:9 ÷OCZb\;MS[{T \BR)+<}ƒ TiĤܻG׻KS֘bi)tHo;qOm%:O|\V($ɟܕIɓc'ū>8N0s617)Q޸G,]_a]iLqƁ(N) >%^ٙ7[Q~Dm=ԧ~9;ÌD˖V >K"?>g˞]W[eLjQ>l>'_/ԥ^Ԫo? "'W7]}0{D{>rM;U/si؝D\A}kqI Ĭ[>D ?~-ё(\[NI"zlBE P"…,&h0ĉ%PXD"c;zXÆ A45g,[| -̙о} 3͝7sy.СD=4ҡ89-GSuT6Y3f\A= XD0P(wܹtڽn <@` 8LQbi:vؐd1~|9 UuJh4K쉓͟-} ;O:V̼[lk۾{7a8ƒ s8ܹ;+k܈٣U|V9zisFvjneUW_Z^=XA`Ymw Gy^z7gI}vKh4LZ~@cHP Xv pfiH[ W@N.gsI7݅d'bq ux#Xe9䰞qp2K.HZǓ9gKUB[s JYh$WO>}V%a!FvǩA]!YYzgY&қ/9'3ڨgNT J(fHoAl]yA7^*N˩ xjA1F#g1Z.:Ui5{tĮel@RaQpŸ*Tَgz |rxc"REolXJ,6G))z9âjn ϚR?LuλE[L5f׳b?"m4)~ŭ2&T (gm- oz`Ra 4 g;%J}#)E»Rᗶ1~HE8y'^ WB^1!h>^]xJ*P+e)J\X'BLB翕DXD*20" HH@c]58F5i )-tc/G"HH*N&,La vN|dQIR@ZR$8Ix\aȐjyIvd%UZD!|XV5K-=0IC)ҐFulc̠~F0m)3M 5 ClvZ9Hx:ʁ! v,A qy \x]QzȠܧQ#l4a U$&A = ]P.GSJ-!AJoh4Gb|!ZDLvZhDæyNs}Zj5.j6'Y(-!Ā6aO40M "NPXBVR(ZӚ(&$9yH?u Q!YQy+'k\cmzs&Rm/.7Yw/fAT6>Fz WpE~`B۾-oiE`$vL؊rAhj 1wp ۍwA\"Ācoi*v׋$+yȩ~^&3;2eZr wc@c/  ֧;aXY,a9@ zЄ. mD+zъp(@L0pIT*Ta c0PzԤ.ITzէ_VִE\o &}vs=N߿nXtc% X 5E؄T(4RF!f܈fmM oD+bDC&>A]Q$QQ0<T}qDwEցT" 7f)W|fG Z旘iIfb馛I2e (@Ѝfl1Dn-",QF4EhiFCit:QNJ2dW #Yy~RcZ`a5gYal&Ev 2tff(g"NZ4R *ո6껠ZF-Hz *T8(1U49VKf 3,k C[d1M"[,c$Ll##/T'!3j*.pHͺѼQB$ LS~Qj#S VXgqZwXk]++rlc)rע&Ơ.27u0-z 5MkJ]$^AK{5ٵ5cu2Xg".nǼXxaq﫽D|7*L+9@T/2a5XsMn>'aX/~ļދ,aKY/+"oZb⻗L%Q8yNpB{S>iNs^ ]6pt!~˟cd@vJ;07DK X<np a"Mi!ȁHpb+^^ QxBoOMgaFl!B;b͆gawḋ* w,HI%*M~ hm&Dъ8OzUC#F1f#wkэSa8GQ'?ᐐȜcu{B*"9QY(DD27Ek 5ʒSZd=N Z1% ei5Qk2O^&2ƸOa▙gCN%)rf&ɚi!Yލ, sR [*&xQ+,!ܒ8+P퓠HFN%43Fё>D*z X"FL`/p1%9 Rnj5{u&laD dk-QS1i@uӳӠ+Qu٫z\Z[WmT]jȼkDQ+V`ok_zƼ-X4~C5h1[A.uTJ 4p*D1Kɐv>ՑdE]kuY"3ZmFZ~72T-%bJFޞ̚Gwz4[/\&&dt,XwٵlD[h@"c x}ĪC {t WȂX|QTWDxЀW4x~1y*r&vz1#qe*\dR',)Ycl kLʿu ]F31ϩ̶n?:V $ -g(}N5$h*І~YF"Ȇ9p[=#"dsu̥iNB0`S&LU{bE-\]3c0H.!^;/`wƳ$ !XܼMPE[Ǧ{%\L61i+!cod2^Z@'5,蝿{հ;X98cR\t^ 8kqtq<CJns˱Cdb< |8[W湖>tG<~SzQ2u3:ÿ ЀUnv:$vv=]LdXt+rd1&| s.U#,sF3Mneu(3^3o9[Ѹ5ٙ 'w Gs|n?5e:}G}շdp }c3u+kW~^GzwgGqPulqgp'h.gQwr;Lw0 sU (䆀6XBŀEL#C?F7tOpu*8z&~ 0$$k`gg-W6q6lW6hQW3QQ:JJrmFF @ ĄTBuS5FtlcTYtoo] 'b؁!cziȆ'$xbAQQelT5gc~X('w 8jdFK9h!t}`62 E_Ugwu ɋwbr~1(E~ȇ6VӨc= ݈sE ۈ@ >L ( OCŐ -ɉ.y蘎TXxt JW @pjX/. [!'uwz gQGz'x{C9@' 9rx -7) S1ْȒ29Xw:)sؓ@%uV؏ uK9YMYO RI^cMWiYq!czU:X$bIReyBȈr n pinr99uꓗz,Ȏ~it #;FbИ/2^tVA-ٙ7U{=r8(a)9`6U0vJXiHm)udP $  Be6ùjIy9r!ytbXu89{FQ}b{āEbrYv>Zxx(z iczO*Dؠe}9 iʜ*~"Jن㕢E,.l /:f7rm5Iiv9iV10m L0M*P*%å楁IZ{R eg9j lڦ+)0¢j0 zc|:8,6NWУ@mr%T t j ]o pwګY 7"kppzMߩɫ髞 Z5A0f ? ȟ|z㊅'}e@`J=Rk@ZkxG4Ag[74vZ, IfYI8 Z`G+"Pp툲)eJ ] 2u 6p{$سwpc/E+I1^ NPIW0V Gx:i hYe'۶ ' 7x{k}ۘ+uvpV0ؐ F{i&AT{_ JFDJ %yFsT 6^tZk  4 Jzu}p!jgϛbV帺jkM۽&QPK Hh0 ÂB+yyB<đۅ| ls7g gU<2,ő!O9IJfyq0A: 4.lВ.T\ j ]ȫ60<  0]x|]WWq\k,]dN5tvp*~؁BEm`xԂ<ȄPjyU MY =Kм?т' ]֮ *@+>`r5) v`d؋؎=ВMĔ-Ձwٙ՜}] _@ ڪ%pqa(|s7ۃGIgY)0`ƭ̊°ݶ_V]ݽ%ّ,ޡl/̫Y޵ޕr)fN+u# @ M n.ȑ=eUIѮ^\r&!.#^blqM&}.^|Qw=66~N.0 vIL~W,O}ѡY]Gb~"NmJ],,gk%>`}ACVܷ@FS  M }Y{١ NE{  ʨL5^8kvgP]gyJ^莾.l~پ.h(i -rxhnp>lmpN,|zP>>~[=dKjgo T^& lܦ %pbӼm0?27/};F?@_ԾE+h p0Q_l^-\6zmeo7B^.0f)6_{ry-dR~W6P,LeFQypr=- /w6]wz:͢:d}'(0;+!=YF;Оٶ]4!wiK PJpwpo>+9W鮚Ss G2*bc~?vo9g]0xIJMyy͛bI9K8ɉh{aC̅s\З>G.ZG/%J濸M@AAP2@7[G•$K^dU6Hmʜ2B :aŒQFA6ZȰv+A Ef$] euML&lFK2S UD +QӀ$WTn0X$̾p1{d*C }훏'?6z)q].񋳥\0H->> -A[}q Hf(d*S%-A+2,`&R`3FJ3^$3`#6$p/~Q:R&D"R@`DnhB<(y[!L,Ԝ5lgС$G#MAKoТsSV;KhTˀ P1+lOc+A-^IhYˎI9hi" ,5!RPS&J&}egf^T:ST;*|ⓨ%V;TblLMMcEb Î Ck؀~ho>QѾ}@!h`Y3% )֔jeq}L(O84hZ)y"">jPaObXf?IT% %w;uE(> V䍥_+턮R% AybrhmcD֝9=p_ M0+F%ےe%dsSxAD>fR*J~{W$CB3U"ۺ:^G'VE؍MXk Ȱ(Ц(y"P-S; c˗;ȍb x+_mc YFީII(mIŀF ]2Z5l5 ]N2E *H]SYj.4pWZ֒b&ǽuvgX*-l?lKڟ&y3}Bx8@kXnvVV餤ܦ>sV}5xHțeͮ7޸ֱJ9)Cbe;z> *`<8v<φp+~q_[ʺnȽl/(?rTWE~ HĻМTE+%ikt3j0*@ellBSu:{Uճ~q";r?p  =fs9ynR=zз"FP O$c%iWֶ_yÞ G #7Oj>ǒ6¼{s @k d@.۴ =1C<;!6/??1Ӓ;Њh>9,\8pxpB+ s, BkӼڸ4ˋ. 8›!B;u h;֛)҉LV-(B7';4D+.$<&'L (o+D-.BkH 4ÑIßXC1l8Ĩ Z<+.A;=Zc{˱\9E5DC4Ɂ" ژ\>JD蒶.MN G-j +Epq !(2kEs8 zh-ѹ0?:J Fý(FВ ںdF[MO<@d,$%M4@CQҼdI&}'.Lr)RP, R. 0N!S3mTOySqtф̹;u`=KB> V C]D"X"o# :TKTLTmTORkgլ+U Uզ`UX}W6-Ё8}LO:7^_7bm]7;V0ԌeuG; JF:k-Om-njopq%ם:J1 rw5QӔW4I{Y1 X%X?cь)M"U`X%˛G>YIdKX,dMvGs09Q^(Lq4f_5σpLX^9|ne~c@.Bd\ X`ND,]sdP`V*fJ^,6r4nf/fuK.g5=gtN7cᴘa F|]g/zZV`;8Z\h)` iVj fO>q.i=i^f")we[\&ܥiTcfLIlOBnbύ'ꪎ4:(piS6gy-f"tW~U=Zb$ԌLXK.%.lhRľ`&Em4B&?&i5il;6~mFz.=M%^o.DRNZvlnnNjn,:2(g王WsfvaF>dL H%nY(ov jb+jnlH%.l4XՀp˨ߴ.m X ouLT*jdqoV,]l.&1ZhkV>qr:$%'=g o(`&κNj–R/үj ϋ쎱q[sU* qؕsRs:[ '] 7ZVk At:db@:oGoK.lo3OsNtuRv9aTO슀-n_)uD[u88Nn]h%vJ3v_^ ؀g_]v[e ;v=ooҏ$wsuNw[wvow\kwrc/LteoNw~]]xhwa6vX$`C D_you*jBP+ @/UFǺt,t4_Ce G队h6Ç(uOx&gx'@rDXuÄ8v}z^?o W| ugXPJ ¥n8b _}yvfR]>Wmg?{~W(Ō>̾Bg|+d-o&w~jB,[w7} uWoZs\~r]s%M"<DFwh gYo1!p'RhѢ@hψqȱH;Z1$_*WU֭YfR(I :wlaÇ-jthPC50m)ԨM+Tp*֧X=zذbt,ؤjמE-ܸ(^}q zaȑÆ c$fpĊ3l1HgRxbb/s&dǐ/>eKb0eҴ'lfFvܺwzPh&r߲h>7`x †-.2ǒ;Wn;yΥ6Q|sf7s+pGoW'Xĵ5rlA\s>GtUX[V Gqwc3]Dy*AX 'ҷjv lXFxdfa9ZJp]U ۱X⌐ϜbI$mT&6jC>OC $H QyEBNeXSJsť99eh"/6wAާxt ~;#{2'UV Z$+le[:(XVZVgBgyZ&K&˦4dQ_" %?@}EbU" ۤD!լWBK VD-zH,&y|brI"yb*q|Ư˫GYŠB,Zl־ wk=Hx5ؖ7ܮSbl3Γ4>{tpBEt"qM?4QW!Y[{̮I\*As6x5-=z7c p\J+ɴT" iץ6TՐo.*J .(ynnK?_?oպT{ne`v1Zc,?eϛJ*.S$W+*G>M)WQZRõwZu kcۖJ惿TSYB3(y4g *t"S" D9r,)Y;E:+_r"^b.鵬3,[Ewn63 Ѐ9hQx$>ej>)`H.2rF-_2kFᅽq}J n96cD!SdYHXK3}Qr`+9Lfr'nvԮ)ZTۥ#?H]0%1FN{|bKSJJ4i1@l >ɒΥPTGǬDi E@BIeը3A4 9 ~Ҵw&S$MjSeBl_sSiYJEHaP UBvT CRu 5DiJQҕjrtcb}?« @=[PoY^:-8$ *Yh/SSmkcICV lKR9& wūM}5` EC,8 Z,eZYvNd,IEſΈ6+%fi]jO 6(s}@kp8Sh&?-CZKҜ(nvSj_+ܦ@09*CmnH5\}hqђvrCNl̵ 0]kJSLb@궰-aTw/T,Ie10|&xF%KlN(@ykW}׶ZuΗ棞V!<7,Kx+dّR1Jr0N/l ˔0/+c]]cȖL v3BRR{cbld1NU/`VWv41h'٠, %jV2lPO[]XjjZI$˰ V;MV<(*jx st nt+aNfI-Wk_9\q;捉 *%o,"cXÛ&LAb{df=V$MOɄ+ wx3McYNr9}ew hPɉvW 3fӈNл3wsZ*Z#Z : ^:-2]uO87~JNw;v@,_^\Z>pi4w%= F#\فoFՏxStlv`yAL\Bȧw{}\/wAD } k'No?M~*1LOݲD< U`_'t.xUD,Yh!_^XSߏX]x_lY m* VE~1VX.M^%q-r) S _ b XMU:J_絜 4.a]E ӣ]zI]ab^Ox!wE5apMXolEo p!!Hār2! : _!%b ~ =Xi%ve `bE'z"m"X,I +!buA"b`'1J1c%&!NoDo@`"|C] *"8R .,#9N-Z:e!<<= #>U2r#,r=oQ Ƭ"/@d QdE-Z}F;&\G~v cX$I>v\ oX+$Q#bQ Fd+.+~PƢE^QER:STT$UvI W亽MYZN'8#3D0<13LH<3'37Gs>bFgݚj+W[ #tSU8CO4ZN*W|;DH@|t. I%"I_n@t+LW%V@NNDEޅ^;531R'4xXF:J(K2 s3ouWW3tX+Y5d7 \5[[õ]73D6^gD<7qRaM3'0"a+hbi\?CGoetuP٭-h7Cts+7<%v/6e*nm{5fMXz/ e~5U4rr+s;7ts9lgIZ6&5vs7dixnv9k;z$Anr(&7}77v~7EX7oR,7{rLJIF *2jOHY9Q3whHRÄ%*87J&H~30قZDdx0Z%t9 &Dt;P]kLyI4d"#9rmCn6W8G ]}7@z.ğ}%z94:;z֤97,#kz$X)עJT?'Ƴ7n73:sቹ'c@i:$0*[즕@;?D.N;i?t!V!ۚr{GzVD?8c {@;{ ~;ibe"s8 #)gz%n~۳'2+nlV4|3 ~̓xxJ^eCw/d3Z7ƻYVуҿ=܋v;QF=O:}9C&)?7s[ە_|O kc =Z2#ƽ{ g~Wc˒Ukux}Jr;{I/VGQ/g*kd9}~_z֫>{ה:4=c: L>E_K+ғ߷uS-+s{;?y u @#DhP[{w/ܸ4xP!CtxF7lrēUl2J54}gԤPFSXivmM,Zl0p7wn. Uqf7v|y{';t S4ͪc*mi1,H< !5H4԰;BB,)AKT\E*irNz@kH#ܓ#'!0R;ɬD-QҳLD1wbrzF5 PER3β8; <V :ElP `.Do&ZĴT'L55F B*w[T1kU[}+.X̳v30@)Da|-R^*"=e+e'LM;S5TʾlvTp\V$l4.ZߍB^-\=yRz4%\*bFfeY1'kgQ=] bAYT| 7UU-,݌ʫcӥ !N?SiT+@ Wԓ,Drܥzye/C#:Q9b$sSLцyU(@&XG'iI^ INL]p^6:jUEbV eFOSpFHj]kQ˷+O—:SXWDu}cI"`&Ccy{-NJdY+Ltm Z5Њ62"8վ$" ^+U޳h2,M-dJ\˾8Y|%\h* q/U+D܍YlRv:M[kW`qo_ W!-Ўw)]$ SIp%4 kx}<~KEU7-v`G5qw;1 eUd70<,[=QF+"ZZ1wY38>^3sfK s`|9ЉP\J#1R tDvfO:#M3y:}$Q)]FT%ڴ5Ezkڸ~'ut ]I*X!&,-fj3]S5kۏж{7k~-aiR TؽTo (7j[Nɵ₿`0Ɉ5 #Ӊen~b^6G<.̹sD6p aL-tK12z&0w,t\Lfo p]W3YNڙF=pg/Q&y׻q ^^xe[Wi^sa 291s`tsg  >Oƙcjg7^R1<)އjru'HC|ۻo{|p` QGRvo(4!tˈ֋Oc/JI |E 0ChmK v@)i/m+Z3Pz2@@pBFJVF P;%Ql3V$ 0~rPw0xП~,[?D)nTb+,+~ t0eK0Q Q1DQ$jd 3ngl,gڎ#*z[ެd> e2+ٞcQʰ&P/21Xd =1 p%|ˬ[Xq(lÊ8 DHxjħNB!~Bi9q1!GRDf!0Q+.-6'+eIeo9Gʢ/J;eO".o%z~ %b*m['7o,(} 9"PODOI/S:Q-;R;]3/A55(w|69:p =d1+Ib˴e X"sͬs,Fl?z@:4%."A G# Q0o0V1Q&D1kqbPrDK^bbk&LaX-_?h'@btt.x5N[s>Ms|B7jI_#TmtDIKMtLSs?0ִt4;2NdGVjvqHc =j"`lJeJ4%0RoK+R)LtSs#FFkT{KTT ToQNWSUWu"e[ó BIVOnUv{+KCX)uu'̥0̔Tb¢U\ut`OAv䴕U[Nn)uI˕p"tQQb 5K XR56DY&U`ZQuNVaa-Nu: brxcX?֒dk^I6T4L_Ź1_5-DfkfO3Gmv5"gux6ų 14 2 MN"i5X%dv.jM' I55: fvZg`aq6mtgN <+hGFn3#ۦDQ$V%ߕD ri{̪Z0=epqkGqqf%7m[y "4BJ3`r=9[,+k,C7X[X273 oS 4zwvDy;k'(@'9nGA[oM[Dǒmy#]sϡژ 2#Z8m,k:15CkFZ(XS:-W5zdl9xz+972:ňz8דڒhuYe{Z[zz3P=2ZQ}RoZKTfV2?c* {( }ȟ9\A :+7[+=۳1KSa_@ֻ^n^A{!*yך Jʠ;I4`Lɻi[(̻EЛt{iځ|v`A::Ζ!;ÿCD{%8a;[!NyDwwҽ9["7{V;2V[DDѯoAq{ :fsf5!}+;N815`Q||\~ۣ8{G3dc\8an!;ͽ[u͓BMKOSԡZ\ĂM]'2wq^qGݗS?o_?__~o^͢,=,~ ĀŠ H~3$ _ЮۘTSRZ`w@  ͜?-1=S'so{Ub@n rf` 9|1i&N㱍:6cHW0ʕ4!LjnL4[^M$;ժAdݺۊo+H,Y(p@tY={ܻ{_L^7x?ۿ%] D@`6[,HV<a.(!c\an! ."2E&XaVtȢ:Ņ16AfcW(! ,^@B-*\0"DH 3jܸ ;tXA$QXɲ!0cI3!.S7qJ0v rӧ>|`c,Qd+q4uWlܖ冖۷oɝKW۳߿ L_Ÿdž תu,e[4Q_Hʰ(`=z rI.s$]teȓ!2EPJź  V2?FR͛˫__߻KSC̿?b^ˀ__"SM5Hs-|DkC 6BE@ۉ(zAH"݆n0pC+\r<ׂHLIT%p ;䠤V]uGx֔SW2ey[2x`)-ؙhW}eJ(bx& 9T.ڨho(K;ԒR0"H)BV݁GTRyV6]fӥުke  29g瞤94@f{vۣ&m SQ7sJ黜z*TYe~7VTk/`/& 2JulrrvyԪVjGu`/,Kԍ1AR=<1K'ZG7ݴ" TW-+f B&P]|lLa(-,wn1})J@Ξ:B;_ZK'.N7x"WnW^f[M K+PC_'wJΑm-i,z ݸT7{N.8GHK;/=9g} S^,ײI7p+ԁC!9٬.{-ʫ瘻'%;KGxE`*aFk󨄸eT0qӓ,`1=_m ^W&ЅONȁ̇Ձh}^ J6 ?fa+Ã$2 ` JU2MOr x`MS-PB bkjw65wC!QZ`QxHLv08I $ȸUZ\(]Xd (;׫3~8a Q7Er@jHT; y?.ju,!s%ȃ"$шN,o@&9U̦6GPlE-1Z\ 7`+_i>@ `K$ .wG?З1H '7RJ= Ed3ik.cH &͎v ՞IȦY̢j'.DCy5 Uy?~[?ٟ@%fTS1Rh,N SR2+zՓ! +<7 Z@K45Mo,4%/;R3Sc=8Ap jѠiыr_0V3rhIj5SNi]g^/"e'=(Q Uu8Dqm(Ft eՂnֳю0dR֖,#ljVwm^w[snUQ_fPMߐܩ! o-8_c;65)#C'obpKGl  wp@9}.}%m5m` 8x P~< :x ֖]&vF&ie7VP W bx+&X4gC:}'gx1(x-!*UXH W$6PWLy5XZ% 0p8x9Ax%G8vU@mdasvh,I+UxqjZȅXLfdhL'7s'wֆq(Wu)H-XXLEOL! `!)dOe8GeiՄaGO+T(fD{b)M'h[8%H;t!ψi~F6D J`DrP+Sx wx'*"7'7Vy6j"\qxesrcx١tsVd @J@y%'eɘɝ;f~=huBn _r4y&0ǚ)WI(gPO iihItgÖz$e Qؗ9h iى٢.ꢵY e` @~N j&! *%Q9|V\)C9bYGrO= ? o x P z@ %I@$}L 3e?0ʘ eU?zɀ~Yu40&Y D:GZJK XS65{W /O:_^yQp ঝ@9".v@989zךݙ z:I3J0#5  F \I- ;]COb:]ZÇbHΧlp WɀyQ pͩ0-YJ Jؚ㊉؃ʐ b= îe⮠9z5j{i*s RjTzw [:k_ zxd:ٱU_d zQ 9`F 3 &{ʲg1{=h+zک ?)Ck+^!O* /RzP[\ g[˵)1`Ea!eVP Ҷo[r;kڻ ⪃ڷ~Y/˨MӸ:K S|1)w [ Q괤[k{┪ắ eZqcx,WQ}[j(LY{魄*< 68br+X ;  1O+O <|.'A*JLfE @  0&zѹ.ʻ,ԫ$ً̝2퉔 V03\7@.jBKM :]k;/Ƨbɀ 99PƉphژklmIz (snfnC kN1-!ĂMmQ)c 69ٔ}} ݀ #th\D Z]."_- Xѽ,^b>掜.z)>݂]r=]tVE}zApّ D.>PKx̨{5j卻Jdξ_^Fd@0d{ 'bc+ y l,0 n]R~'+`H~pQ~la$+n}Ė% F˅{lR'Ļ||!J}}hp,RNO}mh֋._Ky[z x?jK9p \>1y`Ʌ 9/EO2.o?/s7 ^ػ?Q?9}6PPW?`dڞ+IqΛ x5 eգ zy?ќJ4 0on??oאדOHro'JbA@_&l88v-լ $XZb.T`2!*SLY/1\!sѺUV`N;v DMhP&4|(OAvIeTSP-@J5ѧn"Q^ņ@Qٲce,ٶ-Υ]wwa#H8f$\x`Dk(VqAsM\=̘-3gtCWVڴi U;,صƌe{9䭕*Q|sM9w)OMnUޜiT Fʔү`;uj[79tg` *^x}%[ߟg &;T[5\{ 6]fD"$tc%d 8t⩬KN9Sq袣:JFċ+@=$h1(@3"̲OKPAh 5!ys UxQEM˂lW^t_4o5]=1 A -OSvZe [[K2\q \Qzfvߍe]u {kv5_&xKXNy=#aab#v(Ico8IA" <-y\QFWev݅l,WEm gygsՕCK 襐fŧnxzwjk}fvMl6JYIvAυhZo{C\X z60Ģ]VpߑJ/Vj *?r[ Jϫ =2VMg|uumu|vۃk7}x?JjQyyiI1m1bK1*kX){^j R.|'[`1vj79G V0~Cl()m Pdx0ȩ0ȕ[vꢠ#(2 r5J YscCUv81 DVi 4G,h|d$%9ITҌgd&I0K6JRqSf,ҕ^ė IZn"N L_ڊ%c*Gs&PjL j Dz4ou.F *,0sֹWx&q NA*? M'|)Jj CK"-PRdԗWCsbC[ Q>%ReTHS[e>.Hv`&1D\!=rC 6WAMbt+X}W&BxwsZ茷nl!`ŢZ5G<]\Ǵ lpS~ZUB Vch6ctl TredHX"p*J0p;L^/U9%S=zbZ,>ˊ5#[yacMw5۱,}d' F1|zeYW+g:Qp,]u{ަ@E#7e+f3y ˬ-A.]he:r}_3a,2]7iO3pK`լYWGxDI\lmQT-` ;\*dhnIkf8IZ)%6[čr;v1d̚Z]ϻzľ򭐕w~?³;8x @C'ЭNOZ۷EiT͂7uvwf򂪜mk_aƮFwwv$Gg8ldkN7q:ۢ[k|W/Oգw^O{M̕9N~Tɹg52x~־^pE'ө {£I}|[(`!2v^jomfSINNr ~zI? ` C=ЀöhǫGxhvfw.DWK7JѺh[/˓}c>xC0qjX6+[; 3;Lڳ<ӿ@c @0( N P@#̼ @"@ 0 4?@+?2“3",`#|BZbH.z@$;<& 'Ң9s4BB0A.ʻj۰L8b:8CäX/@ <@c\Cr#C>{ۼz DcP2B(l6D +,tG.?1lPMA1 3Dã ;+hQV,$TXC1 2 ;;#xG#"+ ,Ơ#Fc 8KAJ =s۪h,i2mL>9LNXEV&{ō !Su|w )yzO|a~Hd?|\Aۋ̽>?lHp3CC2HXH:ZЅ@vtzĞs8YɞK=}lI(:;Ib3&ܳV94Jf mIXQs4QѦ*PhQQ"R9/@Rp%esRP$<)#4*RCJ,=-FO0ӘS;ѥ6E TEI>;-%P ?7 ԗT)C lEE-zGT1O2mLET ePrc? ;>eUYJ@՚ZU[9[ҡщ^m=`G20tݽT5tSm!}"}<C4kSWm-2(%Spq]6L\`?y]RcP8~х\hҁuᚩR:E쏮D$XXvODNYg=EXJZ&DYA(YنBDىOX8ؤuXZP5 %}ZNp|5P.JE#u,ے+8gC[ N}[e؂Is$DqΒbSTv sLxZvXŰܳs}\]/dK[X]F4!DI1\$ݬ{^0\5F0؋[xCa^5<|46\]ެ O_LEp-rM[]rH_߸=Ur^_μ]Q(`=`ZXMV+x`jJ n} 4`IO=?a'WCaPSN]Ja(aA-f`G YjZ#.Zb2_z}荤V^-H:9 ŵa1>$Y32gPHbm#nWWc࿍V+\@]^rB%<nDVd4b_ d0uJn:N> i%(BCveFvd!e`mW-֤MV^.PpúH&֍1\ def$. =~eGQ?޶l6ZnX^n{0gsFg*:WQ7$gg@n;@xg~f س%[K[Px^vM.Neį*i^fdxi.NJ5SNh|&= `W:  ~RogM@]㾦f jjIjgN"Fvj6OvEcj>jUOb=hжV#>>gd<f~Ң.؃5,VFn4Smm}mKܛPA>~ ʾȕC.9oxoo>`[';6)guk>mn1L-4*k.o^v.GoȰookW%6p9vqd ]>` p鱰p} o(1qӞfYvuq7Io7|+E$sf!"gW42%&ausgO*wDarr^qr^f Ov 4o.+67~89:sof`(_)ߪ?rsO،$htI+pt#?;38EMNsPuQod]nSGm=PhWBqXJ mtGO\ s4#qhw`&b7vSefuh/0'\?,vFutw[GRҵSeu_wvwwJ<|Ey>x|wfw~1ertWx(>gg]fqR@]Lww)z!vw<'&2Gfg'unFyiN8x @ 9zOgwz|"yUwS|G7N4pkyv/ /{s>{,$g9y{wozaG_vb7Vnmh|wxB '3\Opok{agǓ y]o.d@yuHhѭwb5Hlg'+|"Xd@notx\zv $Wo?ɾ~f͘zƒ X!Ĉ!(` Ps 6!I8e7pNU'aZVi Yf"NT'J[/ g#~cF @G .(RydKbWaMMQEQB/$FRF%Yu ߢfΘV)"ךmJ`EQ@z9Lސ$94(JL*vm]}b5%N8o=zU~jf\Y AN֧uޚ]FQ,Dl[UȞlVeF9Uu= z r>z!|n+Q g+m7<=hRLŧ⨪@BWR,2$jrn6<3Χ/ 4mmi;4R65';H'j͵ M̺ Ne,Uk#6:sZ(*-wҊ:I/Q[w۰i8pqĜ{#CN곲n;8;y0ě>qO<*ȇ^鉙GW-/~V%+| <1P}Bc' N_]FPO^1?Φ*BN |ct< MʩB2 Kb]6(؅pPS5@ , C̐B ~1ۑu@C-A%!)ə>Q?g@qY5"łEC#I&R&1۬ ZHsG9 y:CK##9Hqk3Dy=@gH[efW#CcdДtZ:=֓rG{OQQF광 j1C|i!2r*h. -…GTbFR+X0u܄*ʴA(U ;+.fEEu2Zmm[!#V1vk:`WA(ҰE찚=Ee d'[Y361ͫhYGZPmaY v6-mkgz>~"n]t\J5f\J7_=uk{ָpR/ċه ǟ:HPʸ0l}}FW?EVg[G󆚾%0rtdX^*z G i_K^7ÔT 4&c>+Q w^;pt}FfCސQMIfߒ#dL*VNQ\qy-xՊy@f,O|Oj9mp)r7'?vTk!i ;Eh%UϤt"o䏐^dmXs5ZayQ<ﲎ7c ΐ00.Q5޸J:~A;r݃E|dk9_|6ǹ9i:{~^RAzҕn:3NCgܶzni-T3_os&lj=7" \z;Q?,M ?}O1WՑx3G>p?zO:S8W?)0QiՓgzP /^x@/s1{b_ c,גe4]ĊG(=,cLx e|R"-P; O o3C F, 1Es(^p P܀ޛ-{`/9ZR@S fDQ)`x%Xg) : E"[H$a lL|>X*-}ɝ\@Zt|ͅ ;d&nA %tD1Uљ!s qꐞ`Ǒx M!!V("2"9b$!%F̧\"&n"'ae)b̉ϥJ$+&[,Xb! .J &|=R%-"l=[c>.283>dA~47a㝢a@* ڂ` ,^ceԢ9M:֟9<W`E#vLv}#=L 2#oNj(FBV5:CjDN\Fjdac SO" R$vEdL#MM!@N%;0 DB~J)LPyfc[\\IN#X\e4r_˜`&afPU:b%9$Cm#eF"2"Kg2fNg#^@N0']Pޞ@s0em&bZbdo."Q 礀ed=`re6g&sV;Ht@\'hNq(Ux"ganm^DbV-bUء0|>J-gYEwd*~& nC&U@Dwjb˄]:%u9ee8ס@$(z~ Zgt(u4xݍ(! 9ĩ^uDII)mVcR͓B=5fJpV0#9H%ҋ(hzuvĹi@ĩ6luDwbɞU6t؄딂;rfNꖾGx OZ&:tqoj؍*F^JfyF&"*9 .+6kXGD%fLkTޅh~F)ڪɑNǻ. Ϋk B׾0‡~PNJk?v6Pêr+EHĢjZ,&r8^!촥9+1lN*̎h,km<"dflh^mk)~c~Ū*2NG8 H- NllA¡[Vlr׷>'ےϾ@m ihޖߖǶk /28./8t&319[ke;\GDctkcG3G[m#-WttJ3'Ad)t:C4j>M2 5GF߄uqdh#WMӽQ#5FS/k54@1>_5>x[BM_aܝ5 JuLQiBIoi]5^_6`arV2bŠFcMdtZZR@O\zw0S vjjC4IDvb"Զc1dd_*&[;Zi7Zq r7^/i7sj7,kv tXlvmJnwPQ!ep7h{wZvnv|6tӷtaWLlqmu 8YvxX! 7Is0?8F211Tx_3CqOku9mnUFȂ10ՊP "wQx}1_&\~3L4ňuk697uID $Β6<:3NFtCsDy)7験`?yORzWdQ_ëgyoy :o0z: z%ޙ{߯v[{Mxz{&L,;"E ?gQj34`{hC_S\:7zczc؄뒋>^F[v%<dj V(úS_C3L5TC;}4o8K(9fo3;;$俺_=ouw>g~W=8\>^#S>[ڳ= GlN> %4xQ4dFĈ/(R(uؘQJNh*v0a(AsfL2մi 'h;yL߈yiRK5u):SJu:f֥Ju:beض4Ե{o^w90^6|qbņYh\2ņ#nȘF#KTP/j.ЀYѥZ;f=Fgiρ5>Φ]-\tj@uչwb+n8rņ%ZfX?:gMbO& Ϳf;M)sΡ%p+ uA2+}0ukN-袋k.[{1 ۀmܠkh$ct88)~ !*pus1GNoI))f-5 ĜP$R959͂9N;me1Ͻ<^q؃ +-،$YjKI-@#TTpG]RM MuRM7_uķP5[7׺v5W?b 5LfeRZ(+Jɉ5[Kq=6s95wRUu|^9U^[E_V`A+"c[E ږT3ډ*4c̏=ڸuUU`wֽb.9mߞy4Xv>)cIk*Kж\E5eY_z]k6{ZHdvti5'٦.*rn-4o8=._sE=uW/a_ s`o<]IW޴OZby̯?=nyv-ihEb/{>,vB|Sñk[J6Տr1l$`[8@Ņ l~ d`7>1ɳԷ>hcR =H=ML] bE F$SC3:䙠fl`SMj+^ʣM2uJJ#RH+/+<Ƚp#mF4Z2oyH{G>~tN 8}_]9$9ErёKn4I_RRi4%:="7A'&c@xJTiK+])kIyL\&N_xi_2g,13d.EjDIhPJs[lY(2c&˰h;@Nur;YIs X{2sh ?E3MU+ŅjV8;E_j䊪Q> waO,TUe%Jki@;T U*š-c&DD's+&I'_ڍ"NV᪋biFPNm Lj踈ٳ^h" ^S׵n, _6FLr-a {X͂LX2<=f*HiD%& ttxMaX3|.lK[=U0_rMVKlp? ]rQ\97tӝjW0EHw%W y -4V _V+oq[_G"O@w1\}.y `,9pEā<~-n}t,Jf+wiXFƺa֜#!W8U@-[QrbM*TBfY_vֳU4c,ZXzDtj46Crx ov#Kg8J*pA,h*R4XtZLeW[vUZIǐ84S3 =w4 Է(3g&ϟcYh2Ʀ]׋v\c0 $smhe )k8jwtv S;* c-tiI/t;Ns޽z=/0O[>0Êe,%7}Ѕ|gxTsygVk++95Hx(Jo>p~JQ^Ѭ.tBjج2Fڬ0/F-RO cpiN߄I2TGGύFnX\p`P0Fzox0#Xp" ##Όv g NP\P,oP F0C l<0 fv(pJ&|{.$AQ e*mLF b `&(#gk! h!f6qB}*9+PoB R%.Ř,J xp@בi* PzqoX.FY8# gI'T.!oa9RA ȱmڱ0*YL<"i 1wR E7 7!q!CqZ("/|m#ّ#j($$#boDPQV%sN(&&h &&S1'CЁ'S],R( l1)Sk))o$L"n+]?+PR,r-+)RB,,U1"i'+2.12Rұq8`//.PS0+oBS+_A7w7 *x!!O8.3B{24H.8R)wrG`S") 1W#z=U*j8mQP'-]|:S:4Rs55+5v>Ю$lIF==s3d>C-AS?1.Q4@/@ T;$)Xr@A306iBs&tB)BCSS9/N:4(?t(E:7rOT/s;C/_v%r"'r7z4Gʹ)>UBE!SF"@Iq44+JbTEA@g(2F38 &QL1'1B>SQSrS,tqfTOwfXt&P5@ĥ5'pKF1e ܇Ri2!YXс4-aSUSE&9AA[TLTh4?+J4Ԙ``v@^ 8Vy!Q!WuuWy'L#X'j'ab#VbX5Yc36c @JdOAZe]laVR \a/H5L5B Е3P16/ˆ` "6$jM'k$!pB` l1+"aabnbX5o3U,aށDVpEe pS+A< ĀNr @gwV\tD?5TxѩO^֣]3b!bU jv  z7kq"AvkK%K`C`u76nzanwn!{{owppӷdb`\H "8ݦ3~vfjo`Ph}<6]k_#ar+xayxx܉AWGi`!K7m6zg|o{ǗȗF||wXxaw9A@3#C(CmȐ>4Ku- `E ` 明 `@혎؏  Yr 9#Y'Y 3 XC N Y 8 ! ,^  :thbE#Jŋ(LhTQ !Bv8$D(SL ˖8^ʜIsMFrܙsIM5F:uޱ۶hѣEIvXZTiZ.j`Âٳ[˶ZwpʝKWۻw˷߽N-* ݖ81Ǐ L aB&C.ٰi&.ɺk;b˞ ukGr>7գ)5Mzb1o!t"6ȞU:nd:m$((]5aΦTe#䐄@[,;4"=YZiո*WաZ*ܰ`Ľ+̼ f%嫃 Ѐ%`liG=&65bQcǚW"|Hd* xf;kF8a$`b-M2xI6ih-%̎h0VN1gciJS]Cdi#= /{󖌓F5{kK"mqE4^yK]Q L_ ߓp ! 7"&FAf!yuJW ؖrnNeLuoJ$!tMm+25#(`6}1pA +g[Vt+:6uo2MUj&6)JSa6Z(FlwpH r4)f"e R˱(gEqEl}*ES}U )'Em5&ykd-X! 96F5F}е ^t$fFAL ݳźsVr/m۴̩wΈ~Zm ׃#X\|lL|"Q6D2t>;N\1נ9muE4MB?A?L{`wk諭.t9]a)#/48b/:|$$r^4YmkI㍨ϘvvҼA`k` FQ %)ha9ዜVl<}"^oxNYddh޻^ʚ z|'×V|s|Ǘ|||Q#amӗkWb1}T^y7~r=f~r870 |wxӀ d'e6XwnjW P3GUp(&6xէ10]msCu=}\')hv";N0~Ce4 O%=E gW ewEh~5OK|fj5 w5W8Y&sVg'^a8#h'u5i^-EPx wXkD 0@U=@8 %}|8|H03o$ei Y]Hpf] X5p9'R~&1(AZwv VU DJ?Ř3`H e͘}Wt 2G&B /xH X P2Eh踊0Q"!mǨP gdDfUJXh`4 YsHI_fGPQhOt)H$@b`)~W!-2' uF6 8r:t c;ك3 `I9|x[LjNYQvRIY3b4dpӕ"a 2zQ,q(0)jy 2`x73_6@ ֗hEyI[e+MxGCQ בYI9%!zÙal/ᒤ鎦YPyvnt+y pWcXW_`qFYŌC ȜCGO 1N1јiZxMk,p ":(%1y1yٞ+ xpUP8Zc<  dUX > F3 R yGyuy,")95/qK.z x vj %[@VWEnGzɤ@F1QJ*PWyp bکgSdf[ ƈ!0JXАwG[kmը:Jg8g+ wy8]DQXi Ъ骯enmn :AL[: S! jʬWxp _8&Ch*mn8K) e%shʮl Ʊ*VA:Sk: O y}IC%,)2+zCZ[O LjR{ڜDbʯ01&*U]5+9$AJJeeEGeJ˴MF:~K"̹F~[,& a+)fg7[lxj-@ x s{ `xzیX#;9LIS̡۫[[}4$=77!Ҷ[~:˴ N{Ik)K8AkzG1&ѻ,Zu(''J;AV6銔G|;ۻ׻~+[ۈH2f;#S8^ [@pߊtykv[|k&P'l L ~C-&H9|:EpF ¦[e-QˤM\1;ÜxMɻC웊[x54P~kwLS+X|%%ܳ.)\- nL4l `a.Ԕ. / yPS.! Ċ>^n>-ncn>!I &4o|P<0.U@8 뚞f>m邟e7>`_Sa~ōkgj?s?@L-.pQƃ`+ R@ܑڷ o/?ʍGث}ݝmu즟 мP# |Mix_?}`h $8Ak &C->OŽF8@"h$/ /p 9Cq<Z)[ĒqB3ͱn\+G܋H>< T, rw rRPL ՚z3_oͲM~GZSP4P`&LЏT um"%IQT/StT85TQ6SSNt5H[WZhI ]y]wւ])VcLL٪HmXSah+s@u\YC](VN^Ur{%@_/2J=4VM٬Q!G,0\ɵs{H$xCڤ IWDNFV6Sc~fl&o#h%6h~:ifox uj|p \36ykU>=]vV泻2rS^g;EKo= wp[R'f4k_~Zks$NnDtAeݤ׳Òivr/#ӱő_ۆ X[!C>J蝟zZ/)mov|-Ӏ(>1 ,h<%a oE-n᪡~[ԑ=~< xaV&łk]hH8a'26B0f]UoRU+-qʰntd׋RGq(fM;%#çy[ǮK߲7XKg#U3E |`X$Ij]Fi E ChߛKX"V:;a~PA ܘ9` ٥ZwqcQUKlRַdd$â7 QY6~S,1 : ؆9{"ml\UYsJKܵn|sPЖQ0weE/0lg=vDl#фi2΢s?PHީTشyG9ZbTޅu]WǷ4̽k^oV- ACG*7Ot.b^ {oӫ>̭ D3+A%KurVBI=3@D*@˩.ыXe8JzIKYs`7X;USHVrսXCH4X+u45TQ@7dwsh>@߸etAYigߦ;z']} ן߫7!5-oHw.7l&=7cdz|<Ϡi' Z6 8܉g(/4\M1nɉ-Hwq_5sf+pUʎ[H{>ɗMVhR Bv֤ӯ c ‹=jú0?A?߳ 0y":4〉㰲?ky5) n\r;>@̐ҐR?k x144u#+B;9㍅lϙAAA\ĎӽJ3!0j+C A{ϛ)B+,ܿii/dZ$DC:>q@Wk 08q;8õÓz'5)*6;S;sc23DL4M#= ŚCR<&U#?CŢ/T2Dg+L\CzбBh&(ET< ۉC]tn|YB7CAG] s7RJ43tG+BozǑOQ,ilk<$+jFB̬hHQyHH;YJdXd73=ܷSj<`9H%02ӨI#X3#+l3Ļ 4J=k2Јwx\@y))?BJ K2bdPK۰M1҄39 KD\ x:;̨_"JK`Gj4KLmRҜNӔKMllKaͅ>@Đ\ ܎Bri|8<0ZEN BDλH+O\4ȨMt/ɰ&F2PunBM]d<[NTP ĒL*,hE"QlY!!mQ,uQ$ -2) 7ʬ#$-;\$S(=N"k+RB˟ I4ӈR'h56=+4*P: SSU (U W,TBRCHR`8udHUIJ}Om+4C{*MNuOݝЦQ]$Sm RBW}UջHZMQhT4O_E :BWJOdHK )jFKFcV>l̦rpSax$.Nk^e8Fl`1]Uln>igcΛ>>nͶ6CNnVȢlV+`#OFWrӫbe@X]p᡾kNp?j缣hp4 K~tH~gtKg#7On8/q(S\>1C.r&]V=Z iRrtIu`otb6?ڎA`O_vgJ7LOve'J~:vGTWkqe0;E~[]3sNjVwtvwwgwyƴi(v~vcX<*q-.]CuCWʹ7\wx܎kxIww+&)'n5{Zeʻ $)b$GHgoA&zg屷.is橿^y,B;>y-aovvcwp&" C}tl> }1~wbGky?W=F]Eohl|`W5}G_D {R)W}7}iVj~(~iO}kvK}ch~?uo„&l!Ĉ'JT7j#HAlӨ-ZVlRXh2Ѭ M&hv^'PrδiMJ 鹨RRj*VzZaŊ+د]^E,ڴgm-ΪkW]@=AhĊ+Ȓ#lСd(r١EGaG$eKa$4'ϝAw͉ͥk>ZT[ ljѺW.ݻu6`<@[fpre%J4GӧSQI)K'NP "H\q!T9t5;չu]\iǝw~'^aA|m{4X}9.C(ב~HV&`.( m4!`n `Y*[qdXavd5o%\h^|x.x3X##AI~H,:YlPJV6[fTYsyfZiEbow+Vawzz|c;+D(+ 6Qˣ cԔ:UiPjzQp|fii]( ^xzg*AśkP GŮI*$lѴmD)-4=ͶVu:Z嚋o*W/ZFbZ/i ;p1RI+=qŚb9ve1bZ'O`.fo-2ʬ뾓k[R)tf*J_ٴQ~8x)uWǁzUcu[sn݅=f3cj˶oh38 lPd^+N6enxR3>'Wn%k^Mv`.豜/ ,l EaR| {G3޸ba@dwD^wǹO˞fyO>Q5,%$qL%ĢpW!j Y 8Foz:22ܫ`>h`UP w&PL*Ji ;CK^T(0>tU@hy^\]i"G\HqM"hb`B rd!soB 3HQc"URVme6%FpF} 5x2DJjMB0*ӟ$KFS*ZJ'⵺R` roV<':Q9:XƲ5K'bˌo]juÜUi%=ުf5'\s* UuNMut+ 8'(s(;ݹG%.c%MO!R&M.t9Ta(=N{8hx0Jqԣ,AD*UYnNXIH Gh:Ph4)%')&dgAK mMs־v3gYկ-w@uj-Vfw{,0+47g`%̩e^pU| g^\)pp+8MnZ-8q$>d!-ln&OqbR2|s.ʰuҎS|xeإW0m@\6b8ڷ+X4~ɰ#|d]LS֖7ap͓ Z}#v)UӾtx;^6>\9^eeiYU`&4j|Ѧ p{ؾS ['7L}ʊYV2}MyX4 ޞԃ~^Z9YCm5\ZU5@HHdVFJ XCqVqaq Z~ υ`͟Y_kl ǎ@X.Bb}!Is5!AZZ ;iVya  Aa.R !e!P NZѭ !!bgC|1-l:ʙabjbqrա'jE3zbN!TLDfWbh0"nU"1V#CcVT3q:zb2]0=#4bڽX*n*]E6BP7",#IpJq/47CnE=_;^ 12T=rJ>1_+ʍ6.@dAN@"JT.ꖧv5b#1*:(! .KB|λyԕPQr !Qye'(c3dQ%$_u$+FAdLT$eD8V9[OOƞZ*[f["%ve>N^:_bx͎ eb]Sd*dRfeHb&m;c1jR]B]dh58FB ,"$ۼ L̚1/83,E\pAGf_mcr]5R.2bqqh@eۼtHll kt/|gxo(-Yym>zo>6B}gUrs:FLވa"( DzW.4(x>mdƄCyvEezj[n懖X>gz`3qeg( &kZgFE@ )x'&i^,Qڞz>.{啖eh{鋊hTNXv& ²8F}%E)N j%>g‹((Gj,iQ$.)e!2%|vZ)ڧ^*:gF@jBijGC* -i'ThZf$z2kܳrhT"6uȸ JyҦbYUj$&֫epkUX>@x+ ,>ķ+tf0"bLr^K.j(j0j06XklbʲF(̂>Nn*:û«: Zdd-nfS:.ekVm-{} I=V'4[+2*)^'BO=}eH}Nę[eTm`~@a BD~LRR,Zfb-s.-y . *l,fDR뎣k̮l}*AVXNQ 'ZсU*oڝTDꖆ~O docn~o/0rL*'1Ϡ:cʪ)oroqL v0z/20Vo_œ)]Z憦Vqp}0;/epkRoVF I-B. wop60 s[0O&-3\0Uo /oL"1+2b]n ,T0p (ς90Ț"w;%on1,/. C`%^r (#q$rG/$%_rB@2 ȍ w-(23eVb߅IJPo>:(2r 0W0n1*32 C3*K*_+c5,s381y 8%39 Im` ;3ӳ l5`sss-Ct^5D9Cs%[.-l̳F2f3΁tVHlyP+3D04A43# 2E{MQNa4F2j3Í`%@I@0D8uB&cN"3״)3cju*s~(k+۰5G1@iZ#4zE4/\ʲ=$m$z0bJFr"gu btV^ZqVrB`6DdduF>3Yg5َ6O;6Ydl7_2mWnp:6Jp5qu27̡ZO3.s'tKas6vI4vmowhtw7E-Ei,VAw)wVCåL(Ww&}}O7~F㖪.\ 'DnKf(xF19u>D/䬧gv}[}ST~Wu86xL p !L,H2w1u2-8ժcK9wyxK_gˑ2/8K$#Y1)2tWU7~uKmـSy[9Wb9ܸCz'zk_qHS::6P9["޹CuϸjΌ-x)D;!yڬK+uxύlmz:w/i# n@{ﴋ/a_;¹Ho:8\;ё:[MG ӕH(2M;Yۺ7ytl+"R~ ˲sUMs>Bzr+h>iW;a,у 3<>jSD;fo~}ar$E2ثv :6  TQaB 6tCYTdbuQNh*VL0`Ѡt ZL3iZ3wgN;s^PC4zhқGϝQ}N]֬m,!5{m p۠4hphh00|01Zlli/'S ;fM1.JtjK:vlZis V,Yͪe\tqT1)Bq]c Ii4jԭ[9{.zCwOTv|A˟M{kׯaﻭ⚫8#,l豌"(1d#Z;퉞2,ȊW`(4LLIAGqG1oh3T5=hQ!"LxuHKnvLzL@9G}>LRx@B'mNnתb! -iK@a#8Ak݉43xDiiL絤I\p@䔨93C4bH% 9!'2[OTWI3P8@CЈJ愗2Όn1I{gTL  t}DeܞgtdӜ42BFbX#%@T*)=SS:P6ѥ6#Moڴ^ mYCmW5+Z;Z̢%p[s#LSYUiޕMUKm\v1uaΑl4Ch0gHe/مq A,K*BSs2Q^}-l uu^eӭcOd3AhԌRssJ wiWjP΍wp{޽w=x§Zl@"A;ء Np\,[TTD]kz:;7Bs?0@H@Ͻ)L \^<,_3|?.Ͻnݟ2_e,;0bp_P QP ViN Lg]N1BC礦zX!J);"!<F/NO͏_ꨯ4nn`PWpAɁ A@V GXs*.* pGB 0 0ΐP  ۰ P ѐ 9 P  \ ` q q Qw+/3'1'31@G@+RQW;QbQgk1c @8@-! ,^H $A CHAŋ3jܨǏ CzlF10"受FDV\ǎ *ݺmH2扑: !B;DU+u`J,ТfڵiK.ڸxۗ7ݸsncV*^0B!v8eG2glYȊ+C6BtL%LR4 bۻ(JrD %DC1F'I.:س]]W`d!UJ'eB =C\v RaLKPZMsoEQ& X1o&n (7 5lCs6W xfy1 ԦQz_ ej8hvīe*E$Ժke(<Ɖ345 &eI 2jxifZRkb];$'Զ6L)XA:jd8Sg_Z6,ˍ&K!-$% HĂ )#|mZ{^|r\r߮29fަt>3ao" ]4ԴFRp@*aWܲk| ](6;ގwGmX"jmnS:t3&R2p]m ~Sx[,NK+x [\!`^s7*3 ;c1x#B춐=y<0cKC\⸧4 NC>A1 bk.l6\hCihw b<f"G& %E@ݲ8~D"aQO P l</;C#a:I Hn =AJ3ĂK 1WF7)ZR]j(n yh\:Ѹm`kA5!ha?6ʎwԣhh dBZ"3F [Pժ\*W ] 5kDѪ֊^ [X@$W1$+5l^#!k`v]a2n2;lPʎ0#0~`(Tv0m5ڬnUGEkaKV`-JDUW,3IR5očq9X,hνك;]5V)P[ٵ}pmF,['ϳ]zmYU}ޑ͟yumu:J28V ,aR3]I.wI4A7Qt.UOʂw\<p*hYhjTêV->K\3>|E[J"K1(QibC(@KU&K=R˨S+"9g?6{j.4.0 9gb;6(1_@/t[!ڑK*4n(.}hU=΢ʹ7TE~җ&aCA%niU&yZ b ]nr{*Vg]p /4p*(wD#L1 StF6Z s;*7y/r-w`n=,͋s|UUn£#3J \@+Ilv1㡱5׻~p[D$f/CE6I;t%799 I%wžf g zjGzʤ|CqW %}&ioFh~WS9J sB:vZxŸ} !mF%z#U# ؅Uxh W0*h Pɩ?ztZi Ȫ6 'WWXnP:Flj$tiX0@:MmupZy2T #e:>/( :l0< ~&j<;K uGp(^Tu骉PH/P'yĺزl,կ=ZBZڳ=ˤ@CE gV`|Ӟ@-ĺA bkZ̬JI#%mHҰ:.)fWL5,ѰK>@D׵;JOQm6I˖ұ3Lδ\f,~iYK:jVlڦK̩v}zmk|}k,?[- Նm#">M:P(hXM= ڢzL b࡭}] ۱ֳ#x@ ~lI;Mҩfɭ܂hPtAmݧt} ӌAAإpݙMUCU6{`0=݅"A@p+j޵dx-7# Id'4+ ].>02>[ۭN흹ND;DCL_NGPͯT`m希/]4^=SP2g].߾ymXB F P?\O;!>H:=/9}2صؔ;в9._~M? >ϧfJ@QA .dp YLdqE/Q$[44bŠfK1eΤ9͛3ɳ57 E=I.iR}QNJuիUOWU%ˎݺu۶54q(  XᇇQPxñc;r &QdAYzgD]:ӧP~u^o:;hղu wnܺwe\`ŠOO|aLޡ#HˍFͺJ^LӦQTM}MZ|+ jKnBâ";0#<:SoD3QDm)c@"@fLpP /!p:T2$[@LH)EpE2L4w#KunTP@8hGL/=?PRKҡ@ p&1zR"ƈ*=ܒ&/3=ͦs2]KsTRBq-:"4=W.Q<. G!2IuRe5&ΑVZPCTl,|ޏaysoUrc=֌]ׅ>y+0}'͒\d6jmF*ggۙ@NrZ9=$9yɦ" ꫷ 3(EŖfp+[{P9V)vΕnTodRdbFG6YfS-mímtJ׻'!BVVLQJp܁}槔x3[清6ym_DBJ˛֑ [xT'$΂9yt0 ~ RB(ܐ0,|#;}t@t"h3ZA$"XCV4X8- 1'7Y5>-~ Ú'+-.Yl!-F/d  ;GPGž74r<83}cSK,B."=Gr$)J&sIFR<,yPJyKKV25-kI41Ic62P$1hBS]ӤEO7SD zN[Մ(ɛy˞'2=^ JMN^@hIj:_(9XQ|C:)" )I[O_&U(&(s/u!Mg7-vt"ld0P8FEDjz lNGIUXZg:)XvND@/j]kSV&E7Sb׻FcQ:G^EHQ| 64 ؔvf:+W2 HJ Iqkhӈ/*֔+jTSۡèl%j[>6knG*9e* nd5].@\HhCҚ(Q;w-*i+om{2*W919sv)5=uvm0LD#aN\$`pzQ/;b Ӿ Jb i }n:7*B8\g$Lm\Z+#oqec_.k!If30yR\lZssg }cLe :_b-j 0u59u#m arhD00}ZMӞfER5?EߩQm?5=kZ.1s׿2z)A$qs]ˋNTStV`k&qH  ][^ t:KȾ7:O){m"< l \9dkn1X,Swʋ:倿-u ';wsC(Љ^PH\1t5ꡡeNC#[Sw_عG.k 4E ywޙ޻>fᇈu95cN*s*@ pة>yisbn%2|2J衱CTz׃x>)۾Lgʽ0ww8<2mf%]>;+#9e۾ 8i@Or)? {'R?ޒSK>⋀K0;.s|;@K@D ۨ׻3b:vۋC ? йS;3c&Ğb7>鳂?S0*AzAk"D›kBxBߋBKACt+;<CǘPK>hC ڨ,ΨǠ3P '+v*$D$H|N MPDMk4M* щp6U,RPZ:Z[ҭCՠ3(m*-T[@\QO 1QXEM<78M!e % I&= VNCXDUv`ԜOS-'445MTٌS[1#Վ4P*> ;{աUn֗̏INivҷոUUUKEVBLVF[V;EW(-Z}q#s"*h62n}X?) qE?s]`jo))`ן,Ӆ|W48;. )V Ƙ]8RԺYTadՅUؖ@4 ٣I- =eYjY){ŠLًL&F>ُ=ՅE :[e Evڄ!D2r%W}hZyڨN JZӿX,zU0>ǀYl XM۠FDkM-ݸ7R[e'\!m…Ýĵ3Ft6ܮ-U !U^V\M/Vq0]EmbݻXZm ؏MMM3-YԭZtɕhD]X}^+;޹u[C6:SˍuZMߐY%ޡ٧Wy_FݔP3k%`@r'k R  4_`m VE `5G$}Mpٱ"3|Vu#Kӯ)fPWvb50YV33HC^c} 7~>!BcMb:c)dW>~˂I^-n/~^F~dMFdcLFOPV;#KPUfe dA.OOCaMdme]>c^Vc|K f46QcdZ Nfg`i.)޿fWFf4][Fq4 $4N`XLdMvgeyjٻDx}l!/Fd[~#щ<f3γǬOuUN'Bgcvi29h-SKSWIzArfs;Xf b~%F0q鞕i i!bkLQ42yf)56Khzk/܅xꉈfa "GJ.f[ci4VF=dgk빆B=:>mh%lV̫\]LVXǞ>TNíAdQluݩlf}lmjĞmtNN뱾DTek1nh2ڹnnTnJz.~Y^#iVmfo3~T fpevfD;jļ=f[oudfK2vln w0PIW lphn~boXŨVF2Or I~G} .j8r$ %&'N -=&^041a2g8VoU/h(E?6ABm YsVns^huX?Gmt !tx0ftܑV*cIVJ7o34>59tQORc϶s xfWuuumE_a^_P sHZM/iKvk2hvDKhH8jNuUvowrX.RY>r1NpBxTcw;fw viNn(L 瀇pxn)"i$#>}:E& 'dݢiYM; kU *Vb3« @joguoߢ*߂BZȴof6FkKtݺ0:kh|{\őe3mf=ZP/AY4>48`1pkILj*ï-p@XWWоsuUV 7 IK q UZeÄb{1|f" bE몭.0#|_ϭ1kBHG'=(D4Un`Sgyqu#u5<9OڡwmR]u\G 3W~J6`QڴZOŸŞBkuB˘|a0#1nJt^#w]OWre|?<-fzjd\dͭ&8Q `D |MrڤO닒v) ~ ^.ǜYNM#ϡ̀eЄ=]G{TP >NA3X (|+KZr)wRgm#4C0#s;W/dnz^ "t#$m|R (l-7NrགV4ʸmk̜nȐ%"|Lw$ A"sFVIqRK+`BQiT<?wx37gR2s ChJ9KIB2ֆ"D+$jEz\vAnP{]ۥE.]L}p2qU#i{^øOrIc߷l~CRU-p^& wBo :dл[̆a}@+W 5H8r #=^A@Zx>2[߈xG! _g℡6XVK'lxiѬ5<4N`sp\2Τ ;A؜}+|٬2~ FPDNm]ߺ28ji!ɣScA \\ ʑzNt#:X!˘メc{="00 1Zc>>fE?>#jZ#AAB~Ta_'I$.Rda;j$.Jc|Poogp g\t̥r]Th tJ~^ؽ&v[{ |lCxnoglg{pH^]rh}J~zR*`kefM$(fn6(z>(eb&I\A$-hq, ߈ zJ^"ƀng(LhygS>l҂]:mբ)i6<)M)h~H^T <\%i[W"^&fIb6fBW. HRh;%J1ee禖ޘtܧmĕǟ2y ꪂ@V*)jJr,tivV$k>aĝkq$ EvD*̫ڵʪUS*ǐZC*渚+3WA+򩻂kJ뽚"Y).NdW _xy.i"lši<,~gMZ0nlv~,΅H lN..ݰLB]$$Jld#Y8}H^ (6ʟvjj9HGY&[*t-nc؊g2m!۾b--iխmQ-Yx.Z(JʧFCۺ-Bd(6,Dގ*:&mUnR`iꬶ=^XtNnܒ#nނ6kc&-/U>0"]$-nn~難ff ;|o"x>n,nR(6'/5t(YZ.Jm 0JJ|4/s.Z'n(bk}h 힣–蚰ph/dr@ Vp0mpG",qK^`$q Wnpq;H1 WNXb1p\#a֧֋ѱ1m%W%woOqpf1 -YVqI>)q L2-8X2/S2 g݉o1K|צ?-I#2r,Dz2V-%20 HFz23mjp0"Q32S^sl,IritǠ3(ry< <7<=G="*"?ot#0rtizj+ᡸs&jql"M=W4)a4\GﴃAA~t8Hu(tJ;tжt>zƴ#=\F7 Ot!iuue-SgsQ&U4~Mgt5鲭5QRR+4uHu2udUrEE_W_宁#hvPsZ[J5=:uCMP^4dJ$6p$M',IQkϣAvev̙3mu(ŏgJT7y^z@+qjcFs?vl۵t9]7SFM@rw7ᚎrYww cqs6Kw3uu_6V6(A=w߅wHexx19zOuxA/7sS8}_xtgxmm{8fqN/O*w74H7HBW1SCwZ0ne1,y4T }JyדW,`oeU$7JKwi|9HƮm x9P8No7tȝCxCh:%?"գ\Dz.LzE 0 ;L_:f$gԾ#ƎY y]qhW'H: vCG;Y40WóC,Bq䋶;vGS eGظS]e+GIzw;ּWzx7q˰s#\W7`T{Aiܧ1Y;>4=K>(NUs}UjVc. 1^ >Ͽ~#~K>4>~%e3~(A$/^..oHSo74xaB s(aDxcwߩSnݶmRIB@J4xC*hִygN6ehPCedFRKpTG"[4D+/`„u. zmZkٶM n\hѠխKМ@R!Ŀf4cǏ!Goef P@&28(ƨ:ɴ+j6'(BBqƪ/,^e2sFP"CP0 21"!t:(\(&*iK*cKJݪd3"74NPsNDrOIB?<4QM[.CE# RK/Sq!OR6Tr1SPIU_*CXsRVC[uH͊:A_aq+>*V2mvNĠ-2<#ˎ aAWB'hg8=G{a6lˡQttW\W̤VnYo> |> vXwȷ,oM rwEůؕnFϮEꭿuTg}k*˶=U(WzEW\Z6`"=0.׸eX+Ȩ8"%xA!LwB\RԖqUq0Zg?p :6 oϫ x  ?Ğ9*dulv8 6@ .ŲU+è?~/T,bF7>8 X@퐇I bG?*Ё| 2E*ZvvE.ZƋ5$/0}e,T85эlc BG9ŀxD P%ՠX)zkK7H8b]$)0KwB;&w.T|o4vAJLygI\"lAfDĥ'T,ZfI戞f: eF Gkє!7yM"SzT 2ZJQHJ x]ϙw XFh>``5Xʄ*tt4C IpJ,%*^pBhM虓yJ(UQfҟPO4i 5DotO# z֛Q5)|)0Z+Zu(X,N0}bXRwDkDYֶՇp+h8 S`Lw4*&qh-TlEyPN:oTAjED$Z&TFN=TmbLiYݱ'4f]h @4HzY"HZ,H(It'O s%V5k \HS,5iK][^!Ȭ\goCOA߱1SQp ^`Z'DF0Mݽew^Xc 2ZN$b>,0|#s%Voa.ҎxģhǜA j̙"h]vE ~3ȉ6) \ ґFŖ݅wV]Y&P0j qG`QkVGF6|ZZE-ZO"B@y xw#?t=9n]s6a/H*;p0O~w~7@׿`,O"\brއ+/3P73p >p Cp J7OGPG[M}M_VWP i <@ ! ,^ :hĊMH\a8pȱƊ adxqɓ(1$yQʗ0c@IӤ8 T,\݂sǴSwKv+G!$v6E(\e;ڮFʝ}x˷߿ ˯Æ]޶m@0X-IzdӨk6$ׯ9]@fPGTE#]Z6FDxlFy`6r3T(Ru-20r jy:Fvwd8-u h*!w#K2W59nފ'8AQޢ^)/hO+ >]D+5" \d~KX,dXB |@,P r[7<9m>0.ک\^ #qw=x}0hl AҸ63chF*b@Fp j-cvȈT`"PL!5]J!^ )zdB݃tNF7 # a„q|Ԇ6KѹLe 'E\ 0C8TEyh$(AE.}&8+V.1IDTɸ 4k PVMsN"ԥ.1^@C"Jԁ I%j٘b0,fH3\Y;RHI:Qi^MϡLx/(ТҠ]Ŗ'ӡTb#d@S~{ܨF;Qfs`I' eX3\jSno4}'s]x8gu^M3ڌܽsRzc 06&kLHv!hgcvrO&N^YEiOL׀|O!Ȏg?)0KvxW+O},?Ϗwۉ]՛)777)`k3^r}ԋ_Eu7||Cu]W}%5nymI!-r#swz+ă'w\~`~wIRKbPPh4xI8'&l78PEX7 Їq !%?P!7k--g,\~\({#Xo%sabi+6.Q0Gg{9|OG/׃||E`E g MOf],nDž]Љh6dhV&(Rhk m8t'*t{cxt}XxX|J@χqנuQ~8X9(~x~f8R,TxtH6ܓ;0}˜hR({mь(fh"s\~tȆRox%'7MȋOGHQxrV3Y萐8Wi~-ŃMQcLq=q8B9HH^Y;eA5 P0ɈB9rf؇eψFY@9CwF9q痦*VIX{畔Y>(I hikm)ps9vm (h։Ѩj{9~gA!6dș$WG8yܖÚ{ HPbv#Cy1ՊiřǙI㜺xuC.)ՄƏɝjƏ9Y@ IǛQip*`\i)H32jfzO8kzIȚڅD֡)w ,f#J P;^;1!d:|kdYL0gչ}m`WsMS4ϘMEʅ Y~yMbsP*f(H{+؂IXPPG@(@@cJy"q7UxpzK!tv<vMv %\qPۙ^$ @j)";ǩ؊(:;{J%jcYy v,Sʫ@]Zšw*ʞȺ*WjKaa]ڰʩ zxڙOI#~2؄fte,]Z֨ *YJ{H K;"Ib|c)~OڱXQe7<%{wʲ Zƪ1Fj7[lP@ʭjZ!|y+S;?UyX ʵ^<9ek;N*bhV paGA{xxʇ׷~KQKVqp([~c* RK:YVʹmۊ=+gB U -L˙%9JyQPs=)G "hcZ{ɛ˻ΛPKv6YpۺB9 h4euY`k{+;%LKѳшGDՋaчg`Ҳzd.3MЃb6Іj$Cm4\L}M x S ;`Ch-կeҨ ʤ1ei]k= ={o %*t]^6|9\|=- RLHYV [M7ElU;v E*ٓM<pL4"ۊ\׬ ǯhY۶؏]JÝ%:Dz]|D%׽h*E ̇Gq]_h=,qã[,͟\8 ϮJsড় |B-qL~0ݺ- */n-Vʏm~91;^?>Hi>Fѩ`)-Vp=Nk29~"78m LpF4],o,@VMn ÈQVtHܳy薻NpH.C6lɤ昞vo/7l@ЋEϟ Œ LfAֲ-f C%N΢Et/G!?#Y$I})Uv'ɒ.{QOA#.:In L:@>l kaŎYfQ`۷[?ĕ[.Yde#@ihԬ[/&X0aB )NQƞ-e 3(K7/$)7UL}S(P*)TU^jk׼z .[exhRe È3&2CClfΝM~'"l,eԿ(z**vÊzNZࢫ9d0t(b8ĊQFOvJ +=B $"h"qO؀2j6m 7+)9˒- rBP*1^fD!H Jq;Fd OhhG4% kQ|d)I%ĭ@@.*a0r8RaTRGTT/6pZnISZTVe汆"çFAS>=Eo2<2t`DǦ|ڱڟtI)->0OE+/QKUWT=9`Wx"VYk[uE_u"&evX&tґxb =Nk"?%[pWs[PZTwUu^z}cx`9 Vis-HcV(vGX9ncmIݾ Z]_7Sg^L  әg[q`_>xXfxi!i>2>kktd'K֔-NkMB{sٺ?:_9o |v("|bÝE׫qB~몼V-Gla-=,۞:8UVSn6i7OoOG8Z:}Շok]ӴE]#`􄳶MCq; P `s> "[j>΃:IGlR0F?T.mÇnѯnAQF$TD>ʈ0~ S8yƮ8t`FtEkxczsߐxGe1P5/%*3[X% cU815Vu%GTqvdQB2[ (<9mc$sx/V ;&{:O`#e3Sv! IzrVH !g ̥.K N|c29Sx)9d@MY: +@^g/˜Kt):Vfg1LOfC}>]&sPxJAWA4RThZR=\Dm8ƳѐJ7 X̳nD44(͇JRFL' BP23*tsQO}TXk\U%I7ҕQbEYD xA{ӠƵAsQ ɴf8YA1 6=9vb'ǺǂG.bݱ @j:P7k3C<?7[ˋ ?r@ ,Uƒ. ,?' ɋ&8 Aۧ g a:364:+8ѲwJڱJ$B?+A(D@m:s/ wk:)L TIpsͮ4T?k-'5ʅܤb8K$Ewԉ㤌8K4z> %jNtGԼN tIDLCjHܱŰ eO4]| CKOu}RI>|ԀʾD=뤔nQ2<˅L!LPA7MKi5QcݱR(,Kԍ8 ^ 1Q2-ST3,: 64qP&S'mNN--)0P8}ēTy@5MD,N KGlu&V{<)eWm XUZ}Y]ŒՄ܋Q EHptS$EVpR-RViWгGk&GWnV>p!xsU\"u.vIwU`9Tc+z=NAT^fug|CqSjE$S؅}oeprEX#SE 3p{UҋPͳZKS#YSw{R]ؠZC-ZѤm-qZx6:z!Zdٯ "%5ΣٚXSM[Iیچ}Uۉأ*NAMD4-M=rWZsHŝ aY`_d;)ZU^>S.JF5&!۞C b6Rb*b`Pb-Ѕ^(bQe~JMda?06u3_2kc `Td{\wcRDߕ߈*S)H ,&`.V 7Gd]QG^_$Fz0b:&cTNs]+@ޒk(zZCTe#e`,G ~uQLHMfUѻ i%j$*>f~Vb9Y\#}Dc_vfC7Fg 2e&qMe~neVamՂ6h@hۄ'LLr&Ju|ddf߽h%F͘E|MO fޚ.i}N8Vmb?..ZVL^ sxq錤Yl^l^ʶ.f59vjlikjj^io RS$JI ٵmbx>#2,~neJjn˲N|Onsp^Rf:.n@gk@<nIPb.pd܎OVcnn>W6? ObK0lq]qp!q!哺Vuo~pEO{ZaZs8qr2"7.o-f:VZUgh>,4=o3mtm )82wK j4dt6&ˇ''o^Z^G<-o?υZ su#tB E_~t vtȎl`H t3P='{18ZT'V׆DXuu#l8M=v=-R/n@CAqkplvmtu ک_qt{qGwQuݐCe6WA!w[n_wobqXaOPOvO[vvUp/6lwiB `'w-yGw[w/IxŘtv`xS"y_yy/c/ze_"pW`n6Tc{Ӈzx)pzUGu>5( m7u`c|>E %y)/|Ïeu7iv[65ȿw$|Lwb}/}7{_/wwnt} {7[|}'\:'}?ZG-/AoozGFf w,h %2\8!D 'Rlh70fF9l(%Nx&lk2g^SF&Ne:wН. yR8qZkj RRj*֬r+ذbӡ+k,ڲ֮Unݺmr"޼xCWhxD rdG"G<֫04ggDw:ХI?n-3+qgˆm;Xhs֝,۶oέ{7޾3oᇊ/ E4Q'̺mmR>Q$0ϼ6m{w?Y[pɵ ]v!_9``E'uYGmIAX^f17yHPن~i&lH-_ BxANXavDu.xP?Vb"uj1wxej"jdz%OI)|oz[njNTj ;y~:( *1䢌` $ifY ѺHԧzjVjU*N\FAڧrF l&(![ң)Ml*\iiʸD1r:Tz;*nEQ"*]۠"0ж( şF, )0 4|F9pl=4-hyRKgӮAu S]5Wo@9$-/̾kvu}ن"[eotF}7sot߭.SI:.Y)d yJy 6+u6ϏJ .ǝPIRdF:l7R$_֊N|PAN$Ou96J7u|m llA?i1aIW* 0O0 @2&|`4IpG iTE@$f% S6M 1Cli.!sZ!UD&DQ\Ck%oANd"bFY^|$$Ǣ1i7ɐ:M#IQ3*61hZӅŚ ( T^ *\g1(2 SY|3'jIUMeM8$'Bq+Ygkm[JI*~fpv_OW ӬVخ/ kjZVom]9~qE*]A@zK*US XtTUWf*\J%i:-d)"YčqNQR$G:ׯFn`:(^ٝ5k|Wxϧ}kz3JNU̽ haabM*[x\gGt2pÂ@BᴟxK>$)\ s5&{Ek- 1)ZT1~leH,q}c͒BegzvxL6aE=S#¸ˉ\! eq:5SnԜ3 xvg*Oʃ.ZDRŢ"+Uĥt7MzJfqdQKB+t36D^Fug n F=F}7'S6,ʮeLcH>抰j{j*QW544S鮇="^U[.~7og"9TY+lwQTu5pRqx#nS>uSCs9ZU:rb1~b#ʙ5"HNH Id@$NdU K9$`dD1Rd2v^O 1D"2X>[%R.?6S>%K&d} C_?%_u'r :Fe؜!G%#VZq$"Q9\&9lũTae?gc0\W fs@,bc:&\5@fddeNfff:Ӫgg:$K!ij.X!PFQ6fᦐ馥@.lfM/rR5>'a2Lkڣue1Z^zqyKdߦoJyyTa'Mgijj#| k"}ZNuf-~¬e\w(Q(WozxfŁڙG:|f&Z{Jh Va,}4^I~֦mPw{h8Nl'FhVz@1'fjh K*Z*fi\OQ%)\<h U'TW8X VziORNi4ZV`ԣ^ܤě)yʓr!ETeWX*vmM &*_YnX#)U*Nݼ0hjRy"V2)W*@2(~ibZ _*}BTZ+"g)6Y婞*=+϶v~FjG+k\Tj\zֻfۋck0X ]Ҫc !6ظ+R \XjjѡHVMq_*Er),|l}d a_QƬjfyPVlpiyZ> 0*iV*mC|J-XD+9]ve"ZG؞h *%j"d)k8mR5|*e~.,rlf..jgVM1Un. PtB|.芮fa )p rm.fZ"!z-l2ZE譶ªj./qaelʮuo~B/ܮmCn_ oZկ"O"j0 8XƖpLBY 2/o0Lqu<4V+/nkoES㲰/>; &pB '0˚do9 oYOcV.0 EWp!aH"FA*,eyۦ܆ITCWprٖ&2E,rk,"M i0q 1yC.4XL) pnӺs-F7@-s@]JYi~%e[ơsk@q\ȎE44<,m#K7_9]Rjq N3  HIsNīt݌c(R>4;[DEαGgHܖ.2#W0J[-p :?""- Y iM?‘N fP D5-4JRgZT4/e$ZuaഋtWW+?4c YtR?Q[,Q4:5M_MruWQO6Pj.paoau-vFe=\GdG]e;X6 vLo+S O0A4Y&F!k!?&eWLi(~9N岲_ v 4@q-uE\N_r:3I>ɶVmnuvC>v 25x׀2n.2Cp-kD`rw6B 5E?\6 :wmw?g hDb"B08#ʲ7.9C*g[n{I&KXxǀO8*xwvvxuyu&vbwljbV}'fo aDyӋ{+/85;[p >9g]9ndr~9WW`k7Nꕗ@pwFRN,z(\Vc2:ęHzjՑCqYL٦pK$/cXa:*MVJ:y߬{qZ;J@ z71;*Q{3ei2S:ɸmɹ;;hװ;z2~( ʐ&9%E"+E |Ş| H44@8KoT|zeae70Ӣ|ɻy|=ЃV=ϊ9T{|$B{ypux/~&#Gs@lMX ;?VJqt鳖~~Le(Qxl>>37l&_ ˰8#ʾ>ǾQT7,.a 4xaB5t0ZDax89vq9#G4dJXt%I0߽SnݶmRIB@Ph(BKpDTS"zU[vZi c0CG"E U\T֥ o^xuw.THqOt|"F(S>y٤ʒutt3Gό9]gj̹OC8썧NU|8p[{gӪmS'v߂|bOre;n,~ϩKm&<} ( lC*7v;v .8*AC,[-梮00C,l"n%\ӤEE+` UQ/Lr@.'9V8>-ATܶt(BʤUVEX{ӢYs}^]̑XπVz}&G?e)mIhYДڄJ+u \VELW]d3ސEgy{շ{6Qͥ~I]٧BXaj*ۈ%T*Ì1tX1YE"ȬWlv ?u^4c! ț-%kg*A'F4X鼘in9Z2yzt^9r["%Jmg᎛CzU cSD pO|ܯqޱrYif ]G; T/,"XOnZ-=wO\P_.odӮ-t~JJ.zSH|QḞ;]C#{GX3/vt p6VJ'/z9ľՖ?(p} ^"heI,]`: H"t KvF !ReJt7sH mћt@5 !XH%PT$#?#UFȡ mx6zLz#4)hL0n  aS-ݐ r\W# Td C@`aR6 /| )F^/PJUdbMԦ2i L5;֔AU7iώ P5MoXEO6^TԎ\0'8q Da U&%@Vڈ)UKD%I7PS7A[@N]i.XRd5i1Ӝ4wH|2z :U,hP5T+o,$dJ[ 6-& R1pX` ;H4se K]L#vC|YN0wP/0:@IX¢+d-[ߙ^L~u>fҿMlڮ6˘8ZV % _6p/(a*@H\ PY?11Ye}Uu[fʝkgصF&ؐpÈ+^81F9z,q!DhFrn06ӥ:+iحk+V*R$=Z'O9'ȓ+_μsu1sc$p^$3V|b%;T"$qG(PCM6$` hCrq5.`y>hNDէ 7,󩧟jxک!SHFAk4 ld% 3" ӟiHk Y{m܆ r${H ~*s4Z3jM @B %`t>q'FeXOjZKsi __Gh6D6I8-K16<ۘO mkJ#R .R H>YwXjk ';pX-q # #t; /|k7wsNcF<`(TO5ԐCvpR-_9~q%ꓮZǯ5? 4qm߾(& {['9D# Hq#ZѲ ܡKF6q/YK wsr͜a `ױ Ő.>4 X$p li"nlyBU jAB"Fq f#%\6Q5I,WH*9 @\g![qZd{Kp$&1eLeH#Ec4T0nlztI&pq p(M)i)&/^iT;N4.OM@%wC擈L4&QyTT/XD5Ɏf@B"J 3iڹV+PJ٪-)>{KCMF! Bأ NoK,QvIPh̞zNUUE%U(N8IIW}+MYbMzPtzύs`"n+=Srb&ѠDB%Tfzòͮf1M9hR -hs~/8+m[@5tP /ysnwW" 0;܄8bzX9w@6պ.fmY >`a FA6;^!I)Tq- iF_Ym^{{D>N*v}Oo4`KŽ0/8(0@890(\lw眩cn&gk>xm.}<>9HLŲmi8@Y}.l+YDB? x c6̪48ΨNKcx 𫧞yZ?z4 ݂͵Ww^+&"A*8N.f#̪! \={ra:s` W(v,iQQd&7]= gD?Npt&t\u$f9b91'0`qvi]t3g2;:XH LtO1NU "k !ЀjsZ x`HX}x}Xu"GD )yz@rk0( ~#GTG\aowƶLBD)G{5 S?0hSHg)=` ̈́wacg[j#dž`qt/hm`k<(p'ww'Ex]. pW=$H9fضx hԅxcȊ}]}&j(hz*((Vv-viD5M#Ճ8wnj)ws [b^C$:vcFF清TGȎ"qjoۧ%( gz  ٔE\j@AHe^8U葻'@SbAvE@, ](Q$bHj< [نjяp1E2z0(zR )xTMŌUEؕuI^{b &0p)[BIY`w{}8cijSt6)DiY%X%zP T\IYlLeZYΈ{^IaCQ e9m;&;{IV[0x0 )9}¹Zf(_Vʹ?Y17ne?ax)3y{PT{H+C_`w޳qvjE _ z N蜹#Sً XK@QT\HYYycꈑp pT`_@w+ѵ P #4gz"y*&L*gy*ovJ i*Xn2 ٌ1bY `j z+*hvy:٧}Ч4}J~{V=鐼ؤIbU@2M@p׀ (30a ` j - $+ee QE+:yƪ@ZX3 ,bQrǘŭOjoSDڲꪪPigzA:&w{\h ™ ۨ괎J"]d8"e$[ * -{/ˮ3kx` @ ڳ> *G ˊӷLV[[/g~,^]ۭ[ P)Pi 銈ڪ[g戾2 p 0 >&ҷejDz;P v !ظS;is𦩝۹ U:ff˲iKm붬b'R K۷B+ڻHKJKiQ ɋn&_;b [ \ UJԽhCXI,3{ 7W{kn]Dbǩ_ ZxpKs\qu11pnGoy 8.,02ܼVk6,##+^=l A\~CGwK Lnl5Kx;Ŋ갌T;] aLdL$)Y̷Ʃ+3wFrXǮ y|ú|<V|{UӚa~˲ ŊșȺi`;K+i'h|JLP ƌL(ո7pXA%4˱û$aFEjLlӼA,y W i֤WRZ|δ,b7ι`kmEPf;< {8J[0wj WsjF-<@:d1у_{"Vh?,ҫڌ<귐t{z\c}9Ӫx?YB}lF} RJ=-L 朡 ҞAA"[=fzO,ʢīĪ֫{Ӿkԧ9q2tw I#mm,LYT}؉؋eZݖh|ҫZn|=Hœ=6}E#m͸}׫=}۝ԑPak}5]gDͺZIˁw֬X ]ױ0β<`ۻͽ -ߝMm١\D-(Niಁyݮ NՋȉ}ތPޫឍw!.$Ύ}],E-㬽bW$> p] p ֠o=KN0M^XӬ˻aX嫈^|a?lqZ PHvWAk y~肎~ܝ.挮ˎnb>,_n .i=z~Y> a븬Vmf@`0 px,NʝmU~G ]N9^ny,=;,ܬMn^6՜i6 eah 1^1@ ȍK1^>!O-9`//~389|?_ڪ]CEO4H/ M_APf; -U_Wy\8anm/^P덾X .dС>":mcJ D p ‡,X`%K1eDQM3u3L/\ ZtIsQdKN)V*nڵ+:t\YfeWsqΕ{ݹc+XN$\'Vw1ƎCYdʣ/_&gН?ڲQJ2Ѩ[ʨZ[]UM[n[n}N`ѥS$\c=\$J̚5&_ϞY&jNDMvVZʼn[K[~oCn۫{."P!ӈ&2T%B '#m(c j?@2@ ZA&Tr 302LTR*hzA=`1mNF̂Ӫq@to/$!\P"I$r˪,AR.3L0{t>F9lkNV6kًV\[ # *0QE-l4x.'K1ZNÌRL Ul5M1-ZUճw+^y^s_-{J,cmLY Yh%=jڝ.vbMi'ou5^E˫uK^^+VѾ\S9}LA&`dzf2}$i/icpm`YPͪ7MZƔe6GfkyN^Şy9$ߑ{|&ذRCU&er鉣I' w-ẫ3*l"{ݴ;_o~禛 [oƄ6|&pGbŧnQ] ,ZGCɎe RISQUFiJS2͜S 8 -g= !)!L/!, nKL#"1>Mn2):h:A ^τ)ǒON,ݝq m^3?[nxa%:"05ꤝT4<AFIe *?iq]ƺꖃ B5Bu1r[R#`O|&-F2*U2S]/fLJM-9-NB*˞^_GPiQtQK4іT7zK&Ty1UzUm? ְU7Ul7z:G}$\FIRwkź%QT%`hQ,5K/ƾ- Zv*,Ŭ]" qJђ(2WElE'/f_d[@fn}eq' PV$3My,tq_*WsS MTWmm3bBܸ0xKqO,XWvZkA jВOQE}{bV/tav#YMqWzǏ[6sKjt*n[\/nls/Io͓AjGԎ4=Qw#4qe`>[wK_Tu@9ΟY"uo0\v X;~mSGOVO[^cBϑǝ٧yVo_=- yI.^Qr/Cғ;;Ӭɾ&Y:+zR?!y !⢻Y;= 3iA@bٳ%m#?8 ̽:@W5#c@":>L4~I ?$~7 5АAwҫoA $]#B:Qc²a 5+ A™uPBsA,@ ㌼0Kς4AAóCӯ7©Î;®KB&l!yB(A)#˷R!-S=a.%x{6tP8++ ERKB DZzGB,E_>MF/L2d$x«F43x$ƕJ6kD\#IVIH"0>x†$˷Iğ|FlǍ GLJ D$IFJ GsJ́řA3s ALK+9iˋ|w|9K`ʦ${JDɿA0L@K|Q̇$A?XLLT+ ,ⴤϴ,CMj?ibMq{\H[MdˠݔD̜\/lO KdCOj=*L}EɢY ӑN`Q̢K\;>NX5\ͯPZ,•#auP4O N, ur {K]RmI L.=JqQrhN\HQ~ MOa\ :)IdR' (O*ݯӢ -mQ.I)b-O MQ5%6H<S:O;FsSkS}E1T{1m}TIuO"u4IMN=E,QQˣuA-UUECETwSG5SHc$&]%&_-`(Fc:R%z7iEE km֑8a\PH3Nԁ smC@X3k׬yLT׍ ӸW[ݬk,R|ʃF]mX:TrCiESkE^䁏͐ ґ%PCYMeXB{IvYa&x ]P YLZ-Z:_`Vt| Y`JaĬV$]]KYPC"YUmM#J}ݎ9b*=+^vɗ??1|_V2Yex<_3,a%0C'pc]cb-_b8@"'|;Ο L)(`d]nkeKZcZuNKQNYaJՒ!ͩXWIg`_ c璴]P6jgQ}~fy՗FPhpGD=KhuL.uf~gڀmg8nOPg%aNNmkNe]a˓^_g+3ÞB~%TB1U6۾6a@fk%O l1`<cȖ6$ نfhmHؾfgΠ6U LR^]nmK[ noo*M[nnn뾐&p6polĹ01YEWVI =0qAEQVn%ٮL1,Fƥ];qNpj ś T h o j(*fFjn]bu.tC?Bo~/?fd5TR4s ]l[6onjBX-d 3& %FtY?tE^e%n6tZ/ ;%T"眅!ݸI3ۿo޷G7"m9ure-mYZ-]ool`l\*v;KegvpvkkvocX/Y?r:ց$wG9ZbccwRdgf}GhnrxۂGn_oeWqn7 %zfYAnaa|Wv/wЦέ惒yכazzlP qݢ~ڥoz'{7Ų5pNέG{E{. A|̷f.qr _XwwzʵtXuqyz^ \lgKl&hoik||ZG]/nyէ*}wz-}goǿ +uoUI\{ͦzwWekZf Bq kPÇ"Rh"ƌC;u궑lVJ *Wp ‡,fҬ9SE:wTNB-jT舤<2Eaf9laO'T^+دƒ-k,Zֲm+-arkn۵w*w  „ !nL71Ȓvrdɓ)Yt SӧLy(ꤪW͔NCGJj\7pyޥ;w/5,pa +^+V)6(9w~OM.FYvNӧ9 EXnh`p 5.{Gr.72=Y3u,v!QGyYxגyVO|5&cS5:V E܃I09sJ)vXd茸F%vwGghS8mW#7l6I5W8$E*rIBdrRF6Pf(PYXeU%RZea8&gex&M覛pYߜ<&UU3$C H *{1z@^+4]%G*{d^fѲ]x"hZm*9Fk\kJX-t2峖n*ծ(j0y*jd떥ĹnvkF/.-S0ўHdbM?ڰCnO@qb,u+PN,,;ֲ09<ELwVŨ-V V6k4MB5`VMYSJ]eY?Z j/ٟihqAh[@U~uM;佈9EJy= /yۻu 5 7L~}kziVcYJ)|ᅲ(jvlg;,C%WZ" &u,[da"3KɤKIy{_pv 0T+AG X"-}R's K@d#)̄ 2LܠV .. 6|ӛ$g8(ŇO_)AY^62]|aL'l>gϟpVD@͗Bj>4ǰ߸+NҦ'a;k#GqKwy)[~qL[iu*E +ե3}8,GHz<}-X~G]AT<-X=[?\`?Qq`Zퟶ^EPZ Yu$ "Y fDyh@L Ȓ̠ݽ^v!za,_QG@.NDً  b%vab^Q2*D"9Aa"^8Ș"W#BGN"MTj!~'r-( )"m*W+fH,2!%n \b E.Z "/mؠ0d16X#*#0E3J4>!&R&b6n#Yt#ܙ)8 $A9db82D3;N@U6>.cc@d 8I$-d^B6;"D2 $ `f" e6a}LYH#!rJFTN%J8bcɤФRdN A`. CM%}d U%^]%Za%%O!5#¢$:MNNj4aZY'[uם H*8DKd^&UJOie``RTLL&C.&cD_ZZLeM q|d4 6T%&rF%id_YffOeaWrNl4ң&P aEif)F7Rr&i*Tg^sH`SH,<@p@]tK;ʣ @e0d!if2ښhnnJ2*~E*i\\H+Ţ^먶ҽuF뾶jٿ2| ,*‚ZL:N,>I\ ]kC,*|Ӓ˦ZZxob,Vײgj.UCZ6uD.H&NZ'bci&+~--͊mІǦjJkRgpnG˺~PZ-¹>,`.Ng:+:%FDLXb.nH=~.߻6./ ^8`Qu,ֈ/dZPP%.~j%BO(wHn/ᆰIU݂oI# 66R+ONF.pOH5ZDkSbKoApMH0$ {ˇ /1d0_0p ;RY Lt RK/!(, h5 /V ƼfCb+ʪ4lMqZOoiD|"JN 1/2I,Vne [ Zr.q]Rt=pҮ!*'* KTBࠧ-k&.O0!k%-c/K)0s{1O:&)r *dUPV5k|s6˟.3sD9{MG9#:/:2<@qB'Գ]36s3FO8QGEp11- sHQ3EF7FG4BX I306\0nrKWB514N#/nG ѮAwQsCɭ+CSUW\\5VV4 z-z5F-}IuK5I[[35J/]S5]^kPm*`` ,a j(vud5RC6m^'X6fg]ov|MvW?nXtjPkP,u}9lӶi95U0v^s6pGpsYyq_$ux su;uvPBng6^V6 _Oym`_Ǿ27bt h}ӦOY Do2fow .^&_0WK 8Z,ڦ28'jlO8bwA#-`ww_bmήx{7wxC/E9u @D(hx_ys&9#yB 7u29d}T˵S %ex1yxm-98cLN748wcP6'l0l]zzx3of6z8ax$cMv e^v\U:71kg{zhv:Fl4[xAu6u;7sroqR`'փ-aD<{K} IԀde6rp~;:K3O:ј̟=;l;S;~P;;[ma 2ǵc|~wU|6_]Oߡ|cҷwu:=A}Կ'lcZAl+I51|K=/={{Ҽ>DNǽ{޽/sظKU8sO4qte>7=ߞjǣ-.M{Ͻ{(>>};#%B$ }=] ?Dzzҳ.&=󿢼Ӻ8)vV?eno?=?HiD: 4!w S7mZfp8|(dH#Kx$yeJ(XI 3o`qFN6liԉ/aÈ5ziҤј6uZTSbjU*V[nzM\Xcɖ5{8W^n\-!C%Rb8PGhإH-Ksf暛qQgW*5)ꧫu8uÎölݻyMnر/8v noĉfPa)3Nsk <0S"pGkPίB:P$J<o%CV4Ţ^ 5ڱ KhNT2&)CCQR<ԋ#3K(9[k3=MӦ θTP IICLT,;i<{ bNr1d3 Mٜ9JUkv,sVuŒVp#Ȩ`,HSelYۜMPis#Sn|`]["hܾdVtӽ5Q^{̀LXPD|Ք-˫i+8a9.v$_]Rr1:7ËUcTޏ7a'{MeNUzyUiklgt9ЉͽV]SBީJIvYjKkTs [-9 ַ1W'ЛM?oNgVo8z+ocntrq]Ջ\'h0)5?C_^yQnB Feue*o?}oJ[6-~orl9Ip_БtA6'@x АWA]z^H`" hL?)-Wb|(X p+וZRVxwFA 1F/-<1 NbТ?QAN @@A4 4N? a a| ]0}5 !"X[kF2 $"JD(Q!Rd"'.;l U,Є&$hE^j*h4QnD:0]oz4a HA򆆔7 DzCT '*P:I1ٙrڌJ7iZex/'# ks[A $-CS?POޔaMͷ$*Y*BUnȣF)TghK2pu\Z׹.`v^J<<[1v VHX>d#XRf9;;|H+YҞ}ikZ_li;ip+ WBJ! ,^H0Z(\P!CbHBŊ%6"8B9r'/Ф$T"uƭ'ӳg.;-vm,2[ L!6ܸsͻo%{9Rh< 2(ރfFVymz(_^g} Ga^MNՠ8(8.<>eZmw!-4|yGL6y_~'Ĩ69x68m2f)hfihaVWB%)D+u:i'w%E/)12]z&W t5F)hUgh;ݸfk䜤f'Qd,M:ʪk](8cufZQSTX|Z{pjm zmn! /j[Z.]JZHoF7? ʙ-zZ("!$覫oIv쮁,B?d^6 Lm y̪& 1JI+z{)qE*2 PG %7(ZOf p]A1{d9$Ќ^G=NjFNͷU_,OZŵ(Ff7-jEk?.Cgȭ9#zy"ۋ:T]ٞ^e7<1-`D9'.J?oOZ){ /3d->ۣryOOQ{_@5|O wvA\ GBr"ҽvœlO$`o? !9!֯h^ IX%KKaa 08qG*,HFt;a eBk 1u١Z2ЊLZ٤ !t'1ʢ 猦e,xFes(!R$JVm^w񨠠t{T 3"&00"#(fFЌO$,h NbPFPIrSLg:d9Xc Jg!/%n .L# I^k&DAF]E'6: ^BgIҒ|JI6yfŅ= 8)GО`}AÈP.A&+4N4GAj<)JG˕4Bqri2pNQ0Lڕ@<HMjBhU7ڈ6v$bE.Zb]Y7Ζr=os] }e_:*c!–k- öʍ`m^.deedj&GV <[h-.j^yK3:)˷S"}X;uy COXt\/ E$"/\! x A7]Q_v3;qv?և>߭{K[t'^zᙌx$p(d;9A Xy6~z^Q_R{Lwuާw p0Wa7TtGF ԧ}7@>%PmiGvp zz j'{5{wk؅LxWķOaSvp| pQؗ,hZ:bPzcWr Wro7:+16k.(dk`]"KWHp6X 0TاOX'E0Pzp Xx g%^x~`Bcz.wfwcX{lhW:UHvl@{a}A PE p @bWJȈݷuݦS8z3fgވ X(^ 87(#*1(Pw?8Hw&=xg@ӆO;xZLW GFM(j@ 0 Ϩ}hmظf@zW 蘅%N.Is討Jnkx:>)o{(]؄yW )ŐtHpQI @wpٔuKxg#訅(+/&5#'1ȓ=isGI)RiZMP y8C:X XI[Ǹʈ _@cI!i3G fF.iM+y.x8Ҏ|iw 9oC{^3xfhySِ|(P @S@ٔgmAb%?IrYq16zzj;)c?y:7̙FH)LyWqBܙ"*F9y ` wH'i?# Tlɣx.#r9i7)|ʩ7IXI9JS)HP\9v0 LТ(9V66e:IɣlyrACv*V}5;IwwgZ{)d&ЉEwWZ\عd>ا@i UjJ(Jy_-zt&nPէ: (jD5rh{ǹsYw o֜щY٩Wʃx(cj!AYv(w; d6ߔT>z :{yʨMڠCw^%fWںzjꡗYZ亙lwMYmEHPzbM¯jK[{ʬƙҺBٰwRk0ydטqe8$;芢 nx rÓ\u?+3⛿(X'e 7J{ƜZRF9ڊȱ`ب`bp  @i뮌Hm3bt /&)y iǷ`JE˨I/u7[wD<@^ktZ;й x H7؈bWU MEQj덹 ໿J8Kg kDʻ R&+qٺ'ǡɽ_cW eI4/[Uj|}.^)i k|[pz ʻ ܸ *]\\vE9*{k³KǠ 绞B47[ګ@̖:ˉ1F H4LĒQ#J,_w%g>ƞ߻cǪSKp6ܶ-F[0 ,Pl(̋J\L<Jo2+ VYlO[ZW]L;_ʬ|®| @ p T1<Ëx#hy M`ǜɬz\KĤ| U|řl,Μb8Lŵ[P$ ǡcIm[]y Ō: -= |Xɑ;!Z%)#%0HJ |ܺrYr EmH2MO-]Vӓ2[a=dM(`']l,lmTR o ԇ99IP*6nB-Y| MF- ԁNmRȌ*=K Z՟(֖ƋݝƜVYڲU1ĠȀ ڈxHWQP6o;T Dm }܉= ؽg+MWQ*ޢ`TJ 7p>pIQ\5\Qȼ*؃mƍMN=ᖼ`Z[lb0ٙK((%) mn}w@b ymI@_@c`5b6[ۑX[nLi*2=P]}ᑹ*_ޔ#uf|m`J8T@cpX5~>'͐>Ύ՗nYw=p/:\^[Q~Xc_p# <,̐~~_.\~[ +(^X9~Tv+y0> `IPF1o?ʍ˞N@ $_}[nO*# PbqI$3_Qq֐/CоGM?Oo)P`K%\/ )/ ` 5Z ؒ/9LsOn @_NOdc.GT_Lˉ'(v[j8+ΪJ*r֊N/첻νs<ÊA<2=,0Fӏ?Gf+6L {0?x +PӁQ<+DڮD2uA45/ޣ>qSGrO>g6qK6%j2)2'[*2:+z/,4K5uN3FD</6ܐ*P*%K8}2B,"8ܴ/wTLPDRKTeVuqɍ^v(V?Y \i^PD-VR&Ҹ=*oA,g/LDkm;楳S) U̘:_]LIy]ޔwH~U`ra-5nᩀ #fk5S7cQiLƒbQze]sm$}BZJ>8ߖuAjz[GE7lYemE{Ղs_Anƚ;h[abgٚŬj,w9IvdU+"wruqim?H_GӇ2,W? |v4ucWE3*㓷wy=t]JF}*EL{[gEbg>mqGr[gOyӓ~nc m+`@@v D\pFrqH;C 4KtRvW7=-X84 1,Bw8ۑwW+Qg2F·_H탧"J~w?:Q* נ+0d2 ІcD_"( Okߘ8v B=Bя(lR )amQ]l"O1wW a M_O|KsS̏9hMF@Lt(8%Qn Q9mbq+ r+X>]zd5w#{b jM M](V=MewV5mTZV՟n@c ,IbpDʢi(\,3r+OZݽT {`Ұ SRjJ+8VsEN^ΉRz<2MGz Njie`zm, )&i:5l䊰 K0.Ƴ*gZlxf@q?{fc}3n{k ç`rהd4iFS0x0&Z2 ؝}UK[31L]- nzܔk>|+B5H 3H]@z-1 ֺaZ WҰ͖n$%\g]vZ.LiTGGb5PJ1Aɚg}⭉ :q(lTp2>"Ta$UFa&vU9ԦBg 4o ܲ b=^۬Tn"\H"Ut2iK' m8 _xrG Lwwqq—x41:'(?uNޡ@R($5JqgI| w]Q&zIҙ%B}R߆˭e] 7m^Ӱ|SS7~n=]mQbY$ߑJAl^㶇?n-=|/u:Ć)AB<@ DAt -|?擿 <2!Cy8 +Z3ǃ8(h:DR$D x-H=C,a1Y"$7K29IAs((BۼCؑ9ʚR5˻TI@3Kr$D̲H"nsLܒyG'&0͒䏼B˃TLHlW4̬b"-$MMH*4 |B-ۓ ̅|D:@j{g3O8NL?'=)G\&>.@ZuTTJULm+M}`Ua UTQMemAL.BWP,X Emm1}2]^U-W9WOsQhJx2ӐW׺l{mW/uDF Ȫ*U-XK=X UXN=WohAlϻ?ݷa՛Q)X[DI\ xa nmPM `!Ə^WTƧrNvXb@~1db [q,L4Wtb4`v=͕%F'6ob1Wмſ.&5{cR#]PcEװ67㟨c(~[\Sa*% *c9D.F FfHI~F}eϝTx=<)*庮 Q&e>$M`Ðw}IGY6VZIv^enW/ aBCFfe&:dFWAk@Of{R5\f;FaG+qVTNB6aP26vvwv9Lf>e"gg@نfHnogqa s6hX0]`:d膖KćghGTTE:>ܵh%83MiiD~i#i$hiY+@rͺrDӣf:ߐ`j>3Tif&~jj/&j6d.k5™N䷾-i|>^j 6^xXh^SFeghjtFjh fʮ>P@dlnSJ~'٣ȞتdfUvnRDKEiNm^8VۮnnnNeV o&ouH~&uotnoNnX.Ֆ?/p l@%tpC"Ӟf w>p]alpv76m_]vXMqq_im&"#`b O6o~arr+hZpQ+-.r$.s wr4'p5_b7svs<ጰ=ѷsU@pBwn5تDrs][htKncNgmO?sPQtU^m*]HUg+WXHu^m27Lo]u%V6GFa'@c?v~egXgk%zNi:jv߽v_^`qr"8>O? vt\7t(vzw|ww>wtx5kw|wxUbwJwxx=xfxui\'l{wnV`5ՊnyF 6xGPyn"y7vn~z҈xG؊7o[N ӭ q۶D]?v>qPvWzOƤyAOD{E&+pwq7qzƆduR"{g|L{vL|nZbgZ:|ˇ|Jorg7^/}f{Ƨկ﫧6+_݂gzF܋ׯ_ÇNE67L2tɖQfhgJ:-be{mmrvIvrzpojB++F*ң2zb*Nި)evv9*xuꝩ duknZY袽g1Qrl;R,JV[yfq{0kk/{.B]zI1N,FU=F^ŞybUdRPڕ+q,yEPKr '{6*mE!.Wy:"wdrKNw:@/{ }*I&ś_ ,)/^]nkܛBV&oYs;_oQ;Hu_8h[ {Q3Lq4'`$x ڌz,K >K ,I8)l M.]W pOѐ8 #" Q2_S"O 'lPSE+NYwE/" AHC.4ohY6˅}h"φ7H b܋!3בDm JH%+K32l]'XEPJ O@Uq$+a}l-_ 쑏:R9/ mX6MJ>m;'YpsI$pSR&&KS4y$.'ԉBDo,̶F~jyZJC!L5}T#~9?+k Kr?r[2 @6TOG# uQQw1KUկUjUopլ6FWSigYTT^p\[֦vteAKתU~`G"K,[u($UjĹbVIzKW mIkؠgtҪVeҤ_ezt_GA cpM8gMr=ƶ*lSݬvsyM 9l'j;6SC=H(Nke!;_T5n\:AwC| j20;Ұ% $ ϷT!. KlZŘҊ9iAٰ&yqumUs؇ d!5pXF.cqr&LUSMx!f[2>sb#7΍JbFyY@ -17.92i6لjgR!M:ʒ>sjYVo/{*:GJx:d*hڟLkA)85zXMUK{q4x Bm{3*,f ZzTkLؿVF[ mлֆ7i>'ۗEgNZ +*/u#]q܄f'22ݾ P{bW"^`W-`ba#C><#(։njQ;>#F,"+@,JjҞc #SVV J$l0ɪE=>(d*1:. \%ćuc R&@vrSKB:aW NenDFeϹY Z"6n5]:b^eJ%`~9a:&m&V d3ތbe@\`O0rn&/sMqi^.Jb/%A[j2`6da>EuEe@m@ybcHzVf(܂Or gs>gݤ b\'/rv&dS Tj@UfVfm dhbzD=E|4Zf!rr~:gf # 86JxN 瑚jvh)XUUWVɉ芲sB.2azif#ӘN R *'/B'h^*dH`)Vk)ͩ:i֪*m':(*/+62r>*- 5Lj= vb6h*$.," Fyk8ikVf2+ 2k^55KpBb8,"p@*Φ¦F&V,Ja(+.jsk.0`ܔ,V՞lXR. ̪fg*@q ,**LPRN,,m-:&ANʪEA-Ǿ떶+-p'D"+KվR!,t &:r*߮BcgF^:Cғ䢬h.epnWNf+)*--mۺ-^o~/`&&AΞ/g.}+jǂlnoMLn$!eX8t%~n3*3ں:ADܾ'3r3#Q5R#uqξ6cgk'4' +9{99\:tHSӯX<ti>>[ ~@oM1]tO4'An*JYn$6b+-TUsG3JpuW4;5=4Y5F,c4[Ǵ[@-jsݺmNkvkt5m׶m 4/p`G56phUcC9d4XS//Ru85L{Fhpk¶kmq^_qM6-gaw!&C1 7k1r/ճ{V~'u]4̗}D2,'ѻ~'>PD%>U78l5|x|*nq:NKnݽӧ_wT-)S谡wʊ!Vl`kC>e˙7Ӵu~1?_='~jج 2!"24B(b @"B(dt(B#5L>$F m.ƜsԡM H %D # 54N1>M[RRTq\m-+qAI(\G4R%sA'էd ̊|J4A K8;sn9諑Q=#^Hl9yZM9l]lEDg^uOޑӆ5Wzٗj9&*LxK T!2uek>Suⴕ++[n KFdN8de9Նe/-! 2*vBn؝}!p k * Cc=^d^nL#ܣl&)kJ_}7|pm\Q\&)'Da QB `<0+Vaph pQ8H*pt\ش !T C&zx! ;!/e7a,֢evT>,~ld;B2}*+7DQȃr +(JxjG?qYBFllr5iLn=L cQP${-Ȳ,6щ;_GY#F68?Nnq+9 ӜGF6AiޘȱLgVL49Mg^ՄMo~SѤ )P*lPzƳ EB;9Oy c@ ZP UBP>c@OzZ э~4H?zω~ WT`! ,^H-bXp‡#JDċ3Rǐ!FB($Hu%0c䕭lԦɓϟ@ZѣHm[ʴӧI]箪իXfǵׯ`ÊOٳhӪ]# $hx?*˷„ ; ÈV##DIY @^f[ƹϝ͍[RSV۸sǻo ȓ+_μ9};A~hXwz[dI#)/E&͞vV3hlWM΀fX&WcWeWV-hPT E  Tz4`/]#RACT̪T"%VՌ d etRE X#e)cL>VvVeoW&#7ES$9P@K/U*ܮ8eq*5ƥ{d엔'?yUng`bt|Ku xy'{RL&=o؝FVtci4Lꇿ:_+;qm~Mbfq/hDC!67hʳ VF%PWfAG|uVHU sTQ悶WH}@hE@wt׆wzEtF9Pl@P*u{{҇~sSG8dZ|s6% LdVFX9\Á38 S{؍ShQ]WKPXxP4ǘh({8`:{O8ͶI~#xw@a"kȎw9{HXUhU9Ct7)~q |Bs+9!)nToxY0W6I:i쇓p y:r0xV7,gxh턔֎bePRhlU,wؒZ=3 ؘه9drIuIR胗F{%}).Ljk8MVy)3t#7oY970u8R4 IILRR: . OAQQHx3Dz{IgO)L؜KM MsWxӝ*yHxQGנ '+{:<)uD1D ؑYXɝ:| J&Vjx_oiř]i9\ii)H':e= لQW 'W ` #;)GDZ0GKCG(ȤzZ) ׉,!Ǎ{lX:+ A`YQf5æAԡDAt"j[y(P/Ъzwi߃ zQ#`sӨ/Q l6llٍDA)JeںbtI7`wڏ b@jz Oڰ 3z( 3A ê TxtIU> ٬^Xlj!:!֊ʭ{}*U`u: j䮛f/0/A & ( 1ïZiyA+0yΩ< ت7 觧h:۱5Uz: #; @ ) @ j/.GZuZ {> /@ݧH+ 6 S WZ\b;C7f+ٸj;7E=t[ z{|Kfc~8c P[qPk_zNH{%n+;r I / . 0fx*PנIem[ٹ՚{̫v7w{䫑 +K;q :3Jif_wQ¹ԊbhL;`[fvo%) +^k;7 ܻ [ z*ˢ:5 +"LJˀ:ſ$ X[kcpUPX|[݋^<47<ƿ[O|lj̳yV`: &|UAʬ3Eq p] 5 L8l̰}7E0ؓjDʝˆpcNI606W\.l0! 4 AR,J%&e\k̬C;\cq@/Q͢M\3* ͻ'w q0d@>`5).PE&k$- &]3 Iy1ݦUp;ӕ?A=F ˠP }cNT7)0ΰ;Ҽ ce)i-R;4r l\%C i>C ] L؈`L5WM o2 & -\پh[w'd;q{ {_Ӊ\D}mr5 x0 > -H ʽ܃*};g`x9BKm:͌@z7y=аɺ@߃mͺu0ȀfIMMe< 2MֽlJφ8dV)XXeP X~m? ˭:> h=3J#N؉`-ܲɝ @ܚ㟽fLUC>hE.(Ȫ]c6E;:ð~`,\^ p w)dm3PUPjn&1p;'pcq=nFH~^~@ ͔./ΥR \ }T7,^c0 m~N|!Ke亾׽ YÐ3Nx$~ pSIK>cvM ݑwƘꦝ$FZ8Ȁ{x3ي->p @^̵C`9@ o;@mOo;t{x?zϫkKxJT?oo\_8Cd/`e?w] ?jUܩo/yhbxڽЉCѾ_S:_ɏzS ,l,dpF"[t5k-^ SѣGc$YI)UdKټͤYdM9iflޭ[nݶm) 4(UYn`Wa^}q u3 Vef̮aydu 1XM24 6l8#H8:YQ_!$]0cyut:)QJ>mujձ~^{l)?k@ЁHo@܍7+⮜BbKC被C2=.?JD(a\ȅ[n}LArHD)HؒT'j1Ij|PK8.ҹ LTR4Id3c|$9Xk]:mҥm2k7n%H!c :_].#Q^A|FqƁiurɫGrI1-k#F7lL?=YLم}.?^\Xфo]h BFq XbJx =/z9imsO U) Od;_X>MU?eЮv[կLpLB08IuX E%ט N*hA r잔 a+$,aNx6 [ C2j~h_f;\h_BЯ6(hA ,t:~IT"('Z;µ+S gB0 F3T.).4v9 c jT;Ԡ'( gch<S3+i#.XJ$#!I(Jb٣9lB,'9M]td]/]dY !G;& QL:`,B ^𺺾 -Fl8[P꺤js(oQᖜPbcvf0;7#=w([%CTɋS;kӸ;KA{7sȆ")@v膜Q@ChV:= r)) ^Ӽ S6BA8о{DRX@8."[7$@!,(C$AA; +1C)Cp ָ3C3.B0 L)2 !ni<,5dCP? ļM.vj &?=E8cxB 1J_5c@ٺY{["^c68vU'^b&(>e ۍ-&d6xEd>kXdmӄd&kd5dad9[q⢹=^S: V&.:ceeY*cd&tNgu^)pw%Jgyf$f{g|g$' P6Z)&֌SD_lfmF"n. O [a%gs^g^ngw4geMA8Nݿ"2}N0h>f'kfhP&c Vܳ<XY=t>hNx|~|V"`^^:knhi '=N&jeh`janM&fj^%.v)(^$v hxfxYdtkҶgU;κPN 鄦.mTx'YlNre>jhо-m`?im*jS ڮbO%s(l6.(negt>wҮ罹\юC^2lkwU 6o˾lFKUvyK~n&&bO^x*-bU`q/OoӾp{vjc_f  qB*a(F8qVq6ury$A+A%ipqyg&8s4OX%owfY_&wgmҠr@'pvCgs]N4&08`(Frvs@qT irtUBrs'Ks~t't5/s _7vy-+t,VPQsD>7pkumYuDpZsGos7GmFs'aqN?rh5f}ajvN%7nr|vs6poy*9:'4Q^0qbt`uyVwpCwwax`yNqh9wrk' ?/zwY0^> 䂿jyd QorQOޢWMC'z'j'ywOzPwyup WzQ\{>yIrd7zWhş]7{0y2}|zΏZ~8}"''HY{ꡈ7eww}RBa~|a||lܿ ,H0 „J0laˆ $yb"H(IeKFFK0cV*3J]DKevy РB,j(ҤJ shRRVjBպ+XƎ6Z5iԩm'<Э 7/>.#Huaƒ#l'N.>i3К$)R$ɓ(URYxldԫӰwUTSMt㼕-k-Zھmw]xȱ@I l|׳gƌM4$ʔ9[-vNTF 7OXhTr"՜5fAV5ӹ\i_!"Jv"d")pbcE}"j_lvS/%N JdoZuR NU~LԔ\U\tR!vw{DFIb:cauډلJBK+ey&UTVyD]uz8&*P" *0*")xf*0k hj 7\z@Pb9tI~ g/1(fJ[WR'B˓JS22`xiuyxW§%pBVosy'{qoGyQ_Iӱ9%,ӷG Fu{1yDsɘ`[2*f@5+#3,b 5 AGp.'Ұ% K'ˎ[ )6;1w0wXͅ;Z_f]|*qûϧB.F'a4I[10'OG0U_=[ؙ+bw]g[h}/+d2Yx$^/"p/DS~r wc˭;~4Ls]WN.P>rߝpoa"»0En%.@( ,ƈ'AEcAFO.;P=M|kH4;gp) G0`/hщtGIrQ0$!>*,bM'e %]>-~#`]FȢo}ADD _0]d?І7 yC :I bD#Weѱ[8[IX.rTv} B W8\VYQȣHzzIR ٔT"H*V׋$QLjFpfɪz*^&*dtW< XYa.a >@~rb HIkHg^qt "`͉V5#c̈1i$(ԗM,̔Q!u%uY8n>߬eʁs9$ csY̢x-lo^>%/DB;%E(,3͇:,e$8vk:@7) egjr"j mʋ}䮻|`SR%.a{.v]Jec+nR}0Y 8*<%R _N&vmle;ڢf/~"  ':s3Սpv+n,x_F!!Yo$V4i1}s ,Vz6V:VrxeXޙM¢?=$]$PUT&:^g~&hh~]Cu(F:h28gx^(fw^l&G.|ByBUEkf{)vӸ$5%L p@Е p'MHg/(i.)-i>)BB)V₉}(.&h1>jUeIg{6FTVB4nWRS {ݞ QTZN)JҠ&.* ]kAJV*Ra鉙/|$x) Y=_ʦ˹(V(T4nـT'~O *Z>+>kNk^+fR+l/LcAՂH2_jn_t*|]A)YIB폳(DsQTk&.,kuj-0,:,QR,fl^,v~,ȆDz(ɞ,+++k*7"ݫꫣSJ 5,l>jF>-v*F-JN-d-~i<[ѬF5acjU2&I<̰RJi--Vvj+n+,"n*mՂ(FN.V^)UJn"nʮҜ U9 ڹmBA)5ڿ-sPV\n.&o.6nb.Ff9L?<~\/횶 =֔mZ9)ֿRFjB^-"o^/Ƃ..Ă;/0R[MkX+&tЛcFV\1ݼpM f/kQ,g.LR"&$/177(DO1Gqjp+Pnnq(tq(1{1NO1Ƃ$ 0N} 0 o \ /YҌr&}Xp*RpCFfC0(2**2+gq'anF# رw&0epnU 2.oV5a{}Xi'pk6g2܂!L&7#"x88 3::k%\<3=:3>s$3?>8_! !. kf 212/.-$C#Ғ&C6 4KO*L4MGoGt,@n-ttK&/5tU Dϛ? 2'`(43",˲ *z IY2k&$ [5V5]׵]5^]3"8^`6aa6b# |P5!+QpCgWs*EGD(/'W;l2Rsl&S3+v6nn_jB-׀As2gSv/)R,u7u6dh47Ucu#j{p7'8Zk-Zz{7|w]{95]7~~{7i|#xw[5c wPwqrssM7 Pghv(t,X4x3k&l8+'1n&88k$ 99'3_?Cp?DIwt#w7e;4vedJ@8ɖzٮv`xえ8ry&DBbu?z@ޒ3'#,:7zGB=pqB_ׅ&o|ΊXyi/{slU(#j*^s7$x䖂K®;;38:?7'{;y}z?{<C;}ss"@w2d:p?f[pϸGBBiﳬ:[,rB\{-ṣ8}ua8G£7?=aGԀ\|2zkW||wGRlUYjla+';:k8&`B39;=/:'X=g< o}_7)s]_-S17]&}o[,9>O8u"<B=3/?6P>chrdz\fϡ~>O>oB(L3#Ǿ(dn>|´V}5߿!4?G|$$M#,aB5LdPaD)V(B?7dH#I|h%E@_jh8|Yg ;wjhP8@p@RK`P S}V3g֬6?AE;6mН;=,)(sMڔ(ɂފRCqb9r4v8Odɓ)K.)a"C#thw* zrbիY1i׮ڕ*\Ƅ9͜^94AF2u)S+X%w V,ٲg3{lxlP׍wQ׷_Y,S Qv  ,葆 %B>(PB5, |&"θHTD碋Jꬫ8󎧰b@3/B6GD&| 4)JL1ز52 д0#;[S8M.N: :m3ᢄ>N*8UlE^}q1KD@brR,# ,RLqEEHlK.t墌-}O;-@4Ch O=4CB'2#RLsq!~4V%SbI'1XGp#]uł]_ӠbԪl.9../>U^Ye*\~-6Ѹn@\Qe\&m:l+ ߂U^wX␩ =RsAnc[+d ?qa֕ft)QEyr6褜JDݦ4M3bI)}8)$IAbn_~/F;Xοdߞ1.8[?PA+q䟿IgҫG;Q&EiySfaWd(#z$H$wqy!ɐOpuOa/`2,| 0O\O4>o~ITJ:@L!!s2#P^}O6@'B;b%L 0ta uE)'"=x8鐇:K(p̈AAb&iOu)$Xʌ(ddREe.= 0)֘1If5#; eF r5<$"`^sn,c"٭Iђ>\?NүEF9RF alX)A fBm$d ,7,bt@ݘ_ ІNPY8uZ\&4WC,LsՄ5M.1;7);9;NuN;sD vK@WnC!@ Q*T302 MCU9f@Q27J=~~u֏RiI0]_Hi7?Jt15'MmIROJp&Irh&]0{RӠE`6m# qE!jYbա%֤!cY<3-"&Im[KX"(GΩuQWÂ`j$j hyn؃S9t@jj<ޗ/W1ߔ%_}nLĊGGps0PJ~H- {"1eJEIQ7N n7A!|pCb`T?b"+:aȱ$ Qd#ɐpDР5־gp[e ̸I aQ7\@ cʙBNJoF3Ns` ,b щ@|H)חaS4i+ƏF1`iݳ#`Vcp_hٌa72%ׇW&ˆtܳ2 _⸗vB6#c-S¸isɠޯF0Sڶ`fᴚ2 *#]95YøvkrҺFUb-i [CChl !vIK(W~-jle孼;a9X־088I1=#)6\O?JqYf B*S7K'*ﴧW_2gj.X"oG@ L >9#5k$HS1]U/|[J{Uj}vu֡z,_[6!m`,Var @3τ?2I'_!`aҼGoތ˸|72~~]taucC{߯2Yl/ O(n*J'OJ :+ZZ$ \mJ"mooϿ\N>dPE P,$H@1@O(m1( 봪 PWd^pv[Jp0nAIo؎ pK 9k# F ; +f*Oΐڐ`.&:Ø/Cgp W  P˲1*~#q yQWKBI&y@Hq( tp*" k~bR# €  +q qqQY O aBÉ}cj!0rQ$qQ$ǔAM$o%g&er!AQKDQh(;l# R&;@M$- 'k&G+Q$q&qr+_k'WNaN$*(/23F>P40 p%Qr$2PR2|+2111Ւ?PM2qW#&2\(?(///+$J02 0*wS12C-Ur8kr 8#%8ѡ$C4@ %@}&~@ot*;Op- d0387#q9s?g|e1A_x1:A :Kq5VJ辳Ϫ @Z5E'od% F*C?yF+y25>}GA,gl5B 88t'VIG=ORWkMzX9Q;׊K4]TY'VS @uZm@aA[a'.2VYY:c\icCZWe ^I'C5^}09@b_U'_5'"/+4US9u v2"c@bbb 40v2]3Hkc;1TMj[5cA6LHNCd+?הM` FefC(f5rv`PψLs` Z@iiq'iqr/6[6cIZssM$.sCY&+VQNVn}UQoODoowge`WSSiB6UOu2dt8DE7LBX4Mc4kil%TZv$NX7gnogvdzoo"E`}p%C8+YF5Z4j2clqT=V equvc7okWv}/~ ~7j+bwk͖23L7O8Ll6^[W|n>$(ׂfi0Xpe'8q'x$Ӆlw+#$$%D][5d)$҄eK`n!I熧cKo` VT>UV[%D?8dAk2>4c$(M䏙7WEb L&#cV)o8( oQe8$Τ~/c9x7w~AyCb$i9%WEx{9u-e P8L3dzKLX+hh bG3fGA2r)(rA DxB`9\"OLĄ@Tk㣐xY+x4+Ҙ'&q::DG) Vh:uu"i9ba9 /!!9z\ǎuq))R/ÌxX>(XJD`NdZԔL7A(EJS6Χ)j͖ݴYaHꫫHT0N1'Bz)yѰ KthLƔLE Ԙj7]{iڎji▻PȥPU[pr]('`BDC IE@PHKD,d5d썹rq}z٨\fT=׽tn~N8#_(8ӽD.=T7m~3ac{觯Mdܵ8}YJ:C=/~f< Oys 7F.{%-`YL:fߢ6(F0Z󌶤dVvS<@D3v+! |.z19K5q1%F0kr<Fe .Rc 4~۸K^tͰg5}pX$oQ& )RMs|b料fZiגPZS˦6AMo+g ÷l_\';W`w v!aIHafMbMDf@jЩ1bD(4І(6%: Ң\7)XwC*NH&EؖfnvK^}q*v 2V;bKM%,T{>o ,;X;`kdYHt2dgjjup,>SWIMr-Ӊ0XEC3#c]߼O:P:5Q2E֔B3g!kUZWwk12Ou4Xӵn-uatϻtbjӲ3Wjз5_dMk\˘ t>/yI>>͆(^aA[đ3ӅZƖM)o;}sG'}jpYbHN8w.LjӜs)N-d٩J0{^QI\~NѠgE0tONtb 쑜k?|oo)w(<_o۱_> G7 w}(F]gH5|}~]GsTb~ǐ~k'tm~ 偻ow@?#S0֗@{7g p g{%rdjvG|!oP|ͷ8,(x0OU}<5hs8}}8^@)hExq7ƄŇzMͧJPGKYH2؅7}bp ؀uڲVIF{KX| XtH|V{zxo!n&{(^0x \8 ,W6wsubH5In(0kԨHvhȍȊOxoG>T/ш(S `}0 X~ᕏ8m=~s6mSP$Ft(|S0S~HozoCҎ 0Lr,ْHvDv9'.97yveG8# JFY9JɊoGz "9* .ٕd0^6m49]s=Yy9AhEJJhxoJI8#T;|ٙ@0IɴP[.9T IY vhG7;K9p{۱ IpJk*I-)huQ_5ڠIK{0{TʤBx [ (,;+Ы؛[,JʱzhkK봼ۻᗓ: ;u{ XK' (p[9xt۱!K+ p혾JZ +e [&/ϋ`Իҫ%P/<3˽>+ kKex99Rˮ|L{ \3$l,Y\\9<> @ (hġyjȔ qQ~S,U|˼;ʥ-\*0Fd\ @ n)w%qew { | !lkۼ,kȂ<ȌJȾcp Ězq`!)x]9|ħʸ H `%,<<ȶ;prA+@Fz|~؁O] $@p҇y\^ shkSm{> @/C}Fêׂ=zԇ]7|؇rTV<ՓϜ - iM {ӝ=͟} צvݫp@4ҫc59 -x;XK <<ݤ=ח =>.MM+ʕ@-m0S[-Ԯ>HI^.|-R;@C (9+N 2֞#z8}͠B?AD>Fn pxNPRο$xtًa^g>ٻ檼 /=^]S>қ;0Ay];b'S...Ȭ,M+[gf놇k> -@Nt~~H~M^1j\NRj@w~L^xn%~[AJo}  H >tWNo~F= /!Own^){W\> DJy:v?9={*;~GIoKoPR/TV=\,{JJlO N4E OOހ_ c/x57j?pu+nLo젎R{}6^~>ȟ-<^KP 7L`C$JL/VL#D !E*$Y @ʗ?Fq4Qt9QhQI.eSŤN*UYUڵRS%[)2Wc{Y3\Pn],(!_P~ pD)ZĘqcE @ iҲB*['N5q3ТfUNkdONZ{vXY횷kkپ;ݼ{ 8D 7DPqE8>1d._ b?CN?9Et}_˾mZ궧cMT{-ҋ#( κh;*ϤrA/)ˉ:>ӱ@D*@ KzH#z0º /PxP*(6CƶFtDN, s F`4Zh)>n/TG rP"GK4ɴl-' ,QKkQ2SŔT䔑=Zn!Q ]N~~\?֜:j?CH@SYmcݙm M ?4NPj 8@JCQz#\LjO4p5@V8o[VDTf@,r~QVD!Xc,%<-1+)Py ܝA,:e` |M)0' np@0RJxcHEЗtR&OUkFc7:%q`E H*ÑoQ'&I|:~ȯ eq,"#[|1aax@-hcM}|v\ =P3 "l=QR$DL&MvRY-IUQќl%wvXӏ*}!@ɕ[c?a6`08|$1kxa ]ʜ[^5hNSJ96gMu&6 89Y'eRH3iMijÞ'R[bxPPs1lhV'0vSۥHRy&4r?0jS@ab:U,]$Kk*Igȁ((<9:Kj07Ls0-B"!^YkW{pb?C#Fj֙gemk]ZwxPhpĪ!xlHRY8㹡(zYQ-lgw;ۍCwM[N G l+[ئWJDz*[F¶O޾ cd׀@5@;Cq-7a@8Evluq쒖1a,{Ūu%WfR3q{^["x|i7`Y>&00 l̅#WânS-FacBMIF bxC=\88i{&X{[>y6q+ V$%J=޺1|i딒dɕleH&FHش\kv6y psl*A>yk`ۼv ]C#:<`=6| Igɛ. Hz "MRb䬫]4;Ļô5"`'A2,A "WZwl[T īnG:\if; no$=E8GoN0\7yЇ^'}M͇b<P|;}]* (NB4 r4ݽ$CUR#<oS5aС> 2ѺNh;+2*>%HH+4CD@!t>@D3 K" D)F܄FD/GJT$M$(.. \HlHl$,tȆBd.HH‹HD-BLI@FPF4bFiKhlkCAo ?CT4+FN; ĠLNA $- M E5F2xRRN*P5{GB#VE^7[$DcZL^`=c.hcZ%[ ;8fBf[Fa%ḁ#l@eeh= nGqny}WH50DGRMU%E^U9V..)A $e6_h] =Uj`6;j=~U=d&Z`p6yfC8heM-ig4<v`NŽB5]gvFSIL_l١Kȁ 6 P.6ncu>@U>zcMmԎ|m| n k&Nk/X:;ndk4`2^ UlIx^@H4l~o~ 06MFmvj^F2p/p.4 <gnpwp p.p ppFBn43 8nN_NG/mUS_,HJ*(~W,$Or%_rLx7(0Ev$V꜕Rhx pp.@pgm2 p1s8s25p:s=s,s6p2' >qkkqn6?i `΃c?ŽPLpu ]~A(/Mrr4r!s?Vs<vb/;s>7Gve (t.q=hߙ"Xl^oRSLxL/<`Lw{w{AX%o~[/;Lr'u:3Osaov9G=xfxgW (!kcjnlHb$(tx^O7HSUpy7x>_y/'z>:w=0Nj[ՁЧ9?p`/xc'vxxv6p*ygV^݉XynnyS >7p~]Vs_OSHHX|HPwB c&ǣFM.x'w/G}ԏx&O{Z#ɺ_{Yz>؃ծ=P4_agBX'=3肊g)<|>}&}2/~p|/~fs~c_G{(T( 2|!D HHD .QÆ ,i)_jM1YdSΜ93 8-J4'B{tϨXLQb >rj#ذ>Hd *QFMCM7db]+3Mݼz>o_.l޽K41 8`a2f DŒ%^(QƇ?i$l7p!'wӸmċoʁjn=t`q-U&WV,ٲgu 7n26Tl}7X2e Ȃ@ug 50!iG vLwMFhrR!ѡAuEEUJ`VcI$dU[]S&F`u%VY}h :1arJhanf\Kw2GO.c~舍7h;6kiUHidIj*9ơA~ɺy6WOK| &ddyf(& gv:ljцg (QmyeLhЁ*(^GU.QrE^T)ajVHUJ8xa.Jج /luJBg5BvjXz0XBm7C+2DW! rO]Iʯ S["=SVհUё4%\b_/Laf`Md̒#(8юi7>nFB i~R6wEȊDz$$u_fHPv." \Lqst'F\W~{V(ytV9G|ʳ* S'-ywiE_9QH#j [-n}b.<{ -O u%|^YZzեaSX `_QiAxG(HC VaUٜ_& )^ A zE^"!͌rbi@ T x4h Dѡ]fm@[ `.D\^|Eb &"_UaM]ED@AāTG` v J]+!%a +Db+ޝi5%_^"$$Ml Ĵ`  (u="k@V*z,0cbK!_2c<ށ?bkb,2 :)!:954R$rW%sy؁8|MFb)vVM3; ?<Bcd3?c OK$R 2!2DAEEZIl&_C_( )$C"VLb<&= Ɋ"^ccO"""1Ƣ\"a"IUe\%lؚ̍ }$YjV  2+!0Τ1]&0eh !l&! %;"d^$LR%Q#%8uW`(^IjH! fZ"_ Ld] >dml֦Pg+\[ޥadRKEnq%P]6XjeReMgrJv#w&.2 L#KdAlm{.(~d<fR*F#qbac )j%2'7B'8Jȃ%YJ }f(]h\&, ҋf|֦dg6cAZ$hrhn#~5ifjH\#vc2]"2!K-5l&#Lj"$m˜ާvP(ifFjL5n!g@|q億d)6(Ii#f爾fn) *oBɗ~+$*&%*ꀲ*&ī``fڪTf.JEhhewk2*ko$lV*m2keVkITPb%Q+:R':y8}go-L2$`a)P,njʞ!Ƣ"¶),,w d"跚_NYj9Hڔ+1h6eޝi ۑgA$. vji N, 9ڜ Ď6V H(mE+l=l.v_:*yelv-冧z̒-3֬*.qmgxkж\ݎBopL&nL:oC0O/hB0'hXD:.jY\//~|%!ah@eN#((ڠaa oa 2ad>pnЪ`ъol0]fj2^ ˦֨& C2&"CoWq{oeiPj 3k(bx"?FV*Oj%_2'˨#h (")?F*۱òrΪ,qf^pHCBsl[Z+b( 3 l1#s2#3's2q'$!!o1)6k*q8K Y^Rp2LlGq={r;z1>1j? cR^.!`5CBg6;F7?4pﻱnfmV uv&LKz:5h_pV$f>O"3徰2&4LL޴Bt:4LVnm@AGKq"nI/#F D1wW#v$u@ `M\[7*Df]sRt^(i)rYAS`ǶllA2!؄nW&o4R eSvݕ){hD2FjkAYl]ul-36o6g77q/`$`3 \/Oõ&fsPMAOjov/vVlJJ%wɲ%/?M4f{`|"ѷ}g _,܂"퀟,n$۸~uwAz ,##Lc0VnZe7r{5q4~ A7<6߸9[R -J\ayK8qzC6b+sxsrI,0Fh3 hv<$fvxgkO_:o>#毜wu xaY3f\os(gơjE7?:Ғ<׸g:w7`dK8'/0;?@0M90v(v۩\lfy9 ;hz{/!?6WW߮vxښt~Q?<˄oG{ jՎor4<[-|#R$cߏg+|f rE'~) /ƳI>O>>wzԇ RcFsWŎxa6lbDJи,eʤcH#I4ycJrXtfL,Դy ;yhPCyiRK6ճj=|+V,$kJ0{l >eC\m a \hDFD\D )TyyL͛i. 3RuzhФZjU{d,XdˢM-[!V]z{`%j\̨ !c,r|~іzt5iӨ?)lٴk5-|SN.s$l9T(0QC[a#=-򥒤VSplX*XsV[oŕGx%H譗B'%6.,ݙ[O);ԋOuIU?^:U,J5S`帞daV(dY"FHGcvMkx2ޛI\LU wC,PZ7Գ zW1[* j+ >R$eT9DK4bgӌT񯬮Uz19BTkh[euʝ3mV|4/\c0E*jxS?kze3rCxU@t?HB'щ2bK)46(RLp71SjiD#K<2)L.t 9*8ͻ]9M;`J t=d#)YIK>Ђ&IL`"I(VM2uTb)Wĕ!je,v2x^IrRr{J0J 8āiܨ$&K:$No wVBE[_fBˑ5X?RL{2o,&73*DT$/P&f&QNu QH5 !HG/i#S 0՚=5 3l4A.Ӡ(*+QeIsYѦIn|H+my+ sOS^qqD7Ђ6P JX\JWZFիEAWQ0!eHjReP)KsKv+XqL胷*4RDQhRU-`+Z׎֢$IZmKU;J”WuY P휱|EƕDtPW:v/ QIZNPn,eiX"uY94oȵQl{Y?ş% ܸ>Xu)\Q5y#s!9l^-X Ek {:hM =Ez<\LT\!uYa UTy ZDJ*R=oP++,P0J 6v,; J-]h]+yU.^eXW~_+E `MjV`뽐ʚrn`j[%mc3&=A6dS]]tVr-vH`VmQk//!emf@՞`k HQ($1-,qcN ҍ*b4liV-5spnNux r9 ,iI7Lٖ*:Fp%h w@xxwxyxy +.VrCr+P3N8,MaJR?Nt8< S&$[u륁w4䀌V=A){(&#;;B&aD!S[_U\gk< s\s|4h&g]"%N;x|@) H} .jӶõZbLBڍF'Z\*ɕarr@ vA5SM++m\e79|i<#/!I=WS]ww-(ZQ̔_a v`ЭXYدW֛ ItK!\C[}b~(6;a4dwA {"= 7~== XYh^N8CgE5=s'R#~,Y;["s\^[7݋hX&Bh22mvݤk v 0ލ;=v}~&F[4~¯}1|E0~;G C`c2tCwTj ɩB)ĉܿhM8i'<]'#a_w__m&Z~؇wש8?jJ K 2"_E%t^4(… '}zDL*NbA.2ȑ LD0,[T ɘ4 H('!y';r4iRA8} 5ԩdp$F AR"Ad 5[JD'tu7޽|R80`* 812O},qɍ@tT=>.z(cЏ:b$)Rh]vM5ش ig͐-rċ'eJ߼t~]J)L(Xd4E/g>PNU_B[X;!" ~4yPlƚ~)d!-ؓn z:MHa:^nqi8MqCvqp ,@C6XC:c>> IdF$A.ɤFÑP6AeM@eX d^.)DbIfQeje>8fryc9v#5c~c 1PBZhv! ,^HÊCY0ÇƘH⊋/v؈# C ٱ S̘ˊ-pYċ9mr|ϟ?m ¦ѣ* g =]J"j0gjʵׯY;VaҪ 3۷pʍ ݻPJ[w*Xc2 4R$o@&Z`HeDʒC1 7y >ly͛@r+bĻwo6ma!eD.uJyDiѮ}5b]sԵQm˷Rڳ^zUQۍ4i(XhcAЁd ] f u@څ+TxE8:R-CٴbRRo82H!udVw@%kyiM%Vq^PU$ygWSfr1"( >fC!0a)FYjmnu(ӌ.XP*&[*2ǑTfZCf5e5 fC)P'U^*\p 14eykiv։gvbmTl œn1[niv;rWJdo}75[E.Af*xV뛽&H'JgBM3Atm>ln(U,0,s7Hណ}ro_01znw+Q QȂ.q3o p{\N=N|CPv>awkh-nXㄲPkz; 90\By" ٯ`4xz"L,fR'2*VQ[\] Hl`z>3 ѓpD9RϔmCW2fD[,ٲ..ݤf5~l/i4:F>0sxМ NNɖ"aCe8642L'\otI 6zLOigKP&=9Igפ^=HJt=E7l<9xŌ$N0l/!]:z:l1NE{BL%o$jQW Hup/fPJÅ׾ҩZc,/:9r(WCwf?m 0zUaiy~_G*vdlj$ֺTA1b hJvi e}8;u Xb_BZw/_vli+/6yCn ٍ&eC-uwBNxօK]<^2E5$;(g `ML1wa þ*z͔%}{QY&2߈OZ{胘>g<6ݱ,C 1ت\г4ABkiċDY%+'X$2hF򑓤,V]7ͨNSM{gM5k~e-ϭܙC [L(T(A`ڙ&G8 OsFܷF s[n\zͳƫ@UsYڳ[H 0\IiMȧUGKPo[%À\y'rF9yǺ,Y"pc2vHAi5'@%Nko9~VO<\n.u`s\d^3tg9* "E{HZH'tK?ܖNY:빖0c>x^;MݷݺyywiΪǹry|27E^vUsCWo oxI=czKmG^7%`}~ַ3g2/z7wn:68תBW˔ޠBІg|E8 xAq|iзVP}\k~ݧj~y؁~0f-7vw엂'7zbGq`*vHf$l+F|gt`|f'ut׀}@xn'xk\x,(~$iv(xk*k_ghbVfnvHup.<l'R&0Dxx4Ji (GHu _*T?wnY]k؂bx~d(ri#W~~{7;W%9y(L`\hxFup 0)xTh Hx#u'f'~-xhj'?6`uc*k&=HM2MHmrċx(8uXÿjΘ(#j}灃@f,؆2gceje8SׇgEYX 4qiOQ(xy]]w]Džؘ|`6z|DYDiy6JȁWk0牭Wj\$n2$sqvTyn%\IxqA%IKw8 *xzِL?z*gFiH k֗=XR{wv#%]h@`ӇVF8myAqpT(\&hvv ) {7阐ɁyٔFk8(Ȗ0qG%_S(ضKqn)up)h0p\ٞY~﷛9阞9ɘ(kh{$@IxiWiiq nKp"ڞ$~憨Hɗ)SIiX *nA)Wj|W6i0hn#Z%@jyࢆ hfgfnj9z`)'+ɠICET&Jjq%Qnښn$_( XP6Zڥj%'.fkj1ZoJp*Y樑٧L2&h@g{R2T!&_yĪ]7Ԇ^``vY~۹ҋսm؜vgz `003>|T߉z>ӝTG.)`LEk)mW-('yɸ[]ceikno/Rmu>Š= }ި%*M2蠢]aųzv+-^ ֕>I,l=鉦1n,etΝ,.$HX+!jpL~K%lin/n 7vr쮽-^)Fk>lzӪ%h`{&ڷ,n㩮MDOm߫n Ĭ-!_G{"m<o]3o>)}aͅw[>D^H/&@+ q9n -҉nbX(k,sZʷfYjrNE?_p_<POo /~=YϓOO*пw{&P{Hq]I(v]N)Xi $.1sw='a9~6Bhj>ilorۍ~n 9HȺC 0Kl;kC$V(=] :2;5>ۀ?^K TR@vۊLЂ *08 !@ 2ܐC.`ènNγP.j|i$_<G-D8Lޗ49/7EU{c$cV6JM )CX+>#! h@*\"(DR5 yC{XPF(|uԹyt!Cp> Pi#Pc"w3HFrZX.E (zIH'=i=l^lJnsc9Jdg;IK9,1_} $ZI8!c1qyBPMk hGp6QQLYsVrLNzRxoAC03Hmӟa9^@PnT 54L>,DxQj"A `hWj='PiJʡT{i[V;Tud Cp<)x`UI5RdPRRelc5*ZI'Tͪ5M5tg L{nhkc9IBCs]ZS X aԥ*ֱ}(ͫanu;ٍ$|h2+7M)jSպq4Hl[Pio۝|/pw78hX0`JPQJ?nE gh=R3X&qEZ mOb@ &$;2S(x ]PKx'/S2Ʌ2W[+ٮ Km c¸b$8Ό զ U^gHwK01 >Yl]plzUa]G! V'<ۛt-}iPCӡGE|j. akOٌ$'x+n5 \ /[usgك((|pc'YVr h'{5їNW^2Dg^pV wmZ/F[%ZZ-v_W1 lY.%@p| [ _8 ЦF]PsLxu!q'95f\+KnwyjmkL5(ϸ! < o!^1{qg]vuGa7 s3 4sO%,K\a3G9p#JpnMocg֩-m[}|'}z/ow/%_yTR,j̵OdwQSGz:gua4<*l,>Y!h'!3*ـX3 iO3??< CZ/Jt/[6N$V%a%Vr4V+ŏPV Er HS2=SVRm5%XtS2k¦¦Q2;#9,jyMDApVRFG=]V ȨQ뱬WTGW32 /2vJvRW :Bb;Z1Z%t3Aҋ =Adk[JcM݁ܨNTXS!X"Y$5Y&:+ve+XӪU *: ,LWRӦTLfeI -wq/Z!2hHWٕ]?Z=!2m^zr*GcXp(gS602/z8 *<"5;U#F#\Cܱ*955;j/k`O ݊Л}%1 mb(#5-ZQBmn':2Ӻ/b9+wvE`\T4/n⎤<.@ԥ)yu*aGvd,% <2cx\+Ya?aa!Z%^T^eVna6:pYb ڊc>(R 'z90dYAI-#c1fQB%X/3/Qf;+3f32 ڬe;f]'9Xh~heq4l['acZ@T_q֍d(Yc/*IvܯU:fpg9`hr+x75W!|ְ%X}`'2k#Ӊha7V8jF=!^<@)^Q<ڍj;fj@ij~9.e0nkO_H91 2fXejXc6^h3:Ziơv*i4*Â\k$hk엖^`kމChm~m@՞m~m۾mm~mB A njsaBmn]VcsNjcu=feln mfX %fv"+l~Nj~n1HqP>gu voGY2 o WXF.XуhEXGxOq_qoqqqIqqqE?N"Gfhp!&q))O)'r"rɡ[n4>E9@V~@q>/s @mhGo!;8<> AH 1c;fH4G̶>Rgs#x<q?qIuZ[u\u]u^u\`vavP(vc?vdOve'LhfaDŽbvom,OFrpDOts6^.mr@7+qKmr?w؞G/gA{48J>Tm.Hmfe>t w_xCH v׎q6wlnn'op}vv!gDp.}*c8s4OOsO)X#h/o$@8oxXoqqZ?zOYqu\Jr*izzauBڌxZgxl-GyۍaU!-7x 9%hu('zqxO|qr)vo^l'ao_pZ@N{Vql|"Gr!S@42h$'|W|x|w|qDŽ\,=s嵡X?Ƿgnryk4, 0S0d!)?4`bJ2#Ȑ"G|(J*Wd9%̘/1Ѭi&MJ:wbgϘ5',jhQ&Q2S@Rީj1fn"+^Ŕcl׶l*ԟ/ᮭkz՛/z.l2&p d'Vh1$ܙ$E-R.j;WVHDcDj;褥M:«)nwhynݭN:!C~"{4Ǔ/_رd BnOE"E˗85i¾m{5 (5r * (8 J8!RX!/T gUqcA$rubc_%ri5gs*(3U9v="a!YdA"@FN4"-`mMbyaYrI_~MoiIU^X]&K3 u:jc@BX)ACH'_FJ<!&)!)RJ)^!z) #g&Y Ybus QRm-r,蓓DNFSjM zZ! aJxj]k-%6K LΘ댿ݑhxR,= mEE{if;~K1nz!nU1[*I$Lኔ5O{K||!]SPeM.̰~҄ơb )b,aYk!=Eb"{hbWr6 ۷>k]7v,~W>IL]^|5D\5֑Oa)؀A2,n@U:39CvR!E Õ;_R#oeRjuk9Ùi[9ɓ~2u-4HUyat~;ᳳB & ?#[K_BƻY!_&X-8߯.ᙼz7-ןͯ L+9ǛSI|54οFCw|:77!܏nn/8:"ǟ}㝦}!7>tl?fBHAJmޑ1şiDťR@]ߝq ]"4 DDixI̸@4hiFf`lu|D`9 =8#`L B W͋XM]]~_Ua]*٭MYfZ!jᕝW,NĨIaA~8hMMcݡ"e"?M%Y!.In!%,_$YjLQS@כEOqbi (S$֌+"biDEhQlS<A00#5Q_ytX;)V *!narFDŽiY$0]a%c'0OD>>>[5B>@+&BM0>Qۨ")r/!]u'a=[KnLauLڥ"JͽdA$M$K0tH~LQQOڙ \I"%Q[QU2&呵Sc3T:d4> jOK&Vf#d,r%IzEe5ObbN&09\#$eGTD9 N* |#b2EdNdde! cQ.!gʕ%ϭe'$NiBT&ܴf fB(^XJvP f3񥆨ctu$Y)S_sJ<#edD$Ti.v}wax<šF<]s`gw]@kNAJB[gf'(wn(&xZ"( g6@gr^(ReUD$ $b0gD=-v'OS~qr;fsq=JTeiA!'R(e% FQL2)JHjep NhY]inFvO~ihLi^QZh7N&Nili66)U1?:'ĂV$?e)'B]$NqӤ"QލUh#-%bvmi^),i*:I(1ZH)ŖBlG)ijlSt,L(JEklr%:\̒hjY>RΚlz*騞f ӆn=fJmv-,~,oپڦm~m~BWl֭%-mQmJnn%-zZ:AY2knmzlMx׀~ -Ȫ.Ѧ:mhnVFtڬ ]E'n,.-:m(`i-o3M5=/ڧVNrk~ooji~/IZ/ίZm(b[2~7hQ=03mi"D /Ȋ r axΨ/wp&q͔Wkđ[X $ pb O 3š'Z`/\0%2Hg=r-pBNׁcmqqיJ(m0pi6q"RI1Ա\GmjzpM!s!oЊn#CLrW2fJoc6^%12ٞ2ͥ*߰fr&gky)>)zr*) 3Ls4T3#n2#PB3&4s4bjs)DZ&t08A32*,h-jMJ_ 70ߓ X.MA:wG--. w\ʲ&h4GgRRHӢVqԄI#5J+W?tKBc3\Po.4OSiYF?G&R'8:Sի4KO:YRoVf$W( >JYZQ'86R 5J_|A+4BO5*z}2`oW#-<`buZZ]+K6\e4f?f4g5$ kVaY^5_T6'bb6cm#n+ue9Y.qJ7qqYNyrrZjh5T(KwRu1mmrnwd8[q֌J 7vT7{$ gV+B8i_BXOF+tlPKPgGfww8x#x[#.6.L?umvZ,nwSpwk'C4xaB?5t')BtcFM1u$I#I49RH*1fL/3vuM;oVlQRPIQ@RLII)S B IcWW^ lXc^2kv짮ug\s6I&Ϟ׬Ȓ1Oƴ+C<˽(4h$ *@iԩmbgƆ JXiu4]ٳkW^'Sf̎_Nxi壘yֆ<\,CC'{Z-VZ9v^qpܹwןowY|,J , dn'瞋nʬ # 3< KC1!L240h*p9A5nB* o,O4:@Z#%OҧUEdK*M$:  ͨ,+$G2${|M*KQ$5ϰZ꒤TLFgJD6QΥP!(dKO%-h"0MOFPLRxWT+Yi+!Lތr!Q$5=Om4VEլ`s5_֙ s׵RW?!a9-S;œfM\E%ZѮPmFw|z>7adXPnYM,Du^&7^)w~=Si-q3R Oԅ #v!e CB=dDDM>) W_+E;pPe/ڄlˏki"M-v>t(d59cH˶pU[0Y OzO"O+S{ܚƛ.6M?]S{:!\Qc3*O@B'+_hsI虏,:U`=^qHwНʉBsxN^% O~O*{8L9b2F9d9uG'9Ic%.9\;]D7E '>%)s Y D>"f]&BXxĉM6ȓ*o[`R MGB2OX( =l5fHIvlqJJ2щKǃ`WWa²,P$c"˘D᥍J8T C QC\ D2=D%'IB4H03$hIc"h>rB`2'˗Œe%o;w$ɬ Qf[&3)Jrҙ4ۦiK\^s>Ģ`;o8Ӏ95Zj9&_NAΜQ11K0q"krF)*(Xz,{g~Tt1? mmTpRRR>DxfSTS*Cz6u)X Uq-SmzU(C^QQ*lQ"k/uiOEm +?:N~+q*0 %5:m+2[CUm'qjچ"ݬ0Y_M]S$'^>&EֹĘi#+?7&dʅHq\(E}y -[brxn@HNqwW m䥍y^HltO{r}šTeF%<Ŧ!յji`" )1 WJ8T 7aרf 1Dtޗ K.>`ŕ @X\,/䤲MJI8#n2+.a "!]::=7c[ S/%Xmfqf L^鄕4`P2"Ro lUqV+x4Qg[+x#Isr˦lH5Lu+sj&< I.v|iW{ZMp^pwNv'a~wW<+W)Oy\{ܑy#GH_zӟ|:)! ,^aŐ!G*\°ÆP"JdbB"D1ƊBJ)I $ST˗0;|\/^ԩg>>ZѣG)]@P Ϋ=u䈒CKC\Ӫ]VJp!AbD]x[,ҳ [ϜÈ# K(@# 477`!-.(HDSbk;lM۸cͻ7lkTfP }-8Ѱ]V䕭jOr7ˇuĈ7蠄RG-`l&i90TUEKƖ ɝVYJ{ea֓P*`T'~```GJ6F1 k&lxRJtۛ!i"qWS!},v]rLP3kcOI*(:$_4J޴'&6X Z abj*(TnK}u#TI#ȓq[#pY#iKmu֔-tFR{F3\l*nDkXp{ QJcH eg#o [`e l/GX&Yb²ˤB1w 43Ia6<aзJGq*Oi{lupjMmc_-v쓒XslxkY}aEFQBFBBo- =#in+$MDLܹ>U{FX܁%g֏j~;ܱGbYeAH5o[: @ Z008Kr' D[ "sV @չa8̡bnr.Jx2 PBpX438+EE3 Eu] w5B|Ϊa@1Ql˓? q*i0'Yu)4+fQ2EZ P؁8 !"]5lnL%ḓW,e);/4Z$܁2E$(l`"J)6Mm1M(@r6v#9OIE=CowyK`v&Ô%e.STъZt M6nz4'(2aHiJ͉,uL Jթ8韧lNrc7&^@v|v͇|lh8`7WNx8gWׁXRn%@fAf~@zz8GUw&{ywk{M 4x|3&& ؀*p`86ҷ/:WdȖS &80֔ A!4֑ Z븃%{&+L359XIdpI;)OАHPCi#ɕ9M) a `hM =_AaigsYZHkj R`o'c,]L@?yE 1ypyy_SHp҄H@IP剔H깞쩞BОYQ>`Q@í:_pdy`Jʭ :QJ5DAꪮ_ꟐIzQ*&~BQ?˳ ŠfmڰeeZkk +{ o{۵":ԘO@iڱ* j3J)&:u@1KIU"aUٳdQG{1[b埯9PRkyfkȑtIuY* Z^[)e۲y,-궀 :5@uk>JѴkFڵrZt$DPn{ tXSՑ%UĊ? Q˺iڱ% PrLۻ?@ 겒?`ƻK?tb 2kQq^KZ_6P5V"@Pd ÑPxCLۨz: lQ ĄIܲ̿:sjHPQJ@$`zϋY3Whl4ܽe˯Vr,cu|\K;k`8er:țPћѐ@̝ޙɚ*˺"1JJl'[FHƒ^fpɑ0 A^+[1XJT}#~-KE]b% ܴ]H>Q3O0 /n: <\-  㼮<%^ݩ˜.|+Oq]Q.؉+~)9MJt^ñ.m; ^  >ZH~g-Ю*nz~B QM9x"Ȉ*h] +w@ 聆:É D J)SmC?N*p?ҹ^"Wbdod\dhjlld^oK- z;)-/>ɾלͦMKѾPR?J pZR\/e_l?\o7w:?$ -ZNV?/^dPG/߃֙D840d@iWOXzn]!b_4i4P&Sp-dȰV-SBtRHtw$9R2dҬ\)ǥK1ӼM2\tRϟJ !J4 C.eSQ.Y2ULneȏ  [HD[qѸS׮x鸥oݿv &\>'Ӹbȑ%O!&MI ͓,4UtiQCm28bƍC[2_nb$Μ:w9(ФIMt9ѥON+תE5[ ,n$yb_h{ͧ_#߿~Hp@LGtA0V*4VZeÆh5N.z D $fc[4)CSEqT$/(n "<'TRI'`B媓r*;nX.31> üt>8OA:N<|3>)O& Ŵ c9-"6"F#yDm[1GhTgF_é TNHtWJZk]J&* ՏXdUvYf)YvZP:+BV6l[ ;4 FaӈO, O{]C܅'x4qTII)TQRuU ~U mUxa;.x<3LKO06=84YNiUG"Mnco[pqYDJw$vzh?W_I͋Rui ^ [^#.7NQ]?;d)WnZk$[ y憦 t9_LgE->3Nz[R6ݥdJ`[zskڏXkh᰹Ll@>VmUo替{ZooPB·cqiE+\D'ĐJ-s&\z>ĿAmgM7/I:&jZZX!w)d !Ooq#(,Pz„GBJ?)tT0?m 9FG>+ŏOc-";h$htMnT:EjS@5L (@`cF8BbZ a%(F:OB#٘G}̄ 18i{7[H+٤HIm? #e4)yp A߽&`1 nY/~A",-( 7P3XG;:ώpT&X BfS!MB Aɖ< j.qHJ$(yr3#MRT%E7EhΞCL"K42 gH@\6pT~a.@)NA s @IYM<ލofKUzS2Vh8Ix"lH+BD%w P'0TY^B$aPxGK[KUVn(t^3'֧m"]xӖ: ¹(pyW(@vXGpAgTf&J <b[P'": KXbRqDy*N`}r;USIk-Bz%7ڷdpZk8&E.:+^KFlM$ڈRVDK[~M!l-B#e%q; II w0#3cA$ 1`bB$f%9 C,|>3nFri|:Ӏ*6,5]gWmc/ Dq O`,2+\@-(*RL(}R?ZЃNh=Nnf)z F"PNieb9ˎ2\jsc>[4ahϢO!aA.Ko}5$hfY!$`d{ gx)v"Av pm bO|Z@`ZQ*!yBoo}Ǵ5>0,` GKR8=Pz* #$BW{kh;c?9,4PgN\۩Ap}Lv m{IbC+Kސa %@:YUÖzip/T"%#g;Gr=orǃ &eh)Ҹ׼[ 4s@S.zx';39H 9 GX=9*[-TK7[)ג|A>@3:>Ɠ??KB&|6u4DKA(&h$龞A+Y< =9A#06$(*2>;&J1#\bBB,DC$DBD[7˭aB 1B*C1 1$Ðx HCOzj?70:LS UD (ֺC@>l@ #C FaF8.Cr[L <2$Aɑ̈ DxQ,90 G W뱏)oq 3sDG<ǁ„u?G @<{ |GbLWxITHBRcdd$HC{ºƑ02wm$*QIJиC[Ȏs3%[:(_Ed`ǘ\KW÷sEےNɃDӡ J <ʠ$CxHn7= Edw$ȫ,lD Ѐil:DDatIəLc|ASICʖK F$ʞL(JQ M Ίħʫk HN4AN:!IB"\B<ԔIִ)ɳ(TĿ٤ui´2HT9D NpHTN̍`NPj΃+d ЗrlHVoħK; xyyOy# %fټ;P@ %PL0PɃ)H}P$$R и#vO}ûLOZl=O@P>.ÉңQQ4uGPQhE8=57m GR5 4RLpPbS%H'G(uTm?e /TDII4M:?9URO=;<=->=bTdBebwkNkc"mk*]ڳK:aK~gttS#e8M|Vlq|ޓASځFk;޵Y"9О\B.GsMS46E!R-D:]]ҍyQ {l3ޝڨ^m~Ul4M;JN`\U\m5DkO~YMcM6oe2^Zfu>2F.!o9$NroMPFЫ>Qp7/m̻jmШlpb^դ"Ϲo?^"-UXw? }6c" $$Wrfr^dFoE"N^Ѷr^[p r.UtU f,F^3HkgHRo:s `5n<q=WgbMbVr0^/Wى 2?iL61OFoBgmQsےoڥ'1$XT~EwENP`tM/qwd^s~p!'ymTwեxA⏯y(y2s߄x@1Zסd8num,DoKeN-a~7T]CbajtaOayMwiR3^㓎6eQVu٧shn}|깇 O.@Ov&Q_|`'9eGO/~ύg o~4LI",hࣄ 1l!Ĉ :P@L1i#Ȑ7,i$ʔ%Iz2g`A"w KI'mihMJ2m4ԨR~4@WCbnux,ڬhZ|Tl­&nIUۗK4gI/Ċ+*d>MjK}h`.mZ,Gf5۶m߾⣻Hcy3gRʗ/_MRnye=JZXn';jg'nF9@7`G։dEEfWd u]{Zy&PvmakYE})`N2G `d(b UutޅVdmއMmF*ZG22gؓᎡ@BaHiEV(iZyIMVzZJ.vYgaɅY)Z)I5Ogi9֔Ue'aJJt`1@=ʥ^N挚F!Z&FvdFM[x5V!F%պL֪zb/0qIj)NQiSN&цJթX{#F ypiߞZ|qDӺ(U1'QD:rZZ&S!z#plS r ]|pT8z ᪱oqиq&}Lk#|')l f(`OG9_VΜ$O| Vbu!o^J2}UtF`$LPGe,R" p؛=DqAFdѤB4Ή =- PȾ_KaL7?l;ֹ@&&tc((] Kv_$A!&k HWBGd$$+ n%u*MO; (DDD"{NPUT$?i@\gC{/w>x~!.%*\J*u2ECvy/Lb&>Qz:;ҽxQQٿPMsPF1YOg=`-_\ٸ]lȭ ]b\ +Uڠe5n=C9` ՛d ` ̠h ~]%Uja~-U[|` "\:B^) Z"ݤ-"v!!uʐa `!F&*BU.^ Zaۄadb!Bd2\KMpb%T"d)B,c,\6*eI"C;"_/8 #)O "*.3b+"I$8JAuaLTu96):6BO48R<=*m"YIE ":*a3*⹹4֝YݢlE ]jp@塠Ady]B.*yUc\L#͙\m!Sr RMddAd"8^5F9]>`T"y (@~#\Ζ2.بV:W*H[#ZBZ%v<ťc^b X\d` ]XəMsa>SLcBaeZյAFY_rfgZ +cYiZb0fj"ffk lZevef&QަX}v\2d#^̝bUhak>Ȱg2bg'w>_dYv{0I [*(_n ^z׋6F~g;h,M)Qf~zޥP+!Di&Krװw~dZ(A$8hzBzUa}f-)Jx'H2*<꣆V[U EhjiUI(*bs +ijv|ODyG]ӴM*HjWwuKdh"ѕ"VƬV2j}* ύ&EH`JEIxD$v2nd]k%=*y~$*\+B\JQ&'ybUb+jكAEڛk^jc"H=VĭA,SiD@VUŪ+&׬f'.`&*4kllʪ*Ԫ"- [DF|䒸jy,&lN^,V%b!B%|=Gmlk/bF)lJU'-R퟾-)ݶ#6^NThR(2.:.bژނNdݾI&uB`(yɮbʊ5lSa@nddhnƚRRdnPm/fqNX&*NbU-ܖ>(iGb#sAF&VElVo$_n2S9l̈́foBG/@>Mo𲬽xު/(RpԎ.vu@/Яnۆ:a6bmjyn[ { ~`-fpWNPtf\d4sᰧtꢇZ6g#^1-C+ V/Grp)tpW%\ GR p(y iQQl5!:re-2s.$7].K&[ku_ȱUq))U*qCH+ŲFBr27Ӳt37/RE&.;q0k1cMrh>2W*O3*~(y 8r t8399 #X:պ K_V+= 2>KZ>c";ua)NAAsB+tĞ0cD7TL134rFF{IGG2Q56KG@S8W5.W/SrM1te5'PFvsAs3r33!1m V:r)H]UuVf.SrWj^1]srTkKl=G4D]YJfWy<WCt(L4a$btD7?v>5/s`Ko^^u`W E*4k2*kt qP>%9urvfUva 7/u*|PŪeC5vvo7wvoMx簝lҥ:gz3#;]+H7[C7x^]׶Y83gS@ӲwǪPrˉ7!(74sǦq]P:fZw]Gux]Tk7ws8woxw1~*&,ӸC4Iߥ/p{K9~tny-/fWWO[LZ1L ;9KJyf7G4y(ቧl޹y9s~r.DVn%z0)3zCzzoty'mSlttM7﷑:o38C#W+2w!z#32a2pREz絑׺2߽ssw 9>ww#|eWkL~9Uoڃxd}>P\<JsuwCt}ށ$ ߸mWtH" JF >xoHA!8ՊL)dX;^Tm>wgd'gâ$^z, ;):u2f9b{lZV5Xmx!{5;Xq kovN<6?]Iqn[KfEYi5-t=0swo"-Z_{F^9S ]79 XH=B=1#ǹ%?lT%yfQ"Lk3ɱ ted>o4!+8}M?sS0FI|Kְqh>YMN`(ۥXV[!# 9 SB@ @Ql Dܫչ{Ar!J ׹n(2BQLHBn 5B$ FVT%]nsݔE͑;Eբ,{H8.-Xg!x6 P8 O@p?Ox(^9N ϸH^rWY[]^<ypAЉs' QPҕ~)HPyԣ^t/?! ,^Ő!G(\x#DpH-2f 1D&H9ɓ(MbpRBʓ!Z0b$B(XrbeB8 (Yaѡ@H=sf5Xõk=`Ê+k>}VBpʝۖ+ۺk˷/CLx>j0j|8PB >&uq")jl1 +b˞/sO: Nqå*BVtܒ^6r9E{V+w^{_pjIscgA9E]T[kF[EVFR 1rH܇ %qBr:Qus,jµ(c\؎ud@caB#LFn`ACԂ8ZieFvECYnaB!tigpr*'\marREN'H"PRՠ2THǤUEՌv3jVh#iLAUPg>dD0P zIј1jyқXp!rvҹD>9["qdRSvq+U8jVZ宻X+3ZŽŇOh@^9DMfBT&D&B 8P`nD\PON֜mN2wjNg4꨺*bqqJAZSP|"#EKDEOen=`m跈&jD Xq 0-qDRgi F9(`˩tP3gxN\ASUkS<5B.AKM}=P5OvCZF/؍`\ ^ⳙp 2%'ԴBIȊ&c\W7̠ƃvPvFHBcMV((Om/`I/#ճ w^bf9,b,Ë#-AolB KWfn؃1ROܖؖ)0)1K b4u0 趂 f$  !"X& 4Y&i;py(#O0z;[{8!~b?$ߠE/ŗ~|$ R$3#@E ZaM,Ql 6r<8gȩ& f%'Iܠip%CIV< x8J'єGh$b&%~oä8s"dA en RLHx2'gXT STVUŒ .I%.m7uWgV1Y׸5^zVSZbCC)N8Fkͪ$* \B` >0(lc2 ˾LC9{ń>'lhTxv+dԥFLlEݹEhn|.}j.j={e^oGn\b\Z1 l#()s܉@WpC]@"J4{ݛΆNEYf˻g٩Xr Jz- aa6 *g@nzIn \|3xRlx1Gd<;zE~c*|FV) <܁ #$1jeAr+Y-g&MSf`3*Lָ˖^7GR3Yԣs6YI9[yX{n()i`{Ir ƌ~rֳAe ID"DeNc \;Ox.OMvRli0짭F@<'<y@M'b7xn[ ]w,D@mi@;P7ԥ cJX&+f`{,q%H8$p]S@EѶy{d]<#r< @c.sm {{7w^E0%CΚW u~,-E& 1rZ?س,N|;_p8h=w׹]{hNsXgJܾmbUÂnD{v6INfegzGwtSGuRƄL`{dV!Vv7F_'qS|\|'_i_|%OI3ڧMq(i Q}Mv0CIg=N脽'+[un@,\y6c,SX4wbvN|cSzR0vLWiyp{V_dwvw%X_r|ȗ]5F0H}tk%93t]eוpA`~WD(xOh=7b7W J`D_e]bE,"| (u~P' qh 0t({puv{˄MAYV G'燸jJ]d]yuTpeFe}rEa}r_YdD*&5P =㦊f\8Y+\yjhxAv|lt,5  PLw( p{+XXЃevYuRSvv樈SP3(kZ&9d+ԏ_s}?7%`DؓWh+.f67giH]{47EX5cUjǂw0 .X8j9'jvivȍXIq~hzA_ȇDd Gd(wvDOX_i~ t%fcy;ǖ$x#s8=r拎F]y5] #aAHy/9 - Ij阧`_ÈEY6I޸cWݘ9IXfL~I|YdؗQ6IwH4uV2_`si`%PLhC[`ǩn&h$z "z0 P R ]9W^Ӂqo٤NʍٓHher{_VOr`*P7 HgBqOweV硺\ slؕhU[FPI9c 3ZZ uPpA*ʤj j2FexLꤓ jI^I:&׵|wYWh* Au5@u'bn,gE9HM@Z VpA2aW-ժ,FqUO9WY*?Iv"dHrXuA"-^!`P$w-hu";$]]D] ؀ p 2{ 0]@oB?I$HK'qPj? :zOewI *pSp6cpp;t[?fxFwgdpK@fd|=ڣRR/93먢p ($rXPnF!Hzj]aCy|HPS M^'iMYVFN٪ pfkd`?;[e4ddڻedt{ҋ싾 / P *iw eba+5LL %'x뺯jTN+ʟMfQe>Ȭ(~Hv˼URFP|<Ê[=̾uLLP ř0{NܾQ\WgL `0 ;P0 u<lr,69bqj׮Orj+(ˎr,y)Ȓw F齇H ĠHá,ʣ|ʧ|ŪʬS|XDŭʍ;)` ,Ee|i 0 >p̼taQ*i3a R'didPɒk<㘈͢UY;SY,Wb9æà=S ]L[Џ[äc< มѦ MK/Y< (A*5 p{L ˺zq$vNppYSaL.^=|:<@ } ˯}2yĝ؎KM Ԑؑopʣȼt@aP, h w1wҙls8ȴj76X,`‡%եՖE=<@ N fZ\ ^я] H;ʼO|P{A>왐ucNj꙾>`OBcPͬmD ~S4w?`aU. S ݭ;^!֚nɝމ <]R-%جN5Jߑz;(m_-=] ~x #/-M\A):Ym-!<>1nQR:o=F]Af>S_ R_iL, ?_pl/@ 7W7pXlw{_ o iM{3njP1"cTmYHP*O`@N}"i}p kNUJ)p-dpTFݡH 2d1dh#DDJHdԩ+_(O=H˵3 b|Rsб+pO>  ?$F!;\s" HB2T4,F|ʓ09Id=p40!Z!(cfґ3r ;LJBڃ4S,3(H5/sS'2D[4W)?C%tE5#m‰,(-K92I(r0EXU!^X#b0cP)C[J!sy})ة)Dej=14kZjMlk=drJ܌ͤ4W ֝dEDiPE%kKic6AIr'X."c=r"*0WLEPwl%<48#Yӓ+OgbL&\:ћ tr# 0o%$BD- 3Îz^(:Av@tFv2rj7R U-XWJi%?pY'Sqðsd$?#XO l L@8ρN3u<I?L3q;-(wZBpQAT4GDBZ ;|è:mKC@ i0 0iB@,WP2LT$Hv?c h@bW%c dE+>Aq%x@̺FD;A (D_#|dtG<$P#.U2RdHH1h*b+DS,nș+$Be030) a*XD yحDQf-"nC#<3`');'4 W< ( '􆣔jsP&)Z2@T%)ȳݲ 4^&7#R)S%a[04d*3M4Z,5nUCN#5D< k/ښBY  yPt*44}Ԣb,TlSz4 (ԓ?$laᄙd5% υ>UQ7TfRCh mGD_lVtOxQ#dpQYNRrVNnmn܏-TJP\u0C]ώS- IG~hl @P6Ie5{߼d LA2rԣD*RQ4B[ݮ %o{HFp_i*W Q]ށ(!Y:S @J1of= n&Z%nvs4{;wC겤in:_wsn &{fi(Rݹ F$h:&> h.I)(wrd$Jg9@Ex!T K<Ѐ睋,u!_]Q!$3'R0T0ཛྷÄ A?3A;(U5HP`1gv1> = X $M͕[ʓ?P&CKttG˝OuG+:B+NlEܫ1S(m3 Q LSU]bU>IѺUz> y6D;iҞSVg$VQ& בlSL D0jHX p۽ S8T SpW׳%(MVT΁ב E);=Wg4~UFסH]BNJJ Sln1bZXnO ƑZuvݓu93̌=vIh$UB 18lP[_?uZR`l(cC$(衸CFCb'@ F3O,>;8%C]ͅ5ِ۷ ;ÄQ RHC[T۵}Um#p>}6A+ġEپݠ!?M؉]յܴKϧ$5mߌ-]uؼu]QQ ]_/1ݩ'MK 3<_ÂJ< BhXp;MU 5ù%Me[ 5gXiPFb8@j ]i[[![S .~uD|A3썛= wf"|őLl0֠kVm fVl_R.5f݉>lgS>y!.{ꖝ˾4S,L,sB>`̅5A;傈N fq5 Q_Xma.kBvZ_Np5D_~wI`|JBn}ۏiT֚*Ty( ^܏@L^﷖m[4=ؕ_m+#dIK$E~˩ò,$fW>fa#WʳTΘeMm>$5$]^Sm~m~E9b!脻DrR8B@K@ed@ǂ߿Ln^w̓OlZ N>Y`XdV70gm?Qe637U\S;L8Nmtyǧ|zqxVwU|`d H?uS|-wPyڔ 8W%n}%֤>_a |mʭcqq7o}D(`O/fW|ٗ͟Aq`}_Q> }ӯ ypu~)~o}iА\ymck@*h0G&MᧈRt"ƌ+jQR"EjH1$I%L $ɡ:KIX'Ћ={$sMVLJ1T(RjՇ+ذbzH"D#5H:k$H %2ׯh0A~l0bQu?fHx5DeL hFSЈwW\.ӻҶ6T6 $ᰀDh|-C):2ɢJ2/¿".00!C>9叝1yE,]I&UWǤyAP&nMU"ryoeb!b0v DӅeTħ^egxw=fe?]4`)?Z}tTU%d嘚U֓Hm"՗wD(WY!ciQFbF#aȖsuxI_hJ'WNQMu)tHeE2'P=efuz&i&XQX&bmFZz@ѯnH?bE)Z1$^"4M׫]=>Z_I~yfWOjIjDVUT!Y$ɗ꫰ag6XUScXeDAPeYF⟺(w^~)L<0n^n𲋲)`(kmki(ZpO7*0PE,=& t"A[&,2.$3/Y檭}]/UI=*nyOQvNb\Jm}9]}{NW.ȗB{6ym?t({Df*}^}vBݤmb^u ;mSesJnqf U-uzyQ+zu.w~G/HFlFwh2뎯xw. g&xe Q m^YUzg L(%Wmq1btS¹_/Q)0g\Gؕ"r WR5ٝᎶ{3mDrE0[-h]UdiL (*4i YLBLkS8B'2:La"D'QY`RdqDR Rȅ< FY U(ޥdq_⍫Ա8;1298x[0b1}IhJMTĊ/4$o%hy0`b>+Z(L+.W[zؕR@*Q$gs*&sf\0Ld6^91 5P qas%gyy#:#vҝK~$t=G8r)@<`Zq1UMp:~g˧q~~ A{s\,@ix.p00@h95kO϶+/Ӫ>*@xo!KXaa4&%Jr& X%֞՞mrFBڪuO*aR,=|j+ X9YDRĈe~&Wuh٥-08z$EZEmmm':ˬű| Ce)qk.7;oH^ +DnE]bC 3~Uy/֚Ɨ}"{OB7.gH5,ba%&XL,'UY녥77| /LIp=hTyuN-RVVb˜׼0K^TM\\ )YZĉ`ص,ńp@. Z0QZ圃1]fz]]yMġu`L M } yeQ浞yuD~݅HDd[^oq$_E6FQ}1=8GT!LؓI}`̈!]FTeƗp_eWލsHEF("XD!VR`Oa"SiQĮY$]ɽfAh<# E⮁b" X0T IԽU%b|J>K0c-B#+ʽLLa#O ʚCl\??5=V/V^e<"1 sc9;2c8#q'9%vuJo'0qqe1H^f|E\帝l[th,>~B\ٖycO>'ӂ2chZL6| aH>R`U2@:X@f|1hhr#§sh.BRlh]kl[>#'.%h? {Jh1PǗ^We[fw=%rjIEJiMiNh8#S%9V`| j W~XIY+ģBꜢE`^*e8yM(f7]nf\*«jsϤiZ\zBF!U gJra1k۳F봂f2=ٵjYl+j-)ݽi6o(hQ eB)NbZR*ZhA 0V皲dv-Z}筒Umbn2*(rf"X1Z+4 jvbShFl~بhhU*Bvl,꜁f'lzιT 9-f,rFMƌՒ qO˗&J׆빝,^m׺h5fJJM-VRk4K˱`sLKHxE1UOsN5Rת-FP QB=?g2{fU[u٭Bl_=?`XSKA'WgVYEOC?5)s jtu^u&4`t6b'GA_g2+&ML춴YeGFfUh!Zݐaُ։/kcfr'mU*1+2ŃT_=q/JsCZRN77G gpwwwxc,767xt%aWOP鷙7pfH  ސ&iva&Z`omqzkM 2(&xw3o6F%*fwx^B+U'9 Df̔?w[2Ε*/ZWNhB7]%PZx,L+Wɹ8-J9G7[*{ߧ%P%%o#PHu*lkXcmӬ/*qg@|6[",IpuTQKq)4DA,nغ'zc77q; 41=&lzJ/^HK)VufcsxWKJB +F5VBG½A/sUѵFvq{#SÛ;ëwU_󰱻[DHHx&d KV+{BF?_υ"Qs\W 7ч$o=ӽUOsGdj+o{+37s,&H`č}& zgU׽|@9tէOkG֗*~*n|;؛H>S$]3ec5a观O)K|nǫ*~LU3/ ?t?}j *S;{L<+ JuaB\JE "RHM8D+jh#)P"Aa2yRӧ t9P*2UN4uljx¨rNCK"jKK 5 GSxp%M8ʔzv*k VSܸoS] Tp`CKEv{;^XUzE6~7M CDI4A C3ݤv7vٽb3EX~E0 Ij%QO"H&\n]>H'R0$ҦU cxZXs_/]d]:+>U>ԮL=ޏM0t`Zk]mI&Zɢ$iC dCdLCjɬ.ͮ5 @;bMj^}d"Q&[h$NXyJ C k@zP4)˵r}; 0FmvZ:l-Z7cqdS)w"DbV;`ϵ5"BJ1Z@,N^bdGf5 b',|KY8untˉɒ1d0 x-^q,cP97}Q2cҰQPkZI7/-)%&牀X ik";!E΋$ ɅPVmɷ Ph'?j~Dc4δҁ#*AXpE8)"ӊw"ޒ-jHa,d&JA4;$gF ݡ5HlrsHxў qYH`HDe=Jy'<9гŻAD@[B-r-|QԢ[ةPP) u6#ZbrIhĤ .99 -X(eS_VKvR4,f?= P.Q f|@}bCPEV*I+!Ƀ>2A! z`[mӏAȁxkk^ӷWc6$_V1.9(SWy6"Ye||6JW<᥶-0K_HDw\njw `nt'1l=a|O2P-p~@:OϟphOoCҼc" i 0*-|J V$a@a !2P`z=|j/vJPr.OJ`"kqq%$/HdLJHq2qρV^|뙠ɰ`> FѮC-M숉 ?¾PRLejOȧ̮BhKTjuZb!$ q Χ"V_Pl.fqϮ}n&\RHZB뷀 ʍ6ʔJ`cNRcOx-i` )u$N@*rԶR0+,q-BϠ(o(#.#)3^~2o\gXpy/)n+h'5CZpr#hiVe *5./Ѷ"i:j"fe|2U/X3,C`T16Eoz L%psbT#Jazsc81Vq$5S!Љ 19k j:?r:o80sje<<7&=3#=Ȋj0Ʈ&3V?K1o:E;"(T79A-.ciI(I(<4 AtlODEMTI74Ise,4;(EA}*"t<-?wT S9[iL+&3GqhItL;4iOJ'3 F2Gr8HY/Ic2&BN/?b3AJKOt*t=4H4=8K0TfN"4(.W442:STEiD B DT0;`H:(!h}r[_SLuUƤ!UZg(2rHu>*@YYc&1 UZ=ZUIWkjh-}uZ,F5j 5բ K!U)$ xjHCdp*B*ZjiC)IS!5{X=BR$9SPC]A]#vcb#$,YMi2am 93q(QeIjU>Sfmv K43`Lbh>2K _E$i$AR4M$J@+ AVG#BCgV,Tl!pFZPD3S!S>n+ Rj"o°3+o[("e2(rSuB70l#LI"_ ^#_'3(35_AKOS[ ^c Zkm_K ^@ `*! ,^@B( #*$E$ؘ@G AB aIRn0B؁͛8r"Y j USѢ}1S>PCUI ĕ롯`Ê먬ٳhӖ>s궮]U{V߿z }[7,Èю]XպVUF ,`B[x1a/">TB! I`Rw;h޴ɓ'>z5CҡT>~^ڵwĨ3j<]˗?>-!ϿH'cgUifuh& FQEX[6Hنk)p">ahgHwTQNVq]RAidU%Iux#I'Uf[AJFiyjfaXF0p*."r0H6Xs%ݎF5䣐FݑM5%buZI^9X!T d k.Pdq؀Hn'M+gqq3J*^@hPfn+mUN._XM.T=JU vP9f6ZCKh+FqLĪ2 wOk$F$'ѱ+SQ*QrͥU\`.ag,{]ˮ]jmTMn (9ؼ*̰ DkxFd,xMG@ ,2%_2-'>t =nb>y9,En2/PMf ChyZD kd='J~,'n,ww=!jxt2ooxlsS[M㍆/wP?# h!4PCl IR$'PNmlSVq,ERX=Be/GfHZ |qY\4l: "b!p/<`D&.ai **p dx3&q8(Y4;%LO }c(хI̞5áA kYHEGLjHDF!ٻߌEBBr*Fh!+XAX;qQ0<2ǂkAyF L.x#*Z5%~I#^2x<,)%A@ɜx ;YnC֟/Oe,7Y7mM Ɛ%:̟J8FK(J!8BBNd Ⱥ njzz8X N6+~rkdͭR3nlQV"w',s(Ѓs6jsx5gM  ao~c*UhM+&ִ:o\ֈAx]%׌" B)@X4ٚEH9ewQ|GAxuֈK-eS | hHCZdvz Ѵ"p+X^M+pWܸµjWPպ}j@@ baXj*Xl$C)TNn#b\G=i] L'pLgDZdEkWxZ(F ;hgL]t.xr& 9I:moޭD jB'31N,#%sL`iP62Cqoox3 b %6a\ڬm u,@J$4j^Y!l)QpbY7xҍbW$YFe3e2̴i=fZ 2ig & 6v܇ n-b 1 ^gAOrk j !n cl4y'Mfֶ;FɬMJTj$;窍Ӣ|nefXF/rxqk_u8HgƃY܎mO4wH#!kDIb,H@]EӚ>oņ YxAT%tR Sz&NxrOZFEp0|}m_ƃVIrM*[Ao} 7Hs|*)nw ،M C%`(0[[Iqz8Fþ] cG_)`E> w%_ L0~|stIemquM`]l)S+WX djnw7v~p|~vurZv3SJ\0L{GZ&rvKP{kv 8|vȧ.r|H|$h|7{slGhw}0_uld:i79*~=gyFy!;$Q0MXtunR;Z0o1zWvssNY7Nq?vηLĀ\@qwp'?g6z*Kٜ(::I5ԗȂ"w>K9B)&1cwcw7Y뵸*^* P f{`: nR1x+m'tK67a s}ʢ* hYoGI&๜xdna+o lq7iȥYZ०л l[: `{+OHNЗN'&$[ʬx>'z-QJr;@@eQ ,,>QlZZۉN`z,0+5uKnڶo+zTЗrS@ ـJH0ܫȐ,ȕ6eRM ǣ+ ŭ@KWE2}˺6$KkCrFU7 }nA,|xmL\ 0Jga= QӦiɀ?elŨ L@@*?vZڡ, 9 @`MIckwgTeD}wȋ  `  L̆0 Mli2 V ˸CaJ@{|,1kY6Ed/И m ulĈ=Kl+ݦ+wzp @ |ȜD۬ [fo91<?=2"EF]q hǁvP- b?DZn8ڣl۶ڬ}yж~а m v H0`1ƍL +ajn`IdI]:ݲ=޼ufЈɀ,<;dB6úC{pY[:-PQ)~Wݺjmכ Mz`OcP; .ݐڂJ?ݗT'|\Fޓ 1З JL7n~~LPs=V~XΠTY̺ۣ аva.#j͝/ wm|꛾-.fgW=Mf#(B(n"]RY.[Y̴ַp o-а怡Q 6g  |`]0M$ͨΈY,hZ:D]}<nn.Zn\aN\^>^ꦎ p బ ^y.- hJ;&}h"iu׸T3ήZ221~;,.ߚAei<;/^ N_B>g{~T!5p _Jܼ9ͅ>8FvfrKű8X/䎐3p8O^ @ >??+/_N~MzA O P  : aa.`,tvZ#^(qkճxs`n}:?@   ۔> p .__  ɠSfJXʡÅH$H("?#ҤH>{Tq˗nR&9u ŋM^%Iy&ei Z@#|a$#aŎe`%i.d֠ڴνxq]y/X&z!%?7H?~R%@,)AATo?FOZT1D"@7>.90D3`4`s4dD=k!>z-R1@XdѸ\>tS{4j):HT2HH.'uV(u,J.E31q)SM.Y1pC͹._}0BE+GB)(tJ4khO4:1,(r(eRJk.ȥPXT[; -hUWR=LWΦAa5ŰZRdV;Ȑ h'ZlM7=+)3=׈dE=WWj R+&&X=@RXᆡH"bx5()DB*t[oZO&*vPt V[, d4;y$"'%r>Wi i`i5"@Wh"xXlh 0n괲M,|jDD20hCNBrJEkZ+|a 5r(TܮFZu:&K7 U3LDzih!/$$B L)P_)z{= 'Ca1\ ly<$X('>~v %mz ٴI! N5 vK=֭>a1Z%'ݶԩnY4#к >f[bȻ=r=$o TfX)3.r$@ז5jB]gfaǹP'l u;' K^uPˇV,5cn]4ՍfthMk_}O[R7E,2pc5&9+ (Tf? #È?A_VdIV⬓ EuBe;E'&B@x'԰I: w\"G@r&?Ӷ2ۉ8>Z R{Jn8[o]Ą(=S#ixKYTj={= ٓCh$# t">42p1).'cCˍ7 ፴ ?H J7GB<Ѓ'(4|@dკBpLĘADs<{u) ?ˠ Ba+ѣ ֘E TOEIж?iE EhF:FJFWXEOE;ߋ +*H!ǎ9wDF3AyH <3x3I!!G:Hm+F? = @C2"2"H|þ@lFsH P>!2I2(XϚXD9"Fp.D ۜp I2'O)<ʉ.R̿E]yĸ_L@LƯ=ٿ<O@CAD"O44@TDވ"4DC4=#ó$(J ̬N|_` Joy:\MP CNEضb*}Kͨ0PdK(4+6,ռ'L6 蚽EkͧFj;#;͜QS RDS4$2}=:OȺcesPɐ; B<=/ɋ5;tAvj$ jD%IMƇCkj leTV\:ԺUQ%OS:7\UN;24(?ȜնhO?UJӺɭHh,MhDł=C;nl9Diޜ^cij><[YS3\gߞ_EfċGu$C>Xq5oFRonP!0S{oUSk)s38sEh3}k,PX-I n w U'fql0^[>\J93vP acU'EJtWB/wmuT{(on*gd n.N7KCQOs-sjcnlgm4934 xe}B7 \a/\cVHK*Cp.>>dD fK6uQ'!sQf>Xgo־ NK~cu H'pEum>SCk:JС)vxBGhoFpuqwF57_uVČfV*?zVRï]B3aOy]^_GxRxΦnto"͂'xЋygVEwЄgЍyw?|?oCO]ʹ2r;"f_& ytlf?|dtp0R,6_slwx{ߔλr ޡkE 82zkFE[{gP/s G|ItI<ƯdhMH(ʯ}q,|oݳ&'y5\Cʽe^K:?o}t؂8nL*7w`7hJgRKi҄ „ .,'N4xPpG2e$iGV\dh̗yfi3Ly iЅ?}H")T#~ ֬ZrbiH&\rνs[n%R;Ae B}h0ن D1䃋 kF<(˗SjMI!V \6nb^7WVYaiCIkA{.tnDo"|U8r[<c6h{GTA_$g~"MFr ;%m&lU)RdgimJ+Nuמ]גeV"ۣ?"wWz:ٯiKWVtIwحv -2?]OyhQUJzSٷVEMv9j V@5#6q- $ƒM@i ߍ[YZ6''B''0NrfB4XAHקoqpbyx Z\[Z؟#FDEemoP$| +)wyEw/G7Cg3O:Էp8@>7zl:K6#TSS] w퉸v'w2Cny!sRhc8|k-6kUV0H,y$:ƌseSFx:/Ze%KfuO6oōivgrʼnu[1r^WDdCXjnݸe8͢δւqA|ӷbjo@|jmYYQsK;D۞\#WZk5}O/9 m$nq^y6TF+)Cڲ2imILcMՓZx>+嵠7 D#z_Ow҆y5zoS׫^B^Q{YkIA~y |yR]͒=O(_RA7ןl[gC \ݑu_^ʜ깜kO t9ZW^~[jM 29X"5J}t .XA`I] ս = ^ y-`1a] ![ݣ:kRPF Č a|8% !_ :Ryۼ*fn|LrYuf avy!&b"P#>"AoRS)\&r"/n(NzS)b@ q7!ͱ`^%Jb6"'n"x!Q(WbAd 3Bc-F,%Vb=r]c>F&Y70&B&q:@;2T<+V##=F_EFV^c)^Ar(bAdIA T4d"^6]!Е?>QqHf9DJ$C>K>L>%TA0DPc##%]$%J2%#aTZVXU_[de?#Wv1e2e!eJeK `י%H^m%G]J2ΏYRK6 fj,&]!`cfccd~XR ~Dmu dm\qX6d&Y&fF`lDf#>eb!p&V gňef_^m>$bc%Ju"Req*W8]GޑRxV=ZYg2!pH7{OXR}^Z~a(.eߓQ[d$Š(Lub3)[yaChhKz46Hm ]_&sK&hˌ%j!KY∎rC)hr}'x (3]y%EhY)Xh>=X:BSi U~qlE)AeʱnR%[*i_Ep`B l)&*QHPYzql*rz*e*F^aߦ*fp*Q5φCɭRq$-܄ +L^-+ΥM_\+ꯜE#Vk\PbkPs'fjbEzRɽ愥뵞I> ڢ6.*~]!jbzlp,VHXdkIld['Sb`:і 6-YEe*ajÅ[R<U -Һ}*꿪r yi0T8u yCK1+mF'FrV_xy Vd3$FJ.N$-nJiҦÊ^6"ml6o*=n"#,ƈmv~-,m:jȺQQ*D bӺnF/znbzr|n-)m.!0s-uUmUa/-ikN>$W.8*f Tl^Ӧ-:-pñp smO g!cZg+~XNoo1֮ -iV [߉..B)g1 l/ S.=.ln"S, jn 0 Q#L*r'Df2*OƄ K 70)l+ߞjy*f#kg$nr :Sղ2 2!bQe(#Ff%jbnVAQi.wr/޵k |(:Sspq0{(i!3`q,o@H9pAS3t}" WeU4I'R '.t.r2@ UHKޖ*5fpblaqݶDQIY Dg3M;M_e9ð @O5C+)z:qFoMgu[pЩpǷȌUG^cƂ/vkp+a׶msE]XAtdw4>:vKVp66w~$hSdU[mmǶ`lm߶`L(pXÎ7Q[:I3SIu Vt1wlx#[Vwz)rw8?ߪqeg$~~7&̭dh B@2{$ ߶#n3ZY[GF/p$TSDG4{,vy{ Es¡H31w1f9V8Yh5ӨȒGG}RkŌSk5zosףPyc'jR/cZlLT(sAݲy?:4/@3: _{0Mo8vJ''$&0- {szҠqs˵s7p*t]C`RpKtG~LzjNSer3{7aw;M4'o+5/*t;k_FŵfP n ,3YpC+#D7z_7yG"c':7|#*XR5;PB'^Ksdm|+)Z0-ɧ:S;T_0-uw:/<ջok1%rK Yvy{o-շa-G~ʒ ;ǿ}jߤ1ec4ׁLe)ӪB(l6n}*hW #xc~R>٣s>kczr%~'d@Q=_ĥOX4xaU b0U)K tS)S☐Hɓ%ELfL0aģ;<IҦM2ڊeRK *TSRTPflاȖT-ژU%R5{cKsYcވmCQ-!Jg$Ida0k̹SgϟA,zkR~My ؒQ-ٔӨ5vIkbX&L"EW.bx*`^O,u;'P-wү]oV:|mɻfڷsc,T6#;,hl‰$N{CB* o-ܓm6jd[ ?.H9V)YHTz!FʬC:ܐC S%>0FAqRͶ'Rb@Q0qsXLC|:J.P,΂F/RJ P1f" 2kSPkr\5٤"i,lR<\uRHK)U3CQ"MjҦ\iKIK= mI1HT?Y%@UqhJPC%L]ճZ_ J3&C6e93`"ZP0;)4qRҝʘyqe%zWa=Q14]5x=vAWሻHH. mDǔPE>dyVC~e} _FҥCVF iS9x Io͚\ JR>峩lJ d$1#n6D*4{%(?4XnZ_A#l'򭔭89s9CW,/Fot)-&4li";(?.ww]kMJ=J|SL\z aZy; ؈u)_d]}z6oxیVșdm@Z'/!)a9IazWSD G`>F_JA?)B""A(ɅO)sa2 3Ar2,rC xL x' 1FhX|%hӰ4}!FE++%Fyis*HQ&cA"4T-eHydhSA\" 9-|$$bڇI"q{$}I,<)#22”0b"b>]9spbYY3/$Lr0g? b(%0~dPi[bdQ*9C8>jD9Jre@1|/*Pt$-9E6)41Mhr(w|!t&ugJ'C8/)]pzU)X91Kņ=j5TZ[detDXi4s=d_. f:UF 2>{('3ٝk+HtrSV%@\j=vՖ )U.@qT W.-mj eݞW"vtoR n [6Tr=& 6ٽ7xK_bWvQbO铇tCEF)“e{熧jNBE_/&gTF7|b2׼0" T-aUntx[&G3b*#苍UR7*yg \yJV+U)H4 *H J B'z*)T4S%Yֶ{vefpL,W3"Xk;3T2v@KLEB_"ʆMw.0;aI딖Nd,iZ$-I'9zn|XDu0|}[GL%֍:E,bDOzrt 'VڪE-Lb9zmfoJE,| " d}{k4@ay^;V V /to{dǘ/ׂ̮H!&pLm b$$8 bd֎$20oPg&*#hJjiڥ~e'cd mk 1%"htOzP y( -0HPֈ Y04a jnmJu².fhn hjPCHPBF/F O~O"z&v&K1l ĉ/k'e[weȁZML իdYSŐov&Eaj +1q%4 cṐ,ujBTj1gb嬑Qhv!)*b6ȱ}nWm:5WoGǘf# /ߩ0ō Q!QRT2# vMOˮ##!$5A$QKNmRx%-/& rc7 a 2}s.E"'wlApA)ٱ)$ F1PGúr+f,.|@ҿ4)r)wb/Ma$ qKT05n݋1_N"bhC֒4^@0/3Gҕ| ɋ r6k0@r5?h뎩!A%"7 s2.W 8O834Ee&&O5=`40AOa 5 @ 8h`*r<Ȋe@ 8?=29H' ?kX%@ ftFEs>gi➫T%>}v<ӧ*Hߓ^DC;S'>% Fݠ1W܂3,MNA?K*KF4@t K:ȪHRI/4 CsETذ'?ehRJ>0 TFtS3LsTZipPUeA)SBHoO &P94$ M5#FQ}+Ϗ4% L(htTS5S9T{@܈z3)USUOU 4TuJeKo5Wy }FTfR-”_2H5UN UWO Z[[eS+\KN)^@cX=vdKEu-r`aHaaa?1A5(cUc^dd_ [\|Nc[gq>klVq9VZgWT\c 4iˍi$6eۢdF*j'k Pk'xڱ=Sl:ϋVX R[Hn5n_Ɏ RZJfkrpe5lsdZZ:$7d)/wn37R 1l HTtGwk)x}&r^-\y,c7ĦKaEN/L/L{[7s)+ڌj}poVIGUuU,5{KzH,aIwwTaI}O}5[VG/bBWYթ6vw e$l{ꜘipqLa1.MXc>o#/NVC%TӔ]`XcX~ZGy4WE7S ]o50GX6yg\m1wMnG}W) xXk"8P* =Y ZP>Xq32y*טՕu|x_ v}O@J$TژYfL*x֚:m}mZeCZ7<uBZzZM$akӘ[\P*;= ؕ8uoUHjC7Wh!a [K\s̪_۱Z36vumٛM=;i9u`a6{Wٝ'F!ѵ50#bv}ۖ;su WbِG6_d^qW0 qA#_9+\Ǜvpδ5VT{^NY7fMa)i6yw<Mv]9Qa{'|q%sXV]At"DvtXqy×K[7jK\n0g\'$JciH֤ S||>Sq Zw[iYӜT @E#V˗]Y4XYϚM?Յ_aq'S}{C()Td ! J4 sO|7iTOYwڿjѷ:Uܙޡ:&@ ~@6`'J "ak(ϯv%1;aCj>HەѴ J`!$>@, e;"l݋S0YϟЏ5鱎ٴr~./}uH X2 |    #7+/_?C_1K%_K__ag?_os{y u{_ A! ,^ A *\ȰC#J(";0 cx>(:T>{t2͛83ϔ~]yѣFsʴҥ4["5իViu_"u+$H[:zg 8p߼iCݺsXE] +Qd9rc&Q 2k\qgCHƍ?hC'B2m#)ﲁ> IȍΕWXOI~ u^%N<0[|Ë;7ؑ$>F"D&`D9̿Vjzځ@uTm)F[odǜZ` v݈$Bz`1!߽ŗ^套aΡ6*ro96@ݗP~@(hH`k :RTJSINS&w yUW%"\3Xԛv"㎾#!h !陒hZF9 d*ieNz PaZޚIGj'©s_{Z<6'N$*AJIeJ,Qy6%jnfmnr ׫"8_ - ofZg7ѰZd>ZeZj)!%؛F,q~;z#v?B$a8C -9fv \2S@KO( UV^;W+ީzkXnM XYj<,d/q* DjV8#jyK 4MRA]u`AYkmZouY^G K0Df6,m&t mtб߼;L^}06롹FSkܾ{|"'M!`8L 79T#C=to>&07y}c6@EowxD롌\I "W i#~02h·ѝOt3ԆЅ߮f X؟pG<ɣybG mp+ MhH"" *UAt{+NAuEhfN(Ea?=]wZzP!wBrT rg9bs p"XLL QĢ)QفG r\C7L^I+Z1".31Bq,I@# Pnf'W,:Gz3s{'=9yL43uAsa& @DGbwlE噍r18'ՒzդcKǚ;.MaϛtpkL֗x 7h0'JPjn~څz5b%f7;Vrg6-ǹLiOƄ>)OשTt"@"k`@@E/8vQ{(KْWQJGzw,/j{ajk^Z4Iy[Զ "z! ~o{J&ɕ"o_ɤ Sv,,ҸQd/O|2$#$MURލV _7di8!IXct&7X8`M g 咪 +k;ָ.lҪGͶIyגx6`b‡ X@磀ҍPq: 8x-S` l~&hٔ,N{PLY,l h3USm#O(qMnlJg{A]=ju;\^w{}ڬ)ç?{ H&CX{LD`!yGy<9)aŸ&6=Id2Oٜ=9ֹx.ɼ{S׫{;CIiQv };#1<#hBWMs{h_޶{{mwW6RI x2w,ii#||5js鷁sk;xGk(saXtǁ,xaidvbS:n޶ddswJ!$C: h2s|nnv3ׂinX}uxT|'sV8daGxeHY&K ?`+MF`90P{&w*;me$GbIÄc(mnF38|WXWj`8sOi(Oi؊w8/W|F2sB@`FX :!G2HЌH`70+ȸ{d8=H:F:H :X8z7oʗ긆$iH؏Ȋw8fMxis2؋3`M`(z;Ѝ)w? hXnxMQ(H6(:8B\E]bȂ/n؀FhC DDyBJLJR6VyBZɊHG2-+)Mv%PFdӌ?P!)/.%w$Y mx2Lj(09T@Bs4YX5]89$A)SvKM9<@ gIXY隯9YBw/ dfy$dX,H2v)7{~+'yw 88̉.Yd3#Si79x\wvי9LI9/AʚJI[y9ٛ_)#2؝?uHbr~y)eӝ86e(1W iKН9 ɟ ʟFJ4@%8KyXz%v1ڡ%z6 3‘q)Aw12J3:60ݹ=z,؀AـvYL)S:JZK2zZ D9IY@Ȉb͉nGqj(ptv:rاɝvʨɜ,96qy / HpJ?FI)暮jJJJkJz j-C>Z a 0myڜʹ$uig9{ڑjʨ?[ uz֚yH^̙Ip H( oAӈXi2Y[JT`Ǩ28Qh%H[ڱp۱ > (;*5۲ 7觙h8k0 ۸HѨA7`F9:jAJ$P`i 9LЌiz.%0pm`iԙoj۱?w:{:6 F$ ;~0}|ڽ۽0ڸ仸ͨi:;9@BuyKۮVk?`lH،.zI9{'kIkz;ɛʼꊽ}ڌ0Y, Ψh 6ʘ34Êk9Ka >KNB[*?,:/hzX{[\nnJ9;!@ HЧS0>_)ÅJ`Ą̃)̬/?쌃<嫭KڸMLxz}w>І[̶±X8vis( ̵۾[!ڝ6|lT@S⌣,:ܽ<șܝ8”;@,2 P˸>ϻ& =uh$EKpg{mg`ZmJռ+ʬlߌ|l΋LBLH͘ Z@C\ԁ+mp7Чl Ì?J20Pv!Ͷ[\ R9lҒu)ɏʢˬhu,w<=>1ؐTl᫷0jC C-M}Dٜ Պ|*}Wm *_r% ~+|B nklMFkT`y m|ݝ͸3ܽ/NB<-n|y@<Π݁ق}޸DnJ6 ދ1=YQÍi{!kֽ!y姻f>fƼm[ld>~a0hV7`%Jxr.ҍ+)|nd@c6NqS@ɞڮ=JԁNL0IMx)ó[nCzN볎?Ӻ= |>&ޮ92:>ypa:*QJEժUJrW%H%[ٱIԮe[U*V^eԯ_&KdRak,Pٲ @`ϛ7w&]AԩQd: m7`ݛ7&([g8.Pla#;t*hQI3eJ;y8;N8 ׽$J}|wJe?804l1ƒ nx*L`42,(5:CJqD?0DP®hQkQ6 %9f)t $ 4j(2;Gx.*.z/{rˮK b?6bK$J ?ſϧ '$ , eHQ   ڀ^x`+"$m#`VzKrx &4@f#C%?fYD]MJ:bYxw$gv%j{~gkʰ5hpbc" eR2 _}h^ {lS˩&YsJ7)}{i[IyML1[$)7ze(He'axxC PC2Ow+g@|s G,Bܤ#M 0&x4moVV¬&8шF$B6q+AS8VM,b@#BL%)f \b^C;0PԜtw;2k\ [E'rI,]N[63͡fӮhB·'"o[ô"7Nw5>oIb/dM[Ũ?a=\kuD:rNҰ.q$u{ߟ Ghe3^??CE- \mF|N>_ܩW}k\= R8 |= n}I&@ W뻋[c0Jӥg#I$+SҹAӿ*޲nK#=;[BE(@E|[SAދ:-7Bkk.{1S+C3.`K)k>9HJsA'DT?V` ܭ!3N(B#T: s%4%RXAHCKWH,t0\1R$}KEc1Mz:{+Q"jC4 lA;T[HlFkKhLVl9ۭYÃ; D0D`D?cf:IhxB|+{E0DP'O-ʁ`;P[ #;65kf"-O.iDvI(%1,]`;Y|CLRQFSŤ4nlp.9n@..2 @qǾnqG#54is=X*+ڼ 1 ^9EtU楶#~kAT+N4?R-rN8K ?s /iKZYžnVs9;`2c=s'Nӱ6q5₸R$fmsM|&f~Nm/0NwPu2?dFTv57qmsWB$bY4rk(EHGH3!6QzdDgab%?ũۧ>pn`pqx/r squ_1cwB{wt:V 5V pH%AL=>{7s3\KKx~P]ɯpSlm6Kow9'NKt2w}"0]$A%?KRD@f_8@%J$L jj!ĈNhI66 Ȑ =~Yd)"?l %L,IьYj!Μ:w) %`̣\1m*ԨRBI*ͬZr͊)$Iƒ-zRxpr.[Рy ]oڹqѓ'Ϡ n(y*̚1]YCGrySÖY:4m%KPjœ6?-z4i)‡gZթ]o=5Se˺Nzml!U>)R6~K%}2&mEL-ɖC`C:&ToIyHqJ%s%r)6X ).DJ&ZXv 8"wu#`1Nͷ^}ndFK!&iY&Xbz)!Zm܇m6r#BUH '6S-ҤMXo5Wb9!5E@ƗB_\MbdM: FRr$DN qfO$j:&&fo٦i윤k(,m'yuVZ^nX;} -$}.cIF_{efFjh偬AƗ}B?o+J5l*6IS|rڲ.'%HH '#D@ۥ~jKj6jin2KTCԚ+0(=հ"V,.t폎FHXd-ڽwP,6*GbB$5x3@Fw:T۷IkNQ4#ZMMy]0^w)ʏ&&jmۀ@ 됀CP-QtJfD&0DY#tT[[#d_`t75=!D [(Y|f(~>e  !HDPlӨLrYS"cq#>H r8`(1t$c)|/%keH5m#0UXɘ{8?qA  殆2;d-QbF>r K]&t%&L%)",Fyx(.J0ȸ=<4v 5EGM PeܳGQ=c4eUY!̦GOxF+ X,J,b{(4e()#O/1a KOj2DCTS *Gs"}(c<.>V dF7J4e! H7Zyȶ'(b9QZEԪ*%"Č'Q*ـd%#QgXj\\">YeYQƌq[YR#|gKYAJ>y'b{R5F4%}n.S=fItB`").k󔒚Teb\s WfH$9IM!\Ȍ* /7CaS q~4s:t(j]g!Zf& igFcn1hڍqxgGMZǛ^qc#|`j6٫ ymVw23pk;>X|;fRhEUi>S*/|l Ih $ ~ܓˏԝ'D!GUb3\IP}^4ȏ7 M~nD?KᲦ&ɈΏTxyW\Yȝi{$yT)_t6 2]g˱"S55UA9$YU5] D9 tKPWexKD b 0 Va ߱*NJ`߷}NPDZиiM\6a fNt9aY!af& NI^Pe gASM` ]`P$b$hBtMniI^(b&6&1FEb[=| P)bq]"6P-!Q"/" =V0 [ 1:ݫEF3nfM}ν6~p % FEZibc.b"H@j&lngT"bR"ɚ'R`b @PE|4h3AgehFuf/JgwmaC\TXK^+Ng5S ]e"fr窹wDma9S iȽ~ v^Jtʨ%1r>e2H-(D,Mh3#mxs=^%^X'%!P6)%k(i(R#vjFs s FՁ +BSib iyYV̥I #haiƚۡF1j>*\HzgKT6zcoN۠"xUY:'RB(#)j -1h)蕼߬Ҫ *ڪ`x6(&~Kifv#~EiY5 *ڕf"h~NK `VbB(UPbɒ4b&B+Ei~fj% Dl:,lfa ha!y2+I)q+`FQĹJ֬r:&^:H# *^g֏NR^Rk2n_؊+- m+1Ȯ*!T )RNmb? tn6>F'2j5f^-m-ş?im~ߊnsꀢnśL4hPybR-$骠To3+bo%>٦a.:Bo5*2*VnL].o /,.nZ%\J>k 0oɵ򲆵.$n ^c\ &+\m[Ko (g)-u~'Vn0̒ *p2pc/;qS~w?o #o*q,qJQ ."2 q 1Np +q(Vqz!l hZY L/ *#qc2i0, .''S1 f-g *q++7$hȲ&.kŪ2?fuVm01%6122_֩C32DK%66<s&3k?3B35:GB;2q6s4S$!@p  t$gm7;Hfς9('[4:C0 JCՁrBt-f L{4-YNNmEI۩r0Q Lrz`SS{KuDU$3V y5$'5%RыTuYoYO3[]tQ?Am[ \*5\vbt3va'g/)'lbg26kDcW ]7ovsN$fWfo6!8"vIθ6vCCuBmmK0#+g-o'Z3>أy?jHdY$0w.V@v2%k %vrRc5lw`y/8yu-ĭ$N;˷ҷRK~*.1xyk03Kjw28z;xHj焅g)^ 7uet;*48z[B3>9wOtRnx-9?xi8K G LK~vt3l[l8ӯxz/x+?xPy987B t'7=hC0Xv 9mBH,7n*H/w[̾z:DƐlLO`w}9_x?y4ixyo#ܳ;y{m&;{wz$N%]'󷃣:G;2r9zQpķxy10am:Hت{;vW |;W7z3v;7*z';W<+g<:{, ( w-;=r«j>|lWTξPz-}-G.{j+ҧD|"7nrG8軿3AgDG0/vhv2=L8@g($K;B7ejj>7O;i[ϯS<{YC@6_>%CnYs! 1JnO~?i8}?ɔ?Mk JN /0מ:6T:k =yD(>i2xO6t))VB1f$ȱb)#?4yeJ+YtR"+3i֔YJ"" Z)qgȝCj6 J$I*v4kVfEcP?R!G\KHe\qU˶H{e"βBE6ZaH6u U*ՉkzlɄ`)C\ɴ7}[կv;+P*nU۸+f1QNnW7ghm]S^~zhjUر=Ɋb홷eݷG6yr+3:/&BMЬ>jh<,"nRAv6Mαȹ 2t*2E;.Y,B06x,$zrOHFʍ= i( [ M;Vlh/Ffd$ m|B3+i(0T&08ᢔ,LJP<-]|K7 9#"k4+M"?T8D0 'A|8>A*@܌4Ce'0574'&טXKddX.#š:ē=sVʮ+\ťִU׻.jV:Bvc]^~W2*^>sw= T E+F+hdk6.&ݚڵ-M$DL>!9YBm|AY1'Q3ng%Xe<Ӹe6XZiIJ>M&@i65OU]uͦgLŵ\eL淓v{TSڐ e>+orPu+7]$]xɏ E]<u@YbR l^xM0}qq/uY%-%prO /Y-ZN$p1Tdd+ǀ-c%$2W&*m}M\9T])nWbҎ]:^`I ! /DcihrUq~QLlΜ?p=,bC,1I9gt m !K_[D~u%Ҩ9/1~b>5yb<GT28_g9B k\L @Lͬb#U~VS|_YcJ"dLcf *[-c4ӊ7 G:DRKP(vY[#U3˜f+@4_7@ N8Gj1$$J@ %{{xn Y_~&1l4Nml8u6DX+EUj%!Ehˏt7Gr2ccsawސ~[-Mzk>IbW.G*GjkX$#cb'hK;UmA~bWJ.ppc'4OEh37!%ߜ\"3j$D ն'[kk7X=E?}&Qwo\.kƻZ]^^q[>R;7>U'o XuuAWJz[MMĥ%^C''6 J/+G;ʫx(VIϩ67R,\i2cA^cq#_d>~(!O$2%<5C(D8ޮMq>sb6B֖k mQC76CT=@T7kbD; >!q#XA#'T$t6۴(0z~SLoH;HIeI ԜEtNc Fk4((P }(RL2?-0cTDs24EiN4OGMHQ/+4zNUp 36HaVR(:M0M5RHrI4OR"iAa-ĠJU[u7zZ55%(ralV0nR3[n5pPX{RXSuEY 1|!d[/LQHz\[(0[aõ`#)`UKZ(ەy ^43sSN^A^Es4rp Ôb5rawP\uQurbo'W" Ie0IYcAiHNM2PeE%| PWfCPge"bbU`FՒv1nc*w3dwbdKTKe5=r S!g3l#W/PWmhMr]S UqGfr-V&C2cU?Vd6YM1Ik Urwm)Wx+6Wv:t֪n?-7Mv^+'wwa;qPfY`Ax?7sV}+Q;uS%Blj FjA{!WsB\i{m)ҵmp~ a7_2E4] z3GCWWXWU8 r=Whחm'yȂ)_jt~ oiDckҍY|Չ6Іo"nsioPcC~WE Z؉2-xl;a͵WTw MؐЌ+MƇ#x]QҎSw,[%C/yuL6UX֓S>t.Q痒8.t!1[2xS}1<$!Cd eB Nmܖ7==}o[CMɺ%">T~ 3^3 >C㛀KwS^WSI~c^gko[>m>?>{^q^  ꩀ 1> > b ԝ)! ,^ %`X&"J(Ņ \dc C/jRT˗0cʜI:8)䨧EZٳG档H2bT$F>{&MȏUHJi+$I`ÂmZ RFb:Z(۰Q]_utv޿,da@dv4@$(HĐ7j豳!c&DSɺ8Mv=훯aj&ۨS:"K_:ysf2D/SĩkLS/N,#ݱ_Ͼ=[U?]K˓pSUyA=dY`f1$ }ЄvBIPxgDLjh(X,8Ns S!}XP@%cR/裏:]U 3 t^TMGYv\?\w\>8UM1Y9R"`A NЃFh~$VJ矀*(*-YG=u#^ӏ}!#Q~@6vK) ֒j:TU1zW~ 1֓$}5Q $el&&ud MX14RI'Ɇu۵!mk)!힊RǣAEܤ?Zzl)HՒFjjyꕁSQiw\4c 81+;Dl KDr`8au:,(ںl(&fQՒ(fj 3D)Eo_5eO7U Upl۲JYQhiKnad`1e|Ѳ}l"&a1lᆖ͸(S v*3KؒST|Ycs&~ Q("6pAS=U_uSz[7 mhn-Wbܫf#keJs&/yIWcm;ąhmheRǞȝh.Am{U`M WH!$I4VdZzbVa-=[P֗- X ;[LnH"0FA jx 2 *bU/mf%4aOXר5) ԠF%C|a^aL %~\E$"!,xmfH*.y@x9o (Ǹ:֑zW(C+0>9lRE13^<$y`fM,X "if:dg<4[F-0L(Urr Ԛ9̗Rw76<99k$i&’䜦͆3 I -akht8OdpR!/#%?4]>{3)p)RԇÀ)P 3(B 6 (h$QaD8BR:2&4C7^֨l N~Ѳ*spxpkWkπ&2?YPt -QJX)!&(EcԨz!elGQčb7tChqs"C 'ZrG̘zWFә nB1:]xlbkZVVE5)1KFʳ; \Ӯ+=mJì^5sŝ]%@{6:T6SeAA9Iu9B9%}&8b_,v'rS`[S5]XWۼk| R56d"pFRr5bZ o+B. L=L.[cJP0R>ֲR9m!⮕6X6')ӷ6Yh\2g!Rɀ)c19⒟^ʳ4ʈp_&!ٴiF/3)LsC*m}7qsGH摉t PH aȻF^3,=Foy [ txG>iPOmjG氟 l -ӭ0F?n3`۳NN`~v5~a?R;a #[ ޗuqZ 0T{wr⒜z[l}O؊6"؋\xoTlkF4g- 'ݓslXDGC|;wLATol+Idmp!+BψqBu-PW͵R㣁9z`"b\"F4WuLlK\Ġ(v`iۙ3[(H܂a`Z|ٲS`{1!<+ {s7/Bc8مxzF~ 'KJL[ * *z}wW\ɖZ<[ @bSʺ!Ʊ~1!S&PCPĉǺ˻8< O|̇L|Y<͠Eڼ*Lg ^=1gu!.">HЗh'^XRȽ-<4^ˆn<;4>钎JaNKNP^=;\ `!~G$p;,`;l7 JB1~NNJ(}̶@}ǩˋ~耭㣾=4@n6}ޛ= ޞ7.^>ٚp %p~KoMξӧ- BK-]n~0.]cnkXk:lB/._~11srZ /`$=Lߥ&?|HȝHb؞covxH>~+NK~7Ϲ: CF[ߥ r UOVj!;@`n}v?I.oĝ/⼜vF0y?~?BW$|;;lϒ.神O~$ N{730_F D  ,LA XѢ80G*Y C;>K?n̼ 9u?i% èQ!I$RQQgzĂnݚałVi6`۶(~K", yK;V_ P?`cGanK4ʕke.V۶sXׄN}=B%X\i;죳F>.JQLm:Nkh=t002<3A#;/=Ha g8Zι:> /`JPa$1J;O.JOa/e F,^ wi'0`8Q2 ¹6e$xC. @t1v BLMTS@0iCMm 9vEd$"OԸeEnts#xǎyc G>Ps3V3tJlI5 Se_[,f &bif=;ES$ҀpXz: -DwFV٪j:꠰-ЊInk]ۅ.RMna+F(4~%݃J:WjO]J&SAx7WN{c(a¹DY}"o,`n0Z\[Z`R#W:"i:f$T-AR": !9,ԀL[-D p[)Kh2͵(Hb@,si~ٗ狻>3kAϛ2A%@!! :Y$KBcR&TBGB,DG0EB0,1C(C3K0&T$@>`])ȍC$Iɜɐn|C䋄GN\I\NLOȽÓ|ΓNLl84LNO)-/R M#%9P0>C4F4MScd\*T EIP̒IH-Q_}NmQCQE%OIQ RQAIL6ɋ|лs,US=/C,R.%Պ1bL6}6ƉTF9L5LIL9=LgS@SI 8QMTl$CSG]Tp WTlQ! Нt\O&W{UTvlդW՞.%Z%TL{lLV4Tfu´NS-@ISHYMX(R,nuTdؙWQUTMRYQo\lF0TS5MuZ{O*u&#؂5XYU0S?KXn\2(R@@FXOIB}VѕYEٕRYL=ɜ]TõўYZ}-ZMLDbPQLګ5X5] 3U\M]Ȥp@=ŻR۵%VULh%\[S(ORݼO[[m-\ϓoJMWt H]Wԍ-ZLFS-_U?X=X=X-EPc0N Y]Ol%^S0eXh5^M֏ۙPu^v`M\LTvY^IlfD>>W2ރ(=( Z ]1TBNÄ=']lU^N`]L$KOc5V 6ބ>j c<c|YJxBp#FS5TF>0:j 4pAXg=[e[X`M46QMa-G/LRV/,lCfmQh=aqI[Kj@@2,>`douCC0>7^pW`mLpfTM$^6``H8?PWn<@b6B @FR<=%p~b.iDRq%aa g ]}oVZ&ٓML8ξÎE@xi)Hbk7>\#`f#f%Q16^S``KaF`qGq厃.DhOkvTI7=(nqCn>Hh^pn8L`&Ou VQrW\-^t.)x^TL`S7\ՙa=8^:6QHYt^?8v;w;HqDGS;buqVa4RȽ4T`LwLl]=>\sUiRoTWuNZWoxWo<6r@ Ѐ\VޚN4;SbFgh`6kaiDD qW8<ry; G;/fsG@umvqMq}J7RF4q܎q}g6w$V7][dv7t5^?{]GȃG@o_jxYo{jGdy"Hʓ;n Md)S#޼zbKC*,jTĈرN1W\lJQpbN\cƐ!:2rIpQJ&=jh[$ۋ-qJcI"(M~TkWR2M(i.m)SRE+W[I5Z{qtG]$u~|x'$X#pb'DcQEN$q"I.UidaFifH"$Xʡ4pHIsqEG=7BQJP(YNd_v$w4&Yx^zij^RH!}a٩$B&IW+I~'EW)3S(`HzaWhQAŧ'xH\]irBFINBpMdEge%m&9d[n=WB D#OIy~yWffzn.u$NE5}We=IwKR- `!RVfQC `C ېWѪeW z.I xQE JX-BGk5?[ / -ݚ.GA%UeI-N>S 4mS(\"X"q-\5ؐ #B`JPw8 BG(ŝF@&s*ѯG nڪ&`g إ\SH{H=Uwri ]m͵N^7{I"aQ\QEQ`"@C )%?pwQ Z2+^$FUJd5 aH8L+{`:[JЃ!Pb%bGĠ}&{)b ۩ 6PA8 DhF4"rQ} wpA̯VW]5exbAI^s@I)RԸ+|#ǯPP?I3A(䝝֕(L)+㶀Tpit$^WoEd(UP1E b@4 |5p"%pf K\Zѥ:j(y@0YiZ &n823uk) NBkI=J"HDBlM2J &Aķ7 q@"9|O" d!Bƣl%"dv 1kJi= +*f<A1MmК{;,<&+C5¥p2&@s@B,41%pJ"XeeOJx էr*V*lu SA:1$R &%IDI,$]S_3R*8&Ԙ&gjd)4i_z-LjJRI'45"–ђiE5Ϭ,0c뮷bV f$(@۵( ^LFsIkia HΜ):kN2RY(Pj'V݇UjZ kiҫa5sQZŤ\eZ5(4ؼ4!L u be*H"Qi_HoWΤTJQ,-[<]*8SiX," h7s^w&&N(W # N/9&aDg2O ǤqJ*Ǘoe\v{e%/Zp伞(yva2%+"O݄KHV/oŒ,% ¶ 5 p-}P뀡Ӄp&apU!FJrGC<8S:yףI5L_Iqj:/_?ĉ-u"-XΡ!N;ElqSU+M`z#GGNӓ}jj6J.s>Γvt Rx9Y̴VU%AAZѡX84+h'| ;jlpw(G%8ԝR񢠮RntqnwWSB_h8ׅ%5_eHEC/TJrEIn1_-CWuёQ=VO^ԡnSWq,7q׿ET?]x\+B}7;~:{C6liMl[!^E^wۋR3Ƙ/)[!S㭉%z\]]]A UQh ^a\L}G)|u_ٙ u kL]ZD8ID>QMթ"\z8 ea Q<] ] IaP`GbYYr z}\^x ]p|H T~F_RE_߈92^9 U% i 5a`iN HFp ]ץ 0%t}J\NuE|>\8Yb11zPM8`R!H$Tirx a*"Р|1lEBӑ Ӽ^ ^/[ i"1Qc-^ /YY`4b ^iD 6rA-%|AԸZ|y0zh=Ei "&cePT1E2fM\a%RPnpM&CF VmPB%B%L%F7blPN2 P΍l!DSDi$[O"J]P@"'LO"O*NxT# JC*%D6WCLcVt8^HNE+!zӌ%D`Zeh4j4Fc%%4 %xnz4!#2I__JR`ZB.PPcR.e4 Q8BGbU~KHgw~$DoD D$&cV@2yPVnfo2""g1'c {$g&sG pERdjXF˜m'w"}i^ 0=J4Kc[誰J'`] n\6F#QD(a.gbBE u^(eYHEA|)v{ Zfip暜b"%$Li ' '`hF)GFAR4)'&8dA`@ Q n$widduۚ剂Y:m([KEʤԥIvG*"*25ZFB߄"GGݻptjB__, JjE')`1>jV̊=[|h^E_d#%LŁ u-k<:+PhDF_Y+x-V1UWfhùPBN)JJh˯,>bfi1%*+,,"mP+܅ -YHFV)액iӾ&PJ[**#*l,06 f LYl-^PFӸ2SNQ]@&.Lڭj.Ml#*u]cu4 `*]>6\zHQnnåN|]9a3 8ƙ1sb3}Q0h%2e/5^BcDvZ}rOsNR/: FMIB65%0FSL_0|M61C扎*os")D,ɣ]\άV$6JH7(U@U3^ yTBkNv|@#.,`-$tj]PF 6PaPtV/6!nkdKeߴʅKF/=$mbbN3!&Օ5v6mG&N2BڎnO|4uK؄QiKVpLXhm&rQms UuCk?nPDq7/.vxvmwM^ n6v#͘EN5yh֬Mcשڂ2q%[pɢ1j1fA4SrkTtUw",w8zo&d LӇD7BN@0؋nwwpOGs52Yeanx.9[w$l %nC| ;vy'z!0?sG69u V8G+*cO5}9O5єD7YQhQ sS~Y^'5$7zwO3&y;3z 0l\:ylRzܬj#Mmcmoӑö'µC[%;cZ 0Ϣq5)i?HnJK?vI;y <-;osoG zꂙI˸hWoGYJ^np TG`;rdMj79В59o'5 ƫ-j+vrw_r=5.;~W<0rG±k%e6F+a+ӻG?.D[}lH}7j)~!C{ðm&W/7#}Ӎ~Z-G{L9W?yă+4m2uR ?}bC5Mbƌ+N %1bP#I4yd‘X2&3ejyB5r8)RH61ti҇oF"VzG&ulX^5{Ekd Ե@ M" 2ϴtaag*YjD yQģ{5(VիrZYg3m{[Mqڽ`_^jxZGn"ScivNӹɅ3nQs3Ոj ~٢6p Sĩ0;)$Űʴk&졅LAPQA8D5BTT1 Sж q!z/@:h(˜."l| eh-*7k+>*5=`/FJl GxϷ KI4 I !dKkM$0QTL3B3FCk4FmhnHT4VSʇըO$5EK'*R "+z LouftDTS_-)k#R[%g&jMl4_BKMz5K/MVr5s_GKZ+P%)ȁ ^u@ȓK$J )9{-f%vXro,z|)i k-1yI2^)&Dm,X/1AM(D!Mve~UWe1zjgF$K65/.w,xiui^U8%e[Yk~tl0u7(4f!D.[su[kPk pss\)L<\Nu}ݳ>xwG=SHl q]Nm }"}}{b[y򝟨m"sMT} p$9H"y]~7 igejֈ`V-UЂ۪286oH Ov) ¾0 NwC ӗT 3 d"I4?98؊w1riŋ``BK4# EqoNJęPwB$򈽒&Tɋ4j#"զ"p1YQFV:n1@^շMns3d@CA6cFZfy$eyi4 wi^Rciba"9W&X9IDrd'ihi"!-|QbL_]қYSs1ͤdKUSkGW桯k Iش~zTW8IބR,Aa)ϝ0!*M]ΓhDH`zdBFOQ "Nu4[ #Tk՚JܠEIe}<5Ԗ iUZٚS )R:0gʭn;E֟pl˯ضRӨhd2ɥN=^a- * ?k )]'b[QU$!t3"H9 oLŬ:L DSTs`QEⰯ؊\Z=W۵.?Tl|n,үQLTX^]rrk[*v]Qs4JyBWf@\% s[_ iE̝G#gkn2zn`fV9g|#B VG$Sx})9h'q]KU&D˞$/ْO6q=8u%eTe{EP\LY1\2y{]R~-ZgY )f0sJ׹|҈|gL_KD(ՇU?+&A٥qr̬dIv֮& !j>2WSlw3bdGhT7$)v$$эNUJ{dX-Gl{vxUT8\c3]iV_0vCj ^ :'-/~5 7bM7qw :pYT+W.k /30=zδQ芔_f}+=H?wȽ!ס$DˮB>ޣs>{ŽdDugkni2:oD<:bxgSkQg709b4ͽZeo6S{%4 ?~/dY=r/i'ߟYmuieC.r=ֻߘ0- &k$MD#&E"$ moCoknݚDL fDk$pl|%7oYp$F0oΆkhj!J J/F\ϟccn%-gfeϝЬկx:R0In^ȸ#a"L3aq.vQw { dz81 odM`&p JaR)&i2 MR!C#r"űj^###!я2q-LZ+[O`^q q,!Q'l̂'l$(qO")3( t"D#-$~$+-B+`",ǒ,˒2 anR3or RpRq."r(+21M#II0)EB@n!^~`.1rLg=4E(24gp4I3oO*IދVpyF$ L+q+b7RHvC2$,,r9SS"'- !~`1: B& S#U.[*㌎k> B ɳM>EiLF]?o?l&r@#0#&c;#RQ"TB}%DzE'*b0thC3G0vu"3" 0 G)EFfT"e5; GFxTP2NHvC*sjЛT$EvhO <ʐOθpB4t%xt HNM܅Ԙp.)PQhZ8K5S0()&&#Hn" "X)7S*TTu"ZT 1N5WQ!U_5VYZnupR+,/$U]-ڴUN6³Y4uOFOB[yd?*y\wNPL3!XSR+]ݵsSq9s7mCHHdeU.'`gOHLab)vbRy c*Bc}A7Dd>SUYPBQa6QK:'5X 7fPag}@a-YuD6lx-Z פSy8 96 #(!S\A߄qkYG69FXJW]OZڙY!sڅڻ:ZpqյI!w:'.әM+ut!HgVw7Q yך:٢іp(A Z', '~j=ܾ:iF~^K'Q_ϾaW[7>A!=ʹ3IjE[>9&*?U'{A;XS{{=|]E>aGJb0!X c21D*ZbO?eH*ȑ#A< $T,[tY̓*EZ$#R/1X3PR3MvXQ!Ŝ:} 5jVT[I0֭\zպ#$"= @ AP>7RGW˽-k);I8bFZl0lFW+[FX*fz aDdўem)-J876 /=o* T&O9m{d-zR*ʙS]:`@+B0+WV~',M8S,)\?E>_mvNFhI$Hb>4^?I) H`^RHH#%b:ҖKDd{`BYWQH%h\ye,!G^zIPV0`rəIX_M,'&$t}J(Z$eUVUe>E Qh50#&n!'(oJ΄b&`C:L:(ogM9thE  ?$NDKm"NnmM| ?KnK.֮n o/ދoo:EN Y9mO@@! ,^Ȓ%F1`hĊ-b±1$ RPq˗& R2$FKijϟ@dDԑQ*=TӧPIJM=XuN`;6P>sC9p ݻx붯_8KeCÈ S]\1ǁI&JC=,  X# V(CKF}DvG-^aҡD$hAcqƉÈgKINOzËgeS&*>?u^O(eFI⠃i0@B/SFSF ADm [E(opm44@܍-QN>uu(>WyrIş Rf_>9 \S\"Y!hY$y|6!i`0 E QH.,'܋rPcDFW='Ԥ4yg% ƧौTR%X9 _VFhk qDgEEA-h, ҳ*ԚN}XT&=q+t*ne-ަ]**7fq%@F:5!K1@P1P(ǒ%ŤR;'s˜ZjQ;/,nYpHvg GHܱ,-] {m6T%d̯@ @ tE[wLIM3 / B Q]n'gl+geg%FM d޹-L-8E]`\p:[:;}~U(Dhxb\LRN-)gY<%PE*KcDu[7Ui*#H'<0EEÝ~c>υv"6dEQ$((A6πWHIHRRddS҂A4Jz`#A7ʈjl=52b<9kl!Qn<#3e"ȊdP6D?,YZNr:ɹ,1 ;"9W)lgV A R߱+E.tMhBIA DJQo LB5vv4)>>Sn )IulfS u@f3 4ͩNӞ:Mo'8b=k #NኪjibE-9K4yWZůVtd-%rs|k\̍n)IYtRT~ivAC)b:v]MΤuLM+T2tE,X(PڔA򢬍l =v,jMfeuH 9ru&XG0䕯tsMF(DV X)ckS>6u,M0>}{6[LJB03NUf\A>oګӲjloiV2U/p; \Ӹt]L%itW,b796@.V6PiX3jҔ,'[ ?)*8L# Ƥe_'YlB1`p*gCZLо,^ՇG ^豍d政AZL44Lij}sStK=i:Ȅh]g26aW?27ی_bΚRy͈W{UJM""r'R1L%҆@MLηN}:9e9 inkھ}9\2g~D&JP<-fPH&d$'YmэGdf-Q;/2cRrh. n] G:z,`A3Ee^iKzCf+kbot"ԫ亭NjOPH=w3dNs}KyJu"X}w!Xq ~K7ezV1>zaWvlbbo'JqMyd-00XD\1^G؅]xrx He3Ta"观–p5h8E3Muׇa}\`7QFWrLbg'-0# |jS~7T腠7XaFOEv[kDS[advǃ5@H!p7{obt!08?GpX:v~w^|x9TtTHe>yoW7sG_q!1gG& !2,bҥ I+&X4rhX`ĉHTk`Sk@ch0N&=vgu&g(gs!JpчW9! y7uҐ&u PRYh;h I#Ghpyrw⨅<&ׁ:S/wkIzotVjxN=Y÷YXq!p0YHyw@LMU) h=CI'_9PiNvxo&nÇc?VfyƗo EU4ĥ[xIS؍#X#m;GiBI9K,#є)79b4?-phaPn暅|yxMo$&ȂTqFNfhIyFIqhy9ISMHvoН,OٌF M#g(bMcBp?uypzSF[N FIWyTzVJzaHpa2k#>RQԡe i"$g{(*vS4!CafTqxEBMenUQ eOKNd%aS*hL~HazɣHcJr♦jjDx4!P0IFuNI}6P~ L % F$JH&>gl՛vʥ]JBȩ!+uBgNRS7C+ eueY1z kxI[y5JOKZVPoG33~Z v#rHilRPd.'i?0Faק\ u z><hpwv3vTyWUC7*A<ծj Oa6y4keENџ+pLQK7øv뱖{)Zy0R YPFscIs~u5Z`V_M~dZ$.0BA0>c hf ˈ\*m`B7[=jkZ* , *)׺@cB[u;@dF 6yP*J'GKҫ{h|?g;Y%|WV:J0)0(~.e8ʃ y3rczc3۱,W7vB`RX<{ʏ/kRGpo"#0 !0",w CM 3/0+UpX4]\]Lq",7 rF /ӎuŐMsOݲXm="`/+wœP8VMŤlhΣAFpe3`ci`:P )-mWF G/`P9]d4CɎLS:I-b=/ O0ͩ]Jf]ұ%q6+fp-bFÔa>3]RB0{gZ;q-ԚlU̜ݚ]f|ե*l#Md=fҚ~dNᧆ &nX!ѤMiNSޞkrF$Wdp/%~HC '2'&F$ Rͼ*BpR]9ghN\ݖ3,nّ\㭺mq{ƣ\zGq/и>%p问c۵Ӌ{,m2vSG^]}wij7lmnr%/~þ@ a O PW"@%@""0(vJ,L̹n@bӭ%.nލ2#4'l",,^/ /iXX oC %@4PN Lj +DT˼-r+.wahHݏ 2&.纎T _f0_%0"V)AыR|%p`n/̡o]0f陼 Mn dmCqUp3wHǯ~AHaĉ/V(qa\,rvQvrP# 4VYrUI&Ξ;eZܹqܽSau+WʼnŚ<UVbmLnKwwy&LvǏѯGnϧo!ZΞ?o=:uJT3xzfS'ۆt7 %2D#Nh3D:>{2{H.뽶"Q>fDKl:#o-$BJ`@oZp#K %,j` 7nMDNͩT|űD!켓>;\E?M@*tQj't:?S:c4ʑJrJI,r )LX4)`pְ6W^S\Hܓ ӊ,a\6fc=QlVX5Do}U+XKR LOOGBA$)uSwiWR]U1ɴЈ%6LSWDxKeF1RlŢaY^xcC~A@L0SVY[veb5bT4' FBMdԑ XJzWXok5֢fP y43<b3󺢭$ǒ4iaP⎛dNnUm^0;fQ^@*'IpT5hZ:._Xxa]V}FIc찕>Ҫ>tp)QJ^y 7\C>eb%݊N!p@:r-7WSCu/Z*1\BWN;} lF' =&7 :O5#1HB&SȃwBx(Htm%hZr$&WR>g+A*X+ @PG E'jZxE+(vB؟X;)Wⰸ+Q?`-H< Q B5Pw#X{+Sa!# /! P@@A pC94At' %Q:ʿ矐8Egde+]9.MA [8EԭɕIXX|K^2>2bA3LON}rXɩ SFh%XC@+\%>q4 7]nvC?l@teA zP+, 3#BQL&BD'b&+($4`9 X@Ur7q1۸1qm:}A `N4)M$'(JL|JA^|g6.TWCڕ~k +HJ%,L k*D9։?k\zW$EP xqɜi a M ޠ*'%&&R4qsL,RgQAcf@Յ l*Q,UJ|[5z^VnԸ%)3S=әubB4T,nwhN Y kOTRdΥi2+Rr WNжPmטQ-o;׊D\QN4$%1}Vͩ]a_aq?@M$z=.AsS"/~$Nqغt2S\ ˜2bYm;RSIXo\]W%OzE+nI*yH l`.#,a3 q`ugz>lINu_{iL@$g4 |aX aTZjU&&t8SԗJmkf`w-.\jì?axp_O%P0ObџU핹UoĐ A]LW1stDZ脴H+r |j0ͼ5aUNmV_vs ܵ2a L6\bnW6|ec݋UhѩDr}p Jkw&-b~ױt.@<:=LQe[GvFMvkT|]kIe]}hLo#i}l˱64旾*roh|`t{_l:ԡNkdzկclתW*gkZ턩gygRhP%*ȧ@5xZΧy{sO8bN{3Oa݊|v<0(/n#]?/;5;m)ikʶ9#0'B>;ΪC) rӸC:ڭp:+@]S;.c86?<~A :s 3k ˳*F> Ý)+ L#|CT[iK?A. ?C2*˨|˵5;!lDk!̭sU:'8@ fBwk> $CT ;sD`'+<7;ĉ꿴DH$Fb˿_­y:+OE0k$!xUPUr "+,ZDQs)+K@t9+*FcGd:b_RH ӓF!Fë;۫O#1M! zs\u\iGx>3AYGydžt3&Er: |J7ȃt:K@C\lc?dKDLD3\=:dJ4+^`[+B¹:#EK@T\TIk/'IʙJ"Q#J( Zʁʨ̃<ç5ӿhԺD캭$4įDY:B6|Sܫ0{;QELK2K/RLd[ȼi 4J͔?NL|?ӭ-3JDc3TO5M<tDǛ2Q3G{!ìHR-+-STOEݐ%SeUʘAC\Xځ΃Da+_k:@ 9fIJE߲(ƍ-[hTǚ{E (X.hU.bCH\$Hz Ze:U`U.(?,2x:@)`?m? XZ%łP'å 9E  I çZZ_L7 0 H̔Nٍ]DQmϜu^)^A`ū+TG@_..1+Z$WdJtJ'b@+DH5Gb/Ec1.2.c34F'^c6nc)'~ݩ`ͤZCc/6@`]:^i`B̘|e `EDZy9< 5 RE)C2<ӧh}Z@̌]/eZ.\e[._eGp1fb.fY6fdf=87c/cF\Q5jL5dFG״X( gTZud`.dYMЀ@_V$C_G8F`4H%;FGLHhhhhfIhe1c^f@Hi1aa&p"XQå Ht7eL(jbCxcFiY>hL MVk~뵎kJ뼦>klle/^;X(ߨ4 ]u:=ӁD (sjNgg֩ aM)A̍?&?hhkIk_efk_iN븆khnnքj6k6R8&nn.o6lJLLPnv=?4HWV%H4\.j@Us.gFg_ݡD갚#Xמ e:7fcn~ bnnVJnhVkmV~q6kJenjqqooJcvbub}4/g pp>jȐ51\z(ؽb O02j4fiZ?lk^fWn'.o=S(tn6EGb'&H4N46mv'"9|E#urrNAhuGYXu^Vnaxx쭎V`AOߞkjhovoOtrywwwuvyqBws/lwxXz>lr+%H Ƃ %x)0xG6Yxh匞7{I8G{WjW'you{SjS{y/o/G|r '7lx|ww~/j?m]Q7[@_WhO{O_}_(T񖏅V`/VX{7lO@N~vq~v>tlEJtu1jyYx艆}}nVRXeU Kjh0 |U)R7r쨱<Ը$ʄ%9JB' ETRJ=urW7R# 5vEҵ$$j^J2I GzmTHTIP+zAUJ4+fL+&i'G`*TgȖgHlmJ-Pa&%z$DQ\,A8 meF!W+Ҋ{UJ:}$$H 0fI*Yے$G}gxҴl"! n=vh[󡄑K. %4 \Oe];Lj} 2/Š-֊d7'_2Ɣ`=)_hqqp|Zۙr,7Iz0A XHL!Jkg& )!WQA(DTv/+8h%vg'$PCݺmuѰP?6m= \=7q|Xjƛ xЀ4~ 4`iP1dDkb~([``.-l27Q*0*\ fÛ|]ؿtMx $pq:"0_5#Ӹ`d1#=iJ5L#P]8A#N2ryg3jO& Yc.3ȿ,b#nP-hdx3 Mg,ڙG[8)U$p*nG nvC:钞7GZoWyq|D0߀4O)gcp8HD#6Ԏ_=h+vM[I4?#Іp"*! bH <1ꮯz_~eQ#1\i ?`; P3F @ PbT`$ !Q @>)aI/ a m}oSa|K ݈0~9062v!3ޢ$DX4d,\9"Հ ]"VHAOQ5#FAxbb(~_#>A؀%%"Z)OE  d^3p!ꙡ/Gf/.1^HPD$Ac5-N˸pT@ cPXAQ<"%>& Y_E֍LR[ƥդbf & 1F:tn$2d0ۤ!fJbf -LLDp! E|HP%N=F)~ [3.$CjЩ|z\r#0lsJtsBg)`Rf6DJJXP# ALQG`+9hJՇTChC'}^XYfjSPF&L)ˀ A%iޠ% =cD]ƑvQ%t.WYD Su2P!LP 5(&-WGW)Ox hNb)aiC֐*.vi_" -؍߄HY&]RL i^U>Wu$9"*iy*"p)XHHĴdŎ!JT[}XHbńp~ΜUʼ ?(\"5課e6ᘆGHijѤMj]$uKpڴRO^X+Xq|$ݓqb*n_1^:Ҽ^6'ir&'irnQ@ 0,OߚոrAk.ilԘbav+ߟV[Vpo"%{ 1 j1&SF32 DB>ܙ)BbBd}-Fd3% a,a_m'kd 2ҫˊU$/heP6cFVyyIjN/lf^8hn򐻒Uš]+b ,Y$ osy˜//&/ IR^L9oF.Rz/Ml) 冯kJjȪ_ܮ 0Ũn, W$~i2]{'pPPd.J>a)c T& ʝppz0'F`K,o&aM)PFLRLZXE+0 %p0] .vPI?Econo)%`up{pMh,GBз^#v)L>'/.!$qNUؖ p#pr" rF:a kOV2E\3m|o'_'Oq.Ć`-ݺx^KFXKThFjqz {B.0# In;0$K3Q3~gz1~ZY݆*l=I0ILҳIS>X0@tAFaM Қ`~D'ks%{FMK_|uTՌ3}p"B¢4W=0?G^t@1/FvJXTX\̆/Rt[{n%ZD^&c\Jܯ=4 t^YL 3ߴvbB\d v[P4)D]qV,jVS3M#vb.6V[dsI+)BfC]Y47.27pI6?9܎HH䵺lWA nSipX^(~w$f,Msʙ2Kqs /*-X`vhhw!"xAH5+/Z3zFWhu򶊫8a:lN෌G~dS/w~ﶰZ?/If3P6z6dh8B]'Yχk7-Sm ̲,AZaƌy1.Ό!qd%8*.4"xG)5Y39G?IiK0"`9c+5)5{7k ::xt7ʸ0򷥩j*p 90":3;zDδpcVZ8dc-*, *SVw$HSR+%ܨ⎘1͌/z6ntF-3 DOcX\J+Lrt{Y"jU>_ y27ܴ_4i1ggiX{?Nk ʹy RTS\R* 2<]l<^$-ݺ;f217zcοh];ޜǶ./}{{9`ykV{FʍLO֋@vrr=l׮˵+ *(XhBna(2s1fh{#Q}4[. k}Ƀ+-NOOھ-Jj'1͓~YutGZT(RSծC?U'G/G3r Da"&irO%Bx#&5mq#DL/J4iTʔ+vc34p%x)i"Cѣ'ORGCԐGSF⨩ɓ$v &c7*ʔ)ؚ%۶sjkWD%EKB aJ3.1BBN<2D1^$УAIJ.aҜ Π %z{S>G"Q,㪿P@S8܈z*$L;2s>[DMdd 54>*0Ci|>-%22#%[F+EeӠ<J|S0-$2+d;CLj0<3RTZUϖ4-@]QFULju!v*JRLWM##U# >1 c ò]ǒVCӌ[H/94MǕUOmpZQPi1l>;<ڐwMw0*\=O tIsw RMV"}a+!;]͐ZD~7bu6\5!];ΙdtK,rYjl̈5Q0QSNYj3m]ډD4 R':A %;{N1M}c5ZRv{$OJpibkUfu$ZM$Q͓לy&g_\Qyҡ_ېJx)oH/OQg+F7fȔ4zsj02)KIم v3&4$)j>a,}r`w)~[̔UNQ'CzD'9 \ySL*H>4` 3j ؟~$wB+N lx]Z |2F[iX19O1"H rxGdE#yfR$mpA()1xf1*F:sFFus*wG'o0EȪe ZNC$&IH2S㸠sMlj$] ] FdEqQ\L(c>ԈG.]`.7w|dg0^ +1ς>EsT3[g"><~fC Ɖ^e ] >DRҋ-EH$y!iN;U$P*$"lqj-uʊIMK\RSkQhABFEra4*=bD5qe@#S_'x;jr"1>NEh[(vKad|OFvEyiKUdR! *8 IH 'L #T?eVO JwP O"ƂtN S bRbH#-"Q_ q !L#8PbތD[%|VZb񟴏!3r,t1y<&_$C '1G,Pg])!R$R'bh%'*mp+Xr$Ȅ$|!m-3/"*e@£@+&H46R@r, EӢ.w_"H/\"*H_P/XfK%V%}"WE.r#V*L6oac.\M,G ),P!5oPE60Opr8R1]!V01P"TS$36s65] Ε֧Y,p6"84!,M=S=?1:C@=ﳃ') $){@S3A=h6=B?2/B=B0,51 2?rDyvD7_Hd/P-o ZFo<[垃O,vAG %/HB/ u0GR$J@J(S:99TOpJHͰ;6H/%&-P%P6Nq óotrOBOq/8TCTg&spִ֒ʼ0$o(MGR#5N]oNatKPj<,S3BoDuTI5PM4 xk0UeM"NE@Nó `EXG5]$S^[u[uLK.R=!Pϕ-0_5:ʴ]U8!Jje4<_ԑk3io6,DNVZ5aNvbaE"z4m?xhj00RVB^DmO`IYTABAZNVj֙d[3*Ye]ʹWEKZVnV-Dbm4 $,"J#/BMTiּN6kpVe3O3k3V5f-ּʶhMo$p:g!(ljϢn9>!, A9^ }~~ S04Km4Xeu#i6.'pu"SN.kaU%7%7~mZErxׄ3*L(h;1(X@qE(]<#CĖTlf-)2{S.gJT#a8c6O=OQ q8E/xUʤiv7gfaAԋ2ÐiOݸilemQ/#&g+⤸D;%SYxf&sЀM,-f9Xs˩Lk7]S'-{7DyFHّ*Yn&Kt8oaƍfϝ/IŒOq?Ӣu?9-eE3\Uya5Vd!ک>.a)Z! sD4eZM쒉&XQd ٔYFr]h#ӦT6@ cGhTp_Pz(3ںUYMOTْ![h lЬ i11]oṮ%U$FG:5uHkzKQ}'bl`ĎyR:3`^3%7{9"I41oϭ1#3ag`A^nJ6,t{.s:-;-?ZSkZ/v{+7ek\º9{#_SۛUS$N /k^j;\[%g$ۿjb\t : |41p㍃S6:|Ň$,FfG2%G?[٤e gWe-v|\9[fvȉv$Bo!1J=J"@&ʯ<ܡ \m}6?zZxɺVZSs7"…Wǖ-^Gx|#K$I\#O\H ep~=yԎ3*}3x٢i?.H[;]R_RWZ˥q}̷31} ȁ><Αl7 ,}澿;KHaKӁs-IgMGL]Nu$EᶛUU^zbZX}ن` 6xN`a!)yu`s7Y"FWaOr!M҉XVVLkO\wc(ѫrĐBsANK)oR˰™s)Z{ %'3(Ѹψ&|6/Mp*Q,0 r X%pt 4Ԑj A UJۜOT+%8m-Ia!\bđ^]EOړ`f`ybJ-D#KOyV\`D8PagYmx/d&&7q?D`qt+׉̯i IEzd@R1E,\,a _|Ư DAo:Gȳ,24cazDƨG"t)Mf{ ?>& ,YFqŊhϓ[ ЉЮD WS ,l'1fԄJ6uFlZss4%INq{_IE6) :)A{:ZMo LaNqOZl0\{RÉఢmFQBhHGt1Q^ *i*hˌgFjӸS; (3N'qXDԷ*O%la-ԌbSnAάbjd)Xq5Lۚ֊ԮI\O;׶|i gL4LV(ĆҰL%R"]U#UyKAXk=eʑUMjE*UdTNbLms"a b1 UWkcY 5J+KGgt tѷֶs|ֽ) ,b`U^1BFJdq]D8 6y_S4u6/akV88 -^\6e.9^["̂X [}sHEԗWꉊA8,u˩=^ъLxfS D,e=zAPR3r |lW{0TҪC_yG6WJ!@@ x3V6&G{9b ISqq^R Wpzٜ.)"2ЀU@! M:#PeɑX>k { [ڸB5̮F#A;d( "pH"PSoU9C[qm6]7Z#NL#-Fu{\_ߠ%hXc!Oq馠* =S̖QΕQoqȓ *wλҦW <@ |$ tw'}|{ބ߻+~񂯻G<+k~?&/5w;a N@ 4! ,^ D 8(\8$FM@E%ȱ Wdx:TqK.W/8sYӦϟ@{JT͜>H*]tӠDtJ(=Xb㧫W@`ÊćVhӪZNѢ=}nՠ@UӆmU8s >d-(@12`cF3fYag9J zk&8jܒ5D Tn9E*Z9RУGgLj\p<0=ľ!?&O(l0dmv }_m xkv+F^݆wPm!$h$"^;(ngpAQbAF@4lIAIуE niZjk1xayuzfoo(tIP4d'k> bGWطCFBB8lPbZYkm$N 6aW?*aC ʣhw߱!,uU\Wy~ lҦiiFRDif^;دՅ\k' hw r(q$,e LЁlغJ>mIY~*lKAx)b(2/a,4 G<@\$0RF 8ؒPvKf[Ew=4a=*2#v%F@^ /+|m4L7]R{DuZ Jq`PnF砇nnLޝ}:E/lGBb vGK;_t9{zH>0I&< '|zק~>w}oo>|tgxfe^[ q:yʫ@oL Α.sPAxX i32Ʃ.&fHB\e;<2׃{HQ|><&-} c C;@kkJNy6ص,HX4͇ \bg!уrL3G%|NtD5gpr1`kI 51u' O.gCGQzR=$ŗ6>*LVD@L%clZ*fV@ $1fZp48%4GPxe NIh #AzOsʒ,hle*y2Uq*rL]\&IR'HE?=4!A)i67ii{^(A wsdAzO=P aP}^C*=_Y ֒z'Aw:J u"2{seCTWP*br]57*J"Hz#L[5MsMzi(IJ.mi$Qy4 ZOCԥҔP}*L?٫Qt4e;U͓`hl:.Y$(IM9IP+]GZR\R"J00,y΃^Smts*>[>dDJJgΡ.E+(v!l)vv8 cl[+V%34\ $DfR-^iT8jH=m>xUx[C:U4̇L"Hw)Xt,N2Pәmu`g.%:Btd"D[) Pkf׺\3f1SAnp(@ G6ȱ@c(O9ʱ@rj< 泶0e)fʷhӁt(v`nfl(E&ᇲ vl+Jfo7Hi:F4s?*:Mf}EU7@X\a% O'r#,BKl,/^xW8+< |D,NzfbCѐjɖ;lb9 Ovmw%S[2p#ڌ^N~?Cv?)*;<{=Hw~ cEfrs6g5TTd^>G'mgu^s]Zv&Y0U||MsEw56Q0}g}$`lu@~oAVxV~ydOi]{BdTe hZ[p7idF 9\M.q?PBpxFJLFcc&X< LNŢohsgܾ^dV *q`wnڪzxkm|A7zuSwoi SpZP$Ni4H\V|h,eܖRDZeEBx+ |뻴wB/F|nz0u9 Bu@AiioI&͑EKkd0ǯL?Xq V!:RqeCxg %F8YbA]26XȎE8oLjj=߶JXMPIyѴs8@ ‚vg,v5^o >D> F}S?SQ|iػn>Ӈ(a=M<;R^ٙ^_xyB0|-!wl3S'nA~sdžgX6~xE^}j5<͙/0 æ{pg2{oN.vȖ;뽎g~qFoB^o>oϞ^MGi Mg\ X^^^ n0 W^qq%P6%PhFm^׷^`w{g%nbX1j5<6~atn,0_ДI2\аP 0$>We;})ߞ{Gmo.nR?T^ ov/.䇍^v`@.CL {i @;0\ꊄ^Wl\0yRpC=/4-\^,C(xW_f-_/>cg~>u>O쨯- oE­UX`x"` 7]DIWAy0ioon_H@~] h{ uX`w4$PKLp!A BaEʒ%G$N4bdH 0@c?Z aɓ%T@҂-H9uLϝ;%JQ=I.etQFuZuQYԁt*U=ZV+֫AivjYqfs ,$ۗ0~T(aĉ+"ıc0Zȸcl,#ɒ-O Q4J 04ad Z{EZVOnUgiltOe:ѵoǚ^~ XаAei -B$ˇ__/9r8# 0%J@aH{A֤ HBr7r+J'z)+k.,Ү묛.E4ΐ" Si=%%#IRR Rɂ H" k -L,pj)N:ʑ 9庺S.iBӷԠ3.~+H H$דAILԔ),E:ˎ,pUV[4Ϥ4;\V8ԕ<{WfFLУŠPdNԐV[ .H#Ji(L7OK`]0]B uGCaA,T*zU n.tUxVW8N`l6b8XxZ\C5CK6փնn%JU"IsUvoyKmIMcIx1~L!{i6xjÐ7G)3ܕᰳMWlOuj%\e"K;R\#UjN-]Źݝ33Z*o4(톣ai1  V݌C/žsfe4|Wam{Ձo_u !,`["KnIK=sxu;wyR2E|kvY5tM ec7X!HUc2$wjy|Ap3We0ғJe`Ox4@U h8{1w=1NhPO4g1[°6VH'0&+>A s@*[v jdS5<npأ#H`aBZraʉhddhH!|S_ @&X$䞘ʙ1 #4jqsR{ @,0ǣ{3<i4'hZƥ6rlmc[/JoQs^@p*Ӂ}Euq;3xbZW!_q7M)B.na *'LBU0u{iP/Kufc~->lY(/`4k^2,R63fX{G ]i{ȊB?IhK>As d.r }1^fii3 !DFqn}K&v?`$sa:.y3.O X 1}v ¤<$~җ㒠Ra NPB,5)C>Cҫ2կ~ 4@ni1 uٺ%Ma(ȽR5X;x>;G4 0 A~7}t[Ҙv\ё$$OG*}[8eHǽx9h~Zk|׬dt=\r h_Zu'9p`jigr[ g5%_BA"ĺQ+;AX #5e379R4Ʃ3 X=ڿu G Fy|ǢUd)+BW\#0 D&]T@3C^,@"{:&}ˮ=*C&LH;i/h{yA|6"&1(;|xGR2C`0(|6㹌b*(#[286#]F4˫|JsJ/ E.?LH$Kt=w@RG7@1ӻ7X\=?Q;B,II{a'C#Lzɷ):I9+:#̰0ZA3G;˷,TJ}+H“:?ҔL4wk} ӮtL:y*L)ʟȫH0CTN%0qӯhKDzK4PH`Ϩ,]Ԯշs(Uj< #`}+%Џj`IQ: yO?Q$MDȢ(T*T%]R#\sR3"ϠԹ:I,K|JS0R[2I2]8X4P 1s̺`=>|́MެA-T 2m'HFadȅDKuO{ELcTvKx DW)BQSA-pU;>DDs|[]K{Ö(N)*&z₼RBImE$_4~VTL76=&.XnWͷ)<caX3MDnP/HV6jDTeQ4_+# qa}e΋KlCʂ<%jnaf @[[mh}۾m>I++k,[MA=5 &}i"%r-n%lnշX|OflvZefm.P-F)@3H텦I09 @>R @%m!B&(2?B]&l2qnU,h,J(悟䕃֙ner˖j8mrjr?$xZr&e`m-;,ζLBvEuIE*FnNttGDXuOwwtnvGwzEpAx@@(oK'l\E?GE0Vve*OKN#%hg2o[lf2cWѿ+-) <0ڣ#G|6xN?_?v@X@_zGPwxLxHzwz{nJzv@gxAw@Foq>2(Ծv~B ,"V/QZO_ڰr/]?u/kPGq/vg@ttwL(SR}PGzOG}wx'J~~J JO~_Lzg~GW~/{P?}`S}y`x|4=to`4o \ygR 7`a „H0l!ÇSpB&#I=Z(n,i$) %j%LgҬi&MLvI&&JJ+ejtiӨRRJϬZn:u( }$c6M/_|1H7hb )zⅈ/`v&.s+r1uDDrA7k.c,.mDfG% Cbh3RH5RFHj/jُp73*jWo;b2ejk])2Ϟ% lR:5ėQBef%,ij[Ey\L5edMљh$TIoH&Io/JR$ XMu]c&WҔEV]wJ:Q#k{GZwF9K) i. L hf jV J)%Xj\N'* TBhb[<UOdTCdE:\ ݥfuލR QW:}ч{ԁ^&9BK9vPdJW&7-V+$":o_;T1h#OjTKivS:/Fj5)B+ruJU;Q%Qye|ZA_^,`v6D~HRQƑDդ2XoPBn)lwkAڕW $:E0VjIyW~&D/ŀ̸MGDe/Mm6R+䱒N2)t ;7xy-jMZ]JJUlŴPJ!i%uY-ѩ,Ld|6q4Ip~;ˋJ,3+n^% Co^J"oY~$)Mf[ik"%`쳬2p'-jn/X og3^,  [߂IT 6ASEn;J5ʙIhbt[ *u..\M@f hO0 dxD>N+ $<5\v>렽RuB(Ue&1 !!`04b'&kX82*1B.0(@`+TdqP)4)PfR SFT:/dUf.IX!ߌ%G$g#QǸULHb84ȴ$2 H0@ vQV!>(rZ+׸R6IM9?@Sgu򘧝dg)>OsMH4*O#Ao訦'F.ol[9nn.լ˹氆P2! LXY72kN}9t+R<8<>n :FYć yT\Zق glL _tپ m~ -]LfwEU\Hu\5Az3@nΆqnqQ-nSlH:x nQ.I}ь[IPuvS⸃ ߺןK]6-&vyנr#,wIO0BA~w)`! z"XH1DKnm?=Ƈbhv˵B`e(,,, ˁ^%DBK "^AL F rIE) =99qm)tXZnlߌ 1-`teΓ XR9qB$ '`. B! "A Y  ir۷uaF/U #HFZQXޜ%ڽQJ[I)^̝ \tBXB< /]\X)Ip$waIN"FiGg2|'"DA KF#fz)N PQ7ƪ @d@ A!AKl^$\, F9ZuZI)RෂE\KPݤJ\Rؤ#BDaF@afjhB$̪~,4}i*@@\@@ ;&2Ǎ=DdEl#$]阉4G rܨZ+Ң|}"EGh\RjIMS5i"%'  X|wSV\"L)DA#Dd6^l7iA&Al )mƄn6W:EZ>U^AOub!Dme J**y.tHmuLS+py`Aٞ'Ӏm,k&eO4밚jAxPe L!]pjp`\DlȑKB"n4jϸ"*onȫzH*W萀fN W0&PA.oD 0 |v,^ @ @ p h.̚n=Vt GGWF$]yC1āCԢtd3"Z/x10s'kxJy_b#{,c0fjGX X\W.mDGTVqg?OhH3i==￾'  aBM2Tbć )R\(R"tG2"ETJ+YJeJV^Ҵy͖.y ʛDIGK-ZԐ!@|Tӧ,lI@L%2Dj;kٶu6- FkwRI#^$O N( 7X2F?v0%Q$3e" (P'Ozu L8O欹j?BI =X<1bɓ)S\5T+CM5w 'EJ[Vݺwmq,&k*x`*Tc73pGj A|Lb dm6v 3 *9H .*꼄cɹ砃ιҤ:-9 <!sF CLk*(#l<A2sPJ9&5h+E4PSLK2c@(# 9jzq%Mb|tg !dHH$<(UUUX,@Ð<32}uIT͗f3'4α&*9CAS? @ZzRrTP-e0L2xjk^Md;!UOI;7!E U!r7jm \uc_{d/P>a+di" 4J 3Zi @v kQcM s6gi9-}wȳKL "IX =V(^Hn$ČNq/L>^NeAEPv[_Z?5YmMp&֔Egs"ѪuGMi 뫑1ֲ8{AjW9fmcJ餹 0?˂eٸ}MÁVwROtzlr0缻3EzvPkMu^J5K+*>wCC.0CXnJpK~cbU޹zf#g~ձb>zY|Cw>EIU:^̓La]cBcQYJCì0Zśˈ<iOad5$(azƒKBSOmjN:/YcU$tZRCI%]&$)HL O7"J6A(cmc(V1`ͨ3vTaWB"Oanl./B CZ]h8k9æR·$E jS}MƐ(CC-=PYܝEVg6f̧* 65Qb0%/|mǗ[_1'+Ry=6j^sN :,j(&V=qO2TH4D~JZÈإ, Uh 8vNiWe5tt Njb1b0QkNl *JNqғS)M U7I4Y6UYm!=&Ov+E5],lW, rGH'CLG($Ԣ52AJ[|Yҧ'`0# P-Hvm\[=LK1y;Y c$` `\<¤ "| L/9.s"F;1W4MG|[\U!X1ҩlu}+m߻d|KRO@ ܟ2!JմSB J7m]PO(Xy02ДUއUҼvŷ΍gs5ls[JKcϢrͮo0taH7!yGp}gpaz& G^@Sa߶&|EQ/*Xx ЌePxn=Csvvꕶ)΃C Qx{_}6l-_E79u2_d.=g&9F0цt~xSe~7Hl*%c|$5yHV|GEn6$ HâLR +/`R gqhj.r^MrBSgpm. j0 8ϚpDbm /Oi01T cZaưrEᢄ(+:  aQ,#!f fKp 19bq~rG<'qp^0s@Be}-D烇n>{a PQ"z &2jzZk)F;R>G^@񻬑c,nzзO c1 1T k! CqgL-qgHGLd!鐸d"G@"m%#m` 5z"#.TRR(L\ ֜h^ΦK-$2YMra- 2g> ޣ)_RQ%R߱zй ,WR2"g(hHXnp| $w.C4LUD( ` 2k3@11=2K$'+"%]VI%c;\:*|N,'WixS5"62V!.C.x Ƞ@̂I# s$lgR01: K2JᖄG(6$ҳ =(3 ||@? 0=0 @%4¨)j##ԟ..e)bZE H!R u<<&!dF7&GpGs*hA"9?/ڠsB w !?rDcgK!!"FF a34Rٔ@԰ؑa.M 4+{JT)ru=  uNN ܊4,8UG?52%t$qSiN6 2KtbpfldUyVmL jj K oMmM5I%l$3CrZSq얫5-315VmUfo^I $Q`.`O^._GABgz1Y{1:\q9abktڮ "]G@FC=!JOMq393Cz2APET[-` KõU5?N< 'b*e gvB%j7BskWl.s2!U\wMqdzbALEȴ %W$:6vjjp_Q).%q+#͈,b rXl即7ap1I#jk%'W=n {.Ld j6o2`pukהuw 0V4/Ǖї};>f}ĖJKaST]re ~~_ظ"_p8veՉaY,w3taY} f~m߇{d@׹p__0e'PJi򏁒axb?K98S_ʲW fWw͝JL"v7CrكaK2N+oyrSu|cv /ɖcy}cq!aNj*z;:-RW~VP..Z+bd:D)zuzd6C:wI `z j [{?W')UW.K}{O90oj.J/Q;h) 4|ĭv{9?[_j=+UF1KKs9;I"GK: kwں#'ӻYh^3~(E.Dž\ bTQ ao;f|MbN؏]rrFi 8{+Y5S9c, =iQ!|օeׂ"fk{R899<ª{G8*jȍ;a<'VDKʟV^xh7h+9!<~9p5YRᑠsuوgٻ+OC h{F9XtڟoeP3Otʼ} 9+UPk%$=F;1)ӵ<~{ԇ4 (5yķȽ6`lx,3ҧ\7Oχ}Q埯Q,[|oӪS #rKJ=ڗٙԼ!xO}{} „yFwU9>x1;]Su㋭D-7,Q何Ozu]M;s+`2?g|-5$.Z0Kd>ٜPNO\s 94^^E{20lqg]I e&J[t^= #cNIVp멚\BgI9<8RNZ%Q^ cAX{_|'oߚvC{r%u9>i$ SI焱0R}WW<]_a+bvyI[=点ve]FgtN>_=yYI~gt;T?4… :|єĉ~҄FM7b !đ$Khr Ä [lTR/kxɓΝ 1.(.gI4fťLY9eeԧZy #xHJIO)G"ݎTe7Ai70O;w>OW2}LRUդWbn u-0hE $j N݌Z> X%?m֮[@ч]Txr/e;|I M>zvњ'O瞥nZvZw _$_mՓzHwCYpueif4ub"؝\5ރ!7!xvӋ~g_{PXM)u(LQIoGxUgMbh-b-ԓGјWb*d{[1]_1Z Imx'NEYUiE)VT6\g`ԛuٚ>8JdbD *c !Ɋ]AW(IG$UX|8e@tBJ&ڬZ68fr&cK%Av:dh&>.cA.C @;QBbʢ*ᦜePU-L)$vTpl)BxV媮e|o" )I`TʧI˖",pgKBM ְ^"6S}kyXTyF5!37T$tF;eer0s;[Q/p"ʋu֔InyRˡH Ίx? ;ót$;c$>&[XT?.9ȋyf mԦ:FQ)T D"NH0ha^x<0nCY)E$@^Zc>@n(U|ZU#=ӑP S;ad, ; ':8<ȊXDFJ$<~nSNx4ƂkZuǨx2Bh: bhP$g8]Ab(umSlYˑd;r #q2;fv3+Nr*l[0 T~H!FQjӌj4D">7Q(>Ʋr dbn )Kȵ\s )RG7{aʔ!)>(d zE&♦[hC j>"E'ŠfTgGo8s>AIG2v,?aEB嗅<ղVՠ{k(jԙ4t(2uP5c(}nD](XhOd^A~H-ʜiKPA͜}k_W~K%lbcMp~TGZaDmggS"WG:YEv^QSP4PK\ acS&u :&7),r wa+s$n( LNF  _4cf3~X ⯱hRZs q/(K_f g5ki7TR8H)@E*nES፹(By8s?Slܳ&Iϵ`r_i@۞F|oPݝjE(?b!|R`BЄDp6UrgV{ZL -kƬ >e˕nb%h#89/8SQEv|TxQh$U^V,ILR_r5d)ךXCNY쪉ȡCķCK {zΔyڠ:g̞}~nbxHyRH&b )i:]7줘ލql䭾=DnVP/+?#IV0BkGш1pA/E|)I k"985wܹuˬY" QUp9ڭMgzIW:IDawbحkxt4%2}6Iq:ct}7U>T-WIckLd7G~>XCd~8'Lc|Rqt28FG8c*z>8qMvhGA ExEhWߴa1GIo6y/Q|ކNuC\ ^R-+}jzmn6wtf1[g~d?eUƁPPThPA,5u0/`vGcgGLr@>kgL׆Gb]6%)򆩗l`xy3A 6'V~P7!nCZ$Jbwt8:|&CWZ"L#PDbnazSkCgs{ӄ NBrKv8{1q(S̈n(w_S8wwņ)[T7]k^Qv8[}F(sՏ)yX<vI_s,#~siTTT?@)2d a `D8?Վ(TAyRyg?iØH- Ќ~HZ2FH70dr)pxiEi}908SIsN_p#?*)]Y(h05dqSz&wЗF^^v(GVJJg.@9مi ɷ9Wd6SZd_;GxIdBؗyWy3tY`aD|^ɕɟ7qh C7P@/@p>m /ُ2Bq Պ鉛PvAvW%Z4h1jG@r@ET?#`^CF2U9p'Ƙ;':WyP2עU#P%P?w}–sG:g£FtoJzUBTWQkâX/k;a`/ ϋ%&d,=iKtk-nM׺m^+;%6L2>^ī6lt  ES{5TfqA;r1>Ҷؐ¬ad<-`̡s|t/ml8>k4߼߱6sW8MVyL]u Adz h=鬷j$nÍ*8rz}}#3[.hONKVQGeBjf`-M묧;מ,i ó \Nd׵`[a@9 [jχ@90r  ,+#'t:J' tan#پF$d@ #݆k QK^ X6*3!"OCc`*DQ+$'@PR`$6HHb?;̷V #,,g/p&+ {gd43GJ86Kzs'A)J$P,y ز| 9K}j\#o65ެ, IhfMB b~ 惾IInZfD7:LS6`RT#@ CP';Ʋ3HelVV+n'(43Ԧ:5%BFdfBѨSX1zҲ~4)p$'=xL_kຯkFkI&T V̚MVV[iWXȒղ$YE Lʚvju j1TskMa&nD :`5-2]ac(S}lEYɨ⹇8)F3>a&&GUs!LGu2UO[[Zͮ]rW||#UXCfTINpQaÃ{U^70Dk*5+s]EL'Fp0ˇD,,+ԅ ~a_y_ 6⪩%aF%&;FxA2Q>D! g8N뇽rU1'\Y)OirlE/v){k_L$%1@2uAʇ%M(ys9oY˟3ibͧ.Jd&ǻ9Ϻʪ"$d|=o~'x&ڧ̹|ic1ȟtaα3RtԹisNqqT7>SB\ّ]p0*ًٳXp3LJ<٩vUG{аSjb! x "(ҡB8pٱYFĶQ&뭪HY6E(2:{Y5^g@ۆ zx ʬMx|T|YHcawDe+g{uSVF~ɶS{h$KBKrQX~;HK(ٳ*Z*ʧ i1w!h;VJ[۽yny DufфN zF2yu`țYlK)j[$ WiP § pe5' ݻka4i豌S3LPg$:Pp)g,Z$%!| K~V{4a^ij%uE&zAi'J̐zSS )cV\%"#(`CP&7))#꘣:jkǫ̽% nk_ "뛉i[\H!^! cp6 '(zFAɍqX6̤-={ Ű)ˬz|L:ot{<(\.yȊٌQT l?E80"\lYj"JhFȰif{g~VlЀQQUJ&&3jz-iSIY$" %M(mƛj86Myڵbx‘ Vwj'}8] z!@ܞ Jt Heu^{yd 0 p\_4 kRpJDzʬ쓊{InA ˫?]gyegYj̸ Q@=ӿXɩּTڂ&ۣ]yMH^jw0'gVMw Ir;ݳx"bU+V}6[R *SgDhGBd"BPwؼǁ=lr .yxNsܨa: xW=$c$BBڎ 4J,$ [ܼΛC~|T Sr`nxVЭ>ױy7vSdOhT欮t4ɌyM̩K^|`—m}g^@~ngJ>hx BQNqN#,[hOѫnRXJ9Jl=?>iV|f칖X 6X{`qN$?ef_;pn q7㯭Xn}'^.xgj oh땨MmUQlbS^pPT>NG.,ٷJzP,gowÙwgoހ(.zTM7)w]Iy J/MpOOS_oݚ';ut_TRpaߤct`_al߆x{?xj4_tX1o^O``LFNdN;ު 0?oqT_` ;.ap~XɌiaw5Ňh1rTPA.dXpC ElhQl丱 P0d)Sc1H$Y.eH 4pjZ`(@|spAz!0B.H 9ʹ ׺N.ijkĽ2C:oF /ìBrH>ҠH+&laV2%*]`@JpK 0B @ "S ZB)0`D+1/馣3*1GB/K/QHtRJ%c1(BaTRK!&W$Z%!1\߶tj`V^CW^5(*, dL1 uhX6:濳hw%f1a,Ec%\mfC&ɥiCN yW:Snr+E{6fa!CKgps}"v/F{KDB.N۱\@( RpHaaa<6OmRÆ $@d h5 Xp$V.b\ԕ:.QHX`$0*bC8cly HZ6$'  NN(G:"C(2M DHlbD$MmE!Kb9Na FEL~ YU~\ςzo@10L.SS|OX7XSd,S5ͳ0,'ɕE}{mI%\;9a0X0z Ȩw~4gUTU6ԡ}(iZLh 81*bk)E(ZdHސYt@i#uJN9׊\pB.ES_-ah[)@LmjNU?l4 ǁUWw½~u4kVVvf"Yz\5}zJ4@+nuܕQU+=5/U@hipZԮV MhQ@sJ1p$xpY!Y@Uel+e I\Vw:qnLhrTlWbFVZH[ә4Oh/TiWh ?erO{hKt `CSի+@(Afrj4dAB-1ED9IDz$WT+ ~?,"?GȃB*%< ;# qTG x|u;̣=+ ?ȳdĭt{HJ߄{ΝE4˵t\KNK˻9t'PkΉOLb?3? AcBW1ɥFHC q;tS+OcVB#:<"»VN` J\ΊPY4K8}H::uN=9P4ˌNt?:S*HWu)NwNOymJW{ N|h%XWN4r9;X=&РR):4P /j/0W}͐/mVd1QXV2X L|UPW|W8NO}U'XW.XWMw}׸OH| Nڮ=}UN XKڇ֘Z- !Λ9U35:\Y/ܥ 4?\ /'َd5L˽\%QS<<\YY٢ܣ=ڭHs3mWwHZI oX4L[L؞J}Ȝ3[HX[f4(ƕLOc\-Ѹȍ4ɝ\ RgE[܎VE9JWyVȄ^VHwŝ/]ߕT^-ثm%Nhrt <-uP^ M퍹`\1Y/\X2uTt %Z<^-`M^}ҕS)S_+`]$ֹQ$NU$ьޕ`Lr}X,]mDuGl_ÖܘY   VGAuE_1<ޣN`Youݵ_uKWiW*Өݸάec,nݬT~HLMUL3V5~b3:cEsUW9=JTs[06åd5ӎ#4-`qFD~zLaAŬ `]$QJG}ҝPD(0AQ~e]:sW|zMcyeUN`bfcgU^vl-i[pHauq>d1-,Ę^ gy1͵EMTZ%s-h h0:exEw]WxMf'(ׯTQ3eMf2hīdZNH]TfOãP^@^tCB|&x\lNEcLdd95ªniZ'HPKMD`ng-+kvkWM~c^W%``5|a%d?Lpf웆`CY ElclìNN%k.m 2o2`LFNKlOjZ,׸ߨkN9vT >ڀOmUQ^)~Q Hncf;^dŶkznbU  ʶye]oLVjGe9%T" &FL{3Fc]kԾbN] p,6al09s݁IU@S{^Zv#~qr.FRJXASzFj6rL?ڛm>F2R2P`v{ڵQK$brZZNVbe%V7w_NVQ-X ؁xAs\g&M,iqgclOwJέ(Fg ,ى,6@wS݅=wFs\?s6-76leLx>Tp}x7A3uWaY/:toLx1w,&w7rn(9wK^u6l~Mksh'&-胞S:xgnHVGMD$syWOشiуhc%blp0Yf*8Dq #!Dqj" $:d\uKWb^yZAzRȞdziyHKsK Hs^ JhX2jB@诿:V / {d#F&-H?VW'Adt5GW2AK|첉-!HPBuMz2s.])|;DP Gjh(F@ v#߉}Ap80qT۱\ϕW"k>|챇n3DDF4G|lsn,rɨG,K]Pi4N[DVA: ʐ<)4u$š%)&B}s6Ob$lBw/Aq l"]A$bqsǤ4%n '}H f=EHD"=(BJP=h& JPJd"hhCZDIьh&0!RJDRs"e)q6%<=(iz}Yf>όi34A%D-JLe*Gh|rAP(VVjuP%+#Qihg?!ONb0#1BRWZ3 NE) VR{C"ըG{ ҷpE}Rf%KOSdbAK0p9+^1~IS)` 0@`f$OSTk<%k 4JB/) 6n$B ^ZTէE  .%{7%{eƞT_6` c~fFppA] jū= aTgQ,t~$VgƎnk L@qEc,ދ:Zaz ,Z]v5[ʌ-ŕ؈ڸ\ġ-`%*h',r3L,<' %'7+-CkndiH:nu cPKX}mej*=Q[ŵ[S46l[Wښ$ݠlH>:,)%E@rw<erڢM7D1ny#ܞNӣYX3^z~kO7g*b _o/kɛ7E,ba?_A7-:鷮|t +3\0hnCg^! @  U =)EbUY_B)`_=\e%_W* .)D !ܣ}(B),_B}%+dWYz``,8a=A]`*ġFfyuS u UP[iUWɔ ̽ ! LL"uP^%`f>Z)|'eSၡᥢb"Vaաab-ʢν1 !wUiRPI`٠$z [i72 uћH9~BB*J^'~Bv(J+R'b>#/6b.U X12!$Е1ݥi[ @<؞ ݥY"ݕ| I7c,(|By'c+d*#LjL{u_-TE8&A>b*"#&# BȁfMx5eSbBE%[e  Hd5zU %eU\e[* )Pan{IeRN䅂*NeOn}'|%eEQTSRaћ) &lJ$&Ee>VhZeHbI_%&1UAfV$Qbd)sZ=\<A^FDE)x]aeA*'O#+^e|B}%X%+Z.Z\-TEkgl&CK@$q$8^n%N~B:_u+V&@:R/_P' XGrA[e)BMTmBYxemY/{'3|NB{g^$\%lh"eD^'(LpzK(0*(©LAğ6WL&R$}tz%\:6jd'x5'x=tڥשߍ*Yd#&-g{OfB-*ggzfEz"nNBB!"(5K&*P<DЂ6K"Pdhc'Қ8ZTSB kRRhZz6*t_<D§kE=!xnJY~+”'g-BJ^(!܁d.+WjVKD&>%,-EW=&o-&'&$zWbҫ_+rTh?*ec&z"Ċٞڪڪ)X&-'pB$%mܢmJZm"%!ȁd4vAʦB$k2V&}g!,@dQ6*,*-jkEnukHZ%T^%#>r%4r&(,((&,/ڭ-'\% "zoPAdAհc!p2dCeCάJ(Z.um!>[EKoor-&xWm&%+>eTDB)@ޑ.QΚr'V$>(ȭ62o 7--,, p# v/ "XȆ.! Fh8fY/ٓVkBdZ.den궫1N^'xC0iRm_Tf`Wrڶ'z% 0j ",t'0"lATWT"l BSxL !$¿jYdZݗf.[n߅#Zhi>`ޛҍm1-:'q".I0{ l۱aI3:gj j's87ā9Or%W؁h2"4&BA(O "$$p:6^~Ųo") K&rw106t/GjaμQ>2FYq.te4>-Zfo/"s<(B wk78hp9A%GAVkVd3>'?5/˘@ibb阚&(&ݵUu5EMo7v7xƪ.ָ1g1@ [ ,KD }Cg&BU;AGj#(p 5$ @ d@\@ @߁kBEp(PeKQ16w}q;r9U:&xvz'/8{,!SW7)G€[4 B;v9sSxv yxМZ+s~1&g4'eY1NZzv7c5W:)f¹.hzֻ: T@\ĘށJ:s^$$KE86kG8:{'A܁=8ETnAxL;Zթd'*)ӗ~nIXD3˵Zw/G(O_`w;Sy%zۻ N<ʯt#< *<'<nh$$ $A$Ax%uy d $lVv>L *i2: dj/*;Kj VD_G[:ӷM/?-r;7hVhXS*k=}g6zENK#0@/;on\\PA' DcJdҕ?4]([~2'SM)P 6LH"Ć)j"ŋ7b SH#GN2%J94*pSLiJ2r=T)CI :)OI6E jTSS9$d֔s Z$Imڴ=zp[í$?|Ե˃G$NP!3OCRDdI4ItD#T th.}iYv0֬2x#šq d-a4&.:w l(&E=zԩPL(5?z4PPzC*!a7s+W̌s4K$I"9HL##+%&Y<(C)C)HCK|. CESIhFMht|(*64 #1i,3$nRN'9#$I~3*!)0ĻI CH|30(2Z1X4X t,2eDÛC9@q0VD A#EM$e4 _Ku"Vw4X7B"#PBCR%)X.8Kt.>[dIäQ깬@!S<32OG0L6 D2l.@(@,e33Ip$R0A„ 1DC#DOSޅo4TSOդt;r"'uȐ l%,I)lҴ986A.$ lfJ* 02o}Z^xӍb 9MD=EQE+. &>Ta@d۬[A"VcX8&͓Ec*7'3>cuIB窿 TI)z1Vۣ֕؋:L==@}w< [g$~sSzK C[k7ʾq5Mc OÙFiו=׽I c[b"r&j"Y[|`F5fu<]~b.DSH}+\>ka 37=]0xAdCԪ5&LJ8;I=XΡHeE '"L@H1maxR1inL#0%Xi02ʃx&l^#FJ: ހXtF[m|Dח8%VbA)3lxR+L_l R1s}ђAݏ |&4͞)_iJ<=#I=H$'K|gl?FlT:\+ EP* 2ԡ(y5N)t+bqRq`c&< uJq_N:Ozb מ'*e { ~t#Mk 2lPcY>ОfK{UcR S(`JՔM|` CMBu\x%nۻR>=hnqqA[hO0@ARlI5*Ej0)K3y[ Wɨ\iP"fLn`[4)=sLoHq8t-Rܑ!7K.& <:WutoI٧6v56r*I(T<td0o!/NhYz479‹$" r+)`5R3 "آ.)в.fK PR1 yVbDcH7Dx/z;"'0\U9$>pKZY5.d(A"eaiwaְ!xehCœ&A*_RUv6SDQv 2-TB-Dk;w.Z,$<`F—Vnߜ^eѻL!Leч<4M5v)7U{>T_6^ta~U&7_{[t/"K{D;A 2<] :PI#tax9tyzck\fwyՠ-Lǔ1uVz4h@:%z[ti٭w}.7Ğ+y%be{HgTz*xMO})OB.d fHJdtn\cF".Eb^ror0og$LhpRP oq ըڦ nSz1pꔂ#Pc4p"V+j|L(+Hk0~n|pa d DVmifŠ ?RXӆ*T GO6 ⰶd0baI{% |bz# ^&&kE)% pl"Q)ZH &gnc.dRq5PZCPG6` :ÿy>Q۸D qܞ1Yxw =eFB8=1HdBJbHdɀU8 =#b6z2"##y&m&M@1 UlL+  h s:vi)RM*)%2 n*x @ @ "m3HQU0$_ETF2$QWZ2^e2yyQ2VNΦPxOT\i#-cs R**2/32R+#DXi_ ۲-%ѿƦ$ʆH1Щ(bsܦ2/Jad&::堬ĊF h<=W4=S$T^"feʚ4P<4W (T(AC+GD0 $L ^c8OS/@ukYuS3^5ǖQ(=; rPu%IA'hiv(Y ȔDLVoY(sBФ:M 74XS&Jke=Ro2J8$M,%,Dz=v)t>d|D0-M눕+hӇӃ7or*6#R@Ww.xO# ·7Ey_8-whUc#OI,d~R`sQ X@zKȖ#VN6` >et%wu5Y||TW&*Ԇfx]0~!ZPQt[G.c 4#O\9xl{7А5cA&YXs@vr ZG9Pر:DML4H{M@ڨKٓW8NWoGD;$8o(Q$ š"e0AA~nzF۾Gڟ*J '9+ d\5`mnq"37Duq/ V>A姆1mT6Lō[?;;i72/ǃ|EN'ȗy0 6ʥA qO7Nstc ȓѓ8IN\s<眎Ο{#L:qϧM!_EG7o\ѨҊ)ڦ3M 9#A'rb]ݲE!J=re=z̜u(3H6'Wבܙ]٣]ǝR-| 1|%EW%rڄCdu+=d6OM}dԟ <7}ӹn3BU\$sZې7<G"+p1^kEӡ䧻gͱq07[ au:dC}-}'eE(\0UB}r^kC">YcP\E߲ʧ }~AGd-^KA#b怾K3|5~e$>#>u;m`= E³?VިwOӾ':$>5~oqP.\5hCY0Lڎ"/ a˨_W!AN?[5۳|q1iߤW7>”@S Ԅ #8t @+.mT3ndA$K$Ɍ,[bT!̙+_$i0Νw9РC?̉ҥIL')}0jTLM&Ą5VLRF-3TJF;9uaÇ"NxѥP!Cjyse^Xe%O_is/X=;&\g#92TWjfKMm[-i.Ȼx6+KyVңǍMM'Ox}.j2g䣓>~zB& Um ْt{.xqx N\NQfQ:y Taw^yw\L`:awTʸ/mO{b~# Ű<iT|*9K#߅+~2z]3K{+Xm]?#'aЊOumk)l}ذd:f [ a9;xWωtAKu{7˫6xV. AQAxe `ԳX:&)Zij>o(} tc I 0(]E 7A,zG2SA?-!lsJz`v[:W=qX(U᪗.uƘ0)7\A{ZtaxEw)NGN|FMqk5 4%my YHf;N<5䁱_Z] ޲7H6i gCS$&0ku \QA/Djh/N:%Rv򙯓$HFQZk3 9U bqXHLγ(6 j=))K*JM:B3[%2f7u51)LN+Kk DK.7|Hg=d3_ tQ3Mj4h9e(Q sg*Kʡ_T+R,|=RJqJUE,2iL:SUՔ}7hN GSe,n,iR:2(jSidҪZՋ+_F8f` 0+y Pb94FK _ŲJ]g-B `O6fd\Ca,@Q$ւCfQ)Xg5tBƀi#Rs 2 h+JI V([Nj[決,o}Ѩ~֎XTؽZ/*ku'V'(0(r/vW&{#xi?f)ϴ?k {&P K,a #ZgGss[Md)ΏHx·1/|w0;uZ ȧ)b1 +ǻu *-Do3eE3dayB*`%rA4c 3IHFBa|i6~O2PaۡX?A4aiwIvZS7x;O3Q>i(<թJv 7V8i#c[HRua=Α6,eSٽ;w5/H$ۤF9GWե%{(7SDaJH!V 4ԂfO]gT9cV;QwQ&Z=ԷόK/LG -E(Z/{y՛3yr$-5w~MX쭛:@w>a.6?XhZNE-vr&u9v (s/(ְ͍j>'QT2rlb\^EyWdQX/1M ǧsANٟeUb{" ku|Z'8w[>+%{/9pqe-~v/>'*RF|. 4ft1A\7ml6#t;V2t~a(vfH7!x*G*A&q_!O$m8fUlt_ y嗃>V jGfoTw׳DH.Wt5}ɲ,x)PFKBX%v}lTM`ghk9h`1*eILXw)%KXk2ZG=7Q->?cdU&%@` ~1oHyp(r@hw8h`{b e"V|x!+Bqs=s}Q$s#ӈ.V؁ׅ)^x prysZT QXm?&Bww}ӃwzC#xČh,%q 8 eۨ y`o&=og77ze"1VG' %2AR#Oc'8H^s@&gf rPePh(j`c"mQTm4EKU-_$.SR1BA_1E83a7ieh} })z@i`xH9L5FrRiUy8ȕh^d 2hThO`V&4qq{)9|IA鍩K`90Ըfi!1'w "ROt(vsvgNO9&Qg )HY|`XhW2`ٜSЩ|M8uN3jWT"bN^vy0~ }riؘqȊ8t@7Q/ҹz: C:=Mzk[wk6g$_X֡@ w~ Ͱ _ x `y`EAvsiYld'!VpuQEl Ne'UH%XWzZ b'X`G~z4Q9)zZHE&xhDUZs:٨%s` Ǻ*7F v#A$ ϷA%dD#%.I%aMzjsJ5@utX\ڌah) 9Ū"*)'ЬV72 Һ~F3W:B5r\XeOa a_ȫ"j$A/Kx]q&F:Dg띄tKQ5Xѱ@#IH jǚ vtz-ê+{'.0%fQW`E_Y o[;BpWaGZ!N$>hA4Qʴ 5+υLA ˣ﷾TFd"OxdDbe+wk+ZA컟lNၽ_uo$)%> k |e@K_ FO RTc)vLN 9*aw vJ)rd̯%RqK S dzHf'#@Tʧd[(&88^C^e,o5]K R7NKlgff2LIy1s- THLP^bf}eh l,ϙڽ}ˬp):k[τ;Dб ddPSI<`, u\,Vj{qY* Zɭ j ' J 1=6IsLSPImwaΦW:Ex 3^ +xh]*{f۵Zܞ ؅*kϰɅ'xNJOMm=IM -Mmҭ٭ٍ-M--M??@mM=]Nn = ! ,^Ê1p(ıaÅ#F4F(2Ǐ C~ɒBhe$H @ r朐310㧨ѣH*c4ӧPCUljʵWS:ҳh^3۷nD umկxoV~#B Qa.mG8d *jkc@r$1G䬳$SPRw-P%oXqv쭞lU{Θk,uEk d 8\0 < *J/E$cA.!]fă>rwn2W*kŦU<1bp+KQ7>nb5#tBB,o <ꩨVIdpOF.=&5q/g(f'z5[-qohw~Np 7=o,-sm6}x]yd#)6 = @Z }ƛ{H˪x 4`xx8' %86Ê5q]Lcٳ\֬ }&d[NΑe/  *4 rB\S!oy}҂ @?C 3eV )8=T!؆s&GMF6z v1Nr1`S84fnD; 7@8T% b㚧*vq3^WD+؈MXص1Z 0IL`QX/Ya.Zdd EȦ1Ys !yoHJXhʛY J0< &dɲ!vY@hgTC3L5 K MA P>擟%)Mkl[_>| ""x3x*>F:s(eK'9/I0r%ݸG/?Ц- dzF6 jU̫I|]V-i*D~M@`{lNN閸*=L=. ҧDOnժħV.UzspfB lz"a*,~eQle ֶmn[F@Xk[UZ]iz H&Љ_˥?Xssz1Y+4MtH]T(:d+ono[FLmlN0 ܵF tRڮItQT;SPu7w$oTߺŐ8"ؼͩqY&ųe0b:>2oudCk% OW5 p $T?̞t5F|fqYB#dWΙȳ3q>WJ- ɁhB;zȾe&P ,a2OwwI|_R39ͩWӳNiJyЌV2ykK Ўl/7`b<}P& YLm++:Lb+q9uNn?whbC! cS6q3`!N߶P+{<☮ -)m{z۫v8G){0c@3[D}<`"^C=8v'Ob*WT |\<S& 21PEt7dg}bWWwpu } hghYzVy0h~fWucDa{tb$Ѐ aS]FC yg}g"p(x|`L]ng.shWtHyQA8?8H]:L8%%\YhXhggu8@ xhf}L{L1p݇}e'8'so3؂,Ȋgx~Ng|,A*S:؄NX! GrasE%~ukgXgwf؂u@u0(ph~tz@{0 YuPzgH#x$؆(qgXvW;ԌDBeJx%BÄۈ XXE$[e% qӇiw}t(H(hL't@hqXzGɐOI} X Hyh~#m"$kaA{0$ n)yW|uh hjDc$0(0%dywSIpxAZ9O5iؖgtI I 'ؐi}ٛȆɐyytKA#(@rxb*)XW| HA̧CC8)EK@NIipyghZ9nYP*jϩ{pAHȚhA@  Kx~xX7pc!zn1:SJ5bW ڠSIind|isqxP%3JJw'+FP@gfzfZIlJk]r*X9]`ɋyWq*h@@  ࡙tiJx`y-zIOiz<ʎz8A9y@!%RHKWRx@i944;1|n'QSRCPijʦjxuZj s*rJPJy|ڮP {i`]يa`Et IgpB ( gOvX7j&xJjWZs⫿ڗ $~T.Ъ*z֪<{kRB+q:Rik1:| ڨ Yh:vjuP j?+iSDI:QZrH/aWg/ (4 x5 `zNe.l6 {=۳ZyRIc~犔dٺy` *@ ڧZIlKnzxgqjjfThpAsrv{q*$EWJ'%ćC۝"1X ۳ʭIʼ ʖLgSyC gY*~Y[vk9ʧ8:'Sky%[BWA f A黸4پ-{%\лȿʿD̿ٳ˼ YvņZckqcL2:2f"&CKzy܉4  5XT&i{P>ۭ\\E| mW ;ʼn[ܠ\V<'ɧwv1k\(~v#K(xǡ]0* lTgPo^Eɘ&UkYJ@v,cl A_̱RB˻@㹲0) !bx-[D ALzPL$gIPuP #C%Nw ͠"sOo/͠c<8L*dsǫQ*+{aŒ:E%ć[ъtPYbMΛqKd U ׯ|47 |Ɓ"Ӷ,%SR*Հ씸MJ |\5@F@ܠV=,XͰ[b]ڈ:Mw]MppggևJV^9ڴa V}dQ{;==?W)Ʉ1I}! ݬ]%*J(yl{|b-zUl #;׊Pcm㦭7T<~cƶ]IAݦ]A,PU~bf3:5ٍ؎ >^){%Ċ*/;-tM| ӿhM皺 ߅޼::^#Tk8PvΰLTNֱXg/uo*oꨞϸX2%-(#*,#YY {}zLYly+h`7Xq> 🟮{`ޥJfy ؅wC]ΠCMA ٳC:8!Ň~4*B>."II%dK-eΤY&L9ur'G~ mX:C*-ӧ hU 6l@WaX! 6`- ޺ 7n 6Hw߻sZ$@N.xĆ!^ܙF(A )z"U,I#D-4H2tݿgU^պCW͒5[ͻ7o5G&A„#)LDzf̝gР\- LMD=I>*.(?L ($.(qW?Hn @ԓdF$-Gj<"l!" &@ȍ,>C,`h?,0$@ZsA2>[' 9MHsO>Ktd7nЀūFDw˻ʫ Qk S#A)jrT'",S/UZc6)6,M?Qu@Z{epA*,V_IDGIfuPrKazvAÎ]wSjAa05XssbP1g>ƃFe{uzP8FcI.r%]iJғ(o/‰UIi8jɡEd fX+ C5>5 nT-&gyu_,@D{L"67=n0̭-SmUԱ.HfTwQd8k `X ?b_@ 6pōA\ J؂U ׼mw#?ڞ[J0[ՕNa#w[YU!j9вk/h)к@Nn20m 9a<xbG9`g jjЀ5TOzcg}=h yr7|{ϝxNɯLa1|<*~'*D6RފaO/qCd;^9;1;k@|@{ ;";"3:۾ @s([2C:1#> kA@;wr'@li !{3h6'1!?C5:#Ԁճsj{/ ;S'vbB0 @KA)@55l6;sA94CH#C59CS;7C>dTA Lc{a B{`ك?T f;B{6sYBиR+f!D1)W.9AA[B:E^ |=|93_4_\\3Dc<Eare9F 3i0>qCC#E\C*$ /ҧ&‹@GHz !8SGT'pX:'`o4s^d_42Fd=ԋHB\I{e'|lFniH_l>ȼ*(p/*x +fc"&lB(d);HJc8ۦTJt' tE,Kp2l G;H>,m{!Ib{cɿlJI'eeT)pVvڧb+bUYF/b_eI\NNܯc)lVu` >7 B I/,*R:d!Bs<(JT_u>`xe F?!_ٯ٨%,e-ZTh6KVV|D +~bwcae~n2Dc5]=6ex;n b3Hklγ kB?t1VJ`D]\WEy5\o_~v[N\f)S^e ⪶bbwja_Z&)ӐDHA[%Ԕv|?MHS4.6uOFW%]6uel>hߢ '^ZV6/2ئ~hFhm#j+Hx5ڍk$f& XE>UG'Sų" 8@qFJF%s"rP!d^Ȧhx `hdv 6ezUK~k?ߟ.M'fS,FԪoU %ЪmmnynW-iݳ8lHP[=%E%&aP-K)6rm/W]|.ZFT jz5mqMT W6%&hpFeo6]af[k^ZakIhm^ދtح:htHd\! o&.FeO+x+H(% eVfus w˷Sd Ь6ow/g:V[3뱿%<ylh"hpM t{]o8PUm^g8PO)W ^S7U))!.p1gv-({Ou,Fc{i>A^v?qq1Jj h3^0ݸyeCw-~$.&!2w>cYb/mjq)~Vv~2f*fPOhsދRVYeIBޫljvP|b?7jH'w_{Z5K쩞Q'-{->whf.z s'ohFZO\fՁ*NuUeg5- )Eɝ=70II{n_m͸Ywlx2H*9~2}oxxۮߡ.|_͇xɿr߄Pr}9rܨp T(qa⋌/Pp,AĆ F*W\2$!? dD ɁÇJU0mi RJǏI(Qd X*\rIcY7jC *Sҥ$ z7 DG:#!ΈNAoĂ C3zw+HFǎ98\C+FxC#J?,iГ6[69KdR%>p0A.]B,ڴkC̗_n]w hA@fU6ggMVYg~QfYg&jvZh֕NХ^]Lԅ0mFCY$o@ WHArDϡdui Kb Q긔xU> Saj\}a YtG}݇_~&QDĜ*1{=x@ bYޞ>FZg]Y$ij{!SQB,Cшp4LIʥTJ4Z29IJ2I”`%YdoUz'b{ejpJQvWF࡙Y.&(%;ioZB-(2jFO:$1%YG:P Q?T%!KEƆlf2[[S8qyq]5Wmn](#{e][)jPjo7J*N@LEt0]G*Bk'_Dwܹ2eR5X[18"Ebr}npTߚ;x5tԥWK/*-SNWMQu-f} q?CRQԱoxKu,lmOL2D~8%|OѱX_qq_] 8d44Ğ2, `5{ˈ<bKK* BD9,J6Xpk"lbӱc \dep Ca,XC} ()R1ΨTKZrK$%F;1Mu @0klL]6PJ,B=fP;k 4`BФ(J$G([ R"13) PQd1C6 a v(UX\Y25H#C$4覛NjJq<%lqLLt @NZ`|HX`e>vS)9YP PHN^l=}и4Q4MEb0p&-Ӑ9%(J+*|$b(mz44<%. ԡN`(R(S~aJ5#ȁ ۔v6 029N7"J {Fo>tC/ҽtX`መ,b;X2V\!ɦi>t.#5+Ka*Gb~$й(!3:MOT)uK"mF6@U֤0^]d>1!E7uYԠ. & N.:KQ_T(E)2"vMb+ /b|h\(sI|0K{ZԾ~w\\nmv+bc Q/E݀&pA \E>IUJuv" rt>jk 9Я>Q\#E?P0 r92cְmbXLdn~3<_! |`-IN-|i L5a3&Vx;Ρ$~`,c{YUm%i.7%Z `pU =IZM6Ѯ_XL,v\,F|0D`aRJΦ)Q hcqImk-qnv&/ X.MYX&F3ú7L6:-o.dZnUBq2uU#TsM U/CYRI`Fe%fH [ =`IPBVhW>:n{ۤ6.ufg}Բ< h7R.v6zN)4"+m;#@ [2=E_î6ם">5i#OILB/^t}<}m7QWlճm;j[VL"QCC^C=mrfՁK>>(^/h|7wcEL|޷/_ăF?5yӟ=Ru}^IYAW!Aq)ZjPiPiZ9itI{}T}\Aٝ -^} a !%4ҁB&]mB|^-]^ =+darř.9 x]]P-AzJ 4@-AzD69ga Pـ[a bY & "&((aB6bb&~"=!Faz]m")^+6J]ќcۤP p . @@&II Kb4R"*b"ݯ7—Y#Abi[,"(c:J(:7c*b&"b9[?Ƃ)œ֞ۈL/0! A `4Fm~EF86b+c7n'_9B?+':2)d9c<$}+#NחC$d1@ ѠDJK1EFT`6YGB?+**?iI$W8cK%*7%\#\Mz%9#)椸d[aF90BU܀Ǒ !1헙U$ Tv5`e,hV\JXjf,/c}^&LB`E[$je\B]b)%N:K~YA`h@b0$%Y,@$f֘ Tzzz"&Xbe++[r#VB*B*X|lj"m&)D#&onpvcr#) WjRh'ʗՅכE'c-pPAY`E%V!Ԩ&z(ݠ'B||~}'||je)*'~r[mfn)r>~z)~r[r:%@_dٗQ$Ĝ# X!-4#hZ Uhg"gk'kj[fi&k>)녂*L~=bj]rzޗv#&g*\(p#Vh)*蝥$ (AhK%e0QyѢc < H樴YA_ձ~({T!V*qr'")i&iY㐲f(pj*j"箖jWcE`\b*Fj~BÊm&(P^_."VFYN+MiYdnQ#(+;^dr8,+k|g'x*# lͦ,h`jrkR,(@\JÎcv_:⯹$ _TNB%Raj~F+ PL!,^ۡbF塶R׌&az^Q)l|#l$.tzn,*x%$%P%|& k-%R)n_G+Y^Jmj^)f ^ͬ܆~_jfo&T-LAF"*ҬVUb䂢Jʤ%lfo-^%$Z! N¬G'& X.BLa *!00 M,pWJMkhUNppަ^z#AߞhhQB!a-]"F I"BQ]j"gje+B ւn,p|ւq-櫿+VN!E :/_kgp[ íS0 S/B*&h&=#-p $2|0&h1H9r-,lQe{֪<)032(&43'<3o2O2kOBꚦ)hB$LA"wU r.  -C)~v$7R"ntT"P%iB.-(/ *C)|Eq06_eӖ*bcjn4C2B2((M4^1"B2eAiz_io(lB(P(<3eBdw-܂{w-̂|b/6"~{6"1}x̒&z.[Y[kCVTE4qg8Dgo[5\;E,L]گc~r7w[f)*KKW6LϷ|_v{-(tb7B lTAIA B#\BgG~G$`/&gHfVg~B|AV[)ɅVe)^8spZ{8k%8]pˤ$g73>fGAt3%4ve[1w|w',6 yX39gy#ܷA"L)d%[Z: 76x}c:h92˱Bzko_'tSs 0@ Asz郞U,X&bCz7{㷩3v۷g$y]8\!$B$Խa՗BX?Lq;3 Ɂu_b70xqGy9@$B{>p+}F0{sg:z߻ϻߴ#Bz;O9#|q>1sus[nGh{qzd"E'QbL&Q&#Eb %ʄL ˘,ity˜/I4 O.y)RGrhX**)ƌ[vHcɂd%mZkv<&Ѣe F8ՉS#ı3*U 3@ ;%J4G.*i HI 屝y"NZhǞ#A~9H?5}N5)p$w'T)SQ2Hr$wl|vd{_BoEb ;0l k, \hp@ -zh @s()IδXkO\IL)( 2-D(* O@1'"OɸICD$lI*kИҀ4.zbʯE֪S,M"I.2HB`ARl\!(̘p1*Llԇ 2#C3rMIҡhQ;pH~"$J]L%.DP܉J+i,/ϑW$ 2ˬ`4M 6Òr;2EN촓,jY *pBA .p+ N*`p6L *,guȢ(xqZ5ܒAnI$ryjV 28Dv:e[I$;*ˡd%'|U J4\-ֵ݄#f5 P-׉);NB@≺bP7Ď6 ~dXATMQ!rYfhXtr+dղT=v/Ä<.ȣ֊cHC)ĐeVlbnTCQ@iXXj<+V#)xˆxF6*2Ǜu4Ulw kX?)Ne)PEnR5,`@nwebнN9v&%Xqz."CܰC/z61a`3&Pi&l%&A7fcNR;vW$IPGn1 ApRns])jRP(_m 4 145.;zL]̟ R%@xǛg<"o$/Hk:W>r sx#iARjT&R1p2V!"! 7OMJov11qDbg)K^铧\XnD7;U7KHЊe)!0>R.۰kBIz,>pH_%S!. z!0Yj٪d)UdEDM>p ,42V|J%#hKFj DErآoh%(w  ʹƐ iLDԢ6(<(  tK^Xxg4n / NF(, CDKPΣ6 ??Q"fk\iH~(ZĠy/ 2VH)Yuh]>l Ij  C0;O:(<~I֪{B@>p kfvˠ1x1F%d|lJ"4H)3˾N$Ǝ $(rʅx¥pk-HFH!qhH h(TfqJQYqhD")^i9 LD9ON"/~*''$Ch፞Aeh br`xedK%qRC R ׀Q ) 7IH*Єlz,i(r"o""1+@4Ò k-ffO P2-Ve'&' flOҧȦ$/}rJ@2 !E61ۈ:o((J$˄02;N3y@3mR4ӓ4C mUH^spD ޱ&yD4vעo,~X38$u 8)#/sL(3.Hx`Scfʐs4d-Ċjxc"ɴi6A6@4/NvfA}jZϳB$@#)'r9Cs ŸK΀t,Dwh䀡^(-NN(YuQU,k7"h~O5ͧfnD/{) %ZֵFj]] g"z6#p5>ͭWˠhF`w jkT҅vv5&r"kudH e93㌒@5T'OJ+j7}bsptFd0& Sn)%:9eQbJ2Mgoog;=6#n#`w *&k'AhNl3}~)Ÿ4.RG5~$j(@+m*;"CBx!bF%k&z[ $H6_ -3 ܀{aďz17u`l? WnKD%d%otN jѸ~fTeHjLFyOUY՛2:Xh1ՠ 4-T֣b8^k7/eqx3᠓LBwT77en3ᔐ1\٘v$y~*T rDθ(JYf#z?Q&"̎sRqw3v/ C&aŠk'2L[Jc >jiX-wٍs&6?@sxTY6 4 2)iV8sow"3Éri3f#v-T9[O90g9stTXVkeΙy5hn-3"r` vij/{Km":nOO81 r<8xHj:vZk "K⧁F udc"tzs:Zl_8dYt3{xM !x.D)!9㧃z`3F$>[)ЍחK s(E)[q/Uqi*GzWL$($HMGEc]z/7B{[ۖ=Up997>O;`7zxo<` w4mec ?h̻}: [u׼.g;[لL$2'PD0L-\7 WΕڐ_{ul$cTuD9;<\k6+:%f/3ō%-ouzƣ{zɆ=;Q%cX|?Z磱0C(dԞDu[sgʭ\Z-)u2ܧc*#Hkw'{tк<ͧ!Հ}Qq*)q;Xĕ8UrV.%RZLdZ*N?]_q-h/gy;Y]=&XեT^l!;k&kвعۚ&݁Ե}53/At%&t!(vb3QYPSYaZ4k](BCěm8 'Csp^楅8 );'3z._~Օ!t^G_:sb饄~ ͷˡ_SgcDv1HG߇_2GMٞw8s_B5:0B0AQLYx1aF ?y2H1,I*YL2ȏgysΝqcf8IyQt&fDzړ&x L j4y1_FƗ9넣@E`?$;pkQq3`H|Hc^tmSA0ʩILcV:oZ>OdХZai ˿΅TH$4q햷@XR9Ы~A_༂ 5t*bvfГkҹ\Bnk>3]Df$PXCcDvL"MGM5Q6ʢg~gcTl3~DFN։Rs]Li18rxN& cݚLQc<^Ғ&vEֲVhŶvP aƈ+*kZ(v\HIYy`%vATwxw|(f!o{ F] _~ ;lv:D+8K.Rt,&c UU8=OsVL^'~"PEeFyTnjs-@M\OM֢}:oK$yQ[O@D=2vncUK-cL3.2P:Hos z[+ݜy'rVnyޭ p~m`GDm|){;'gt\I݅=6 M{Z6%NWX4b]xZ)ᅎ&Q)K"*oZEIɈtHg\$.!"gwvJ(sv06z/&8A-ysVe,ax{FT8YGs\70Ac>IAG`K̈́JG5@vk$rr^(KL<8n(dHh  5p,#9|;':HrBgU$W,h`ЊT}2u'g[v⒖9N}%i)x2m_󌩴>Y"^2x1ujfD` I@iD7y`Zdnv i)rR$rBZ22s5f(K9n \6Iypwz@ՒG՞9Ryi\FΓFtÅ7k$D\N%i@ *CJrr`Ia!.xea0aykFngr9*n壓9ZuuZyMDpQh)?|i()ipƜFQ""ĊW(uFN0zr`xFjM#Rp+Q4ӟJLtt_lᨐQjO}jk(`qjJzJaZljf !)]~b:H7)v =I3ɭ| y ztxx V'hEZa2|=9vs@u)78nyw0 ۝ :1 @KUWx1#kbnvC:$+ٲ 0k u׉tHl&K@ ۰ધ28DhS!T $- Z3\fKd ^A 3 s  w|'G*U$ ~ ;UڒYjʥy9"E1 ^, [f{&  7ۺ`-srH[.Gr!:?X[, Ż0X1Ťd<7keqj+=sB۷[{tOz&)s㏞B:\6EaH++nЛ6v@z|„PftWtf 6k#}UJm#z* s {ÂXٷ÷kgHœtvfC.&:Ӣ7Qv*\V}xع 9N۴`I`$5M3]ˑ>; q؜}Ct2/כ Am޶} <QQ`%NHyO.N  NnmO@%n')&/->Mp7$;6:^CNN ~/Q! ,^"GYc1VHآ"820± ;`)(SD9C0cxd080Yr$#!&@^ôO=PFugUX`ʕW8VrK5@~Ξeڴ۷uԨuԻN媷9r۴KXpX:5,8۷)ٰszuUĉÇˍ (!(^bNω+*V{w [Æ-+oD1c<b_ev`Ak2jӫ_^3XΚѳ?{@1c>?je?yc7~g55|ݧ^b( qCAuɦ=Ӌ-@/-PC-\EPva77ZlU~pa%Wr%sX[>i!cSNY_yޚY7X` U``ե[Zc~zULň%Gxl)Q Bhc5ܐ=&[ TP4FGp7s gV&+W氘'N;%bpk쯐孹kؾه"(:(.tio3J0 *Aoip $EZVhtR{en0F`M,K^+qo(!` {J ` D?1zC$djI[HnI00M}wUwS]i"w-*grJ~ll vۂ+cd筫V-{LD0,Q3C  ‰.󺫜0@>H*6if؂y_}*ms cw,-j0K‹D 1ĵ$ۛnN?O䪚T$JW ~\L뷎u qSRt,˖ 4 Cg$pIBɵsMWpl.nR,UnR; TgU[MRk$[Ĕy+^lof E4isf|'lgH֌ps]Ǻ׾qn 7FCIw5 "h!7pbW˚p{χk[)|꣈vD؎<#z ׳n|Yw(mܑ=l"mtSrXqFL- Owx|#׉O|! C,}to[9#_o')"|piEi:,eSOxLkdvn{& wwk۷tڷx8|Xw wGs7|}8Շ%~Rw~S#47`KTWH:O% ehDPY xqcP}&XVXS%Xw'kWvxK7|N$)(@‚VΣX!7b9x&@\r[S+A..&phP0pPc88V}`}8|l1|؄Wwnh؊' 1HU8uRgr.H!$@m5d)?x>%3~tv2XLApx󖅅hȉx&hgX|khqhw}g-*,Hz!\Vm%/@z1$B`X|(^8oX(<X`6)UȒ5׏&H"Bi}V%c 9@^ǐmu\u\Gv=  iR35GpTxP 0 pȓw}xi'xRGzi8R1hwيI XMx@ə蔅CQiG.A @m%_+)Y7UYi/0`i8 tIVhzɓPOu@t`RR(w{0B)?ٞ|7Y(Q,@/dyW)HY\ٕ Р ڠ*zWiJf3iq& hP8.7yH?9IIhɝS)-tpri>)螠HA{RYV RL-ڢJ(_qX;VuDS_5Ƞ+ywxjH( P:9Qa@ڨnpCpѥXY\uy @ 8Uz(jU)|%\LQNڥY 4Z**R߂j'$yAi\gYg  i3f/iRPaڙڮ6zgapJʮ 7e9 TQ:JP  yyڢSjI|X! iа'wQ{`(%H=>rOACٴ {aP$aXkʯgЮJJP RXXhzЪz@:QN!X> @*h P P  \ jȤ{+i V^ sig1Ra\t=kC$SG$PG$(0R]@Z{KҼak˶lWKeЋs[;?}@[̐ +keAJS!۷)4XpYZ@Jw~nZ+@d fJXCSWOA `Gfq!Lj|Q̥1{f}fKUL~w<y h\`ڿ|7 ЗچxB&%ѶJkGh5+xQ-!mNϙ* OPQ&z!4_+mKrѕјw͸YvhF$\,ܮۼ fg|UQJɔJͪ\ݢhչbv]S=f^,X~g;+q9QxtuZN˧P`2,- 9̚ܭ`ˤ٠Omt1nQ]ՙkYǺ=F+=G69F)L/t:Q"W$K.LcN\KEMQpAݭ.ک-ipʢ]ٯM=0v}$ơWyF ||>{v 9Rͻg t0Q,W\`6eJ(r-ݥ^|aSb~[^f:N_㭻qK~_w }\v7$XuԽ1\fۼ^`ӆVegܝZ.'Aw]՚|#>Ya~ H-ߒֆ[>uqH)(} .}4i^(Ϳjrq9MQ >_]@+7Ywdq7* (%%uJBi 6}pQɎGi;~P~,*݊"X`4 NKRKyN.<]* KL.}kg,%]^>Q\WqY ok۬ ( xJMfyKͨi@b ]6꿓-3j {yAJoԭYc^a;YΚ-galnJdCD^ `~ yi~QikYٍ΅6˸*{}Ś  ޽xKnAoY[QܭF{"[wA'a# 4 @`.dP #v0 Cr :{D4)UdF 1LG Gӳg!=OIK.-tQ%̩3j!Yb=W]}f SQKd{.ALLXaڷԣME'%KQ9+4WDѹ7``!CN0Ab(g>r5:A')E.Zպ֠Bch9y{E=~Evww0[l P0aAdA#D""M6b@H5bH "Ra##z[ :p˭܈nGEÏBң:H$̦0 s1 3Bj#b(>5TCA 씈3l.)CG>HRKGtSM5>MrQ(SUuU*cFWu:ե(D2jK3>nէMㆉl0v?3AdSCUfyЗ*͂:I| HعQC瓆vܣ㣣ArzfިnA"ݭT) VY47n@a#";l7VI{ۓQNyel_.H /c 7"đ\ѣӃq|=?rHVT8] W52vva@!A$e6#MAme+C8ެN , 4P\pBp$($_ PY\!j>"MM;BŰT{ZĿi F6GFA,)m1f ,PkL\ܠ7Pc]p${SQ[؃Z$.4uOYCCѪ"ujD*͉ʑd("w>AKYLJm@|&52 B) !g+(O|RlR W-{ZV 4h>p|< w Zn_)K&Ai*м3pa @,Þ̫= ޻ݓ݋:=8[#a>aʰ鲳 c9*t;⋾;> (܁s2!2Ñ!,:K¿ Ð `.暧j)z\. 1 = l#8 -zuS 8, )36&|8c8^HbBHDhg? ٺ*:Йx/=03 =76 8g{\2;h <^SHp F8 LE)B4ėh]Ӏ3IH,L,aR<0ʓ=Y ^ m!!1'GTtF H,@C5E K6=ACVJL'p?KIz-GDL1GRƠ1x|̤J[[JЄJ(MMd=2t$k@dDF KG,μA}K$@ ]31Í@C=4JN#JԀ,BTkXtĿ 8ںCݙ2K$MdӤO3!`b( }0=X^eEaF%Pu'@=^ƳÂ]IV# ޽ρUcuaE䵕$M^!e$bN$ H$0S#N#vV6[.],2Y3{RR-[F凮hyJᄾeP%6CFg\\!5x' 0 ]ՕN-J&]$h]6]ݲפ.jj$xk׾eb.en볶fF 8LTfne,`l`EL@zdQYCT0}E \Z[5~R8vIEb>eno@q{fj @f{~mA{^7<#'8&uoA0L^c!ߠ%Ʋ.-ʿzTVen1'nFk6G=oqNf^*>s?s*fNFqCg7o@nt݁1We8δLv[$O̓54d*Kz5CT}8yfZu:FNNnFheOhsvT(*vkv@Wh C7E7tU!vOI']ăO~?ST]uVZ%l%aDFV8:hv{tVvJhsg pvgg'X^@y'PewDywGwρx՞wNA^P>uX=?XC~% bG 8PĈ5 F$F6rhQH!0`xaҤ $6l%L 2-4iM` BeQ=<ѣ* Uhժ Z}X XM$#ANLa;-.\Е-[*zD ( e2n oԨ\CE#+ybޝD@ A /dfO)(#8#p!A,y +[nr& 2+,b,Pm!u"<}u9_gTZmiN\ѵ|Q ؄cf>ԧYV@wAfhH<F,\mcCMTBEEDGq'JJ9uQޔ]v6U`u/ /IiS™PY6iVbuX`ZkH]H %&^2*q@%cfZXrFfZ^}")@Ip(TRc>jBp< uܒLdJ_FRW$KbeJS U=!}6r19pC^5A&Zc^__jYB&&CXTų#Tk'*rl!{3dLdNbQ-|IVa|1.rn6ndGRzJońVڇ͙Vu+Yn%D+|)- u G,s67|>?JJ sD?@׶L7-qR]~r= a+ل[t.dLCH2 rX)9A&ERCZWVp&2$B'O [ (GEKZhrif\1>)i,Q 5** : 7̆dX }*n[T6 9>aCj$ۨE!hnQh @@M$!=QrQ"x?pK_D%%ܒ }v R8]0p4吆wK6uЀIp_:F TÈ&u;5'PDd0:hΤDQ`Tc 4OmF -O8)^Oڅ.d͙.L&Tm JMǺȥ.ڌ7 )Jjˮ.,j̊&&\4P@zUUKqv &-'DF]Zz3Ev h`]*5 ~40'JHAd;xk ڦ7!Qi૰|rЀH{Y&-ipZ5@4j# 4[If :}_d.Kt P\!#ҝ.: i#|.|iB&l^5(mAWi']J~S" W4fҴDa+qbIS$vreq,6GV6p#+3 ]$ఉqAhr-2Z槬X8 q`))ww6=o4T_┊ZP.>9eHGvy0aS JgmP,B kd"o/E&0!E8  !E8#Os gw"i`7Byg] p?b \axi}I-JIAN]IZ vY?nLdޅ5$ME ]E\XEt֞O=$dB)a& 1B!P_u_e">u^9؝B AA FLƑE}E `a^A f uai1^[hD^u`` XEF㴘jTil,+,^_R/^!0v_& #1&_1#2fB%P3c%,#%\- T5V04Zyf `; "!B =\]=b@hGqVK׆ LQTʑ\ YS߅Zba cբ ^]5&$P"LV50b3$3bNrd2#))PB)BRQ!S6%Q2P e&DeمfBF89b^#Z W\]d>R[ܼXiI\I#$0(l0ڦ^hn PQfT&p2)) "dceMZ!`#]# T~8 A,i NY_}fgnN過fff~g~rN(줂R3cBlJ(2b(bn~nj臚()S&#$HB-B!( QiE(J@@bDX@Pb'ŧZ=iF)nf1jz(& e(dJ12()TPf*)cmꦯb(SS+2BB&@6k^#R,AlD% 4jP&2 be\+\9F*&c~*rOPmg^h %pgS.2lNJNdY%:@8D\DzZ*xB֧_^v,k&Oj&(ڤ,læB*k&"lNBoƩf&L-Ċ)Ăj-30֖(-ع!JǶ `]Il,&\-l֬'欫,+h-+')N$Mk2%+Lm)(-f^nTBnf)m&A  A$0ȁ|6ݶlH^k-ML (./&N$&'|B)l+kdB.R/Z( &mSm*D%'P.犮660uܙ e`4@}#vhif6&L/Z/+m(+/B*oTvRd/S/) /+o*en(Ѻrf-:܉ݧ9h12#Qya)aq1niN#pfֶ䢮Bί )Ln+ B2q0',grq&a q*qSqQ*4d]=_m- hi1R- 1׬cB+ nm,Ds/r C23 +%%+&'k2TƯz21+2)0"Vۑ!岧 fbi-A-Aryj0tD_13DϬ0)43 p$Ks,o)("CHB(l63K)Զ2#%vS>%r‚)WȔ`Edg~>-H< ,_0l>4Ud^c!iRBG1o/v(3 {2Rs55^*6B(L/\_h*`0=dJ6Mdv&4K_M!coi2 RG +%hf\$fjkNR5 \5@M! ^jꙂ5mB,B6dۭ Z[2%`hfǵ]ӵF*l3^Wwug_{'&H`[$qGuNB HY_\qG/[eʴF/Qr/&Lghmi2(!SsSkk6mmdΪ3>Kb_ %N6t4#[.Ը-4r(LB"Xp $aJNBVx3_{ԀWpý7śR57O}kls+!|{#y8/ 'NN:;X^"W*2oyE7~7 p Cw3w4 3GB-`_vv$ ”A,#T{_EWvUڹRhc!|dj{.W232\vIvo{! #'hN %cBc$Bj9_ߦ;-d߭z/4uOB_Ikv-o*6+\|KyAĝhr9&y9Wv)MRgG߰#ɟ4XPĈ5LHL2UxER9v$U'@رdJRTSL3i֌ g)SBĩQ#DDU iZFtQ,ZҥD9dd)#'G@JʕIL$-'=rу 6qG)tI ÷Li=j cǚJ4sdjթMvכLg:)#4𠄆O, bE5JUd*+V wN$ǶǷy뢢But[y՟?*Р\gFC,90$ R";n.J&{dJ< #.&IY0@.#E:i:DBt1MBI66l7*'X 7bJDri#N0;ɼOp:/8$(<Ⱒ"P@Îۊ+DDD"dL4ނ+L5ŕD:2D>H , .8+@jl&PKک$pԑW5M1يD$wcҷ jH<:c O}]"3;1$ﬣ:Sh)S 8gi*8ܵŠ-w w#Ѡ;Ў1w;IM>G D4\1Zi|i@d 9P}EdbFsu5| 'TV6cDրJ@ d-iڕ;[UVLStI\M.Qԍ<c y-urjD‰@uB 2MRIAn% 1a%4#-Kc'B Vr$"0qҔz1 5_g&܎Ź$Dbe/.m&SZ;)DzklwU[?}[ -~! ' CA'q+[Ub!ZGpTH3#_Ռr.\&Mskv:P6[SV,ݬ7 NvբuS&q [O Ԇ@ءzB6y@ែPZ0쀠Erg>hpB)I+8թ2.'LPt[ O~q643ґX7 B# DBZB"J\eKu+Sh"4X}k!+w R s % O Q^G .#9LHG2$w/| X1AA*ϣֆA 5v4HmZ9Frr}@.`! f23HD&p1d Y\:* c1;Vw âߺEAZ$L)fĂ7U R[E*01N?P'X0Q[ lŖHHʬOd)k닁`,udU~F8$P\0) b3j %7AB/bA&>PH<=a|WF7@voK :|Es B"`D&>cNUeu I|)" ?J6t5O@lXobP""49#t#.h$ 4[QQ#2 2 /*TI5~szB%.fLxBBMsM?ϩLU4Vc%Vrq_,\NN`u<`Qa#7 v$Q¶Ɩl(FFaBB6T  ec2V F8nTerf/fy"Wrs+"hqa,QzOC?U]ֻ".ːN %o@0I˖ul 1& d<lE@wAʵrWHx5TAzBJCnq6F!rմ뗊7f52OEsYTwu[~ 0QY4L.f,EBTw{whU.'8꼚y;17U DQd{yԯp. .RW@j3^[ThGry {X=E3imԕVeٱjYW︗Y8#j2U@Ǟ+eRXP:  ~[Q[=,SuL"=X \N2G΋qڼ 'WOl?K|qA- $GTYx.c˦`a#|9YuBԧI d4c+`6hHe;Xyo q?)$H[&d_[NːFG<} t\"LYɟ'DC(sg3JO U_|R55Q7f}i;YFd 囤?O^N j@358*V|._rbiCg[ ٔ&pG96 KDs5H,pvSOIb1 UMeg[DԥTs19&"]' HT<7դ?֜H.!=%!CPb%ڶ7L@\P?z߽NAXFݽ֗5YT7R}/ N:c6-AJ%%*6}T+ao:x2=IQ]˥ 'f͐G.s飰i+X>̞M|IsXqNS>UD\p3.,bP~8\ٖ/4]j:Cm 2eXY2V­Dg2E`n\ evmH Fa I T!QI&b]&!i~ JuFȖՂi qjT'\V։v\ҔVzhH!w=dbH"8"}eK;*H0!g ҁ3 *O` kiJkFH#Dg߉੡*(GxXPU'r$:کXL)诲9 .o +nJɑT.LHY;ɻ*{,2i%DmU\0iɽzˬJ"ɴܺ8jwKs9 + 41ݯa"/&16[!>yR$iY]_D"I*Xzm\Wͬ7s|ͳ?n,[O?JUwfPTÏfNЎ][bf޶'x87M.gjk\{'sί2uJϬۥRnVeVp&=yJ]֖:t&G"7섇_-fN劃dN'>3D,Ý>ޯk9&Ԏ}bDd ~MXi;;\4NXr<+R=QHkiȗ]F+ulbAG.d~=I|_ªHߝ믟i)9lp@)6߯+Jr5왨O=58e7{{#uz+F%uE\1v~4f(sQI?EVI (ml3_ߤ\V*?S1 #_ƀ"YG(t6Th ?cr*4u&mv _rCւǁ!O.}D[&# R2f)EwsQV/7VtMsV3THb.3\47Y]hfScXf@?CH!8*wiGب@3g$X';Ci]jц<׆LZPօke*h؏ٸЍ&<TdHuȆ[x{YlT5S>'"\H?<]w'fu~䘌PUhpYǑuGY-D)*Oc·MOP`5|4)|BDAe7&tb!J,5Cns8lT qoK8z.w D#O;9i"8X{qH8.nV]oy B%xg9]ʴa,v۲]rƋs4#jSiHRIÇbit<)<)aj t6uRIyY^y]I|+*<9y`o66}1]\B߉-1l>yB)q7ƞ$R O)g)ٜj3.USU1i6Ihvr飊$񠈩9$4uJZPwy U>7**XJ!Gz12Z}tH]d-UY=?YAjrDd(ɳtIP8lt Yj _|sVoR" jYka ]y pz[Ix(IZ ~*Q7zrb(V~Wsiˉăd~YXqKcJڥ)c^P(mQH#ƁsZ ՈZ\^{Qq *?7<84I^`rUEe eOJjjaJS%_x G*27ѯgW:IzkjnVpP饾<3t\.B)Z]6#{6)+?k!:,ݚXlSTJ(Gwrùizz}L UD8X%fxTkFXZjC< Ru8 aM Vl[}oV}yi\gG=SƅEG:"Z[[^]8UYQ0jfmi3QFy:23R`ʡ[JO=I@_ڑ4O.م[ʃhY rZ7hx6+QM"RE+ AvfL&eF yF$( !yzsrĒ3~7ktm{e8;<.!(d7!}jH{[al#3]78HÞrc7 ,1ЊY)>v;"(~꧴:4+lƛ9owwO\QyY2QkW|@*#(Zp'p;N2=;H7aP8Ƃ,PDv2(ʷ*|;ʅ4J1`QQSjȻ\h}7* =<+ t;Rb—\e(ϚĤ%\ܟڷ7[8sB7Sʧg*S1&ηҚYfwY7s9;M?=8tl{xxdC5Ioyo4La|1O!26 efl5}dF;b/+ {FSop]z(1:z5'Xtoֵ 1zC y纍,ؙӞ̓*!-(SRXXRjI̬ߺ׉GT0M */$FN1 憿/ػ v"id-VY9"W: KZN #%@UC9C9 &395 Hය{jvjLVofsoLZ}HT dV<@= N+yy0'O4:ޛZ3ZV͜ 8x'n] ןLT\΃ >@N9E.D.#'LޯOfAz0r0z`F+Gf-H? ;&1D [hܚwcڸǭ.:U}埣M:q_N9>ꥎ."gV|di{SƆ@ :w3?U9ʾ\7^,`d$. 7=2譆uN ^J ,TȚǗ9޹̉= 5Na$0|0~9nD>9 ӷ+?8TgVNyX)G򐩌8cj7:a.{-1 A:nA y _?7;@IXDw 颳 -I>AvgK3 .%N$A\}_ߕ^PSdLԷJncEX^s.͘^3̰ ѿ O ` femWvհ?O҄ډ;g[3<ųK |J`R <*3BQbAL])&!%M@%QMB̘>4IN7CQϙEeU+քTQ^}iͣ]mNf̝. سNFlW\qUśt ]k+^HC4y•Hr(̛evm:tצLJUuUPE)gO&ٳz U˳ۃRۊCѡ?7^ę[Zs_GC|L22Xl&^>z&Z'آnͿ qͧJK? k꨺=B 7p㮻<H# 1Dh񴱤2*B/ȫTQSsP*rO$kx2eC町2CQQcORa $ZD0L1 uD  2H|9DsR?FRRmjji< Ώ$S̴2J``< hG4:3 )ZeaZd} 5J5ɂ"蓊&6-KLMNH02 '|@5`UI`]Op˗$8z@`jQ x&udΨV V(|rV>VOū(2+1r=J#C~h`y`"Fףk4TJ׵4D`Tf#n x'we٨Qf%UmIO_n M0YK'@nv@oo%'< ;qgqO‰&&r)?37|rţ&x@=/g[C5:sΡʵkWpŠ}CVl@F 4G۷orMk.ҢZv˷߿~ͪ(ayN_`Ų rQ>|xA :4tȰh#8kCb{m(L8o6!D-bp'+vvI`^as,E:BB֪}C˟?[-l?\cptg`^ BR~B |&Pjf(܊ADl/CFoIr-Q..ZupPCM]w_UIݗ`R>Y^`ae5cUGUYQ`_Ry_XXvoAVYYi`!_p!LOiTk ⢨іG+NPgo鈐>M\s^QFP]V9^b%&ci:%`o ~b'idsvg ɩ6vkhD+6wqلVjM"G!dj*T܍5_+ܬVh0Ķ;VkXs)g?K 6Bm^jk⚨)ͽmn*MSuF`s8tNG rüܴXZpEhȨrJ?s ,7ir,|}NFrՆN։7-Ԏ3뵞~YHו(9SL$qsϝR MD?MLx~a.yFI:ޛ6{3M<3לkeK-q~s,9S~,,gpΆRcB^3Kvb C񂧵KF(1"~SJ?% VC7 M3!5AQ 5h 0(NmburHyf:բ+$q5:Hڬ(6o Hpq[t `@ d)RBӟ:ePnj+-dgAUn5k %|T(l̖+Rb'n&>[3?Rt'|]MJaC0k@Kw!tE(N"XyAJf/w cŹF6Tu#$X(  IK' r+K @7(՛D2JT!*bC"(eTF'Gl}C)["%AZ6jh#pHi̔ U8+rq\4{ouÖZvvwk[&ik:61# /%Z,mė 7~K_ h A|ǧ22*D@9ske[AՁ,kau 7xo`3;;@0jkYPmvmG%+nTVz+2ZTkg?u%\\ovҍ;Pg‰N굮hOJÓKl6Gmp|8 Y k_̵PL]@O|[; 7\񸎺[pG 6/zmkƢEqp;-aBUIJ7s]CD;B s+~ w:{pGFyc8zoQ:|QoІVތ't#iῤnҦc=3@car/!RbJ5gs8W3~kp0t0tttwp`~pkabk~`~t|RWk,(燃7~5^$BUgR{h_*8D=AqJ:PN2V (_?7P.xx ' yփG쇆~gp!aznh,xa k!x;~x(k-؃@Hrґ{NIxu#{2DR6.g)-w,VN8{pGygz8xznr8H7(8苴芯~5؋X2&Fb@G6 QNIkc3գVxvUH0`\~PpHQlt x~؏8hn ~Y~z|0W^'U2wdxtimfMd$GH) 8!X9zkxy_Y`9N!=Gcr)rtW<6F)i@/V =ٌ 郸(|`镳衙5gĘِIL9U p3lid@.mu9EyEDҀC 9dx|?xȋkؚYn@Δ| POY `TYAr8I{?_B]e=rmDrx*gEN0IRt@ H YX(ZH1NPaɋ9uP`PiotLYM~У$hu[VgsQJ0 nRUN i&RI@߉H^yQ!Yt0IJLM1p p =:)*M1'oaX\ _{c{h1#Qjn- iras0r݆0`XäzR{Ii@ig.zڮHnu)V-@ [ !X99۪*B*ПEbWʨۚ3zbQд2:d37 Ztx`iΊJ@JFXiHIۮgPhP :]PU{C˴gUg@NO\AP}- z PP IjL]ˮiGQ OJˮ嚮PAPbSx-tHÈ:cN1w dIuP%qAKzRPR]]:;T[a]LkۻMKYaIQ۷i upPbꋼ^K0LѶnFXX ]|!V?K핫4hȺuJ[|:s[09CRZK«$LL].̻]PFJ۷n` P yKrX~ pNZ9VP;Bt,y , @ xn7ë u3i/ Fk',N#1O;waiA\,eKd ry`  P Dlb}+⑶)УjQٶ` E? CU/qk= 3:+Wl1ˎw4P@ʻWܷ+͵,…ȎlɼG:j| IWLW ˻l˔sk!7fŜdWZWppF\PQ!ۿ\|#նSQ^l<a8@ phb;LԱ QZZN?L5 1]D[9 v^Dݤv-!-C' L$ݷ)#J˵\VxLG*ݤv},JEO=eeR˓ ԓm$dLsG{XC"4=V7+AiR2y{{#»nQ2|^jp'}-J*&,}Mb׏PퟚMmu }C6s*GFV|Z 賧_A{W(̮-ȃܮQ|>&~]H!g\1A'_ Kq:cް{ުxs8K\cF|qT;a&N䱰\ }`~Ϟ#b[RPM9I| dU磝&'+u:̭ _F;:FnC f*׵+#fV'Z:tQ凛ؑ;OKo(qsJ@(^`3 ejk0QDպ*Ҭ|X >arA|MKܐKu!S%oP/p>,v^aJNsEVun[˗CpPuLnC 'Z;Nϕb.nal7ϐLw!&EKajun2>&p|g]].[=OAKh0b4O$ Е&P HlΆeśPa;mm+9F3Q8r 0QG?K5{i{SZ Ыی.ܡQq4IO-to=6*1E\o]g|OX1\/&DA{@`7 x^f#ڣ<n 0OćV!BkVz~&ጶH-ɂ@ }Sg )ơ3]yMt8"Q"E5 $y#9 Ⴂ1a&؀ -tꄁOCعs &:,eڴÆ JU nMkab%kUim#XqSdhuf^iҜ!\X޾q!ȑ%O<.>~TR!ПA(4iԊPrth+GbIƈߵQΩ3d˗2cҬFQ}}k0juX"h^S;Nm.YE.:M)|Fo]o@oQptA"W5 34E0C L! S\M-!x 9喃ʦZ.GZ *K=+ Ѐ=#B  :>L3+d7 LA3 5&T,,E,iO ^HFpDA{:T*""k+"J+ZºJ  KK* 2a, =P,S=T@2{LNhdPZjED5>" QdQ bIK-uGM7S 5HUUlիR <@H"r/S4/#hհݓK Q{QrRY;.*q/u.Gg"HWmTXUV}aM0xb+>r7PAӾl)L?e42Ty2 9C7nf*2'W2R8U (+[}`'mEjLހMB6 VO]$}¶##2GT5lvccATO7` p/yn )Bx<-gz͟Ꚇd'Xv7su]?AxJ^ mlr`Hd]LB{(@ Jt$0A:CɃw3m)R  W󝏆;`AK[#b:!Qcbj7A ŋsW@[b ] c$c&?8!B$"m ?U e~ +#Xr*&̇@4@D5/JD"F5Kad,Y‹d&Qk˃W _U+] L@|x~<_&0ҖV!!HIOX!I&o#yDa Hf:Mp.cJӀcS|`3U*ST8XUIJ]T,>L@W`bE )ܔDi2@EDmkC8Mzjmv1dH;9d` gxS?ZAuGOqՌjLVzPЇW r^qMD$HŌf&%fVӛ(eʴIS4gKf iv ,tPTu)B"wtL0R!a R!S'/k7:I5ȯֺ^1BbJ,HʦN縩(lSVYTHe?qj ҳܪ#- ڎdxI39[k]K՜bnK6E.i*_9v+Rb+WdJC @.A=aґFBx ^f0F;1?]-kVE %Y"{H?&w<h% ,@1i@lLG(B9B#Gc@+oa[~*UOL5P0 J/X ^W+F3KZfƕ1@JJE*YIR`dBXV4_)0s ^p0z1J1`ֺVLb^,Ha F JKlnXnv,L.Pw bT';A|n-|G p#U!~/O7-ijS~ǫ?;z`=C;׃Yڛ/([67ہ$3#p؎Xh?)@7C?#?,@?K=g¿򂝭Z7+¬3@1)x=Y !8llI#xmLD!Ao$GGsz\qI7GD?\.F|#G`9 Ã37,`]a ;L\z;`KIk L!,LL 3d'hL|LA?ǞdɃ$4>|ILMlL4!(HHtHT,Ɔl6;銸óԀA۾@FLL O\ɷp̧ILMnTtJ\OO-O'x-;<+T>{<|Pl7<QdΌCaFpѹ,)dsM3T<>lQܲP,LƤOEQ$LPK(R=MdP+eP%lUIF-%<$HS7[4PPgpR #&hT@KO1, 3E6MSGuSEm6mR"%L,$R#ChETeˍQiE<۳If*I5G Wq)(WsȴdLLp!XMK/0X7('SEUWMT%~%U>HLV X` W]`)Rb`nTI-aCC3c>4c>aE$@68}Z:׆^4H[q^+T6*)Vm尘}3P`_j( mĥ0Aq5$Xc 4^<7GV1R#ECF>f5>>@B6CVruuT?$AƲpggpg%U0`FD\>gCH r]cl,gkVphg06]4E$fi6DNt@Bi4 is\'j4N?0KRn΁`d)9(ڶ=Y8|R8kK㇎'D`eV_O`3.iH4|X3fGU\4lfihki f}Ч>f䧆EKh3́((~y9 ڵ>!xVڻdundb1fغ6ýi6N-ךBANoF6o*Vjc=mix(Ԇ]mW~S@8P;j^p X|h#@=; -V0TXeWa/~^UތpFӒ)rN.l^>CBS~jN(j/ot6m1/Vl*WpcjM RM;Bd ]2c3L3'WXL> +3xq@j4q( Kfoj>6rZ i͍-r]-7Wo//i2@k8޵NlQpΦs|/H.9K%8:) go'q!xPY,J?NP|PPsnWP0mou5oo4,dlV[ 0x?mn[Cv)rvr.Ygui$h xvj0|. >WlwVPVqb>>_9E!0F=|pj Tmj'yK^iWΝoa'o6=2?(7=F// ;)o5slG~ j 3d3/cZ7Mq8+uwCrJv"eu rjs.|iV/x{2.or_>~y?$Wx?}%dWv'xx&ܕoo>OkV phHS#&l,l1Z  l` Bnh D$J`F(6fDK`qF*h% @fҬIS$:w"䧓SR _P*R:igIlr d'ذ;c([e*T[2i-KS'РVFm:jO =#S E22%G&S@ FT )]/V.AÆg#mp1cG'Y`)0DH*N6W>sCǓ/SmʕTϊ[g "Xf 5`Ke'3SOiUS$XQ@Ye]I'-vZpk5n@`[m 5B;6F&]rr9]tk% Sւ 6HY8}7ExMwJ5U|wI_|䟁\M8aMhL;|UW*%h^!acN*d%Z)YxQ@Kj@*D%I:)}JVZңٴaOPřIY[aӶWZ%砄>(NOggՕnK ƓPL774PJq]hܙ蚍:+ dš$A>בŲV)`V9r^˔@{][Y%>Ւ=CD\sWUW3[.RJ iԀq"FůY1wƵ= m0HrB9t%':ó>%fixN.zR\)2\FL3rYhVqK`w_ܥ+0pmO6#c5qNW+F7M~:`%ǭj%3&m]nRBF$R#_ɎBj;39+F!]DԲrƂDlF Y-G:Aze|YȧWG1 A3wr@=ng_W~ B0U*Pʴ<09ȉI'J%aAAk@7^ R0ъ/␛@QɣZO}2+fRhLa‡ 4MLx;FX/jh{4a<ԑ܈ T`V9`6㰓}3 VSdXB haa Rm{Сgw# CPZҐ" ]>n|ORT]#]d%{$xBKs!DA3"XP[ 1k`pHJȭ"%MjC0 m ᜨa.)jiC,#iCDž\;jC]uL¬ HR^ 7 9${6a6 P#89E02{<}١lX3΄=\kAtryFC贘3@TsYE2k+ֱ.aƷ%E` ]╰D*B&/MuNknQ/YŇ:e*Ox;O(wxJkm$d$h"ݽA" $P@v '$$B&dB@)DB)B)TEN)EZ$GV$)pV@VH*}y `Rdc02&2-%^EхX \#e'uTAN8J,K6%B`Kwʼn۵AEZ"[$e ~vdE֣=%>[[&%B>UB=Bu BVB)_CbD&afDb$FrFv$FfbF_/yǺ,\N~&h.c1.rЉ,%! ę-Be! p%߻]K%#a%Y'6gڛe5'%CR'vRB_Bx_&y>aa"faN$caR+t)'}'Hr !݅BAKNf5݀mPH7T~줷dqi^X9``ťuJ(v["u"d($%xhy(zzy6fz>&Z&)GB,g)%x"B"]r!g. ÝI1J DkR\H&G2.^`/BUerJg(Yb(%|ba&)B):`fb*)+8"$l $$xF "Jbm!0Oh@8€|EKiAk%*h)B dsj*jjj&<*frj*bfdBz%{*"b*>ZBgٹ~ߨધ` *i^_`P"b Eؽ`>eFBk%u>k:*a *6'D+B*8Dg~+zc.&k)컆b+bnlE'ϚBZS^Xg̩z)uZI>顢f&jȺB,,)0B~B$-,+-zB*h$bޒ*GFάVd|֧v$~A0 <@L@k"֛f.k nj'&ިߺ/.+B.*(h,G)Ƃ/lvjI&*ҭ.-E+n -l]p@ A'J%4)~J`&jƯms,mD'+.2Gj@F@*/"oFf"EkE~ , n#A,Zn BLՉ$C ί0uRk(vd-G,>GZ|1B+p,Evj*pNF+b1wdjV,Djgс`z܀,ޤ}pxeE^ ^!/RW&(1/G'1}.$GnZ1(&Cd'o~Bj*p& " Y&>W=rd(>`$ "2,Z,!Gsn`v*(fB|B,@}B16&qE'/ꂲF;;*rr=늲G$&D ARAprq^VA ıTkn5O+>2y#*$fm^yg*.$}8,NӮ2~P3%3)(-B,߲,#L$3Q'O1aZ;7>l$PZF4.p?iG&h/OtX!0*^mB2t@6"@"$8R`"$?-R;4MNtO_6ftMB(PPۭV3%PisQھN?5lǶ$XicBR2ro7ߖ-&OT躳B\?\K4E]^f`JSkBkiv2[/>6ݖdsl$W6Ocv.Է}7C*\BmSgX aTvjck xTkǶYu[=6m7L;o@rܲvKshW$h+i';7t# LweTwF_uB!+sZx.t[3zG^O2+@{tM7Cy+p'\w"A#4Țr*3644uB9։sRmaH'f,?cx=zvG>wsNB88eA:$`֎b(4wwRx*.7k-}k6{F9w-Lyg6NpGmvBv[ALXMAyxU{u[*R{274&co8hB\x;z@0(yjzH:vzv=#rnK.y[m z-4<.ߺ)tv(lo()Bw*(g'lpK8S[*4243#/2|YْWi8/=h&u)'[s_X8 {E!$֯Z~5wlKWU/S|(Xygg|+87|sB#=='}Njǫ'!`4BGT7*4sp9nآeW: 3b'g'tǾ&\&o[ջ;g?YxĒ'+C[V+O|gs%(Bo?(='(?%XB" !D/PAdT~)GD=zԓaA 2"U #(LJuQ#G -DxaDIT˖5մ'P9osS(CnҥIɡa@T*LY$I%1eWcɎyLkצbm*oIl^4?u45T(NEux(ĵ3WǷj<"<%s$ψݑ#G,v$u^J*N\T1V8DC@y I`mݑfϛ3Tj&]T$:r֮˶>p;ʵo_i7ne Yj,2^[Fc1" b EdLx%Wdcq%&BMR\ˣ0C 2, (/{+'J (N)J<{7{R+$8R:E# )yf)^H$&pbvzM٤g.)j,p"Pdܰ2Fu>uޏ];^/DA+D- 6`VB -|P< ' qTIa`WrXWr65svC9`y('a_٤rޢZl`a ,Ha@ꞥNNa=\? 3 12lcr+\q 8$kHx:LGY[ Q5PiB!PpwSp 50"f&0'wtKw]]XAя@$AkM $2P"<[jtIHRRzrW)8Ćppx0%$ 8D8D$\C>x(򠱞US%)M)[DB9(I ܯradYIДЎ+ɴ+vmi8sJ%B2EFf#%/|1tuq Edt$LlbOW(N_Ʋu6A |.p4 P.yG01 C TY䐺rl#ЦB@Q6<F mj NF CnӦ)UJEKmmsG$tŨCڃ\5@:pAˀ 7 P2!r Vj9wGkFhB"!%!n٢ԲvK][Vж R8A bTЈܧF?@h&00I=VJHD0LAf4`PXXk3;r&E:dݥݎ;X?ab/fZ$y.]0Yxt黦,J8ª#ބ7J!PFF@ʦEF1ORQFd$F-He:0 :+6)YjmO$f 1jpBѐ%D {+SRFL1 %]C]1o$H(AȰ ":q`:R2YFD3z,Mv (B2P6?r@BJ|!'ͱL*p>b,G llpDp~'eB$H8\@ "L Y#'Bq11cQ|.6X)>!Kf "a ӥFĊˆ aRvR'2`50@f "b!703PZ73>0٠$JT"+l="Dl;q<0ՀjTĚ$=X>352$׍5.r26@??b.|J"9!G1@@'ՀS7cb,"d#R6cc0tB$G3I0>Ԓq3/2J#/Ri/r-G$PEe -zt2A1䥠MڀWR/S>YrGNic $˴MКPOKYpƒNT5#a%r !@1bDGr543B-m~^B# WnJ ɢ v/Ja*CmJ< 4LUєE nVyN@PV?fTvWţH;7tC"pԐ! bne7YZL %vffgzJ^EWĎIݢJiF%CIRTO/^2r0^qWF4*j $HaNkk-Z%Q_h@hc^ lVdi& UXBEBj\! -$LٕpJU%vVMjuviW6j@kkCWtV%)sXV mcWv{eNuYSi\e')0S8ܵrvgOg)EFM(v*53h'>53j˄~!PJpF}"L@9O:K`W ~{o f& v0Pe#(TKx+(5<7yU]t"+:o(Rpro낸M'h"Y)1e{/ Ѕ07}箹QZՖ~EbJ+u1:`ceywhoƵ3%[ET#x3ss+LD?hw񀦄4J޺`W!f P]k؆sx}rcmi p& NZHrqz#H$B %35yؓ}B=Qx^5W9YSwxG7k9boKމ6\f9- ny%u(z]geطԍ#׉ ܍hlFC<ԓyB~J Y/\;wkmki mkUB.b T9lJ{{^|#z')wymKx"Ep# M#OD!QMq E4YGӝ1@9 {kjp+j#PCvsFT˜M:J(s5zq7\ "$8א/OړYMX_uB[Vs d"|ۨ [7GNN (P fdB5NF^@&f;;fի w3uEjTۺg 7U[ WFTNr⦥`rrsU*+[BOaOD0cƠZN(4۳&#Hʻ a\xe1I ׵yvLLU3tFEwjH!["RF{u3UZ" }wEI#(Jd0Fzټ)nVcu"z'Q!Y.۴Ѭ"1oJvxy ?b«\]é#-= HM8PG rx1zc'-;h.q*ܥE[&h68FK x>TGtc:&vT^ɓ\*sCVb!D<%T'e:b֜_7-:;AE3 E8#4H%,ʾoώ S;qvf:DJ.sCeoׄ V)ԧRaDj&I)#&쓳SqAY{]ƍ6Z|0|)ߴ]z5MJdfC hhxh AjIkSbE4}%b'O=qC\G-rT0MH z1vI1En [espi2p5bRD"d_TP=5XOñDMdj"`AkYh2фd1V%Q!|^yhSTDAdQFaL!a[(Yk%y^Iآ!}RQZ&fQy^QX fpAfKg"nz٧G:a_,j%+G@ J A[wN[w͆a퉡h%-ƊQ.+l "Iq"9rl, -BF&ۼy-mԸ8PTxZ^{%I$<J*V EK^{TI<2J Ʀ Cty_1+zRL"&3w7FܨS:W DdWG9Jl2cS ުԪwb?u`nؼ&6w'Q# ɑ@__{˕}C݄,8n8;>.k2{y>b>{.~j>qmŊVk4I=Nv%xBoAf59Q6"۽ f9953{$AU{C=_ֳUj /{oĹb{!ܭ@bP N.8M+0zP$Q>=hBcԼ< F_$6!C-fVYė\~ꇪ3D(%@D K낫Ɏ%cx,+"n{cKQcD\ZF߰DDb_R-I)sQٕnDo(j$Vg_k3-MP-Z,d,lz"F)h02)rkeʘFzqh{^"h Iȑ.B".L-hF1&PQ' Qt]JA$YJH0t|]4>'›,nDt$rg"LA6S5BG:H9 _P51fPUR꽁bZGU԰09[)l(L&;$5W]ݪ^ %ܘiǼ`,uǸLjh^!:P Pr*W3&ZNwCBQEđvdl_vNR|<$ֺ.\/|ל;?=yϯn/QE(fn],+u^4S9 r|Vl?*LaC2y/}kTTzpG*骇{9B~2hA;Ysov7t)WYI7~ْmD=9 z~Op} Vka?ߦ:nqY#y m=vvxbpeUX%B'Gt~!nX25Aqn@cV0рp>ffHbA=S}|-6:T\04 倭Pv޷[(0>#qg@胲3y& m&9g~oG_ 5BBAz"AxKnx9*rt0sU:{pJ~l4&Z%^7=!c_pxx$@Xu":{"UHD&r8,vF!MxA{"KH9nR_}xm~q(Lo8r<8XkW7[؊3[qf1 ߓ3XIbl ؏дk=ܨ gNQd ? I@t@7gYe5'6Z$Ð'i|['i[H"׍3Y ̀g22#ݴA`ǀǖ1#'vbXz MT?J+#Z4WsCIyJD@Y~ؠY7vDo)L)be*dEL ZI3%ꒂe@ wI#V=Q|ٞvh]r^ƷbLXda}Iykrh/ii;!j!i*z?y}:z*D dp vB6Wp ?%"}?qA2b3j"Ŗ4j{jAW@zPժcȇꛭ6Ŋ(/CqrZu:cSZ, 䚥!Ŏ%YeUruY4 }<FXOxj[FBٔ4%FI'Z|ʢMIoYrQ~&*o؛ 9C{rњ 6KI)#:xǒ[N kn8$x^"J2:؋ 0xyD9 ? LJzG::LxiQRUE0;-cpqúwk:^_F?@v;;:jYg/z"8Jjɲ*n}5Ѣ tR\m:zښvj{{7'Z=wu{кVd8sp0:k9+~EYx(K[ 7!K僞G*Sb*wz-$9_Vcܤp:;cWchwckt/#V :T|3 = fX C(W??,WG~t*$h"zk"{lLn+|}<AһvbM"@K,[K€ !Rrhv*ݫ_~`˔rdjO!&AI)X󻹥k"y|ۿZ' F}@-uƞJi”lš"L ܹl!#:>:PFJCR4 97IL +'s|Ǎ^InGXi+]2R Πh(cOUD£l` O΢ MyѬr?ռ`d|+hGX֋5JQgΗ<XjHa⊧!`KМhn9C p sM;F+ѳK m`[ ī!7W?<:ƒ3(-p*??`v+x=Y5 w}RR7~Ռ*o ? 7; S3UML뇕s:bVx<x?NKscfu𼅼:Q] 2LՃmո°ZoㄯeZ@b'!YQ/wf= v&ѲŤtKbqݓY@ Ŕk8 Y^\>mq stF lFk4w}G ^J8Ci Bn€|>"vHr[ܫJ>{H޻MIDJ͇""f.02nƛj n7BQ 3"ôӪrm:ebAb74XdltL͜nJ~jL|t=:YXme6ũqڷjl-ˀpt,~w 717F3(y py[=zd淓)a㳪5:a gM[u ԓa(KIDꩮzNݞχ aXi+ኜPH(f?V뻽E1g>]=8@^C"AA//7zl"VXʎL-m 2GG4xփpFmlz NDHJ)rc7+E') h x @44aa#cζjVK)۷LّcEK9ܕ,sэSD}1iO2Ƞ‰$~Р5æ^&pcS:ˊ㷠VktidL@*M$m߆;nnP@L#8[" |o0?@D|*7VC\ '<"݄b0V ;>z eK&$#% 6ep)ZV1^s3|/C$AS:4\D8B$Є'D! Ѕ/a e8CІ7aEHCʐ?Gbc> Nh ( щG|AB*y6D.vQcB2ЈH8JN `Q! ,^[°#PxFE5t#G Q(&H!‚K *cĀM8sIC@v Ê+="z✫XʵΜ>sKt][Yuݹo}÷_p [WMUr >kƝe_j\X^;[6lfÇˍ ӆ! I8;߈%FR44reK2GWYӦ͞-$a&V4wWX4Ƒj<gnL0ɀ("!&!}p'a=YvՄaxaч dlA 8ҎЂw-ސ1f$:rB (D %`dKYrw煗zD#1 fPD@9цYWU\!敝ʅ]o!WGhAڢjt! h酋j_|}.4 T0TP,R8i’K+8 Qf,MkxQwZ RUpe\׸mW^{ס!fkTtQEe:_Zꇩ* O ʊhJdy?{k .ewCFGXHt1aĺ,.s[hlѠ4Q3 @zlueZ^ hOP2l1y 506{5@~3tp +uY&iKp,lc^wI]mwM&so+oIkx&(L>yʝMfy-_w@a s^-1{qǟtF!fuY].Ty}τԹ7Bo < niz^@\9;O$2sDDVPҷCeN8E*{"6֔JLC 20#=e9+!Xy8D)EQx4/â̲A E> _c3Nρax8 &э@%5h+֍E+̐B64XΫ<@4"*9JLR,kדRC %zw{ #8Vʊ ]dI{!3-2? isZHJm$B4~){Cry:L'ׁfĢ eȌh2bDOQZ ]MvBٜł$JHwUŖ+GyֳsMjֆKkX-[&4IcGfRCaG^8vKFS4uGK.e&7 i ^Vrh*-SkҢ)u"5{W_! CJĮ-;4F;9B!Г񝌓^:(L)e|+Vle0q85nƇ+@<gNሔ5GQ &6faTǨ91vzB5 #&. iuw16bhE^;5ᯠsЧ@͈F;@A $$Q@F@BxLٲ% @[N3|l&>ƓJm[{ݍ/Uӎq)͔РЎ C֎MmG#FljMC UrDARJ.oI15#19GvTן hȬVԛD74qncθƩ=d!o@}q{ 6d8ƴ@̂yuuj~Z^{}萋|>NЏG; j!W`{=.jO}g\jwB"LȄBps/a`'ehײЬBaOقvLJ\LX>ȼ5 ic\yҗ"=rhe{5s ]' v5 ɪ|9@B{26Nx@ў|% qs~xڥ=SF{eu|w mQux{v cS2,MiTfAqjҷpTs3w]eP%!DJtfʦ~yCu0'duwzuF\'c7zPu RR^`X8|ŇH,hI6TDR"f5>brCe%vT/,C\y' ExW8\wW׈^"_uNHhxu8i艞 b{"f(5T#%V`I5Vr(ЂXA]XW"] OaOha\'8؀8 ҅H{\8\hH (W؎Ouxw`%>G2GTw?7ɇ18hUa"J#UP]NQYts- {Pzx鈅ؓ>툐 ɉAHȔMY)xOK(P1 S5f@ŁK+@ב)h3d&&pt1&PPׂ#6}198aR@銒HYG]Ny腃P{h^y{)XɎؕ 'nd$@hJM7\U6q9YƇ]Oei/%'BnC u \xu)jA򸙅0YYɉKI:O)Yio* `:S a+m(PY6ĉ%Xؑ}4x}:a!ZAXxaRp({k.|P[!3\DKm:p iNLIdkqXLd˦~ic+XpA,dAl;,8aDA$>JFm$+JY.%(POXoO'*.1Y[&ɚ;4!餥d+nN F5pkJW P+pK4nܯjkkXD{ 7KX#*,Yœ* ~g#Fɝ.l,ОŢkʎc ѱ `a,Kk;WW R $:HLլǫr CAh1CˇzsPՒ#,߷_G5|J c{ұ'Ĕ)Ookny`@ @Sb ӻǪ !8]A'j bH\7׭}*R=&wd0) \{ X0&c-5]5=Ư\qlKq,dy0@ T-l!~]@}n.:I62&oC-->fϕ|`F[MU;fjۈ;дM6}^!|1-\ֻ܍$}ބ}0oǀŨjXP}7b x MMٵ͒˹KP`,| HL`+dpWQYq*.Ǭi,g>&.'ݠ.5Th+C0,=>owjF dD<%%DkA%<{UC}__~d^sNlf&mEmn$na6_is2=,K76٣SYʨFEoc5"fahSdɦ>.fʅ[&hke͞ǙN <_OViEQTNo.5aX]zg`5Pja9A;̙@W5GܜlШmBq^NE)F(ukǕ {hY'!qAO쯉U›1oh\^ Os:, cBAoK˲%ی91j|oIN2X@;Ь~Z~ò4zAo1=H_g_ġVOgA`0` a [Ġ+.nhG&L@Cɒ6d1!@hPӦ7̜'JAKzD4{9,QNUQQ+^y뙳MǮUX:q}[Ǯ]{3gB"{ڕKgqϹKw5tbr~RAQ8d $J+DzYm>W& eY&O ,KsC1r4qۖlYPrΊW4߱ؽ:-m}:,8!4>cTl."o , 4P`:#{D a  j Fiiz䩷*荦r2θuk餠"鎺.;7j =,Jë,3˸./>T?sC,Ͽt0/,3,*jD@NDEWg1GkqGQ#[Er&xL(茐,31uЋ;:L?,@,fSC-Oltop Ct5EpA3ktRILH!8((qd v@UUc\ 6蠹rE(KW:jʫ@ֽ`UNS)yfgY]er{grwyAu J- VO/2X:>hׂHIub8 `z#X (kLY+M> 7> Ydŵ9gs?go/wdmF@z=jཋ/CE Pxa뮇ua{i`2&^ (@"[cPR('^ƶq> W ܏Y5wߑ<3$?h=347A@ ,FfûڌJ7Qpb4<"mG3W1& X Qe @ a8C\4=S>f\\a s1k/ؿ@)Ar_ Tj x6$yDH.9TTqx5)ێ1B P*CBXؔ&2ra pEѲ5?q 1GR?(s`Zx7@[5)ܥ,8m0MT5+%%Sx$  EF3{iC(.%d){8L~hx""Inl:6uh'm5rܜR -oٚT] 3Et)vclvձ2*<qb+fGQ%@7qIL+hFSQ=聥{ ^ΞVē` {XĢ?t#  -tV~T`&t;6 U-td-qLB(:WFnwkCDʐ _^uLPXFW.EY.">hJ|H;Z4* }@-_Vy"ד굥@k+40Dbz1܃{3A1% 4:0.$!ERL 7H.Ml©(B*l< [jb 5| 34C_\f4k[q;2jʫT+,0 ()) Jl,vd@K/GR4J$.G(pE#38B h `-1,0C $_FHȰKò8$s7|쑋6l'nl i̦0iI8pGKY{HJDJd@Vxa}G)@MA{h/-ł\HД?c;'P6e#˫3a4$<ÐKdK8CIhI&˺i0s825ĖL)q -$L$81?,?a ~1) BaM쑭ʮ/.[  D˷ ˩cl \_,Ltd;L=fN$$O.IHOT/ZX ʿ?]5<P ,[Ť (O4N̐BTȍZ֙mٖ\ϕVIi @ ;O,h(}Ua^cta[sfE)HR,]k]^܍EMM\ʥ-]xݥZk=\mE [R^R2`=ڥ]\-`M\E xDLaDO܁ȁ/b-pVp[2V6}.<+3b]^q̩DMRJWܔ+T=kkb˥'4a.S |` :^e&]ѵ&Ba iaV/ZH³[XՍ PVՀZА K~@X 4?+fa)#-Xb[[G[bmP*V6{W8d](56)V:EY:Xch.i-e vhO)q4Gj{: Hetͳe e ˃95eU (eSFF)b&H@e}ʩDܔˋT$2 Mqb,@UkS^^SXh^V>PW JU_]z P2[^_d%PaL*]vK$@CaC'kl#]L^lvo$PTlF'ؕv%@˞(d.<(G (@qmTm܎R EFͬ#FVhӃØHKFM&ΤoϬn'nnh&'֙oo o>o)v r~n96GY^0mK);M=JPՒgz <ʣyѣ48<tT DLv'n,n\%%jVNOG.V_^+*u!w_0ufPZt'HR'Hl&%p8+usb/aP|4L/a3^m? 3 Wg}O'oMۑ8ՀTbV[!u"&^ujUVx ,xo^r_t,vވm[罁/mxwjᆿv/mIJ Hqw3w~k fRehz8'e&Ih~vd+(6_nu}(u]W1's*2bx,O|{y'x| 8yǏ^_a˸܁sHhA>J!<~D(,|@%T<|A!?` HƒE+V9hKTU&knV/5R$ +_\1S&Svl] g6 C4$e͞k];%4菋F_" .ȓIs4E~:?R< $D 1Sjl`gϡW$Q<.!M$B 5Oὤ *S_7tQ@C 4 SWXuj!fVf~hÄW]J(ASD&͈ĈU~=XalZWf PF S$GFsY6r"m 1pFtE*~$x"dB)}DKrj@M Y`AVOmx` *HK#T*U`yU *aYN("^G"U/֊"U?f}AwzT(xVSiXFH?զoSPuQI&9.去k7*~kM ȫP:iSn{5*a_l(Q2jc:dTp+Q(#!񪮺ZiTfZ]>ZZj#rzZ]H dю214LnRNW]"quPەvhv42Ǟ{Sa6畠uWCW@U֨Vg?ö,]6ǕG^WZ9W[Z kvfvk7 Y!!W1 (oXeDvXQTPyWj6eG sJQT0)b_ͶiߥzU68\Hfb/cG㎥Wf\jR8[hb+g\-=+dHuEZWXdd$" `,(r<8< Հ==L*'$*qH0aB LL[3D0$&BZєfC`2\Fi 87CQ k&q >Ѐ#HJ2 3Ü5#O>['IO07a(oЅ!OCcВ -@h`>a#S"s W8)(i*0jyLF3Mg1Bx7bse.JYXeDH *T (X=Co&1D?lLh)K-(SpsHK E3tY,AnU.cX:-_zYͥ4B}7sJ_H+N}(rg|J| تAհxpiQh \gmRkV5!=椰0!ݓ -ꤏm?X% ecÙu>3VV*-_l|w㯧 *+HA nHcO\8\Ή:n{2=9sg{5y Qկ! ݹXϐ%49d(7V* ̜dBzY6' "//(CyP0a{Jlb lL,;ʁ䖶̥w#$Żp5R̍qcs%~i:'#pRYe%j' RfX 9ydF>'vჲD|:kU7/7pnOܶJBgВ5邤u:XZƴEf29G]9ͼ|'զPW2VFjf4:"cG>+`k c`afT]6HoY=܀şdF4,Avû1X.L YlD/2hUk1*rA@D[Yr!hՐՄKI&G9VTd.'ĦD@Dp])~ҟ'#$zI| <ƞ IJH0^XLOb^ge:cb[Vyf'zv)}JB{iFf&Lf|)e Y>Y>AtIb5IKGh$rhrQ_#hfo )h[VJxR;\ACRud&꘹Nʤᠥ.BRvfwngg[>$Ldv§{'j>+&<+)WBZ)d~%vk)Lk&"'%+XI+(xdIu 0R LN@R}|{ y앟gbUDLܜ0,&&֕8iBdbBfX)$|,*Ffie,&"%k&k|VVmR>N+6>v+)Xkf)8^FrM)#jޫڮF^"bsLnT $ DMB۹ ڑ^(h,F (ryN'nvnbv2,gmld..22-Nkzb+|))$/%8$tm^&*,#vH LqH#(TGRPE@$ @,!P.ȁEgF&rvc''%|b|Jp|r#.-wnnf~fnF Ƃ pfB%4+zpJǷ1q2@U^ >yEԯ^L4kbc~nF'&Vl_p_qJ+ qӚ00 0"W-/p)³B$)##A C._@$WDef_Ę *v>g$ln$|gƮ2's1c(X0цzn! _62Rm"p {#7g$/r~s)8/e,uoAhy 4 XM,LzK,,'-/740gJ,N˂)43233F76m%j3Jpp3"#C9s* .B3AF>oElERCs4{&4/;W5Gon73(|e*+B)l+sHonH B*+#^u09u:0!3-]t2M8 +o)8vƬ0(WjQVg@:̚hr[:j;t0r*Ă/v,B)4sX+23&\?6Y6 21_'7J627]GFwtvbs3r""A?h$@4$D.7 %WSfh)j,j}jwCsnrN5mb43..ol϶Z_%|Ƶrkt.^5ugxGH#_v3#+-%x;7@h  Ɋx[8.%`x|xvgc%#z2§#7w~'<6+B+o?wuk)py-Ķ/̶8Fn|^^]ouHK 3/Vo'@TAv[v'~FaF/Htzq|R6oz[s'|z)Lys xr8{9B+0Z&+7-]w+sr~HW5^ߥ\'e>BkˁYJi[zj)^^ד6\]5s:Gt3{+zC6pú{y;mvz_xOw]yaK-{F;8(L\HZsȁc"OoEh/S02! ž ,B1ڰђB3sz)B3op?8:O+/5[^bSWI6'gS`~ bpjR"K;&#剘0rp.2I*UtW#'9z6k)RĔ.'x?0o9IXҏA׵7$V[!뮼¤0SK  PCEllD G;L;l"(Z(pa7ݦFZZQ%O8d&')J3CL)VJc'⼮!jLC IHJҲ:Rdbɻbzۓ#MO4dH"Y!kB,8pQꑭKl$ϳ2lbM1nZRH.%,9a/# R זOڔcZ,?ȅ^rCl Qb(  t 8 "HmwCTqE@[!QKl=)a)Ha e0^v\"02QSG>_w@7R eHn@! zc45)صю`AIdH HQԌ-EBh `;T! UѲF AA,㊆0dqt_޽"73]O*oXi hk4doxp *)\`<@yT+;! AAz`?NCY!ɹL ȕ ܞ =y(!*/8SK߼GrQ6VMBc fnb ř79V,]x1 HζgIdɄّA IHBPL)?{`xvc @ XdG4,{rZiXrK ZFB倔).Am nLi~mP&ЂF+"M(Nj5[rLk(A& ϽRз@- j`** j =#1l[b!h)%j˾EB$"!%%H3ePnIBaxD!rdl@8B8B`} ԰m,LAd@5@|f*#%Bbv˶0o ,zN@* 8`GJj~ B/D2{'FG2LUosfJkP&ΆP*E)YA1:4O abTDѲ$ݞ+$`1+ LMhg⸌ ؏ ` vp%%Dk0ba& 1%@+?"q#oH U(I0_$1-"\k A` {bhn"r:ebjأv$v!"bH/ \ # N)"Ӥ)ܚ)4R%4_*Q @p41Sަ-h+F2"#P+=Ȁ8U/#!--BB/thE^b`B>FLN +\M2Rl -2N373GeXb (.Z.^3jp b&t@eq)glj48(zф 2TCGC3BqH1PK,3#&h؄(septeS#G4>P QSeU>G?kiSʒ TK D!:yH+ A'A%LEY: B,= !gRQ(S%l1MHB#gvpgOqos"nw3e|tPAvRP8s5s.4-_1@/Ut@{l\4-B132Bk9憠LTNtAy(`O\Wc")c6>I-[TUSڮml*j n*BD3Cv)9&v"Yo LQ8qV0hvJp-`+)he<]={=53)_aThRH(Gv(eQ-``_KUȣpR`uhՎ٘Ԉ)llB0*AX' cЏ)_P\S*SIQapgg!U%̕5,#[~YEd fîL.:VXKz"cfHmnlvn\`%)M -A--_274|-Puqm37]!W#4BRnF|q1tm NW/Ek=-k$ŶhӺvwG&Ae;ӀχG׺^hgPlpv"0gha;À1ۅi3.- ha4h\PcvxUvFǘG|H)-Pj=H=,qR}k ,W\DE, Y7rӣh5pU$!"R`Y3ar\ cd%4%gJPCG5yj–bD.!r8l%Ɔl#[uWC-l1!7mS!hx~? 3L*GP8s)rP"ASkGsǹ'By[+o1'fM_6!Q\sH)xxJ<^Y\Cb,M6TūveC (ڈ:~N v4K CBT6m̭1>Ce膋v>Y(9%t[037a)$5(שfE(a)J<{m#p~@"s9DP!o'Z]j"&(# bu9H #(O $ {:t>Sn,{_4rvjٔm$57[zˍ3n{l뢽ɀ=[ȵd`S%mq`}}{ 1PX(8{Y:fYT {R7<Ǽҥ+О.AعXSߜS=Jv{s/y>'r-HSfX+ =Ƚ#xѫ}wY}(:ÿ"M!!| =,׳YI-,nWB޵)/dޓ_nC9Χ98EdQyoͳ',x7W۫B}4]~<¿Y/Do< QW0%EF4%29)@-Nc YoF $%^%ŭ7|&>S*TLqV= =fn1"GR=i=vMكi]$MCDKssqaj4?PNY~\Xr:ە.E!gvPέ;x[QeBrR>?ZSE=^ Z&P&M24(O0S82< =„1FL:v,ǎJn U2ʕ,[| 3JS4kdN4KȐ?KbEcD5*T@TJkիQx*'lدwJ6IC2\6݈ݼ"ݩQLy$ 1ŕ;*9A+[,9"G6'^ :bRI")tgCy #&v2Ӥ K1Ƣ}ʔ2J6DZ'RC5DȥrjU+\=h' ?>ܺh~߿ np&vJTҘ#Yu0`v!iBJ*|)D hQk 94Ei1!<%oEWH$K%s6T)G?7PpJ1]y]]VVJz^fT{G&|e_^l#a XJ!x`(gfX`ZTr %'kiVmX#&`4 Eq\(JMH:Wr=NJvPb[֩G>VEM!ቷU|XZm+tr~&`pʠ().v),ȉ-BemnEawZؑ[~#KM.mrur%G,5ı) wdW_$K1eHV[& W̪%q ILy{K&m7.Nu .TzEwq4dTȤ0P9y[2CgR18 x4*@xj4AFD&DX9S? Xũ PKY*q[ q,X(n>5-'_|Q5O^T` 0 V4׎LN:Du 8*j)fIaE득w5!tFo`贀Rvp3d'+λ$o|tA2M`'竻\?I&LBd2l||g\.[+п2SEٹz-nw]xb+4Nڄw,Z V5+T,ߛ 㲺,XLlbʹfg]Kį'yRDHBm{܎+M;rRyz<:',esgXbrj#_~b$)-#( 'Q$` p1 zV'$t*'XBqx:T?^M_MN TGlDz,&D|47Wq'GG6ʴ*?HD?xBwjfCuX(t{v`#I6a p83W,JytfX=gA3HiYZW\1jY7 6{:dIj#Das*+~Mm8|X 1&QH|FgIFaxwA+tIl'JP_kyքw4ֲVvq-@Ňqpr#1t+;M|aq0SwUfȤ{QhU*OfGId?yJa1&q7 u>5Q*2Xtw7¸0(y3T{(} E63h#M# LJ8|qQ!  & G &s>]Ví#gcQ6+"{^6V;ubD9Wkee8tgS|kY|}1F9e\'^#gX}M)q`Tג/n9:!q1\ \_ّ.as+qh*IJ's@{&W|)Ger~Å^!Cv3N XF@06|eQs*ny1YGxGWL0yBSga87q>ɃBXY9qY&Nci֋KYN{éZ*m  )@oŘ8wv՚snpVVN9`18YOy8D"{?2|;(0Ԩiw{8&S5nwWY2zYVwLʷxY7@ *4]YR8Vj NpB9??aXJMi w:T&1k8fi꟎)ٍҠɒF9"6Bbj(fASֱרW *y>Œjv}96i)rQ Dh%9iS7Ui~]cyBi wJ{ ꬕՄ-6j)y9;ڣsЪBD3q  4u^u Z:63V \@HO۠٪ޚ<|Sȣts0 8z9Du*Ki5AgB7; :4niۥŁjvsqZwI`z[/f^ l4X<N$c7:Xi*yjjr[> Cy;|KIˉ1yQxX{lW(ꨀx[Ɵ@*u:xv볬7A{*ѷBFJ+SG`뺘ʲ@ɊR$ f;s밦 狾[[ h ;g[)k+i2Ȥ8˽Y2o HRr$'}H1 7{Ge;p۰+ŕkz-f5:#;%I)8زqW7ʰe+3 ļsXimJU,X)'NQK&%gx;2ƊZ}]S$S܃ksĥ {šOH4+NŏE^RI@'+0ȾŅ3ɣƚ M;u w+s| si_-W5MH@L^䴢lX_lD6Fe2-:?H `P N-S}4W\\sŒ{.rZ|$jוz cwu| k1Jq"-6ÒZ|= B e5hS+ȖDB… "XPD :Ԅ <~PJ$MBɪUJ-StjP*TS͜;M֜sfP:MRK5UtΡU}RJUˁ~ZM9irhUmWI0śSԨJjaF/"'̸#H"I\23ͣպ4RfhDuj{2kR_WSkS'V)Uu4(ذeMӨͯ ݶM8ΌvN JiS] ?{h##Kje_M2 OcMdtr5lt#Kl92Ź碓n"ű#(&fH(ӏlj&$ʴBL0JƼ;P:,J8 j:*nQE/n,H s !B==LhL?EH qʩRi3(LUK@=nMϊ:9IWUQ!bM|H^d"=Q)OVap!OٸҔ+'fje T5 e{sUVY„;M(TZ?U2|5!7b4fgei$~HϺ{Yqfj.cs1tvecS9t)Y 8%VΎ`cmMF؆ovbseP]'; yO#C'Dr 0ҙ5 V>v~"]e12?ơٲЂ30r!dwѰpW8TP{ M~Gکw%" "11Dp ҹ3Mpu&^;Tv#'n817kW82H,R*[EYK»0I.yֵ*!< )  ύo|4jV|+-{*c">AnIcRXRR'i=&ZQ4㸟J3}YE+{RM@-8\ L# G4`qKVF7["%E-T/K`.Wp3R%A vKBgl5ˀ )X=wQr`"} |M$5Q""caP=|V,gpp:G%fBD$1IբH#B0NoRyЃ}giu<@7ц!݆IdհPlmSOx5-Efͅ@XjwI[> %* JX Kk03"?+.59e%h>|Qac܉^:lKVk6 gzǵB/zH1z/)" uvyx$~69wsJ6MUxp!'NكK4ĵXW_nDHJl`6/~xĺZz._Dr&ܨRe 0m B&۶E٫ )iFc⩀3 lr>wq_H7@0Cc9! $:Brs@l<|ꖌ2Z3ɬiZB*[ 9S0 'H`|">я)6)Q Th%C!6Q\h\)ű>[齋/ĄP=` /^ɀ/IxoiQA.9T;ri>\i2,'E?D dVD\4)dD "L$>)ȷzM0$x/HI VLW4+h㊄`hK`("@En8 |t4u$cxhX81_}xl~RÅEȖ]@A e߽) X1#& LXI q4A' ؟bn !n(sGݸ$Ux ZVNk~uȀ*bdaևEY =V_oW(⅛FfƆjdܠ@і@O-) !VՐD',+ओ9BKTv@OtݖmPe<%AMUi@@{{!ւv7GiViI~taaןoU(]^ixeHV^bnfE7fk7D@cD`P) ^$|EE })f֍iݖi`4U t& @MtDUkT&W鞎yg].z!Ys^f&hziGa/ :[ B:EBpPGC <^xn>`r9A+&E-gJUVqZuhw ZFݟOWhӃki\o[lw ;L[ 1\s!FUtc&l(D J{B NBܨdL\? mh.6*>t̢g"JxYdU d ԁ ^ gaZ" e%.$g}Lt6;Rc @3@{(H+5($%i5Փ޲s_mLrż#]cnfCTo#SW Z=@Pn,/xҫ[Uwd5* Rmت"0=g@ l!65}ސ Y_"yةhoCi gg/*Yf\.lE0p8+OFDdSN/DBKV tl kɤTGS@j&qX;c%yHg[ {2LrC|re0Ju=L^vMxCqsɖth}0mڣ d=Q_;+і\.KLkxZ2@E}E\(ۧK!j]t k9YrFu- :oW10JUDCf#P*Mg5RɨD,8/̄=, ѝ\ gr%ƗUs-+G K\RLj`"oe؆(ݧL" 9x|)l3Ʒ(kӒ|. 3G$\/Ro 7h[\TJ.fʰ- 8I Pe0T W# @y`{C)%s@Z9!\ K,@(ydzoA;]`ʹW|y'\p8Ε_ObaxaDuh|DRD=bNwCb"$Rb6/D06iĚv2,dAlf$CXPNdb&됀J6Opi=u>٫M>T\ppv8*{` ""|s|׀w|w|8rwȀwWr"r;w]$/@y4<{1[s6~#"Or~4Ht7Oy*A7exq`p" xm|P NX|Uh|H|[8 hYHw0(!#$<2gFs=t$P$n6ч8([!'q؇E@e&C+{JL4͌X}WmԲ}N]˲=Íq ^"=;P+ƍ _ }|CR8"Qp2lra =pݮ n{ńdi8$|S-\8{Jgݦ@Ӈ e0hP\΀\RZδ}n&c<|Pݰ`n!1qx)a(=];o=]cP_삎Nc|kPm~[-#v{N뵾;p;qAlw{1 S5%;oQsl_N-]/>W5NH.- 6]M,o~LR[v@?qaapCجD&#SԓIWBȼݡkـ, n,nX.Ȁl6.o>ռ#`݌p|mz!o8Yz(t##/pԇ`}%bno.q.u/p1\GR_=^z pwr.ۨ+65d@!RУ/5a?0!AN~ۤԯ}Xj$XP .H:u8P=3!F!E*d)5^XbD2,%Zd /] bQGdEEQFP 0plڵ+-b G cɶX[qv]y1X!B!4,‰C0ac v>F,M03?R.&MP!3jB.{>ԭe=}:ڴix (ctIQƙmؼCafSSVUWrMKVZ+ضmrX&>/_ ;l@S?C>B؀z , 42Bm43CmD:J6B5r6\ZHdJ.C3&TZƉ~ F樮531b!<h`nj`,"kL2"3 . @: ,>`A+.Ðp fC3ϸ;:ʢ*b*4" $&>2kJN&h"%*hĢ,HI5-M@J=ʨ,d5+kM6Zj@? [;CArdp3:Ј"46QE4FM kUB8怵:RT> yXaj"/_kQ#) J,J#MY S3۬M6 w\[҈#H w[ P}}і4vhzNat:A6{avTGn[amH:Wmb8U+1UU+Re]EL@A»v ra!4KS}p>x<~>nwn{nME!>5x.9bo \50VC"Ѐhs ]GXB/4(!@dpILxÔmy^ږǼ p M)7xT XZO.C: Ox8@I g=|*&/V`%V k%,U =SP<UC/.?dznfe+ֵ$\K0$A 0OiM] I±}Dc)H Zlpdc-ʅ){D0;j-taz>TICICT- 舁Y@7} ]y(sC!H1 gHC=yO\є[ܣW9 f`byЃzt#*8<llXЖ6(CrfJ|YBdzZPiRL\&T!@h8k\x L Ez>E"=+qЧRPk%SԣlSڴBZ.yyHc\I%'u'} 91hƅԍi.$ , ^ 0Skz(f]rcХr$VMhW%m@5Yb޶FIHiVdOp~(u$ڀU}S^`.$֞rتbԧ ۪ŬU3H^VaU-J Rm-%AGmkȔt\W 5aĻw*T-껐0w+R"e/K}T#]/{]LZ #0F+)H78U˯2P.suj̑`v@ WeJ!]O 54]1h]ȪiWp,Y=^,AT2|PH/dEKns@4'eLczuBgCN/(0 7S\rOS" L]Ȇݝs!V䝱c9@ÓLƤ6KZLפ?NgZRF'InOWQ@C=Rgd.W|>0ײưCkd2n}}9Mg4i  (ptdYrMNdLt;n+[/^~Ϧ=(ۜ2?ᥕ,iQYb[lKH27S!bH3f{R9h$Xj `yb2Rgszv:wϏ@1̖-d]E⥏6bNk}i˛AUZ >pNM ,T<x7cNN!k@/)=G]~j_Qةem |}' fkwO02vX(EN#7Z|j 0 X@ӽݻ+8@<@8@C.)VK>K=*Tks71S>{ j,4`? . rp&Ċ@ ,"yx CB@-<@ )h0(2Ԍ@œѫ,;A69 𪎊t¹|:;zJ3tԚ4;@A) Ab,M@OD4@0E =/6dDO%@(+ Y)x; [a T -;{CK6z39ӷ?<*rFC3ۃXFT3`I*dGш6 EN )xE!x .+ _ $1bLFK @ ,8TƆ384=H7W=˼oT:#6&R 5hD tGwQ%@ۃJV(G#GJ* )2=1iB@(X#0Jʇ DF ;C<{JÂ.*]; Ib)0 I%hȝ(8O(PL$XdHWD/'d@&Oooh=O3uY]V)xh8.#@̭E~=nVKr<;L5($,N&W: ʩD,A!X-XXQBUUdAWe0}PZ-CӰՇSEV'`-KJbUbYf}eaPdiUmم9#hȸ)mTۛBo5"r=WtTuMRTxMos ڪ %*M=aeH@ ؃H[8[m۶MXX3PUMӸb.]VVۓmU#xٳѵbU.ӄx ٞ%2(Z4ZUI-EwWyo|Y\D!0صu[]U•mUe]mYX5-^MXE۴5Xea޴}$5EXQ^m[R =Xe[{\ӀdZWWxlM_B#TLe@?&t{R,ғ!B)hHCU\eQ==#LaU[]uծVش r’$u8QZu" _a&ײ}P_ЀPUl~dȁZm@[. yZP%nWK.%XV$x&'h9fU !|ל_[_yF@cдҀf623QqfVo~i˜O `ceHX;{"g=㢎H(ץ>m) na|"ͭPN@4B# n-9\9i.{ */ puPiW%ໝfЗfTDgޚF{llnVĆXVmfO Q8m5ZmjvߋTҶFڅB&2َJe0@ k8m~5O`M[>5By֓:6,ŻbhF'0X^kc hF%o&iol:음;edyoYNFO&.r_s%OVj&`_DѬQLn)Xd(b=#'srcE'@ۭ&eOl%`YVo0o~r9c.m&LOpNp Xp $P'`*'8p6O$p&ڞ_=(~ekP8 rXtf)wk%gM +8W9[4eF5h|M>X'8c)GarWaQ4O)_4thws&iӾks;7W$ps5gs?w➝dkvG9b@J' Xk.'rnnEҟĘ6_Fu'-UBd.$ fn._B^_O ^\U&.a2W Ҧmwoyvj8wӦy7yjy 8myρyy _՜ZHo&z*V{?߀;ƻ4ŀoJ&u~hI5X!FH3@f5<ywva/Ey&TM:vnw-q~vosv8yͿrpF&G+o_H&4t{#ǻmf|ɊmM!eφZtH,O^~E@P-p`.zg~nyŏog+(p?s| k/9n$`;0lˆ;~8Pƌ ,X"#԰a (^h CF,  Μ:wR3!-_%4pAÃPmd_ ZւbŲ({i 5n-ܸqЭk.>c{&)0I@1 @(r +P)8)@u0dج.q%*c)0Qc*o!󹑚RtrႳ:4icSJM$σ Be O>}:rjڲh>'R{#` &Xo_Vb &[VWbd!:([m٦aJ81h-RA)j%ate#C?(P;PaT HLvinhhvx5]P.d "g |}rlE~%A|$'yLx`  *H iHI9%׃XX!qZihbl{cp "FYi,Rw{G~AŌ4*KC|X\>D]2*QבI*ya.LoCQɦzO9oT)S@ H^{yh|REUx bp"> UPҥ+RQսZ22v,:+C]`N4FÒX#Bmh(D*#Q3qCWFU[/!a.PnVlL/‰aֿgy?TSygZ vno䳴Kۨ]*"1y\gQ8E*X/ˬC̒:c֠V P[N栉Du4Vk͑-n1]bڕZkx@ Pi J-7#lںI*;>aݿfEBِ!>C @uX&%&5Xt!kՅjQn lb#ו=V{Rn0EjRsHe0q@}RA>q@ b6"~Pp3+4ͅ4Յ5/`X|QUBfUHA>xjw9! ɣBŅlқFCA`^3 zp BDhjS;۷8jQ+5E D%+J !CVD6 @rfuddlQ(@=1c\@ w^iHmxecޞG~+^0&)ϪTM@6}solJi n 65'0T "cMA @Al[e48†dR%Q3 CZ_GE]Xt3#f DSpaXүwl'TԶD)|U$J"T6 :R٦ZLuC*5Ų%9&Qޘa˜X%{(HR!KvS2*\٠%׻D=Y3*Tw)$ t]( ".Nʠ,nx-iB~Ul]:~(żu1Qј|XcKĚpzPI)6.:A7ɫVڤv@Yf`ݭ"*W-. 'Wdɢ (AF0C,x8,F!OV o2dل^+d='QʥR,(kYYnZȺ'a`JX2sE`c5$#4>bU Su-R51q2qS%(9 y(r7e*TC,[sA ':/ySr>P ޲.ٱjvk3Ԁڥim*碱%#O C'Z*\_襮IXiȀq;n>7}ʂuy+.wo,9 tcMk7 X]p\2]J1F eRQC J8hplB QtƗ-OrQ90h[dKANT]]?2ֻZkXgg0>FcE"=R[*$ )/?JLЛE@Ŕ7#$UdK7}€4PI;^ fA&&` tSeT\E H\QY$G}ȅʙ M9^{DU!D@ ]ƽ=W A  \vaAuv $`"#!>B",[ADYJֵTɐedQ > O `rhEW% zsG BPPkl Y+n( MNupݞ,p"N,f! a-@X1B.#3 YHPh̊87vNM#u`h WDA0Z%U=бmq"UTy U0^R^WXh4_ҥi`B&H"01͚"dHIG$K_@|YGڄ՚DɁ[ABJ$`P>E"aqKTu5CE޽c#O̒vT9&!j@VE(3ifZ(n&%i%n()j)B )B&Ÿ6*Hbfdb-qA(\uD[)qUɎ*IΒN0|!S()_9)@" \DGNN`h<~"#Ʃf2j&h自).()&DkJBΩfnhhg:j+ j) *+֫&j¼h%r-lO*&,@rCmjDa zڣ㝄w k ^Tߑ|&r +yK줳.,cjff(Qj&3nfPlg)꩞+$ІPi:.ګ:m+L>-P3惵&0,0D R,Utuzn $L pv*FFBRr(>f),f.鹆g*"-Ӟ.ӒB+"jZB,Ԯ.e.B!K,OVcu$`Dۑ@e~عOvFavG"+OBd%glF.J.//&`n&+g.ꮫkR-RӞB --$X#Թ@@2G2 Z]88--AZc$Rno&zhr.Һjqo*q61k*j* +_mgBZPm4Xc*!a)ۂ)8k"ˮk붲! &%po)g.(bj/;qjkkO1)2)Wqfqo1*s񼚂,r,̲,j&<P!"O{mAz/춚&3.eڦ pfhf4W 'fnҪ'7r6SrG֮)/'sfrrj)dz;&pҫ-֮)HB@H[4VD) D3he$gV35[Fctn((ǂ/t,j#s~h,2(B9[1:L3;G2B<;)#=+;->;J+!A hldq2&3CDqZJnX"4KK''B?2+Z.+2)n)(t'M3`sPP++K-P2fs>I2VɬA"`,f읒&uF5kg3+KӵZ;-L5\5Ht\[r5&󵻶+L2`` Ou'+Ӣr$& [#(Q:j3^5+LJIq"HRhiFsk#_7'jïzmv+l?r2+t,5LG^?ҒrLo+Nk(rsaG4* $[m.uSkw83fJR AhGzۦV8jc{#yGoB+B+wBKmr\3_Zo9MW)7)%t)29+(h9?Nq.ˆˁv׍kr&;4@Vo9ޢ m:26wb$ks*}/Bs'w(5sxnc*5sB&NO^˺rkCq[z$ *9C8IOcꁝ w/2쟛W Q"~nh"mfdF[9ꂂQ3cn:nۻ)*'h96{%rM+-$й?fSB"S2a{/Oz p w?7gk2.&$SqT")?8rtoN.e!rsy펺vۮK.خӳ'\%xBw$K˫$#T3[|2{!@G|3{3?kw<{o=PN/&hr#A Fȼ!f[3h{׷46E-+}۫n#y43P=~H'LB"X$h' ApcN2:3Թպ3? p$ķ=;c';;_SCLSrw='~$ @~FH~j{&DL*4x % br"D #:d$8v\4@R#IY e)'Kt2+V|+V+;s&.B6TVB}rU,V sNC@Jg*R>t!kѦmv0OJ)#G<]cI@Zyˑr4vF]fsfN5EsCѣTbH$Y $8V;Sኻ!o UvpDL̚H}Y )?[ʎVZTgt&3fR};Y }ѓ4+?C8ˆLR體zU"J$0"4o2M. E:,4D3 5(i5 L)dGfdF`3IJ\Bj7Ú#:XVJancabyr產:5k%YfxsNydqn<Î8躑v~NykI;hc-52T@Q%ocnPUU!>7(A L|"ElP79g}P`WBוD[& U抡ԘWYtq-T@mkû!_! ՎDV0! bR ( :B@Ѐ%KmNGY(exhP0<pБKLȨF%Vb#%)I|bqbBSV"<v n c =ALh"p; $tEVL9넉%20O~>tGhLLє6~` 89"ryOA5%()mB!LL⑖d%+ɻhmBD6\%"^Jpy[Ip ;ʼn"LIn"Zd,2L.unEFH44)(y71ͣ)UE&g9IC9b!^x Y=EA|Zc"dGD"Z2p^PQX z%.͠+D! NP@+$DHn7P%YILEpチJǔdT:AYڣNuzrdȱAb+"L HlF.upm.JX U PL+g*D!.Hz]-2E&֝'y:VK7dLh]aQ_[lRu4fAq8e 4LPJ@m6'-s}>_$J ;JK(ء3t(`g1P d =p$H|YX6mT\ hmH)D &MPL]Tq;Egń!Ҁ'lC# '7 JXѪM vQJZRU@l`[d =r5+PA MBG}$|jJ80ơMI8p& '=4;n3fB )ĔwVcY±Y2J|ƨ8et JP'ud1yp߁ ރЃ`.p WK+p5k$P d IC&g, ̗@*tYqF7ј[S\'XjLm{ ǵl u.h3a>1ܯ?[*w)EwȺڅ(= Fp T@_0l@ bJVf2<+Za.x TF\⟤ Bd"%4lȠk `@ ˀ @ iDc"B#`B+*`؂*')n.a̐BT*20„oN%HdFǬg \pv/ %# Px ˱` H ="Ah NcTM=Vp*` @~g/"#npP  +TeL)i>fЛ~ V%k+-kq,`zP *6MHp &aMڥu i}QL& iΣ xQE 'hR"A|̬? R,b#<#Ԃ bC1܀'{' )oRٔ=Blnn>>& uq3 !e!C0d!7lbkL%KHpr#QB>RXJ2N"GXb8B%%1S'1=p%LP'GnD)#k@N3+0EpLO]fBL!ض̺RI .hU.$gp//fg$$b:"5%3'_r1,"*fp0mC3 44ŭ]jTZ+6g$> !xos#+.j'`BF$$aLrSENᐚ$;WB$TG6R< -x%F-L=M*+>/B31lp{P +17JIEE6@=!ARA9T 5Q {32 9AduHCbpEC?/_B#xD <ɑ/TZTD?# Td@K,Ldvs**@ Bq "txL:#7DtDDM9#O $W4TP 51s+$1KRBw.R3]†4'0ÛK?pg46A+ T+OR !MR:0- "A%U#wCT.씭ڮ~f`KHXu3doΓ S\KXL){~[9C,wq+Cu7l.2H"^d__BvR'pVxzLQ2a'Պ,P;m#@$ *V2|LZql1 <^5>Q!p\V@U"E@3hgbpq`'6di NEi'j'4GC6 U("ܢ'VmC{5%1+h*S!ذehT,1SN7j:޵^3 +#Y&W :!A$7rr1w;X9''LbkEF#6 }%f#^xpm3Ł0wr+7찅2Uq0CA,do"0p/t  ig|nbpX(:bOgWkBn 0( Jk9T 0I#N&זo J1 W7ݺֈ5f7"HpK P5Q-#q#dRП\sXo8ʇLB vn\Q0 #N։3L570ŐؒW|5+?,"TZGRxzEca(7N}Fϥ3 ^0rwXbrgRFS3Rxjp"*Y =e'kR1K+ vјf?(Eb.G8w\pY'/vP88L% WU~ĀcUuUz:K-Iy'{Zqz?*oz\wZP8X+Gd6>bG={,ڲu9!Qd؄VRUUM{~L_MД]%IdML k"l5Ԝ*G9@nтACۖ zG7P1`1c/,;P- (sG89.ı/IJL!! -5 08u[^>](U6gk%\!u |Q#!u]!F$9% ~:}Oj815ZS_ 0x<0/qKp=ZHH6Fg]dA_ s~ tf5%r^.OOIA0 j}GzU_Y]<е0-].S44c)WR<СCIS'ϥLi&J&-ȐA|6XnzIlٚjVӦPr 7޽|k*} ISK-1ȜMNQEC"< ƍL)}7kn e̚4oٔ)&u\2Mu9iH.e 2itV%;:O/GΜ;_8?_x|%UD`$D"pe ^8%c6`n cIii"i%k.6lNF.:'p* 'q6QZȥRI OJ)UUWmUvr7-z*emqw^D֡~n H`"܉kFYgTؠ k`уeh#)d<$k)$TPk8#h6?$q7鑔>!J)PQRPGt]>+ga{5W[H 'bg^LR" bJH.!(qRJR]cY*]d"i $*EKK 9qGf|OBTx$U,2 զhz mW&c&&"2F"Xr!ь 6fRR&kf>_&\j۠5?찬&Iq8SͫGr7DZ"e)Hrrvyl'Vzf=F,q΀5KȐt) s(RJ챓oJ_u[Q7#tߑP.vU=pBުOO=޽J%"mgY/c旨n~;9s훙«𢛎44"LdG}gۗ73|S*V*IGj 6iJ=YRx,"r'La \YB# jL Ě<%O>__ӊVDsXE(at;ȮyH0#XjrZm9wy [8Am"NƷWȚ#T%# 1Lf6ph$`UESH.y"]Ly1.b]F'mT/9V# ֔*ؒȐ ā$!.#O3˱Gk`]6AYJrkH&j%蹚F}<|I*[{F( +=wz]鰖Db5y@1Mq<+Y(ӎw y@Ϥݽ$V3t2$Au @t"tJ 5V (D"If#P\7L5@]ƅ%*T 2!a)l /= ӈp+d,ɩg{qn7sb2 \SDSuĦr MC$rE Ec$6)" R($V<_~ 2$NnR 7 p{[E +T4gDŕ6EFt.O_4ITנ*T?Q=b{ѦAGЪj%3maTeߜsj.q׃99N,l6cz0IL"[\DMWJUsI慒KZ:bTgP>زS[yhDZ 6Wmhk̎Nm4gʩa2?C0CXrw5C$K= @J`"Ж8[oNpfu vk̠tsO|{R[Vdd ao4M6 KٹnrjֻgG[_x$*ܚ7߽Tw^IQEjN9cWH*~_c> >6536bϬl`$F?:Mt@P߸7 K3$nKw.h s$]zWK@>jch$Ѯn07PG°_M[ |$}YpYoBx} i&jrRFn(3}b?'N~!IqD8"73R|{@7|Q%9)LW}uaVeZjw'UB~IF4oWAxeuRe҂0E_Z[dir '7K aywreBXaXX((nW<#!THxG8al+4DW-EZ{RRph FWzEy"1Mra9xhR`4lf\1lr"7@T刈^ah>0B} y[TF4XgqgSvxnx'wV6 a7α R(iWsA76E5(&a7nx?xgĘ6pM08\9V|]Ȃd-eи)G vy&I3a"8zg+ g~X?w*xiq~)!|Wڴ@V7 QI$ꦍֆInDB)nV |!7Җ"YPy6s457|7I33 fDb]#jv376/i +IpH{1hPfM蔏ؔ3,*V兓(|Ut;'x9zj! U" Y^ƜcuGd9naX 8ᏊQܒ`SY;rU9|]vD" 8ŝznٝXWAY hfw7K&tO8=)m% n6s׉1yuI D8yH?uI~}{lOH U^h0Y$j799ؙ٨DȠj7ib9 GH D ?9 F>g W3‰ PwBFzPlyfj7騖' ifyΥkJy>R*0xcG'QjWu4v7yfcgu yAg헞S*pҩ դw;nt7|ډ*}W7eyvr 3:k0BzL)Av%,,e \؛~>GlgȪ.f#7yJ)f\8fziDʔq4itȯy#D^Y18vPVIeWyZ\Ql~ PC[򣱧X6kgsʑ\t)oD|W!9'J `zckZi#[\ipQ!(Ah>19V-RY}Jе `;ڋڄ3vWa)%۩}CXHPג٪5( Lj   )8{*`lg %ɹ˸pm,YJ8RHf\Jaۓ<SB*,ˊcvi̎7uɆÚ̕ 'f۫'j֬AHg̀bFΛWq<ͤZ|] H!tmϊaܼ7{Jt X?ZD,U̙Tj܂{p9-"l fa+H} Kz9*F;!|<ȣЯl>xžf4-9ΟJɗE!IM[UPh[U= P'<ƪ#ݮ5=?H|Ҕ˂` |huB"̱<[l\< S/ \r*ʎ5` =,h'˯CL>̰:xUS~K-ʉIkik¼v#)HÝ֬ȭ'խ[@`Xĝ$k덹*,+ {-&xI|}C£> D͏I%>ND{r }uҋm捫f彄*fyg(N3hnlzͭE9j Ј>n pD*5; M pQN$lTw}Ԛ>qQ ի@懲- 7֕=Ui֕XoQM5۹jjqy(l&qR^L ӈ-gcRkeQcxQ{Οƙ (ܠ\]Ƕ>eA@W#Qf 2jJTz nvrsa (6?K#4JnzZXx>'ۤAyI5 k(#H5lETTW7ܼXeiIqhF!6_m r})6@Ze{# ?j4i~j?ZIynn5vBMyL?\ܟnc"lqI'>} PB 2eaD"<bD#RƎLj*X']iE5mZ8fR={rdT|,urɏyg=]-uqj)L|Vl T"i2Rf()ݦtZӅrt ú5MX6euWRK IwOd+_}jsM!&Iz˖fTJƔi(,Y\ ujׯbǖ=;Ƅ*Y/P/%2͗9ҤoVwfT'Me,n;/*^{(vj.pΧ۪ܶ", 8(;-);?#!KQ#;=hp!.@Zѻ<ꩶvHRpA\DB, 2Eb -О.&x!}<);:UTN(21+8d2<4;=fCH#a(%w3а($%" .2M<3|$7T<Ѓ 6l{G 5#jLX&P2/P2 Tq}2TS=kչbe1nКݎ.9|/`4^:s|SFÃPMRq̸t-6Pm:!bjܭ2ܱ2L]2}s|ev;: ęCؗ/[Ȇ;T$Aܨ7Y`[|Ê&z =Pwx`}oBtp}١k5X-Pi4JcrpHè"茌hLr0KX=6єr# >Hry!.$CgRKӳjx~O:e%"rRVBkaDEXm$UvbH#K4SBJ`B>q%IβY<&z?c%5X\Z0 d_K炝hl A{!7Ѥ')(9>B CO@t1SQ =Yh%MF3'4m/T\TIrSì(:R>\+X׸RS@YٹǦ1Sn?PEL Q"tBFKu VLҘ1YRX8?7\  RMsGqM͗.|mqNxBƟppwy=NqJM5q?(eNq+߸_+<"BЃ~B`cG->ukn@X! ,^Ƒ#F0YpC8"Jᐡŋ 'j#B 1a‡(?DX2 @в͛%a*ȟ$m܁ɗ/`㧩ӧPJu*L:eUT^Êz5)V>}飴S6Zۺ+ܭv>[M K  8aC7jLaьa| d"7lr&͕8oZ$ϗ8] AҬM'otNnpb+_μɝ~A@# 9 ʒÇ|ѡ#a h,gL]rЉ'SgGojU\P=[Sg\-Wqr!tNs`,a]]8py -!ފ+2Ř";B(|AKweGyFH=5]ՅF܄ 2UBؤr~Vi`~Y!RhvtA4CH<5Í(hj0#IP`L(_b&%楘uU : [~eJaJ\Y0 C C)t)HhI4?ByR* &g!\cJ\av*ȁDkxvwc!!*A!/BN)[Tr+n˭Z:9jaJm"-Nvعة[qZ&C᩷^gI 5,f2\iBZ Zz§b\ o#.Ю "F")z)d8fϐ]rm[Q7LOjsj|w6$p@ tB %V$,ҬolfNlM2xu۽\ᦳчϭO[qL7~8lBЂzfsMl,>~>^Rph)x!' GrgophXskl A(3ڙKLB[58@V'{a r:8 !6AGo8d>i1 * `x0蓟щdAp b5ooj"~Է>oH2ss (*ht: 1d8F4 *x@ bf+' $!I$oZ8`brm$_Ēqq,g :юyDc5|@L;|CI=)  25H7IzS$ %W@J9ӂC~ē63)o< /YlOzqv:4Ē>\:TKˊV{] @̎zv,&DA! &6pHȌ(")SLI VӞBhDOʓÀD?<ΩlNSS'xP&lQ3S}h|)VG*VY]f.Ѵ)57`MKf!銎.Ln OŞp0L:z@fQ}*b"A uP=’e\-J>ff8S[_:!] ֒ѡk B%^hķ=U!KݹN-"ŤxҮ44'1%4Y $#H7LV81!pCV_z5{GLڢЃ[ݎpRajQ܁veaHjus0e܆ nB |xߵM'i—h0^vև hٹZYx̢D(X jnS Ð:8k<>[N-Հ9 3s|v,ֳ=h0 s1ȅ ;a I0sD(8ozӠ"^$`rCRtI{QiZT@ H/N\yt# }>֟/< $>_XC5$,2ܥoRkFUP'͖T'@{msawBfcrigg{ߦurVg|`WQ[yW}7W|W*}'#8{' }W? `JgiFNj$B~PSK6gM/PI>uH'Ptk=D`"vqg4nVaasPa''z+؇,H~8؇/w`x& `oriDHdCNyL؉MONRhMRHYEWXD01H63;jt,`Ыv頽ʪlꯎ@zJ {; sЬMA~Z? LyNG*㉨ z);;TzSGiT/PM;Яz{  k ۵ZM+k0_]ڶm9t;;ǚV˧/6XX2ni2*9j<۳C/CIPgwcB׹n0Pn+ ڵc5զ{;Wg[Х{ku+^_ZK|(r?E $<;K;L陥^KMv!y@$ډkHP۠)Ŋں;n@kd;gK{Ë^˼i˻$<Wa]X# l~:bqn%+oS4kKu諾$۾  UYk8bXs0nп ^Y [Л{`Z]nipgl&Zlt@ @ ]/lŠ||`$\]{\]C ǖLɴ[*L{`8\?+jEBlzS4+E?zHKK<$@pi\Erڡrz[,}\T$VX0S1GJ-(ᚮ>t +r0̐@3]кzjpcAo ǩ~X>ۗHmRIekr.Jt&^>EyjRS&:]ݑǞ䑞12ڟ1i+.n|X8= ϒlЇNH-ΩO}=^Zy $OTY"d;nu<:R0^n].+Nީ>XNaN|[/h7ľ|` + Ի-ټo㗅Pa(<^?jNx凄)ѝ ^Z07!@n@/SĽ:.Nomڍ ڌa[Om'dϪqvoH?Ϝ;[?s̩S{*ӰA|N=u*RǁsDLɓ bG\P4PfP1d#A (IQ0S+NmêթR;luałP $b`p}@]yJ A†G`!p4g!eb5g0nLʄϨApg:n sgh}ŕհϠ]C{k(Gw-|d ɂ͛6Hç>}(5Us+W 00Zj˲;WdъU({l1L.Sp L"Քc,԰"(C1EhC~{$#!UBH sQ9pxBj)*(:oJ > Ba̩2#2t2,SMM&TB 1 YpAFF*S2ɤF .s=Q?X5ܤ9 p@WA)*b)jس_-0KL$'3a 4LkpqD08TAk=Nw@mCM4FOQ%,*?>S7b`uB::<5Ur"#㎨rx̌muʯK 4 XS ꯅ%Luƴ;:.BU_c,h'At9M` h$+rـQ ^*L$@*dW!nvR 2Deq@`lܱ&p(KYŽf*0@| aj>bK1i 9l$WxLd˘e42>$kQU<|KdP +EV,o[J#JSds pON1L|H4ōKO}B݀Irg(b#@tI(ùQD4cb]s>Hp{]DGWVj\iaERF(j&MI 4bB:IMCigoIoB VYK,ĺ=(2<4\^U&3bYByH"'p>MDdznJ9|ku-ZfTa X˻+nyūX*o`*L˘N%Ɩ|j:@Վ6e mׇ6mYX5W;>b0=dXzPe\ ]9%׌s+Ie7^)jS񖗽":ć/ȗ!HP yFyp^$ث_=~9Ñ`Ǖap$]mnB}=` b >jy}gS z+mVa 5E>y}sOGpȮqɵ{h Ȼ<@IRw7شB)V_Y%pyqr̾}-Z@ a(spk5;;7І_ Mlxs%.)7!MRcy TOJM [|L$Nv,  7'#`2fZL!t0@ oʭH'#8G#|Ɏѳ}>PDwGHDp@ #5CS `^>GYЇN{8k/`gx|1fV}w'>}_xfK$q+(-wW:[ g[\tAꮿCC;3x<[[@+>((I6CQ9˶ P<ؾ<+A:?P!SS*IAQ2'(1By=k@]ޛ$ @)=B89,([Ell4$8CI7*V'<䧔Q@: (@34'[d@cTB {8'LB B B#P3}ɮ> b-6\AP2+ R#@A0>|+DaA}VF:43%0 )ӵ&DkNĻ 9o=墺B,T44.;:7Hr/&A8/"DZ7>7FD x3'"8KgLK0FRDdzScDE97$"1LGPҕ"گ\~܊BH ؀ JhA܀H>ȷf\Č,K"kB*캊;mIl캐4\{c.¿3(I@FzG i}4ə (L5#=Cxl0`Iı=\MF=KtKA:דI5Kأ3ǺÍ̛ iɐC`J8=EʜQ{Z;,57Li ?e)< s",qZ33մø:=:r_\cT]KmT1bI^0ĂhG 阎eI2/똰s:%HQ]Q]XQeLQ$@xAKjhO-\30$tR ";fȗ;?-DRt.K5R,I$ӄш4 8+8Qs.S[p3HG8%S,PF#Q .9OL90q `=)OHǠȲ$ϰ@'uKLj@4ܬR ޴ĺb.) 먛H>@݃A]4?R FmeP hK u=y#"!Z+U%U"TRJZC5ȽD8LPBMjJqė38`̉Z!͌*.:S.< @C"EO5%xq-N @ 4L{;J~_!gg}!g5FP>hQ] [W+Xb`fOM6Ph@hh@2fijk^6D>g^ WTC`\x$x}gj&j h ,Pdkܣ b^}.hG%f1)-w[t d<ii̶8˵i 8g-6hMC}ԩBP'~m g6n8ᝆ[hg4~ؖm8*BJ3x>e)oe^Yoxd*$0(mҫoT•K<^mזj ਫ਼n~ pfmg$Ai'gĩwC>mhb{<%`#g%pÍM&O^M Iespa,WC2or< W Q8Qksi$kpm(2hG@qtqf6dFCiwqB gϴ '7rpQ}"|,-l&.уGƆL2O@u6.kU΅#esjo^O.Z(kνpdmE̽otttwoN\ܜNAvxdfQow5c7Lya?ͧR's.Ϝ=,Ŧ`7j]gfj?r$vjoʰsΥ%E32P%ǎF6'pl*ri }xMoJV[/ Ҷ2faoUʞ3Hx~%Mդр`VsGK Pr%`yg}m# gos~  $H$HK}8ӿ_f}ނ>t6} PpB A}2Mw31f~r^ѭ'Чa-z䠗g&7r6Go|ƨ!^bV#H|b $?~$i@%J&Ç7jhԈq!$Z#Ɠ*;hYeK_|)BΜ:w@K,2@Pd SbYbba6 q5DAz.H{@(l097|` N~]WJlO.]z j,SJDZ6D):aŋ(;vjL -Zk  H8Eu]x%pPX@ VN5"VNIjOBYT6#U@Pն#D=2B!盐-G oH@R'QEeƄimH k&g)g:0:ڙ]\.usBo ⤕2H(^)ܸ55iM >٣pue7nϧ#N@Muħ35$*$ .Wa"P-P9OlK I\<{řj,*wrJlpO:jI oz7&H> ]зħډcׁT29Q@?s YZd"i͒>]0s6e\<';9?k;<՟kYF Wn( y8:>iM9hR8A֭n^İݭ- ]<:9 _M'چ7|ARIsˏv%9!D+r(<uU[ mݵ4M@rhRZDmDGE $Ac|GIHL\xܠYNE8Q |ʬ[jpAXrxݔmAؖ ШAKJɩmNutơgY J5m V@ _(O0^9΁ߴ_`:[՝TAA&A:ҵ>1ǣ Ku I$T MOFn iDL \*Q\.^P%6fyF_GǴͮT 1fcn"PeJH CATATg(WVz%(/.(!& m*hnV&h1v2ODv Qa,SaDtNxDSD P$h KGNuS-X\VAxNe,&idcAi|ap DN)ADsL$ 1\?Y%Jtr$(b5(n6@̄RZNM q`[ \V҅twD $H YJ&RƇiW,i|čܐ'c6r&#*j"$g(f2e*(yO` /%OGi&FݨwqR[$0`Ijǔh(^`k^ ^Zh ګJ҉AѻEA/#g`"~htOF/PJ&-,",!,BN, +h:eFAn*rDA)NZѨƣ뱾QGP(aZq(xTޝ qȉ!_Lʷf Bw\d-lȧ蕁Q6,"|:J:؞-ڞm! B¾9έ嬒%,J#->":jD1> bOʒ 뼰 nGrfgvT8^V A:0N:_K<~AJ! o./ڊ#.6oRJN/%8/قmeȁPJF H^lK=gDEThf>$:RI.i@Xm/L4v.p [I(k[crxԆ$FJzѧK/NiJ)ŮjJB*/.203/&dg1'3PoPc/ iXvΫeLYU/dQl@4x)wTV턀@{hLRHBfp Z-D,.&%[2cN,N2 NlqBo%P/V1)IJ,)r-B)%%2"-%-Ҫs0ΩJdNo/,&Pb$qy#\NLiULFWA"`<}I'w6/&t@7 S6A?K&/B$0.%2WFgF2),4Ht,2I)-r,ײ%wtG"DiVL풢#0/A4PtVEKR8 9硟5jv.mlP;s#A(ޟ >lRl;o@Gon oߪ2]GRFG)+_4G4`,t/4K)+,6c)..BOF&cOOPw1Yl`mO0Rq0Q:'ZMihgXLPBa.K9,Ċ2FqwZ[Z#@7w@gZOsct;w+1] 4*[t&Fkt 6Hx6aatb-7v),6ar,7}ӷ+rwWB)A5tvgsϊl~ P,mkߥ@eJE}V. -qS&'t;7tkwxuO7*t.2u_w_7Fr&,xH`ya{3{7_I#vJc2C.B^9gy%{h@,|ɖh omSff}VƵ\;$ܰrk@g7*x9FxExsG4w%+9|79wz)^G1r.w<ܳd|*D)R2u +Xj b,VIUx#I'QdH/2iR$+YTɲ O1i^$yx#0tGz1/P010C2pciR $2Q@ڰ>Q1p{餞\9)GB"IFBDȶۄөG C#R0S:&+pɉK\mD$" T ,PI*;nx 7صAj8 {嗧U a`  r `2 oG*URL0&;r)CM+oFlF!:o!o#-N`N*G/He0A*~@$M#TToGIPRxLC"no.  2R A Jvp~iG$ 2nCVf+Nʊ<]0-1 D%a|xKf( rWO&U\MEhL4\M@ oР  0.0RS @$0Ϲb}ҥU^2|1P$Ȝ0рJ.!2,%A1qpרt@,0%T^ #*cf0J 4!梠 dؐ@%@ `DOl 0.cr*PGR ` '~(FZ#\? 0-1kD]L1+, Q[Lu6L7 G !BF]: Rq6 d2 s\] P }j *zKt Nnu6| |'~p,q@6C92,r-   bNa"#/60ͺ0 ={ N1N2l *Fn`6 yV *$hh>&a+ڰ'2O7 CFcvd.άǥQ!`TRqĎ-5n1/ǂi7 #li= = NK/2F`J`LLKM4 4elc2p2FCgEMQDD)P7O7?!,sXB,m4!\4PDlstRG}S0 dI3;gbp KMVMO0*nWoK$!&,(4&:m@& )Luc!s1hKZ}SZ1.9#4f &t#q&q+S.9UHe/#XR*NBLJb)LG) t ` "+1( g!,2//BWŔxo <3~Lg1q&s3qVpZ Zug〇[BcQ3E /ap\G4m 1 ˪lbS*;iCc>b|M!Py FC(nWt+@b23e"NXt#S*3uP6eqeYe_eO֌qf1poT` b9m].%:i/$&B%CÃ())"N.Vbւu[uPW2Tg86yV24lp a'NnZ6ZؕEOD 0H#zoטWv@xe#*֌`9qw6bĵZQrYhԥ}7ٝ'r }÷"~ AjB̚Zۭ~9OzVx踙]1y1r*2) h*⶛ {dž K.kz,t"<WY{ H [ l6K(p73]WTECLzZ9`5e DZ6ݶE)T+&w'`:uiڦoB:s!LC#t[v|dKYmfP 4 z56yimXH{Ě?אsy̔4+/w/u49'@`w~Dd{[%r+UIϸV-fnG;|7ګ7e=8-e{1qNRn;&(g,B%RPs[%]̀Y'b }-g.dڹ_O;6ms-ۣT> ˰Z3;qDZfib9?8}SㅵҙȊIi.+'hd~k{Y_{0T;2^-lO)Ջ@!\p79H )XQ`ť68ت)H.ފWE٧8zuuL'~|O5_.5b\JGz6R7FlŇF9MAC)xV>x{syQ]4NB,Wp({RŗXc4/Tdi #h*!˩ [mͱ5e%9`ccNjxFNfcgT)l+xO1BSCa˼)$PeC{|?b$`h7v{ՍdT ܒP@i+VR3t)oIHb74feG&l}Oom<*7n6hs-c,hc* =':fFJ !eȑE"L1UaYa%U}bW\r^HyՃGh{ǖ! WEg 5IYFHh]a~hh6"LPoHAx Sd*Q2"|ȑNR,PJIjW^~IU8bcbfE^{ʶK,\IyN] m]~΅`m렐)gZV$F(h6#w餏H/%:HB ć wÑGL ]CzSwm]ܙc3唡aWb;* ղs)ab_\۲ rP[afզkԺZӧVG)ȣJDJ%P!<2&@a7F-㎘vKGQ 5$i2ur &9TlUyWbs̳eㅲЉ BpŮ-ns2}FPSfX e;bigR Ůr)m ws-PzX/%,>.$&DQ!FubaPJlɼ>Ɣ_-(&>@ XQ+ҦD<#)|,C-PDK`nM㖷& 1C-_\SGUs[%'O飄S\bJpq$1! zxHF*BWkH0 DӰ֓0G$V)c@M ]!h"ŚFeYa 5 m7/öQcFFU)T8us(‡12E&תEh=17 ; :.f+;9qkֵuIr%&u%K"T7q1&,"[Le&W CI4@k*OK}M-#RٗQ JYkKdRJ;v/ 4`$q;إwHNE7բlg=롗 - М (WkpCqh-1g@KOgz"xWRLJ ~uZ-а2ioDT^N.:ˊY4nO %zvRɑBMi³d%j)xvDYPKQ>WRr%{AՈTD8C #zʘr9aj%Ӌ;&f"KԁMC}20X^*s ! $^fʡQKX#6 ?BSEQh.rg#X R )[,SQg9&VRlSķ.rlSDOd:6;j-]};%y%H"<@jUJGH !!G-#FI{,ruS\3Q֥b0'$! rғj/]9)Y?Țd%$+ф d(߫ ~υ$<Wل$Orԧ] ӅlcPO+-QȪ}Q1eAqgvg\R@\FwQݶxpUbt4&H.&x|-*wOS}m@+292333ӊ"Dd ̂V iA4 {yWQ)s NX&xev#tZ(`6@ytp@ 3FLi–qkD3CO @c-n/&.t.;}\7&9/!4WwN! zyT(َs؜Gy˦2Q#&9.SDc+J;2}JAqɂ-Y !O:ȃ(IV4"[˜PZXz,T %uRYv-+;eH|a;y wN (=jzNpE_G_j{nErUdZQ:+ WWqV :3#i)=ٝ3V+F^8riynn>uf!hyԇO~Jhw)wG3,2R_2ZMMX$  jy*bF9b_6(QLJ =ڪ2td.ѠZ!єO׫P]hVv iwg͹eIL"s*GJ\2 Db+9ioم!s)8CXH>j@P$E)9ExyK, ؓ;LkR`eb5iʑե5E5Vfb'p[LPsm:(ճV:Wj,3G>*3w(T Gd[o*ӳQƷdԅ0o*w[bk&QA+)&i~m$uۺLiⲞPѮ`KY{R]KۂeWrUwM&[]sq̤תݛs aᰝ@D#&ybe*UƫQ1˫_r'32d 6֋gq+!ʬEmm@+%Ҫ@IB+F{Ja$U>p蹚`۾PhRw[a) 5C :JaX9:jo({k5k(/~LHQ1l~:|39 31Wo5B̢,FMw ŌxY<&Yũ۽;eRux5|ͳƦp3 KzAQ׊dX9kălqk$,;9ǘ|6hɗ&^{lEWLQRH*-ّg{zkMb ɝ~ }oٙA/]j,2M.#TTW,-Ȅ&9Vcϑ]1]4 m/njQB̐ͦ#T ^L9.V{8\{ϗ^, Al}5bHv<߰.@KWZ"jX; 2zc70z˜,m: "n|YSec}^ U^t>\wP nʹƌ:9k *\-^l!RՉA-ȵQT̈́@G M!A'IzϺM]-B+ޖ{T&9/QjsGA~D/8cZHȹ&vP֙oܽ݊PquMk_m?]nlƦjAXILcyLQ"A磋^Pv+R]lZ.}`3B̗fOT//ʡX}njW%cR_4ݻin. >5O=)(جxU,Ygղu\?w.mhZ6 0g#ka5Ui7z7L{>1wT fJ1Oz߸-8JZ&Rn(K=D?@h4T$\NR.jBrH#"*H0,,8 0+pjo! KsP6i5Y+*I 2GL1оL+'#3:#%L@|2(Mp 1D˾@ QnMlEZ5KLO Q±T2umQ!DrԔQ8oȱ_/vv>32*|]EӤ0#R.+J 4w-43Kш볦%MeL%rle=!k.D5YhMF#s70O[X]s1M5%mQQ|zV[g߂{ǖjfe"PS5מEڪ6(l- Z6=J9)iYp`YM޷l-8I\;+FÏE4=S5⧖r<ɮvA(+Ɋ 4o+L;߈]QWi~ O_dP\u`?ٶSLQWy78 qi^E,-;\庲+ZDmN{uq)(,L}X% pz淶F#L ;PO3hҤ"@*x' TI#TKl 0W%Mv3=XSHBи?z[e35$OIhmNo4r$B| iZBzH4 "4&"LP,EN^,k e]\Fb&Ьl^"t ێBQ;;&D,D) l쒔lX$LT8]ˤH*3e G˝{܌TD{:t:=h3V U` {3@f'RatI46g &٥DU<\!DJؽixbɰ SRҠYғrB]yAMKs6ޮ_.T.&L FD~fĚָsIE45<ҍx?+M!MB\bO7ܢ70꿒u>TTh#ٜ4N}˿j,J̊πRJP^%I*akK1ѫeD&Ѫ,/Ku>#tIfEWe,;ڴNS;6+mx暾dmv& dl7 ƒ;Z.sQlD$fe#sWѷMT#49&$UmwXBabnZ(=/N> 0Nid9u:3,BlFKr[ !G"/ ocrs?,H07ɒRl]3u0`g9_>U, H6):\q}'vEdq,8fJf Bѹh.1-IY%YL׶̮8b. o#=橯YX{Xbu \-xGN|rD_be5]V^jeCT>iCMTl[|Ȇwݫ`py.jXWܑ_vSک/!5ၽ~N;}au[ $;gm*pՃ,)Jd}gsHg,r['Q|;!XQ.!^R%75 ,T:/?B_OD0AȃA<AAB0G>#?G0! 9i #; oy-*JA!?*1’O4BAC9 2;PI 9QBb'|AQV p72ihz7귨0ßHc[*LxDD1$O,CP kCXAl ` Ż8W Q;?Y`3*Z7PjGb= xTi3F>@k,.K£hFjFO$ClFm&q IPGC?%𑼰BKa72<<Ǻ1*<ݢ L>̢tʦLL$C@0hb q Iȃ5?L<] GCIRLMdL,J1|̪נh̃@J'ҤK!,WKګǔ*伈0L@LLExLlOL@pό|<2(*)xXp@h(>NI?K*!,F7|IO/A.4OIIGnEb @5uτ4K E Ml*a Q\X`'}Ҽ5ǴT$I$弐$$p252M4U5e6u6E83u''<=S=}?$hA%&S3DOUTE@uT@5CSE=B&SIu%NU8-'BR5S]%UVuUUUU![Y 0;liquidsoap-2.3.2/doc/orig/fosdem2020/remark.js000066400000000000000000051506461477303350200210220ustar00rootroot00000000000000require=(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i 0 && this._events[type].length > m) { this._events[type].warned = true; console.error('(node) warning: possible EventEmitter memory ' + 'leak detected. %d listeners added. ' + 'Use emitter.setMaxListeners() to increase limit.', this._events[type].length); if (typeof console.trace === 'function') { // not supported in IE 10 console.trace(); } } } return this; }; EventEmitter.prototype.on = EventEmitter.prototype.addListener; EventEmitter.prototype.once = function(type, listener) { if (!isFunction(listener)) throw TypeError('listener must be a function'); var fired = false; function g() { this.removeListener(type, g); if (!fired) { fired = true; listener.apply(this, arguments); } } g.listener = listener; this.on(type, g); return this; }; // emits a 'removeListener' event iff the listener was removed EventEmitter.prototype.removeListener = function(type, listener) { var list, position, length, i; if (!isFunction(listener)) throw TypeError('listener must be a function'); if (!this._events || !this._events[type]) return this; list = this._events[type]; length = list.length; position = -1; if (list === listener || (isFunction(list.listener) && list.listener === listener)) { delete this._events[type]; if (this._events.removeListener) this.emit('removeListener', type, listener); } else if (isObject(list)) { for (i = length; i-- > 0;) { if (list[i] === listener || (list[i].listener && list[i].listener === listener)) { position = i; break; } } if (position < 0) return this; if (list.length === 1) { list.length = 0; delete this._events[type]; } else { list.splice(position, 1); } if (this._events.removeListener) this.emit('removeListener', type, listener); } return this; }; EventEmitter.prototype.removeAllListeners = function(type) { var key, listeners; if (!this._events) return this; // not listening for removeListener, no need to emit if (!this._events.removeListener) { if (arguments.length === 0) this._events = {}; else if (this._events[type]) delete this._events[type]; return this; } // emit removeListener for all listeners on all events if (arguments.length === 0) { for (key in this._events) { if (key === 'removeListener') continue; this.removeAllListeners(key); } this.removeAllListeners('removeListener'); this._events = {}; return this; } listeners = this._events[type]; if (isFunction(listeners)) { this.removeListener(type, listeners); } else if (listeners) { // LIFO order while (listeners.length) this.removeListener(type, listeners[listeners.length - 1]); } delete this._events[type]; return this; }; EventEmitter.prototype.listeners = function(type) { var ret; if (!this._events || !this._events[type]) ret = []; else if (isFunction(this._events[type])) ret = [this._events[type]]; else ret = this._events[type].slice(); return ret; }; EventEmitter.prototype.listenerCount = function(type) { if (this._events) { var evlistener = this._events[type]; if (isFunction(evlistener)) return 1; else if (evlistener) return evlistener.length; } return 0; }; EventEmitter.listenerCount = function(emitter, type) { return emitter.listenerCount(type); }; function isFunction(arg) { return typeof arg === 'function'; } function isNumber(arg) { return typeof arg === 'number'; } function isObject(arg) { return typeof arg === 'object' && arg !== null; } function isUndefined(arg) { return arg === void 0; } },{}],2:[function(require,module,exports){ 'use strict'; var hasOwn = Object.prototype.hasOwnProperty; var toStr = Object.prototype.toString; var defineProperty = Object.defineProperty; var gOPD = Object.getOwnPropertyDescriptor; var isArray = function isArray(arr) { if (typeof Array.isArray === 'function') { return Array.isArray(arr); } return toStr.call(arr) === '[object Array]'; }; var isPlainObject = function isPlainObject(obj) { if (!obj || toStr.call(obj) !== '[object Object]') { return false; } var hasOwnConstructor = hasOwn.call(obj, 'constructor'); var hasIsPrototypeOf = obj.constructor && obj.constructor.prototype && hasOwn.call(obj.constructor.prototype, 'isPrototypeOf'); // Not own constructor property must be Object if (obj.constructor && !hasOwnConstructor && !hasIsPrototypeOf) { return false; } // Own properties are enumerated firstly, so to speed up, // if last one is own, then all properties are own. var key; for (key in obj) { /**/ } return typeof key === 'undefined' || hasOwn.call(obj, key); }; // If name is '__proto__', and Object.defineProperty is available, define __proto__ as an own property on target var setProperty = function setProperty(target, options) { if (defineProperty && options.name === '__proto__') { defineProperty(target, options.name, { enumerable: true, configurable: true, value: options.newValue, writable: true }); } else { target[options.name] = options.newValue; } }; // Return undefined instead of __proto__ if '__proto__' is not an own property var getProperty = function getProperty(obj, name) { if (name === '__proto__') { if (!hasOwn.call(obj, name)) { return void 0; } else if (gOPD) { // In early versions of node, obj['__proto__'] is buggy when obj has // __proto__ as an own property. Object.getOwnPropertyDescriptor() works. return gOPD(obj, name).value; } } return obj[name]; }; module.exports = function extend() { var options, name, src, copy, copyIsArray, clone; var target = arguments[0]; var i = 1; var length = arguments.length; var deep = false; // Handle a deep copy situation if (typeof target === 'boolean') { deep = target; target = arguments[1] || {}; // skip the boolean and the target i = 2; } if (target == null || (typeof target !== 'object' && typeof target !== 'function')) { target = {}; } for (; i < length; ++i) { options = arguments[i]; // Only deal with non-null/undefined values if (options != null) { // Extend the base object for (name in options) { src = getProperty(target, name); copy = getProperty(options, name); // Prevent never-ending loop if (target !== copy) { // Recurse if we're merging plain objects or arrays if (deep && copy && (isPlainObject(copy) || (copyIsArray = isArray(copy)))) { if (copyIsArray) { copyIsArray = false; clone = src && isArray(src) ? src : []; } else { clone = src && isPlainObject(src) ? src : {}; } // Never move original objects, clone them setProperty(target, { name: name, newValue: extend(deep, clone, copy) }); // Don't bring in undefined values } else if (typeof copy !== 'undefined') { setProperty(target, { name: name, newValue: copy }); } } } } } // Return the modified object return target; }; },{}],3:[function(require,module,exports){ (function (global){ /** * marked - a markdown parser * Copyright (c) 2011-2014, Christopher Jeffrey. (MIT Licensed) * https://github.com/chjj/marked */ ;(function() { /** * Block-Level Grammar */ var block = { newline: /^\n+/, code: /^( {4}[^\n]+\n*)+/, fences: noop, hr: /^( *[-*_]){3,} *(?:\n+|$)/, heading: /^ *(#{1,6}) *([^\n]+?) *#* *(?:\n+|$)/, nptable: noop, lheading: /^([^\n]+)\n *(=|-){2,} *(?:\n+|$)/, blockquote: /^( *>[^\n]+(\n(?!def)[^\n]+)*\n*)+/, list: /^( *)(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?!\1bull )\n*|\s*$)/, html: /^ *(?:comment *(?:\n|\s*$)|closed *(?:\n{2,}|\s*$)|closing *(?:\n{2,}|\s*$))/, def: /^ *\[([^\]]+)\]: *]+)>?(?: +["(]([^\n]+)[")])? *(?:\n+|$)/, table: noop, paragraph: /^((?:[^\n]+\n?(?!hr|heading|lheading|blockquote|tag|def))+)\n*/, text: /^[^\n]+/ }; block.bullet = /(?:[*+-]|\d+\.)/; block.item = /^( *)(bull) [^\n]*(?:\n(?!\1bull )[^\n]*)*/; block.item = replace(block.item, 'gm') (/bull/g, block.bullet) (); block.list = replace(block.list) (/bull/g, block.bullet) ('hr', '\\n+(?=\\1?(?:[-*_] *){3,}(?:\\n+|$))') ('def', '\\n+(?=' + block.def.source + ')') (); block._tag = '(?!(?:' + 'a|em|strong|small|s|cite|q|dfn|abbr|data|time|code' + '|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo' + '|span|br|wbr|ins|del|img)\\b)\\w+(?!:/|[^\\w\\s@]*@)\\b'; block.html = replace(block.html) ('comment', //) ('closed', /<(tag)[\s\S]+?<\/\1>/) ('closing', /])*?>/) (/tag/g, block._tag) (); block.paragraph = replace(block.paragraph) ('hr', block.hr) ('heading', block.heading) ('lheading', block.lheading) ('blockquote', block.blockquote) ('tag', '<' + block._tag) ('def', block.def) (); /** * Normal Block Grammar */ block.normal = merge({}, block); /** * GFM Block Grammar */ block.gfm = merge({}, block.normal, { fences: /^ *(`{3,}|~{3,})[ \.]*(\S+)? *\n([\s\S]*?)\s*\1 *(?:\n+|$)/, paragraph: /^/, heading: /^ *(#{1,6}) +([^\n]+?) *#* *(?:\n+|$)/ }); block.gfm.paragraph = replace(block.paragraph) ('(?!', '(?!' + block.gfm.fences.source.replace('\\1', '\\2') + '|' + block.list.source.replace('\\1', '\\3') + '|') (); /** * GFM + Tables Block Grammar */ block.tables = merge({}, block.gfm, { nptable: /^ *(\S.*\|.*)\n *([-:]+ *\|[-| :]*)\n((?:.*\|.*(?:\n|$))*)\n*/, table: /^ *\|(.+)\n *\|( *[-:]+[-| :]*)\n((?: *\|.*(?:\n|$))*)\n*/ }); /** * Block Lexer */ function Lexer(options) { this.tokens = []; this.tokens.links = {}; this.options = options || marked.defaults; this.rules = block.normal; if (this.options.gfm) { if (this.options.tables) { this.rules = block.tables; } else { this.rules = block.gfm; } } } /** * Expose Block Rules */ Lexer.rules = block; /** * Static Lex Method */ Lexer.lex = function(src, options) { var lexer = new Lexer(options); return lexer.lex(src); }; /** * Preprocessing */ Lexer.prototype.lex = function(src) { src = src .replace(/\r\n|\r/g, '\n') .replace(/\t/g, ' ') .replace(/\u00a0/g, ' ') .replace(/\u2424/g, '\n'); return this.token(src, true); }; /** * Lexing */ Lexer.prototype.token = function(src, top, bq) { var src = src.replace(/^ +$/gm, '') , next , loose , cap , bull , b , item , space , i , l; while (src) { // newline if (cap = this.rules.newline.exec(src)) { src = src.substring(cap[0].length); if (cap[0].length > 1) { this.tokens.push({ type: 'space' }); } } // code if (cap = this.rules.code.exec(src)) { src = src.substring(cap[0].length); cap = cap[0].replace(/^ {4}/gm, ''); this.tokens.push({ type: 'code', text: !this.options.pedantic ? cap.replace(/\n+$/, '') : cap }); continue; } // fences (gfm) if (cap = this.rules.fences.exec(src)) { src = src.substring(cap[0].length); this.tokens.push({ type: 'code', lang: cap[2], text: cap[3] || '' }); continue; } // heading if (cap = this.rules.heading.exec(src)) { src = src.substring(cap[0].length); this.tokens.push({ type: 'heading', depth: cap[1].length, text: cap[2] }); continue; } // table no leading pipe (gfm) if (top && (cap = this.rules.nptable.exec(src))) { src = src.substring(cap[0].length); item = { type: 'table', header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */), align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), cells: cap[3].replace(/\n$/, '').split('\n') }; for (i = 0; i < item.align.length; i++) { if (/^ *-+: *$/.test(item.align[i])) { item.align[i] = 'right'; } else if (/^ *:-+: *$/.test(item.align[i])) { item.align[i] = 'center'; } else if (/^ *:-+ *$/.test(item.align[i])) { item.align[i] = 'left'; } else { item.align[i] = null; } } for (i = 0; i < item.cells.length; i++) { item.cells[i] = item.cells[i].split(/ *\| */); } this.tokens.push(item); continue; } // lheading if (cap = this.rules.lheading.exec(src)) { src = src.substring(cap[0].length); this.tokens.push({ type: 'heading', depth: cap[2] === '=' ? 1 : 2, text: cap[1] }); continue; } // hr if (cap = this.rules.hr.exec(src)) { src = src.substring(cap[0].length); this.tokens.push({ type: 'hr' }); continue; } // blockquote if (cap = this.rules.blockquote.exec(src)) { src = src.substring(cap[0].length); this.tokens.push({ type: 'blockquote_start' }); cap = cap[0].replace(/^ *> ?/gm, ''); // Pass `top` to keep the current // "toplevel" state. This is exactly // how markdown.pl works. this.token(cap, top, true); this.tokens.push({ type: 'blockquote_end' }); continue; } // list if (cap = this.rules.list.exec(src)) { src = src.substring(cap[0].length); bull = cap[2]; this.tokens.push({ type: 'list_start', ordered: bull.length > 1 }); // Get each top-level item. cap = cap[0].match(this.rules.item); next = false; l = cap.length; i = 0; for (; i < l; i++) { item = cap[i]; // Remove the list item's bullet // so it is seen as the next token. space = item.length; item = item.replace(/^ *([*+-]|\d+\.) +/, ''); // Outdent whatever the // list item contains. Hacky. if (~item.indexOf('\n ')) { space -= item.length; item = !this.options.pedantic ? item.replace(new RegExp('^ {1,' + space + '}', 'gm'), '') : item.replace(/^ {1,4}/gm, ''); } // Determine whether the next list item belongs here. // Backpedal if it does not belong in this list. if (this.options.smartLists && i !== l - 1) { b = block.bullet.exec(cap[i + 1])[0]; if (bull !== b && !(bull.length > 1 && b.length > 1)) { src = cap.slice(i + 1).join('\n') + src; i = l - 1; } } // Determine whether item is loose or not. // Use: /(^|\n)(?! )[^\n]+\n\n(?!\s*$)/ // for discount behavior. loose = next || /\n\n(?!\s*$)/.test(item); if (i !== l - 1) { next = item.charAt(item.length - 1) === '\n'; if (!loose) loose = next; } this.tokens.push({ type: loose ? 'loose_item_start' : 'list_item_start' }); // Recurse. this.token(item, false, bq); this.tokens.push({ type: 'list_item_end' }); } this.tokens.push({ type: 'list_end' }); continue; } // html if (cap = this.rules.html.exec(src)) { src = src.substring(cap[0].length); this.tokens.push({ type: this.options.sanitize ? 'paragraph' : 'html', pre: !this.options.sanitizer && (cap[1] === 'pre' || cap[1] === 'script' || cap[1] === 'style'), text: cap[0] }); continue; } // def if ((!bq && top) && (cap = this.rules.def.exec(src))) { src = src.substring(cap[0].length); this.tokens.links[cap[1].toLowerCase()] = { href: cap[2], title: cap[3] }; continue; } // table (gfm) if (top && (cap = this.rules.table.exec(src))) { src = src.substring(cap[0].length); item = { type: 'table', header: cap[1].replace(/^ *| *\| *$/g, '').split(/ *\| */), align: cap[2].replace(/^ *|\| *$/g, '').split(/ *\| */), cells: cap[3].replace(/(?: *\| *)?\n$/, '').split('\n') }; for (i = 0; i < item.align.length; i++) { if (/^ *-+: *$/.test(item.align[i])) { item.align[i] = 'right'; } else if (/^ *:-+: *$/.test(item.align[i])) { item.align[i] = 'center'; } else if (/^ *:-+ *$/.test(item.align[i])) { item.align[i] = 'left'; } else { item.align[i] = null; } } for (i = 0; i < item.cells.length; i++) { item.cells[i] = item.cells[i] .replace(/^ *\| *| *\| *$/g, '') .split(/ *\| */); } this.tokens.push(item); continue; } // top-level paragraph if (top && (cap = this.rules.paragraph.exec(src))) { src = src.substring(cap[0].length); this.tokens.push({ type: 'paragraph', text: cap[1].charAt(cap[1].length - 1) === '\n' ? cap[1].slice(0, -1) : cap[1] }); continue; } // text if (cap = this.rules.text.exec(src)) { // Top-level should never reach here. src = src.substring(cap[0].length); this.tokens.push({ type: 'text', text: cap[0] }); continue; } if (src) { throw new Error('Infinite loop on byte: ' + src.charCodeAt(0)); } } return this.tokens; }; /** * Inline-Level Grammar */ var inline = { escape: /^\\([\\`*{}\[\]()#+\-.!_>])/, autolink: /^<([^ >]+(@|:\/)[^ >]+)>/, url: noop, tag: /^|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/, link: /^!?\[(inside)\]\(href\)/, reflink: /^!?\[(inside)\]\s*\[([^\]]*)\]/, nolink: /^!?\[((?:\[[^\]]*\]|[^\[\]])*)\]/, strong: /^__([\s\S]+?)__(?!_)|^\*\*([\s\S]+?)\*\*(?!\*)/, em: /^\b_((?:[^_]|__)+?)_\b|^\*((?:\*\*|[\s\S])+?)\*(?!\*)/, code: /^(`+)([\s\S]*?[^`])\1(?!`)/, br: /^ {2,}\n(?!\s*$)/, del: noop, text: /^[\s\S]+?(?=[\\?(?:\s+['"]([\s\S]*?)['"])?\s*/; inline.link = replace(inline.link) ('inside', inline._inside) ('href', inline._href) (); inline.reflink = replace(inline.reflink) ('inside', inline._inside) (); /** * Normal Inline Grammar */ inline.normal = merge({}, inline); /** * Pedantic Inline Grammar */ inline.pedantic = merge({}, inline.normal, { strong: /^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/, em: /^_(?=\S)([\s\S]*?\S)_(?!_)|^\*(?=\S)([\s\S]*?\S)\*(?!\*)/ }); /** * GFM Inline Grammar */ inline.gfm = merge({}, inline.normal, { escape: replace(inline.escape)('])', '~|])')(), url: /^(https?:\/\/[^\s<]+[^<.,:;"')\]\s])/, del: /^~~(?=\S)([\s\S]*?\S)~~/, text: replace(inline.text) (']|', '~]|') ('|', '|https?://|') () }); /** * GFM + Line Breaks Inline Grammar */ inline.breaks = merge({}, inline.gfm, { br: replace(inline.br)('{2,}', '*')(), text: replace(inline.gfm.text)('{2,}', '*')() }); /** * Inline Lexer & Compiler */ function InlineLexer(links, options) { this.options = options || marked.defaults; this.links = links; this.rules = inline.normal; this.renderer = this.options.renderer || new Renderer; this.renderer.options = this.options; if (!this.links) { throw new Error('Tokens array requires a `links` property.'); } if (this.options.gfm) { if (this.options.breaks) { this.rules = inline.breaks; } else { this.rules = inline.gfm; } } else if (this.options.pedantic) { this.rules = inline.pedantic; } } /** * Expose Inline Rules */ InlineLexer.rules = inline; /** * Static Lexing/Compiling Method */ InlineLexer.output = function(src, links, options) { var inline = new InlineLexer(links, options); return inline.output(src); }; /** * Lexing/Compiling */ InlineLexer.prototype.output = function(src) { var out = '' , link , text , href , cap; while (src) { // escape if (cap = this.rules.escape.exec(src)) { src = src.substring(cap[0].length); out += cap[1]; continue; } // autolink if (cap = this.rules.autolink.exec(src)) { src = src.substring(cap[0].length); if (cap[2] === '@') { text = escape( cap[1].charAt(6) === ':' ? this.mangle(cap[1].substring(7)) : this.mangle(cap[1]) ); href = this.mangle('mailto:') + text; } else { text = escape(cap[1]); href = text; } out += this.renderer.link(href, null, text); continue; } // url (gfm) if (!this.inLink && (cap = this.rules.url.exec(src))) { src = src.substring(cap[0].length); text = escape(cap[1]); href = text; out += this.renderer.link(href, null, text); continue; } // tag if (cap = this.rules.tag.exec(src)) { if (!this.inLink && /^/i.test(cap[0])) { this.inLink = false; } src = src.substring(cap[0].length); out += this.options.sanitize ? this.options.sanitizer ? this.options.sanitizer(cap[0]) : escape(cap[0]) : cap[0] continue; } // link if (cap = this.rules.link.exec(src)) { src = src.substring(cap[0].length); this.inLink = true; out += this.outputLink(cap, { href: cap[2], title: cap[3] }); this.inLink = false; continue; } // reflink, nolink if ((cap = this.rules.reflink.exec(src)) || (cap = this.rules.nolink.exec(src))) { src = src.substring(cap[0].length); link = (cap[2] || cap[1]).replace(/\s+/g, ' '); link = this.links[link.toLowerCase()]; if (!link || !link.href) { out += cap[0].charAt(0); src = cap[0].substring(1) + src; continue; } this.inLink = true; out += this.outputLink(cap, link); this.inLink = false; continue; } // strong if (cap = this.rules.strong.exec(src)) { src = src.substring(cap[0].length); out += this.renderer.strong(this.output(cap[2] || cap[1])); continue; } // em if (cap = this.rules.em.exec(src)) { src = src.substring(cap[0].length); out += this.renderer.em(this.output(cap[2] || cap[1])); continue; } // code if (cap = this.rules.code.exec(src)) { src = src.substring(cap[0].length); out += this.renderer.codespan(escape(cap[2].trim(), true)); continue; } // br if (cap = this.rules.br.exec(src)) { src = src.substring(cap[0].length); out += this.renderer.br(); continue; } // del (gfm) if (cap = this.rules.del.exec(src)) { src = src.substring(cap[0].length); out += this.renderer.del(this.output(cap[1])); continue; } // text if (cap = this.rules.text.exec(src)) { src = src.substring(cap[0].length); out += this.renderer.text(escape(this.smartypants(cap[0]))); continue; } if (src) { throw new Error('Infinite loop on byte: ' + src.charCodeAt(0)); } } return out; }; /** * Compile Link */ InlineLexer.prototype.outputLink = function(cap, link) { var href = escape(link.href) , title = link.title ? escape(link.title) : null; return cap[0].charAt(0) !== '!' ? this.renderer.link(href, title, this.output(cap[1])) : this.renderer.image(href, title, escape(cap[1])); }; /** * Smartypants Transformations */ InlineLexer.prototype.smartypants = function(text) { if (!this.options.smartypants) return text; return text // em-dashes .replace(/---/g, '\u2014') // en-dashes .replace(/--/g, '\u2013') // opening singles .replace(/(^|[-\u2014/(\[{"\s])'/g, '$1\u2018') // closing singles & apostrophes .replace(/'/g, '\u2019') // opening doubles .replace(/(^|[-\u2014/(\[{\u2018\s])"/g, '$1\u201c') // closing doubles .replace(/"/g, '\u201d') // ellipses .replace(/\.{3}/g, '\u2026'); }; /** * Mangle Links */ InlineLexer.prototype.mangle = function(text) { if (!this.options.mangle) return text; var out = '' , l = text.length , i = 0 , ch; for (; i < l; i++) { ch = text.charCodeAt(i); if (Math.random() > 0.5) { ch = 'x' + ch.toString(16); } out += '&#' + ch + ';'; } return out; }; /** * Renderer */ function Renderer(options) { this.options = options || {}; } Renderer.prototype.code = function(code, lang, escaped) { if (this.options.highlight) { var out = this.options.highlight(code, lang); if (out != null && out !== code) { escaped = true; code = out; } } if (!lang) { return '

'
      + (escaped ? code : escape(code, true))
      + '\n
'; } return '
'
    + (escaped ? code : escape(code, true))
    + '\n
\n'; }; Renderer.prototype.blockquote = function(quote) { return '
\n' + quote + '
\n'; }; Renderer.prototype.html = function(html) { return html; }; Renderer.prototype.heading = function(text, level, raw) { return '' + text + '\n'; }; Renderer.prototype.hr = function() { return this.options.xhtml ? '
\n' : '
\n'; }; Renderer.prototype.list = function(body, ordered) { var type = ordered ? 'ol' : 'ul'; return '<' + type + '>\n' + body + '\n'; }; Renderer.prototype.listitem = function(text) { return '
  • ' + text + '
  • \n'; }; Renderer.prototype.paragraph = function(text) { return '

    ' + text + '

    \n'; }; Renderer.prototype.table = function(header, body) { return '\n' + '\n' + header + '\n' + '\n' + body + '\n' + '
    \n'; }; Renderer.prototype.tablerow = function(content) { return '\n' + content + '\n'; }; Renderer.prototype.tablecell = function(content, flags) { var type = flags.header ? 'th' : 'td'; var tag = flags.align ? '<' + type + ' style="text-align:' + flags.align + '">' : '<' + type + '>'; return tag + content + '\n'; }; // span level renderer Renderer.prototype.strong = function(text) { return '' + text + ''; }; Renderer.prototype.em = function(text) { return '' + text + ''; }; Renderer.prototype.codespan = function(text) { return '' + text + ''; }; Renderer.prototype.br = function() { return this.options.xhtml ? '
    ' : '
    '; }; Renderer.prototype.del = function(text) { return '' + text + ''; }; Renderer.prototype.link = function(href, title, text) { if (this.options.sanitize) { try { var prot = decodeURIComponent(unescape(href)) .replace(/[^\w:]/g, '') .toLowerCase(); } catch (e) { return ''; } if (prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) { return ''; } } if (this.options.baseUrl && !originIndependentUrl.test(href)) { href = resolveUrl(this.options.baseUrl, href); } var out = '
    '; return out; }; Renderer.prototype.image = function(href, title, text) { if (this.options.baseUrl && !originIndependentUrl.test(href)) { href = resolveUrl(this.options.baseUrl, href); } var out = '' + text + '' : '>'; return out; }; Renderer.prototype.text = function(text) { return text; }; /** * Parsing & Compiling */ function Parser(options) { this.tokens = []; this.token = null; this.options = options || marked.defaults; this.options.renderer = this.options.renderer || new Renderer; this.renderer = this.options.renderer; this.renderer.options = this.options; } /** * Static Parse Method */ Parser.parse = function(src, options, renderer) { var parser = new Parser(options, renderer); return parser.parse(src); }; /** * Parse Loop */ Parser.prototype.parse = function(src) { this.inline = new InlineLexer(src.links, this.options, this.renderer); this.tokens = src.reverse(); var out = ''; while (this.next()) { out += this.tok(); } return out; }; /** * Next Token */ Parser.prototype.next = function() { return this.token = this.tokens.pop(); }; /** * Preview Next Token */ Parser.prototype.peek = function() { return this.tokens[this.tokens.length - 1] || 0; }; /** * Parse Text Tokens */ Parser.prototype.parseText = function() { var body = this.token.text; while (this.peek().type === 'text') { body += '\n' + this.next().text; } return this.inline.output(body); }; /** * Parse Current Token */ Parser.prototype.tok = function() { switch (this.token.type) { case 'space': { return ''; } case 'hr': { return this.renderer.hr(); } case 'heading': { return this.renderer.heading( this.inline.output(this.token.text), this.token.depth, this.token.text); } case 'code': { return this.renderer.code(this.token.text, this.token.lang, this.token.escaped); } case 'table': { var header = '' , body = '' , i , row , cell , flags , j; // header cell = ''; for (i = 0; i < this.token.header.length; i++) { flags = { header: true, align: this.token.align[i] }; cell += this.renderer.tablecell( this.inline.output(this.token.header[i]), { header: true, align: this.token.align[i] } ); } header += this.renderer.tablerow(cell); for (i = 0; i < this.token.cells.length; i++) { row = this.token.cells[i]; cell = ''; for (j = 0; j < row.length; j++) { cell += this.renderer.tablecell( this.inline.output(row[j]), { header: false, align: this.token.align[j] } ); } body += this.renderer.tablerow(cell); } return this.renderer.table(header, body); } case 'blockquote_start': { var body = ''; while (this.next().type !== 'blockquote_end') { body += this.tok(); } return this.renderer.blockquote(body); } case 'list_start': { var body = '' , ordered = this.token.ordered; while (this.next().type !== 'list_end') { body += this.tok(); } return this.renderer.list(body, ordered); } case 'list_item_start': { var body = ''; while (this.next().type !== 'list_item_end') { body += this.token.type === 'text' ? this.parseText() : this.tok(); } return this.renderer.listitem(body); } case 'loose_item_start': { var body = ''; while (this.next().type !== 'list_item_end') { body += this.tok(); } return this.renderer.listitem(body); } case 'html': { var html = !this.token.pre && !this.options.pedantic ? this.inline.output(this.token.text) : this.token.text; return this.renderer.html(html); } case 'paragraph': { return this.renderer.paragraph(this.inline.output(this.token.text)); } case 'text': { return this.renderer.paragraph(this.parseText()); } } }; /** * Helpers */ function escape(html, encode) { return html .replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function unescape(html) { // explicitly match decimal, hex, and named HTML entities return html.replace(/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig, function(_, n) { n = n.toLowerCase(); if (n === 'colon') return ':'; if (n.charAt(0) === '#') { return n.charAt(1) === 'x' ? String.fromCharCode(parseInt(n.substring(2), 16)) : String.fromCharCode(+n.substring(1)); } return ''; }); } function replace(regex, opt) { regex = regex.source; opt = opt || ''; return function self(name, val) { if (!name) return new RegExp(regex, opt); val = val.source || val; val = val.replace(/(^|[^\[])\^/g, '$1'); regex = regex.replace(name, val); return self; }; } function resolveUrl(base, href) { if (!baseUrls[' ' + base]) { // we can ignore everything in base after the last slash of its path component, // but we might need to add _that_ // https://tools.ietf.org/html/rfc3986#section-3 if (/^[^:]+:\/*[^/]*$/.test(base)) { baseUrls[' ' + base] = base + '/'; } else { baseUrls[' ' + base] = base.replace(/[^/]*$/, ''); } } base = baseUrls[' ' + base]; if (href.slice(0, 2) === '//') { return base.replace(/:[^]*/, ':') + href; } else if (href.charAt(0) === '/') { return base.replace(/(:\/*[^/]*)[^]*/, '$1') + href; } else { return base + href; } } baseUrls = {}; originIndependentUrl = /^$|^[a-z][a-z0-9+.-]*:|^[?#]/i; function noop() {} noop.exec = noop; function merge(obj) { var i = 1 , target , key; for (; i < arguments.length; i++) { target = arguments[i]; for (key in target) { if (Object.prototype.hasOwnProperty.call(target, key)) { obj[key] = target[key]; } } } return obj; } /** * Marked */ function marked(src, opt, callback) { if (callback || typeof opt === 'function') { if (!callback) { callback = opt; opt = null; } opt = merge({}, marked.defaults, opt || {}); var highlight = opt.highlight , tokens , pending , i = 0; try { tokens = Lexer.lex(src, opt) } catch (e) { return callback(e); } pending = tokens.length; var done = function(err) { if (err) { opt.highlight = highlight; return callback(err); } var out; try { out = Parser.parse(tokens, opt); } catch (e) { err = e; } opt.highlight = highlight; return err ? callback(err) : callback(null, out); }; if (!highlight || highlight.length < 3) { return done(); } delete opt.highlight; if (!pending) return done(); for (; i < tokens.length; i++) { (function(token) { if (token.type !== 'code') { return --pending || done(); } return highlight(token.text, token.lang, function(err, code) { if (err) return done(err); if (code == null || code === token.text) { return --pending || done(); } token.text = code; token.escaped = true; --pending || done(); }); })(tokens[i]); } return; } try { if (opt) opt = merge({}, marked.defaults, opt); return Parser.parse(Lexer.lex(src, opt), opt); } catch (e) { e.message += '\nPlease report this to https://github.com/chjj/marked.'; if ((opt || marked.defaults).silent) { return '

    An error occured:

    '
            + escape(e.message + '', true)
            + '
    '; } throw e; } } /** * Options */ marked.options = marked.setOptions = function(opt) { merge(marked.defaults, opt); return marked; }; marked.defaults = { gfm: true, tables: true, breaks: false, pedantic: false, sanitize: false, sanitizer: null, mangle: true, smartLists: false, silent: false, highlight: null, langPrefix: 'lang-', smartypants: false, headerPrefix: '', renderer: new Renderer, xhtml: false, baseUrl: null }; /** * Expose */ marked.Parser = Parser; marked.parser = Parser.parse; marked.Renderer = Renderer; marked.Lexer = Lexer; marked.lexer = Lexer.lex; marked.InlineLexer = InlineLexer; marked.inlineLexer = InlineLexer.output; marked.parse = marked; if (typeof module !== 'undefined' && typeof exports === 'object') { module.exports = marked; } else if (typeof define === 'function' && define.amd) { define(function() { return marked; }); } else { this.marked = marked; } }).call(function() { return this || (typeof window !== 'undefined' ? window : global); }()); }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{}],4:[function(require,module,exports){ exports.apply = function () { forEach([Array, window.NodeList, window.HTMLCollection], extend); }; function forEach (list, f) { var i; for (i = 0; i < list.length; ++i) { f(list[i], i); } } function extend (object) { var prototype = object && object.prototype; if (!prototype) { return; } prototype.forEach = prototype.forEach || function (f) { forEach(this, f); }; prototype.filter = prototype.filter || function (f) { var result = []; this.forEach(function (element) { if (f(element, result.length)) { result.push(element); } }); return result; }; prototype.map = prototype.map || function (f) { var result = []; this.forEach(function (element) { result.push(f(element, result.length)); }); return result; }; } },{}],5:[function(require,module,exports){ var Api = require('./remark/api') , polyfills = require('./polyfills') , styler = require('./remark/components/styler/styler') ; // Expose API as `remark` window.remark = new Api(); // Apply polyfills as needed polyfills.apply(); // Apply embedded styles to document styler.styleDocument(); },{"./polyfills":4,"./remark/api":6,"./remark/components/styler/styler":"components/styler"}],6:[function(require,module,exports){ var EventEmitter = require('events').EventEmitter , highlighter = require('./highlighter') , converter = require('./converter') , resources = require('./resources') , Parser = require('./parser') , Slideshow = require('./models/slideshow') , SlideshowView = require('./views/slideshowView') , DefaultController = require('./controllers/defaultController') , Dom = require('./dom') , macros = require('./macros') ; module.exports = Api; function Api (dom) { this.dom = dom || new Dom(); this.macros = macros; this.version = resources.version; } // Expose highlighter to allow enumerating available styles and // including external language grammars Api.prototype.highlighter = highlighter; Api.prototype.convert = function (markdown) { var parser = new Parser() , content = parser.parse(markdown || '', macros)[0].content ; return converter.convertMarkdown(content, {}, true); }; // Creates slideshow initialized from options Api.prototype.create = function (options, callback) { var self = this , events , slideshow , slideshowView , controller ; options = applyDefaults(this.dom, options); events = new EventEmitter(); events.setMaxListeners(0); slideshow = new Slideshow(events, this.dom, options, function (slideshow) { slideshowView = new SlideshowView(events, self.dom, options, slideshow); controller = options.controller || new DefaultController(events, self.dom, slideshowView, options.navigation); if (typeof callback === 'function') { callback(slideshow); } }); return slideshow; }; function applyDefaults (dom, options) { var sourceElement; options = options || {}; if (!options.hasOwnProperty('source')) { sourceElement = dom.getElementById('source'); if (sourceElement) { options.source = unescape(sourceElement.innerHTML); sourceElement.style.display = 'none'; } } if (!(options.container instanceof window.HTMLElement)) { options.container = dom.getBodyElement(); } return options; } function unescape (source) { source = source.replace(/&[l|g]t;/g, function (match) { return match === '<' ? '<' : '>'; }); source = source.replace(/&/g, '&'); source = source.replace(/"/g, '"'); return source; } },{"./controllers/defaultController":7,"./converter":13,"./dom":14,"./highlighter":15,"./macros":17,"./models/slideshow":19,"./parser":22,"./resources":23,"./views/slideshowView":28,"events":1}],7:[function(require,module,exports){ // Allow override of global `location` /* global location:true */ module.exports = Controller; var Keyboard = require('./inputs/keyboard') , mouse = require('./inputs/mouse') , touch = require('./inputs/touch') , message = require('./inputs/message') , location = require('./inputs/location') ; function Controller (events, dom, slideshowView, options) { options = options || {}; var keyboard = new Keyboard(events); message.register(events); location.register(events, dom, slideshowView); mouse.register(events, options); touch.register(events, options); addApiEventListeners(events, keyboard, slideshowView, options); } function addApiEventListeners (events, keyboard, slideshowView, options) { events.on('pause', function(event) { keyboard.deactivate(); mouse.unregister(events); touch.unregister(events); }); events.on('resume', function(event) { keyboard.activate(); mouse.register(events, options); touch.register(events, options); }); } },{"./inputs/keyboard":8,"./inputs/location":9,"./inputs/message":10,"./inputs/mouse":11,"./inputs/touch":12}],8:[function(require,module,exports){ module.exports = Keyboard; function Keyboard(events) { this._events = events; this.activate(); } Keyboard.prototype.activate = function () { this._gotoSlideNumber = ''; this.addKeyboardEventListeners(); }; Keyboard.prototype.deactivate = function () { this.removeKeyboardEventListeners(); }; Keyboard.prototype.addKeyboardEventListeners = function () { var self = this; var events = this._events; events.on('keydown', function (event) { if (event.metaKey || event.ctrlKey || event.altKey) { // Bail out if alt, meta or ctrl key was pressed return; } switch (event.keyCode) { case 33: // Page up case 37: // Left case 38: // Up events.emit('gotoPreviousSlide'); break; case 32: // Space if(event.shiftKey){ // Shift+Space events.emit('gotoPreviousSlide'); }else{ events.emit('gotoNextSlide'); } break; case 34: // Page down case 39: // Right case 40: // Down events.emit('gotoNextSlide'); break; case 36: // Home events.emit('gotoFirstSlide'); break; case 35: // End events.emit('gotoLastSlide'); break; case 27: // Escape events.emit('hideOverlay'); break; case 13: // Return if (self._gotoSlideNumber) { events.emit('gotoSlideNumber', self._gotoSlideNumber); self._gotoSlideNumber = ''; } break; } }); events.on('keypress', function (event) { if (event.metaKey || event.ctrlKey) { // Bail out if meta or ctrl key was pressed return; } var key = String.fromCharCode(event.which).toLowerCase(); var tryToPreventDefault = true; switch (key) { case 'j': events.emit('gotoNextSlide'); break; case 'k': events.emit('gotoPreviousSlide'); break; case 'b': events.emit('toggleBlackout'); break; case 'm': events.emit('toggleMirrored'); break; case 'c': events.emit('createClone'); break; case 'p': events.emit('togglePresenterMode'); break; case 'f': events.emit('toggleFullscreen'); break; case 's': events.emit('toggleTimer'); break; case 't': events.emit('resetTimer'); break; case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '0': self._gotoSlideNumber += key; break; case 'h': case '?': events.emit('toggleHelp'); break; default: tryToPreventDefault = false; } if (tryToPreventDefault && event && event.preventDefault) event.preventDefault(); }); }; Keyboard.prototype.removeKeyboardEventListeners = function () { var events = this._events; events.removeAllListeners("keydown"); events.removeAllListeners("keypress"); }; },{}],9:[function(require,module,exports){ var utils = require('../../utils.js'); exports.register = function (events, dom, slideshowView) { addLocationEventListeners(events, dom, slideshowView); }; function addLocationEventListeners (events, dom, slideshowView) { // If slideshow is embedded into custom DOM element, we don't // hook up to location hash changes, so just go to first slide. if (slideshowView.isEmbedded()) { events.emit('gotoSlide', 1); } // When slideshow is not embedded into custom DOM element, but // rather hosted directly inside document.body, we hook up to // location hash changes, and trigger initial navigation. else { events.on('hashchange', navigateByHash); events.on('slideChanged', updateHash); events.on('toggledPresenter', updateHash); navigateByHash(); } function navigateByHash () { var slideNoOrName = (dom.getLocationHash() || '').substr(1); events.emit('gotoSlide', slideNoOrName); } function updateHash (slideNoOrName) { if(utils.hasClass(slideshowView.containerElement, 'remark-presenter-mode')){ dom.setLocationHash('#p' + slideNoOrName); } else{ dom.setLocationHash('#' + slideNoOrName); } } } },{"../../utils.js":25}],10:[function(require,module,exports){ exports.register = function (events) { addMessageEventListeners(events); }; function addMessageEventListeners (events) { events.on('message', navigateByMessage); function navigateByMessage(message) { var cap; if ((cap = /^gotoSlide:(\d+)$/.exec(message.data)) !== null) { events.emit('gotoSlide', parseInt(cap[1], 10), true); } else if (message.data === 'toggleBlackout') { events.emit('toggleBlackout', {propagate: false}); } } } },{}],11:[function(require,module,exports){ exports.register = function (events, options) { addMouseEventListeners(events, options); }; exports.unregister = function (events) { removeMouseEventListeners(events); }; function addMouseEventListeners (events, options) { if (options.click) { events.on('click', function (event) { if (event.target.nodeName === 'A') { // Don't interfere when clicking link return; } else if (event.button === 0) { events.emit('gotoNextSlide'); } }); events.on('contextmenu', function (event) { if (event.target.nodeName === 'A') { // Don't interfere when right-clicking link return; } event.preventDefault(); events.emit('gotoPreviousSlide'); }); } if (options.scroll !== false) { var scrollHandler = function (event) { if (event.wheelDeltaY > 0 || event.detail < 0) { events.emit('gotoPreviousSlide'); } else if (event.wheelDeltaY < 0 || event.detail > 0) { events.emit('gotoNextSlide'); } }; // IE9, Chrome, Safari, Opera events.on('mousewheel', scrollHandler); // Firefox events.on('DOMMouseScroll', scrollHandler); } } function removeMouseEventListeners(events) { events.removeAllListeners('click'); events.removeAllListeners('contextmenu'); events.removeAllListeners('mousewheel'); } },{}],12:[function(require,module,exports){ exports.register = function (events, options) { addTouchEventListeners(events, options); }; exports.unregister = function (events) { removeTouchEventListeners(events); }; function addTouchEventListeners (events, options) { var touch , startX , endX ; if (options.touch === false) { return; } var isTap = function () { return Math.abs(startX - endX) < 10; }; var handleTap = function () { events.emit('tap', endX); }; var handleSwipe = function () { if (startX > endX) { events.emit('gotoNextSlide'); } else { events.emit('gotoPreviousSlide'); } }; events.on('touchstart', function (event) { touch = event.touches[0]; startX = touch.clientX; }); events.on('touchend', function (event) { if (event.target.nodeName.toUpperCase() === 'A') { return; } touch = event.changedTouches[0]; endX = touch.clientX; if (isTap()) { handleTap(); } else { handleSwipe(); } }); events.on('touchmove', function (event) { event.preventDefault(); }); } function removeTouchEventListeners(events) { events.removeAllListeners("touchstart"); events.removeAllListeners("touchend"); events.removeAllListeners("touchmove"); } },{}],13:[function(require,module,exports){ var marked = require('marked') , converter = module.exports = {} , element = document.createElement('div') ; marked.setOptions({ gfm: true, tables: true, breaks: false, // Without this set to true, converting something like //

    *

    *

    will become

    pedantic: true, sanitize: false, smartLists: true, langPrefix: '' }); converter.convertMarkdown = function (content, links, inline) { element.innerHTML = convertMarkdown(content, links || {}, inline); element.innerHTML = element.innerHTML.replace(/

    \s*<\/p>/g, ''); return element.innerHTML.replace(/\n\r?$/, ''); }; function convertMarkdown (content, links, insideContentClass) { var i, tag, markdown = '', html; for (i = 0; i < content.length; ++i) { if (typeof content[i] === 'string') { markdown += content[i]; } else { tag = content[i].block ? 'div' : 'span'; markdown += '<' + tag + ' class="' + content[i].class + '">'; markdown += convertMarkdown(content[i].content, links, !content[i].block); markdown += ''; } } var tokens = marked.Lexer.lex(markdown.replace(/^\s+/, '')); tokens.links = links; html = marked.Parser.parse(tokens); if (insideContentClass) { element.innerHTML = html; if (element.children.length === 1 && element.children[0].tagName === 'P') { html = element.children[0].innerHTML; } } return html; } },{"marked":3}],14:[function(require,module,exports){ module.exports = Dom; function Dom () { } Dom.prototype.XMLHttpRequest = XMLHttpRequest; Dom.prototype.getHTMLElement = function () { return document.getElementsByTagName('html')[0]; }; Dom.prototype.getBodyElement = function () { return document.body; }; Dom.prototype.getElementById = function (id) { return document.getElementById(id); }; Dom.prototype.getLocationHash = function () { return window.location.hash; }; Dom.prototype.setLocationHash = function (hash) { if (typeof window.history.replaceState === 'function' && window.origin !== 'null') { window.history.replaceState(undefined, undefined, hash); } else { window.location.hash = hash; } }; },{}],15:[function(require,module,exports){ /* Automatically generated */ var hljs = (function() { var exports = {}; /* Syntax highlighting with language autodetection. https://highlightjs.org/ */ (function(factory) { // Find the global object for export to both the browser and web workers. var globalObject = typeof window === 'object' && window || typeof self === 'object' && self; // Setup highlight.js for different environments. First is Node.js or // CommonJS. // `nodeType` is checked to ensure that `exports` is not a HTML element. if(typeof exports !== 'undefined' && !exports.nodeType) { factory(exports); } else if(globalObject) { // Export hljs globally even when using AMD for cases when this script // is loaded with others that may still expect a global hljs. globalObject.hljs = factory({}); // Finally register the global hljs with AMD. if(typeof define === 'function' && define.amd) { define([], function() { return globalObject.hljs; }); } } }(function(hljs) { // Convenience variables for build-in objects var ArrayProto = [], objectKeys = Object.keys; // Global internal variables used within the highlight.js library. var languages = {}, aliases = {}; // Regular expressions used throughout the highlight.js library. var noHighlightRe = /^(no-?highlight|plain|text)$/i, languagePrefixRe = /\blang(?:uage)?-([\w-]+)\b/i, fixMarkupRe = /((^(<[^>]+>|\t|)+|(?:\n)))/gm; // The object will be assigned by the build tool. It used to synchronize API // of external language files with minified version of the highlight.js library. var API_REPLACES; var spanEndTag = ''; // Global options used when within external APIs. This is modified when // calling the `hljs.configure` function. var options = { classPrefix: 'hljs-', tabReplace: null, useBR: false, languages: undefined }; /* Utility functions */ function escape(value) { return value.replace(/&/g, '&').replace(//g, '>'); } function tag(node) { return node.nodeName.toLowerCase(); } function testRe(re, lexeme) { var match = re && re.exec(lexeme); return match && match.index === 0; } function isNotHighlighted(language) { return noHighlightRe.test(language); } function blockLanguage(block) { var i, match, length, _class; var classes = block.className + ' '; classes += block.parentNode ? block.parentNode.className : ''; // language-* takes precedence over non-prefixed class names. match = languagePrefixRe.exec(classes); if (match) { return getLanguage(match[1]) ? match[1] : 'no-highlight'; } classes = classes.split(/\s+/); for (i = 0, length = classes.length; i < length; i++) { _class = classes[i]; if (isNotHighlighted(_class) || getLanguage(_class)) { return _class; } } } function inherit(parent) { // inherit(parent, override_obj, override_obj, ...) var key; var result = {}; var objects = Array.prototype.slice.call(arguments, 1); for (key in parent) result[key] = parent[key]; objects.forEach(function(obj) { for (key in obj) result[key] = obj[key]; }); return result; } /* Stream merging */ function nodeStream(node) { var result = []; (function _nodeStream(node, offset) { for (var child = node.firstChild; child; child = child.nextSibling) { if (child.nodeType === 3) offset += child.nodeValue.length; else if (child.nodeType === 1) { result.push({ event: 'start', offset: offset, node: child }); offset = _nodeStream(child, offset); // Prevent void elements from having an end tag that would actually // double them in the output. There are more void elements in HTML // but we list only those realistically expected in code display. if (!tag(child).match(/br|hr|img|input/)) { result.push({ event: 'stop', offset: offset, node: child }); } } } return offset; })(node, 0); return result; } function mergeStreams(original, highlighted, value) { var processed = 0; var result = ''; var nodeStack = []; function selectStream() { if (!original.length || !highlighted.length) { return original.length ? original : highlighted; } if (original[0].offset !== highlighted[0].offset) { return (original[0].offset < highlighted[0].offset) ? original : highlighted; } /* To avoid starting the stream just before it should stop the order is ensured that original always starts first and closes last: if (event1 == 'start' && event2 == 'start') return original; if (event1 == 'start' && event2 == 'stop') return highlighted; if (event1 == 'stop' && event2 == 'start') return original; if (event1 == 'stop' && event2 == 'stop') return highlighted; ... which is collapsed to: */ return highlighted[0].event === 'start' ? original : highlighted; } function open(node) { function attr_str(a) {return ' ' + a.nodeName + '="' + escape(a.value).replace('"', '"') + '"';} result += '<' + tag(node) + ArrayProto.map.call(node.attributes, attr_str).join('') + '>'; } function close(node) { result += ''; } function render(event) { (event.event === 'start' ? open : close)(event.node); } while (original.length || highlighted.length) { var stream = selectStream(); result += escape(value.substring(processed, stream[0].offset)); processed = stream[0].offset; if (stream === original) { /* On any opening or closing tag of the original markup we first close the entire highlighted node stack, then render the original tag along with all the following original tags at the same offset and then reopen all the tags on the highlighted stack. */ nodeStack.reverse().forEach(close); do { render(stream.splice(0, 1)[0]); stream = selectStream(); } while (stream === original && stream.length && stream[0].offset === processed); nodeStack.reverse().forEach(open); } else { if (stream[0].event === 'start') { nodeStack.push(stream[0].node); } else { nodeStack.pop(); } render(stream.splice(0, 1)[0]); } } return result + escape(value.substr(processed)); } /* Initialization */ function expand_mode(mode) { if (mode.variants && !mode.cached_variants) { mode.cached_variants = mode.variants.map(function(variant) { return inherit(mode, {variants: null}, variant); }); } return mode.cached_variants || (mode.endsWithParent && [inherit(mode)]) || [mode]; } function restoreLanguageApi(obj) { if(API_REPLACES && !obj.langApiRestored) { obj.langApiRestored = true; for(var key in API_REPLACES) obj[key] && (obj[API_REPLACES[key]] = obj[key]); (obj.contains || []).concat(obj.variants || []).forEach(restoreLanguageApi); } } function compileLanguage(language) { function reStr(re) { return (re && re.source) || re; } function langRe(value, global) { return new RegExp( reStr(value), 'm' + (language.case_insensitive ? 'i' : '') + (global ? 'g' : '') ); } // joinRe logically computes regexps.join(separator), but fixes the // backreferences so they continue to match. function joinRe(regexps, separator) { // backreferenceRe matches an open parenthesis or backreference. To avoid // an incorrect parse, it additionally matches the following: // - [...] elements, where the meaning of parentheses and escapes change // - other escape sequences, so we do not misparse escape sequences as // interesting elements // - non-matching or lookahead parentheses, which do not capture. These // follow the '(' with a '?'. var backreferenceRe = /\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./; var numCaptures = 0; var ret = ''; for (var i = 0; i < regexps.length; i++) { var offset = numCaptures; var re = reStr(regexps[i]); if (i > 0) { ret += separator; } while (re.length > 0) { var match = backreferenceRe.exec(re); if (match == null) { ret += re; break; } ret += re.substring(0, match.index); re = re.substring(match.index + match[0].length); if (match[0][0] == '\\' && match[1]) { // Adjust the backreference. ret += '\\' + String(Number(match[1]) + offset); } else { ret += match[0]; if (match[0] == '(') { numCaptures++; } } } } return ret; } function compileMode(mode, parent) { if (mode.compiled) return; mode.compiled = true; mode.keywords = mode.keywords || mode.beginKeywords; if (mode.keywords) { var compiled_keywords = {}; var flatten = function(className, str) { if (language.case_insensitive) { str = str.toLowerCase(); } str.split(' ').forEach(function(kw) { var pair = kw.split('|'); compiled_keywords[pair[0]] = [className, pair[1] ? Number(pair[1]) : 1]; }); }; if (typeof mode.keywords === 'string') { // string flatten('keyword', mode.keywords); } else { objectKeys(mode.keywords).forEach(function (className) { flatten(className, mode.keywords[className]); }); } mode.keywords = compiled_keywords; } mode.lexemesRe = langRe(mode.lexemes || /\w+/, true); if (parent) { if (mode.beginKeywords) { mode.begin = '\\b(' + mode.beginKeywords.split(' ').join('|') + ')\\b'; } if (!mode.begin) mode.begin = /\B|\b/; mode.beginRe = langRe(mode.begin); if (mode.endSameAsBegin) mode.end = mode.begin; if (!mode.end && !mode.endsWithParent) mode.end = /\B|\b/; if (mode.end) mode.endRe = langRe(mode.end); mode.terminator_end = reStr(mode.end) || ''; if (mode.endsWithParent && parent.terminator_end) mode.terminator_end += (mode.end ? '|' : '') + parent.terminator_end; } if (mode.illegal) mode.illegalRe = langRe(mode.illegal); if (mode.relevance == null) mode.relevance = 1; if (!mode.contains) { mode.contains = []; } mode.contains = Array.prototype.concat.apply([], mode.contains.map(function(c) { return expand_mode(c === 'self' ? mode : c); })); mode.contains.forEach(function(c) {compileMode(c, mode);}); if (mode.starts) { compileMode(mode.starts, parent); } var terminators = mode.contains.map(function(c) { return c.beginKeywords ? '\\.?(?:' + c.begin + ')\\.?' : c.begin; }) .concat([mode.terminator_end, mode.illegal]) .map(reStr) .filter(Boolean); mode.terminators = terminators.length ? langRe(joinRe(terminators, '|'), true) : {exec: function(/*s*/) {return null;}}; } compileMode(language); } /* Core highlighting function. Accepts a language name, or an alias, and a string with the code to highlight. Returns an object with the following properties: - relevance (int) - value (an HTML string with highlighting markup) */ function highlight(name, value, ignore_illegals, continuation) { function escapeRe(value) { return new RegExp(value.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'), 'm'); } function subMode(lexeme, mode) { var i, length; for (i = 0, length = mode.contains.length; i < length; i++) { if (testRe(mode.contains[i].beginRe, lexeme)) { if (mode.contains[i].endSameAsBegin) { mode.contains[i].endRe = escapeRe( mode.contains[i].beginRe.exec(lexeme)[0] ); } return mode.contains[i]; } } } function endOfMode(mode, lexeme) { if (testRe(mode.endRe, lexeme)) { while (mode.endsParent && mode.parent) { mode = mode.parent; } return mode; } if (mode.endsWithParent) { return endOfMode(mode.parent, lexeme); } } function isIllegal(lexeme, mode) { return !ignore_illegals && testRe(mode.illegalRe, lexeme); } function keywordMatch(mode, match) { var match_str = language.case_insensitive ? match[0].toLowerCase() : match[0]; return mode.keywords.hasOwnProperty(match_str) && mode.keywords[match_str]; } function buildSpan(classname, insideSpan, leaveOpen, noPrefix) { var classPrefix = noPrefix ? '' : options.classPrefix, openSpan = ''; if (!classname) return insideSpan; return openSpan + insideSpan + closeSpan; } function processKeywords() { var keyword_match, last_index, match, result; if (!top.keywords) return escape(mode_buffer); result = ''; last_index = 0; top.lexemesRe.lastIndex = 0; match = top.lexemesRe.exec(mode_buffer); while (match) { result += escape(mode_buffer.substring(last_index, match.index)); keyword_match = keywordMatch(top, match); if (keyword_match) { relevance += keyword_match[1]; result += buildSpan(keyword_match[0], escape(match[0])); } else { result += escape(match[0]); } last_index = top.lexemesRe.lastIndex; match = top.lexemesRe.exec(mode_buffer); } return result + escape(mode_buffer.substr(last_index)); } function processSubLanguage() { var explicit = typeof top.subLanguage === 'string'; if (explicit && !languages[top.subLanguage]) { return escape(mode_buffer); } var result = explicit ? highlight(top.subLanguage, mode_buffer, true, continuations[top.subLanguage]) : highlightAuto(mode_buffer, top.subLanguage.length ? top.subLanguage : undefined); // Counting embedded language score towards the host language may be disabled // with zeroing the containing mode relevance. Usecase in point is Markdown that // allows XML everywhere and makes every XML snippet to have a much larger Markdown // score. if (top.relevance > 0) { relevance += result.relevance; } if (explicit) { continuations[top.subLanguage] = result.top; } return buildSpan(result.language, result.value, false, true); } function processBuffer() { result += (top.subLanguage != null ? processSubLanguage() : processKeywords()); mode_buffer = ''; } function startNewMode(mode) { result += mode.className? buildSpan(mode.className, '', true): ''; top = Object.create(mode, {parent: {value: top}}); } function processLexeme(buffer, lexeme) { mode_buffer += buffer; if (lexeme == null) { processBuffer(); return 0; } var new_mode = subMode(lexeme, top); if (new_mode) { if (new_mode.skip) { mode_buffer += lexeme; } else { if (new_mode.excludeBegin) { mode_buffer += lexeme; } processBuffer(); if (!new_mode.returnBegin && !new_mode.excludeBegin) { mode_buffer = lexeme; } } startNewMode(new_mode, lexeme); return new_mode.returnBegin ? 0 : lexeme.length; } var end_mode = endOfMode(top, lexeme); if (end_mode) { var origin = top; if (origin.skip) { mode_buffer += lexeme; } else { if (!(origin.returnEnd || origin.excludeEnd)) { mode_buffer += lexeme; } processBuffer(); if (origin.excludeEnd) { mode_buffer = lexeme; } } do { if (top.className) { result += spanEndTag; } if (!top.skip && !top.subLanguage) { relevance += top.relevance; } top = top.parent; } while (top !== end_mode.parent); if (end_mode.starts) { if (end_mode.endSameAsBegin) { end_mode.starts.endRe = end_mode.endRe; } startNewMode(end_mode.starts, ''); } return origin.returnEnd ? 0 : lexeme.length; } if (isIllegal(lexeme, top)) throw new Error('Illegal lexeme "' + lexeme + '" for mode "' + (top.className || '') + '"'); /* Parser should not reach this point as all types of lexemes should be caught earlier, but if it does due to some bug make sure it advances at least one character forward to prevent infinite looping. */ mode_buffer += lexeme; return lexeme.length || 1; } var language = getLanguage(name); if (!language) { throw new Error('Unknown language: "' + name + '"'); } compileLanguage(language); var top = continuation || language; var continuations = {}; // keep continuations for sub-languages var result = '', current; for(current = top; current !== language; current = current.parent) { if (current.className) { result = buildSpan(current.className, '', true) + result; } } var mode_buffer = ''; var relevance = 0; try { var match, count, index = 0; while (true) { top.terminators.lastIndex = index; match = top.terminators.exec(value); if (!match) break; count = processLexeme(value.substring(index, match.index), match[0]); index = match.index + count; } processLexeme(value.substr(index)); for(current = top; current.parent; current = current.parent) { // close dangling modes if (current.className) { result += spanEndTag; } } return { relevance: relevance, value: result, language: name, top: top }; } catch (e) { if (e.message && e.message.indexOf('Illegal') !== -1) { return { relevance: 0, value: escape(value) }; } else { throw e; } } } /* Highlighting with language detection. Accepts a string with the code to highlight. Returns an object with the following properties: - language (detected language) - relevance (int) - value (an HTML string with highlighting markup) - second_best (object with the same structure for second-best heuristically detected language, may be absent) */ function highlightAuto(text, languageSubset) { languageSubset = languageSubset || options.languages || objectKeys(languages); var result = { relevance: 0, value: escape(text) }; var second_best = result; languageSubset.filter(getLanguage).filter(autoDetection).forEach(function(name) { var current = highlight(name, text, false); current.language = name; if (current.relevance > second_best.relevance) { second_best = current; } if (current.relevance > result.relevance) { second_best = result; result = current; } }); if (second_best.language) { result.second_best = second_best; } return result; } /* Post-processing of the highlighted markup: - replace TABs with something more useful - replace real line-breaks with '
    ' for non-pre containers */ function fixMarkup(value) { return !(options.tabReplace || options.useBR) ? value : value.replace(fixMarkupRe, function(match, p1) { if (options.useBR && match === '\n') { return '
    '; } else if (options.tabReplace) { return p1.replace(/\t/g, options.tabReplace); } return ''; }); } function buildClassName(prevClassName, currentLang, resultLang) { var language = currentLang ? aliases[currentLang] : resultLang, result = [prevClassName.trim()]; if (!prevClassName.match(/\bhljs\b/)) { result.push('hljs'); } if (prevClassName.indexOf(language) === -1) { result.push(language); } return result.join(' ').trim(); } /* Applies highlighting to a DOM node containing code. Accepts a DOM node and two optional parameters for fixMarkup. */ function highlightBlock(block) { var node, originalStream, result, resultNode, text; var language = blockLanguage(block); if (isNotHighlighted(language)) return; if (options.useBR) { node = document.createElementNS('http://www.w3.org/1999/xhtml', 'div'); node.innerHTML = block.innerHTML.replace(/\n/g, '').replace(//g, '\n'); } else { node = block; } text = node.textContent; result = language ? highlight(language, text, true) : highlightAuto(text); originalStream = nodeStream(node); if (originalStream.length) { resultNode = document.createElementNS('http://www.w3.org/1999/xhtml', 'div'); resultNode.innerHTML = result.value; result.value = mergeStreams(originalStream, nodeStream(resultNode), text); } result.value = fixMarkup(result.value); block.innerHTML = result.value; block.className = buildClassName(block.className, language, result.language); block.result = { language: result.language, re: result.relevance }; if (result.second_best) { block.second_best = { language: result.second_best.language, re: result.second_best.relevance }; } } /* Updates highlight.js global options with values passed in the form of an object. */ function configure(user_options) { options = inherit(options, user_options); } /* Applies highlighting to all

    ..
    blocks on a page. */ function initHighlighting() { if (initHighlighting.called) return; initHighlighting.called = true; var blocks = document.querySelectorAll('pre code'); ArrayProto.forEach.call(blocks, highlightBlock); } /* Attaches highlighting to the page load event. */ function initHighlightingOnLoad() { addEventListener('DOMContentLoaded', initHighlighting, false); addEventListener('load', initHighlighting, false); } function registerLanguage(name, language) { var lang = languages[name] = language(hljs); restoreLanguageApi(lang); if (lang.aliases) { lang.aliases.forEach(function(alias) {aliases[alias] = name;}); } } function listLanguages() { return objectKeys(languages); } function getLanguage(name) { name = (name || '').toLowerCase(); return languages[name] || languages[aliases[name]]; } function autoDetection(name) { var lang = getLanguage(name); return lang && !lang.disableAutodetect; } /* Interface definition */ hljs.highlight = highlight; hljs.highlightAuto = highlightAuto; hljs.fixMarkup = fixMarkup; hljs.highlightBlock = highlightBlock; hljs.configure = configure; hljs.initHighlighting = initHighlighting; hljs.initHighlightingOnLoad = initHighlightingOnLoad; hljs.registerLanguage = registerLanguage; hljs.listLanguages = listLanguages; hljs.getLanguage = getLanguage; hljs.autoDetection = autoDetection; hljs.inherit = inherit; // Common regexps hljs.IDENT_RE = '[a-zA-Z]\\w*'; hljs.UNDERSCORE_IDENT_RE = '[a-zA-Z_]\\w*'; hljs.NUMBER_RE = '\\b\\d+(\\.\\d+)?'; hljs.C_NUMBER_RE = '(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)'; // 0x..., 0..., decimal, float hljs.BINARY_NUMBER_RE = '\\b(0b[01]+)'; // 0b... hljs.RE_STARTERS_RE = '!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~'; // Common modes hljs.BACKSLASH_ESCAPE = { begin: '\\\\[\\s\\S]', relevance: 0 }; hljs.APOS_STRING_MODE = { className: 'string', begin: '\'', end: '\'', illegal: '\\n', contains: [hljs.BACKSLASH_ESCAPE] }; hljs.QUOTE_STRING_MODE = { className: 'string', begin: '"', end: '"', illegal: '\\n', contains: [hljs.BACKSLASH_ESCAPE] }; hljs.PHRASAL_WORDS_MODE = { begin: /\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/ }; hljs.COMMENT = function (begin, end, inherits) { var mode = hljs.inherit( { className: 'comment', begin: begin, end: end, contains: [] }, inherits || {} ); mode.contains.push(hljs.PHRASAL_WORDS_MODE); mode.contains.push({ className: 'doctag', begin: '(?:TODO|FIXME|NOTE|BUG|XXX):', relevance: 0 }); return mode; }; hljs.C_LINE_COMMENT_MODE = hljs.COMMENT('//', '$'); hljs.C_BLOCK_COMMENT_MODE = hljs.COMMENT('/\\*', '\\*/'); hljs.HASH_COMMENT_MODE = hljs.COMMENT('#', '$'); hljs.NUMBER_MODE = { className: 'number', begin: hljs.NUMBER_RE, relevance: 0 }; hljs.C_NUMBER_MODE = { className: 'number', begin: hljs.C_NUMBER_RE, relevance: 0 }; hljs.BINARY_NUMBER_MODE = { className: 'number', begin: hljs.BINARY_NUMBER_RE, relevance: 0 }; hljs.CSS_NUMBER_MODE = { className: 'number', begin: hljs.NUMBER_RE + '(' + '%|em|ex|ch|rem' + '|vw|vh|vmin|vmax' + '|cm|mm|in|pt|pc|px' + '|deg|grad|rad|turn' + '|s|ms' + '|Hz|kHz' + '|dpi|dpcm|dppx' + ')?', relevance: 0 }; hljs.REGEXP_MODE = { className: 'regexp', begin: /\//, end: /\/[gimuy]*/, illegal: /\n/, contains: [ hljs.BACKSLASH_ESCAPE, { begin: /\[/, end: /\]/, relevance: 0, contains: [hljs.BACKSLASH_ESCAPE] } ] }; hljs.TITLE_MODE = { className: 'title', begin: hljs.IDENT_RE, relevance: 0 }; hljs.UNDERSCORE_TITLE_MODE = { className: 'title', begin: hljs.UNDERSCORE_IDENT_RE, relevance: 0 }; hljs.METHOD_GUARD = { // excludes method names from keyword processing begin: '\\.\\s*' + hljs.UNDERSCORE_IDENT_RE, relevance: 0 }; return hljs; })); ; return exports; }()) , languages = [{name:"cpp",create:/* Language: C++ Author: Ivan Sagalaev Contributors: Evgeny Stepanischev , Zaven Muradyan , Roel Deckers , Sam Wu , Jordi Petit , Pieter Vantorre , Google Inc. (David Benjamin) Category: common, system */ function(hljs) { var CPP_PRIMITIVE_TYPES = { className: 'keyword', begin: '\\b[a-z\\d_]*_t\\b' }; var STRINGS = { className: 'string', variants: [ { begin: '(u8?|U|L)?"', end: '"', illegal: '\\n', contains: [hljs.BACKSLASH_ESCAPE] }, { begin: /(?:u8?|U|L)?R"([^()\\ ]{0,16})\((?:.|\n)*?\)\1"/ }, { begin: '\'\\\\?.', end: '\'', illegal: '.' } ] }; var NUMBERS = { className: 'number', variants: [ { begin: '\\b(0b[01\']+)' }, { begin: '(-?)\\b([\\d\']+(\\.[\\d\']*)?|\\.[\\d\']+)(u|U|l|L|ul|UL|f|F|b|B)' }, { begin: '(-?)(\\b0[xX][a-fA-F0-9\']+|(\\b[\\d\']+(\\.[\\d\']*)?|\\.[\\d\']+)([eE][-+]?[\\d\']+)?)' } ], relevance: 0 }; var PREPROCESSOR = { className: 'meta', begin: /#\s*[a-z]+\b/, end: /$/, keywords: { 'meta-keyword': 'if else elif endif define undef warning error line ' + 'pragma ifdef ifndef include' }, contains: [ { begin: /\\\n/, relevance: 0 }, hljs.inherit(STRINGS, {className: 'meta-string'}), { className: 'meta-string', begin: /<[^\n>]*>/, end: /$/, illegal: '\\n', }, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE ] }; var FUNCTION_TITLE = hljs.IDENT_RE + '\\s*\\('; var CPP_KEYWORDS = { keyword: 'int float while private char catch import module export virtual operator sizeof ' + 'dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace ' + 'unsigned long volatile static protected bool template mutable if public friend ' + 'do goto auto void enum else break extern using asm case typeid ' + 'short reinterpret_cast|10 default double register explicit signed typename try this ' + 'switch continue inline delete alignof constexpr decltype ' + 'noexcept static_assert thread_local restrict _Bool complex _Complex _Imaginary ' + 'atomic_bool atomic_char atomic_schar ' + 'atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong ' + 'atomic_ullong new throw return ' + 'and or not', built_in: 'std string cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream ' + 'auto_ptr deque list queue stack vector map set bitset multiset multimap unordered_set ' + 'unordered_map unordered_multiset unordered_multimap array shared_ptr abort abs acos ' + 'asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp ' + 'fscanf isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper ' + 'isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow ' + 'printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp ' + 'strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan ' + 'vfprintf vprintf vsprintf endl initializer_list unique_ptr', literal: 'true false nullptr NULL' }; var EXPRESSION_CONTAINS = [ CPP_PRIMITIVE_TYPES, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, NUMBERS, STRINGS ]; return { aliases: ['c', 'cc', 'h', 'c++', 'h++', 'hpp', 'hh', 'hxx', 'cxx'], keywords: CPP_KEYWORDS, illegal: '', keywords: CPP_KEYWORDS, contains: ['self', CPP_PRIMITIVE_TYPES] }, { begin: hljs.IDENT_RE + '::', keywords: CPP_KEYWORDS }, { // This mode covers expression context where we can't expect a function // definition and shouldn't highlight anything that looks like one: // `return some()`, `else if()`, `(x*sum(1, 2))` variants: [ {begin: /=/, end: /;/}, {begin: /\(/, end: /\)/}, {beginKeywords: 'new throw return else', end: /;/} ], keywords: CPP_KEYWORDS, contains: EXPRESSION_CONTAINS.concat([ { begin: /\(/, end: /\)/, keywords: CPP_KEYWORDS, contains: EXPRESSION_CONTAINS.concat(['self']), relevance: 0 } ]), relevance: 0 }, { className: 'function', begin: '(' + hljs.IDENT_RE + '[\\*&\\s]+)+' + FUNCTION_TITLE, returnBegin: true, end: /[{;=]/, excludeEnd: true, keywords: CPP_KEYWORDS, illegal: /[^\w\s\*&]/, contains: [ { begin: FUNCTION_TITLE, returnBegin: true, contains: [hljs.TITLE_MODE], relevance: 0 }, { className: 'params', begin: /\(/, end: /\)/, keywords: CPP_KEYWORDS, relevance: 0, contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, STRINGS, NUMBERS, CPP_PRIMITIVE_TYPES, // Count matching parentheses. { begin: /\(/, end: /\)/, keywords: CPP_KEYWORDS, relevance: 0, contains: [ 'self', hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, STRINGS, NUMBERS, CPP_PRIMITIVE_TYPES ] } ] }, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, PREPROCESSOR ] }, { className: 'class', beginKeywords: 'class struct', end: /[{;:]/, contains: [ {begin: //, contains: ['self']}, // skip generic stuff hljs.TITLE_MODE ] } ]), exports: { preprocessor: PREPROCESSOR, strings: STRINGS, keywords: CPP_KEYWORDS } }; } },{name:"1c",create:/* Language: 1C:Enterprise (v7, v8) Author: Stanislav Belov Description: built-in language 1C:Enterprise (v7, v8) Category: enterprise */ function(hljs){ // общий паттерн для определения идентификаторов var UNDERSCORE_IDENT_RE = '[A-Za-zА-Яа-яёЁ_][A-Za-zА-Яа-яёЁ_0-9]+'; // v7 уникальные ключевые слова, отсутствующие в v8 ==> keyword var v7_keywords = 'далее '; // v8 ключевые слова ==> keyword var v8_keywords = 'возврат вызватьисключение выполнить для если и из или иначе иначеесли исключение каждого конецесли ' + 'конецпопытки конеццикла не новый перейти перем по пока попытка прервать продолжить тогда цикл экспорт '; // keyword : ключевые слова var KEYWORD = v7_keywords + v8_keywords; // v7 уникальные директивы, отсутствующие в v8 ==> meta-keyword var v7_meta_keywords = 'загрузитьизфайла '; // v8 ключевые слова в инструкциях препроцессора, директивах компиляции, аннотациях ==> meta-keyword var v8_meta_keywords = 'вебклиент вместо внешнеесоединение клиент конецобласти мобильноеприложениеклиент мобильноеприложениесервер ' + 'наклиенте наклиентенасервере наклиентенасерверебезконтекста насервере насерверебезконтекста область перед ' + 'после сервер толстыйклиентобычноеприложение толстыйклиентуправляемоеприложение тонкийклиент '; // meta-keyword : ключевые слова в инструкциях препроцессора, директивах компиляции, аннотациях var METAKEYWORD = v7_meta_keywords + v8_meta_keywords; // v7 системные константы ==> built_in var v7_system_constants = 'разделительстраниц разделительстрок символтабуляции '; // v7 уникальные методы глобального контекста, отсутствующие в v8 ==> built_in var v7_global_context_methods = 'ansitooem oemtoansi ввестивидсубконто ввестиперечисление ввестипериод ввестиплансчетов выбранныйплансчетов ' + 'датагод датамесяц датачисло заголовоксистемы значениевстроку значениеизстроки каталогиб каталогпользователя ' + 'кодсимв конгода конецпериодаби конецрассчитанногопериодаби конецстандартногоинтервала конквартала конмесяца ' + 'коннедели лог лог10 максимальноеколичествосубконто названиеинтерфейса названиенабораправ назначитьвид ' + 'назначитьсчет найтиссылки началопериодаби началостандартногоинтервала начгода начквартала начмесяца ' + 'начнедели номерднягода номерднянедели номернеделигода обработкаожидания основнойжурналрасчетов ' + 'основнойплансчетов основнойязык очиститьокносообщений периодстр получитьвремята получитьдатута ' + 'получитьдокументта получитьзначенияотбора получитьпозициюта получитьпустоезначение получитьта ' + 'префиксавтонумерации пропись пустоезначение разм разобратьпозициюдокумента рассчитатьрегистрына ' + 'рассчитатьрегистрыпо симв создатьобъект статусвозврата стрколичествострок сформироватьпозициюдокумента ' + 'счетпокоду текущеевремя типзначения типзначениястр установитьтана установитьтапо фиксшаблон шаблон '; // v8 методы глобального контекста ==> built_in var v8_global_context_methods = 'acos asin atan base64значение base64строка cos exp log log10 pow sin sqrt tan xmlзначение xmlстрока ' + 'xmlтип xmlтипзнч активноеокно безопасныйрежим безопасныйрежимразделенияданных булево ввестидату ввестизначение ' + 'ввестистроку ввестичисло возможностьчтенияxml вопрос восстановитьзначение врег выгрузитьжурналрегистрации ' + 'выполнитьобработкуоповещения выполнитьпроверкуправдоступа вычислить год данныеформывзначение дата день деньгода ' + 'деньнедели добавитьмесяц заблокироватьданныедляредактирования заблокироватьработупользователя завершитьработусистемы ' + 'загрузитьвнешнююкомпоненту закрытьсправку записатьjson записатьxml записатьдатуjson записьжурналарегистрации ' + 'заполнитьзначениясвойств запроситьразрешениепользователя запуститьприложение запуститьсистему зафиксироватьтранзакцию ' + 'значениевданныеформы значениевстрокувнутр значениевфайл значениезаполнено значениеизстрокивнутр значениеизфайла ' + 'изxmlтипа импортмоделиxdto имякомпьютера имяпользователя инициализироватьпредопределенныеданные информацияобошибке ' + 'каталогбиблиотекимобильногоустройства каталогвременныхфайлов каталогдокументов каталогпрограммы кодироватьстроку ' + 'кодлокализацииинформационнойбазы кодсимвола командасистемы конецгода конецдня конецквартала конецмесяца конецминуты ' + 'конецнедели конецчаса конфигурациябазыданныхизмененадинамически конфигурацияизменена копироватьданныеформы ' + 'копироватьфайл краткоепредставлениеошибки лев макс местноевремя месяц мин минута монопольныйрежим найти ' + 'найтинедопустимыесимволыxml найтиокнопонавигационнойссылке найтипомеченныенаудаление найтипоссылкам найтифайлы ' + 'началогода началодня началоквартала началомесяца началоминуты началонедели началочаса начатьзапросразрешенияпользователя ' + 'начатьзапускприложения начатькопированиефайла начатьперемещениефайла начатьподключениевнешнейкомпоненты ' + 'начатьподключениерасширенияработыскриптографией начатьподключениерасширенияработысфайлами начатьпоискфайлов ' + 'начатьполучениекаталогавременныхфайлов начатьполучениекаталогадокументов начатьполучениерабочегокаталогаданныхпользователя ' + 'начатьполучениефайлов начатьпомещениефайла начатьпомещениефайлов начатьсозданиедвоичныхданныхизфайла начатьсозданиекаталога ' + 'начатьтранзакцию начатьудалениефайлов начатьустановкувнешнейкомпоненты начатьустановкурасширенияработыскриптографией ' + 'начатьустановкурасширенияработысфайлами неделягода необходимостьзавершениясоединения номерсеансаинформационнойбазы ' + 'номерсоединенияинформационнойбазы нрег нстр обновитьинтерфейс обновитьнумерациюобъектов обновитьповторноиспользуемыезначения ' + 'обработкапрерыванияпользователя объединитьфайлы окр описаниеошибки оповестить оповеститьобизменении ' + 'отключитьобработчикзапросанастроекклиенталицензирования отключитьобработчикожидания отключитьобработчикоповещения ' + 'открытьзначение открытьиндекссправки открытьсодержаниесправки открытьсправку открытьформу открытьформумодально ' + 'отменитьтранзакцию очиститьжурналрегистрации очиститьнастройкипользователя очиститьсообщения параметрыдоступа ' + 'перейтипонавигационнойссылке переместитьфайл подключитьвнешнююкомпоненту ' + 'подключитьобработчикзапросанастроекклиенталицензирования подключитьобработчикожидания подключитьобработчикоповещения ' + 'подключитьрасширениеработыскриптографией подключитьрасширениеработысфайлами подробноепредставлениеошибки ' + 'показатьвводдаты показатьвводзначения показатьвводстроки показатьвводчисла показатьвопрос показатьзначение ' + 'показатьинформациюобошибке показатьнакарте показатьоповещениепользователя показатьпредупреждение полноеимяпользователя ' + 'получитьcomобъект получитьxmlтип получитьадреспоместоположению получитьблокировкусеансов получитьвремязавершенияспящегосеанса ' + 'получитьвремязасыпанияпассивногосеанса получитьвремяожиданияблокировкиданных получитьданныевыбора ' + 'получитьдополнительныйпараметрклиенталицензирования получитьдопустимыекодылокализации получитьдопустимыечасовыепояса ' + 'получитьзаголовокклиентскогоприложения получитьзаголовоксистемы получитьзначенияотборажурналарегистрации ' + 'получитьидентификаторконфигурации получитьизвременногохранилища получитьимявременногофайла ' + 'получитьимяклиенталицензирования получитьинформациюэкрановклиента получитьиспользованиежурналарегистрации ' + 'получитьиспользованиесобытияжурналарегистрации получитькраткийзаголовокприложения получитьмакетоформления ' + 'получитьмаскувсефайлы получитьмаскувсефайлыклиента получитьмаскувсефайлысервера получитьместоположениепоадресу ' + 'получитьминимальнуюдлинупаролейпользователей получитьнавигационнуюссылку получитьнавигационнуюссылкуинформационнойбазы ' + 'получитьобновлениеконфигурациибазыданных получитьобновлениепредопределенныхданныхинформационнойбазы получитьобщиймакет ' + 'получитьобщуюформу получитьокна получитьоперативнуюотметкувремени получитьотключениебезопасногорежима ' + 'получитьпараметрыфункциональныхопцийинтерфейса получитьполноеимяпредопределенногозначения ' + 'получитьпредставлениянавигационныхссылок получитьпроверкусложностипаролейпользователей получитьразделительпути ' + 'получитьразделительпутиклиента получитьразделительпутисервера получитьсеансыинформационнойбазы ' + 'получитьскоростьклиентскогосоединения получитьсоединенияинформационнойбазы получитьсообщенияпользователю ' + 'получитьсоответствиеобъектаиформы получитьсоставстандартногоинтерфейсаodata получитьструктурухранениябазыданных ' + 'получитьтекущийсеансинформационнойбазы получитьфайл получитьфайлы получитьформу получитьфункциональнуюопцию ' + 'получитьфункциональнуюопциюинтерфейса получитьчасовойпоясинформационнойбазы пользователиос поместитьвовременноехранилище ' + 'поместитьфайл поместитьфайлы прав праводоступа предопределенноезначение представлениекодалокализации представлениепериода ' + 'представлениеправа представлениеприложения представлениесобытияжурналарегистрации представлениечасовогопояса предупреждение ' + 'прекратитьработусистемы привилегированныйрежим продолжитьвызов прочитатьjson прочитатьxml прочитатьдатуjson пустаястрока ' + 'рабочийкаталогданныхпользователя разблокироватьданныедляредактирования разделитьфайл разорватьсоединениесвнешнимисточникомданных ' + 'раскодироватьстроку рольдоступна секунда сигнал символ скопироватьжурналрегистрации смещениелетнеговремени ' + 'смещениестандартноговремени соединитьбуферыдвоичныхданных создатькаталог создатьфабрикуxdto сокрл сокрлп сокрп сообщить ' + 'состояние сохранитьзначение сохранитьнастройкипользователя сред стрдлина стрзаканчиваетсяна стрзаменить стрнайти стрначинаетсяс ' + 'строка строкасоединенияинформационнойбазы стрполучитьстроку стрразделить стрсоединить стрсравнить стрчисловхождений '+ 'стрчислострок стршаблон текущаядата текущаядатасеанса текущаяуниверсальнаядата текущаяуниверсальнаядатавмиллисекундах ' + 'текущийвариантинтерфейсаклиентскогоприложения текущийвариантосновногошрифтаклиентскогоприложения текущийкодлокализации ' + 'текущийрежимзапуска текущийязык текущийязыксистемы тип типзнч транзакцияактивна трег удалитьданныеинформационнойбазы ' + 'удалитьизвременногохранилища удалитьобъекты удалитьфайлы универсальноевремя установитьбезопасныйрежим ' + 'установитьбезопасныйрежимразделенияданных установитьблокировкусеансов установитьвнешнююкомпоненту ' + 'установитьвремязавершенияспящегосеанса установитьвремязасыпанияпассивногосеанса установитьвремяожиданияблокировкиданных ' + 'установитьзаголовокклиентскогоприложения установитьзаголовоксистемы установитьиспользованиежурналарегистрации ' + 'установитьиспользованиесобытияжурналарегистрации установитькраткийзаголовокприложения ' + 'установитьминимальнуюдлинупаролейпользователей установитьмонопольныйрежим установитьнастройкиклиенталицензирования ' + 'установитьобновлениепредопределенныхданныхинформационнойбазы установитьотключениебезопасногорежима ' + 'установитьпараметрыфункциональныхопцийинтерфейса установитьпривилегированныйрежим ' + 'установитьпроверкусложностипаролейпользователей установитьрасширениеработыскриптографией ' + 'установитьрасширениеработысфайлами установитьсоединениесвнешнимисточникомданных установитьсоответствиеобъектаиформы ' + 'установитьсоставстандартногоинтерфейсаodata установитьчасовойпоясинформационнойбазы установитьчасовойпояссеанса ' + 'формат цел час часовойпояс часовойпояссеанса число числопрописью этоадресвременногохранилища '; // v8 свойства глобального контекста ==> built_in var v8_global_context_property = 'wsссылки библиотекакартинок библиотекамакетовоформлениякомпоновкиданных библиотекастилей бизнеспроцессы ' + 'внешниеисточникиданных внешниеобработки внешниеотчеты встроенныепокупки главныйинтерфейс главныйстиль ' + 'документы доставляемыеуведомления журналыдокументов задачи информацияобинтернетсоединении использованиерабочейдаты ' + 'историяработыпользователя константы критерииотбора метаданные обработки отображениерекламы отправкадоставляемыхуведомлений ' + 'отчеты панельзадачос параметрзапуска параметрысеанса перечисления планывидоврасчета планывидовхарактеристик ' + 'планыобмена планысчетов полнотекстовыйпоиск пользователиинформационнойбазы последовательности проверкавстроенныхпокупок ' + 'рабочаядата расширенияконфигурации регистрыбухгалтерии регистрынакопления регистрырасчета регистрысведений ' + 'регламентныезадания сериализаторxdto справочники средствагеопозиционирования средствакриптографии средствамультимедиа ' + 'средстваотображениярекламы средствапочты средствателефонии фабрикаxdto файловыепотоки фоновыезадания хранилищанастроек ' + 'хранилищевариантовотчетов хранилищенастроекданныхформ хранилищеобщихнастроек хранилищепользовательскихнастроекдинамическихсписков ' + 'хранилищепользовательскихнастроекотчетов хранилищесистемныхнастроек '; // built_in : встроенные или библиотечные объекты (константы, классы, функции) var BUILTIN = v7_system_constants + v7_global_context_methods + v8_global_context_methods + v8_global_context_property; // v8 системные наборы значений ==> class var v8_system_sets_of_values = 'webцвета windowsцвета windowsшрифты библиотекакартинок рамкистиля символы цветастиля шрифтыстиля '; // v8 системные перечисления - интерфейсные ==> class var v8_system_enums_interface = 'автоматическоесохранениеданныхформывнастройках автонумерациявформе автораздвижениесерий ' + 'анимациядиаграммы вариантвыравниванияэлементовизаголовков вариантуправлениявысотойтаблицы ' + 'вертикальнаяпрокруткаформы вертикальноеположение вертикальноеположениеэлемента видгруппыформы ' + 'виддекорацииформы виддополненияэлементаформы видизмененияданных видкнопкиформы видпереключателя ' + 'видподписейкдиаграмме видполяформы видфлажка влияниеразмеранапузырекдиаграммы горизонтальноеположение ' + 'горизонтальноеположениеэлемента группировкаколонок группировкаподчиненныхэлементовформы ' + 'группыиэлементы действиеперетаскивания дополнительныйрежимотображения допустимыедействияперетаскивания ' + 'интервалмеждуэлементамиформы использованиевывода использованиеполосыпрокрутки ' + 'используемоезначениеточкибиржевойдиаграммы историявыборапривводе источникзначенийоситочекдиаграммы ' + 'источникзначенияразмерапузырькадиаграммы категориягруппыкоманд максимумсерий начальноеотображениедерева ' + 'начальноеотображениесписка обновлениетекстаредактирования ориентациядендрограммы ориентациядиаграммы ' + 'ориентацияметокдиаграммы ориентацияметоксводнойдиаграммы ориентацияэлементаформы отображениевдиаграмме ' + 'отображениевлегендедиаграммы отображениегруппыкнопок отображениезаголовкашкалыдиаграммы ' + 'отображениезначенийсводнойдиаграммы отображениезначенияизмерительнойдиаграммы ' + 'отображениеинтерваладиаграммыганта отображениекнопки отображениекнопкивыбора отображениеобсужденийформы ' + 'отображениеобычнойгруппы отображениеотрицательныхзначенийпузырьковойдиаграммы отображениепанелипоиска ' + 'отображениеподсказки отображениепредупрежденияприредактировании отображениеразметкиполосырегулирования ' + 'отображениестраницформы отображениетаблицы отображениетекстазначениядиаграммыганта ' + 'отображениеуправленияобычнойгруппы отображениефигурыкнопки палитрацветовдиаграммы поведениеобычнойгруппы ' + 'поддержкамасштабадендрограммы поддержкамасштабадиаграммыганта поддержкамасштабасводнойдиаграммы ' + 'поисквтаблицепривводе положениезаголовкаэлементаформы положениекартинкикнопкиформы ' + 'положениекартинкиэлементаграфическойсхемы положениекоманднойпанелиформы положениекоманднойпанелиэлементаформы ' + 'положениеопорнойточкиотрисовки положениеподписейкдиаграмме положениеподписейшкалызначенийизмерительнойдиаграммы ' + 'положениесостоянияпросмотра положениестрокипоиска положениетекстасоединительнойлинии положениеуправленияпоиском ' + 'положениешкалывремени порядокотображенияточекгоризонтальнойгистограммы порядоксерийвлегендедиаграммы ' + 'размеркартинки расположениезаголовкашкалыдиаграммы растягиваниеповертикалидиаграммыганта ' + 'режимавтоотображениясостояния режимвводастроктаблицы режимвыборанезаполненного режимвыделениядаты ' + 'режимвыделениястрокитаблицы режимвыделениятаблицы режимизмененияразмера режимизменениясвязанногозначения ' + 'режимиспользованиядиалогапечати режимиспользованияпараметракоманды режиммасштабированияпросмотра ' + 'режимосновногоокнаклиентскогоприложения режимоткрытияокнаформы режимотображениявыделения ' + 'режимотображениягеографическойсхемы режимотображениязначенийсерии режимотрисовкисеткиграфическойсхемы ' + 'режимполупрозрачностидиаграммы режимпробеловдиаграммы режимразмещениянастранице режимредактированияколонки ' + 'режимсглаживаниядиаграммы режимсглаживанияиндикатора режимсписказадач сквозноевыравнивание ' + 'сохранениеданныхформывнастройках способзаполнениятекстазаголовкашкалыдиаграммы ' + 'способопределенияограничивающегозначениядиаграммы стандартнаягруппакоманд стандартноеоформление ' + 'статусоповещенияпользователя стильстрелки типаппроксимациилиниитрендадиаграммы типдиаграммы ' + 'типединицышкалывремени типимпортасерийслоягеографическойсхемы типлиниигеографическойсхемы типлиниидиаграммы ' + 'типмаркерагеографическойсхемы типмаркерадиаграммы типобластиоформления ' + 'типорганизацииисточникаданныхгеографическойсхемы типотображениясериислоягеографическойсхемы ' + 'типотображенияточечногообъектагеографическойсхемы типотображенияшкалыэлементалегендыгеографическойсхемы ' + 'типпоискаобъектовгеографическойсхемы типпроекциигеографическойсхемы типразмещенияизмерений ' + 'типразмещенияреквизитовизмерений типрамкиэлементауправления типсводнойдиаграммы ' + 'типсвязидиаграммыганта типсоединениязначенийпосериямдиаграммы типсоединенияточекдиаграммы ' + 'типсоединительнойлинии типстороныэлементаграфическойсхемы типформыотчета типшкалырадарнойдиаграммы ' + 'факторлиниитрендадиаграммы фигуракнопки фигурыграфическойсхемы фиксациявтаблице форматдняшкалывремени ' + 'форматкартинки ширинаподчиненныхэлементовформы '; // v8 системные перечисления - свойства прикладных объектов ==> class var v8_system_enums_objects_properties = 'виддвижениябухгалтерии виддвижениянакопления видпериодарегистрарасчета видсчета видточкимаршрутабизнеспроцесса ' + 'использованиеагрегатарегистранакопления использованиегруппиэлементов использованиережимапроведения ' + 'использованиесреза периодичностьагрегатарегистранакопления режимавтовремя режимзаписидокумента режимпроведениядокумента '; // v8 системные перечисления - планы обмена ==> class var v8_system_enums_exchange_plans = 'авторегистрацияизменений допустимыйномерсообщения отправкаэлементаданных получениеэлементаданных '; // v8 системные перечисления - табличный документ ==> class var v8_system_enums_tabular_document = 'использованиерасшифровкитабличногодокумента ориентациястраницы положениеитоговколоноксводнойтаблицы ' + 'положениеитоговстроксводнойтаблицы положениетекстаотносительнокартинки расположениезаголовкагруппировкитабличногодокумента ' + 'способчтениязначенийтабличногодокумента типдвустороннейпечати типзаполненияобластитабличногодокумента ' + 'типкурсоровтабличногодокумента типлиниирисункатабличногодокумента типлинииячейкитабличногодокумента ' + 'типнаправленияпереходатабличногодокумента типотображениявыделениятабличногодокумента типотображениялинийсводнойтаблицы ' + 'типразмещениятекстатабличногодокумента типрисункатабличногодокумента типсмещениятабличногодокумента ' + 'типузоратабличногодокумента типфайлатабличногодокумента точностьпечати чередованиерасположениястраниц '; // v8 системные перечисления - планировщик ==> class var v8_system_enums_sheduler = 'отображениевремениэлементовпланировщика '; // v8 системные перечисления - форматированный документ ==> class var v8_system_enums_formatted_document = 'типфайлаформатированногодокумента '; // v8 системные перечисления - запрос ==> class var v8_system_enums_query = 'обходрезультатазапроса типзаписизапроса '; // v8 системные перечисления - построитель отчета ==> class var v8_system_enums_report_builder = 'видзаполнениярасшифровкипостроителяотчета типдобавленияпредставлений типизмеренияпостроителяотчета типразмещенияитогов '; // v8 системные перечисления - работа с файлами ==> class var v8_system_enums_files = 'доступкфайлу режимдиалогавыборафайла режимоткрытияфайла '; // v8 системные перечисления - построитель запроса ==> class var v8_system_enums_query_builder = 'типизмеренияпостроителязапроса '; // v8 системные перечисления - анализ данных ==> class var v8_system_enums_data_analysis = 'видданныханализа методкластеризации типединицыинтервалавременианализаданных типзаполнениятаблицырезультатаанализаданных ' + 'типиспользованиячисловыхзначенийанализаданных типисточникаданныхпоискаассоциаций типколонкианализаданныхдереворешений ' + 'типколонкианализаданныхкластеризация типколонкианализаданныхобщаястатистика типколонкианализаданныхпоискассоциаций ' + 'типколонкианализаданныхпоискпоследовательностей типколонкимоделипрогноза типмерырасстоянияанализаданных ' + 'типотсеченияправилассоциации типполяанализаданных типстандартизациианализаданных типупорядочиванияправилассоциациианализаданных ' + 'типупорядочиванияшаблоновпоследовательностейанализаданных типупрощениядереварешений '; // v8 системные перечисления - xml, json, xs, dom, xdto, web-сервисы ==> class var v8_system_enums_xml_json_xs_dom_xdto_ws = 'wsнаправлениепараметра вариантxpathxs вариантзаписидатыjson вариантпростоготипаxs видгруппымоделиxs видфасетаxdto ' + 'действиепостроителяdom завершенностьпростоготипаxs завершенностьсоставноготипаxs завершенностьсхемыxs запрещенныеподстановкиxs ' + 'исключениягруппподстановкиxs категорияиспользованияатрибутаxs категорияограниченияидентичностиxs категорияограниченияпространствименxs ' + 'методнаследованияxs модельсодержимогоxs назначениетипаxml недопустимыеподстановкиxs обработкапробельныхсимволовxs обработкасодержимогоxs ' + 'ограничениезначенияxs параметрыотбораузловdom переносстрокjson позициявдокументеdom пробельныесимволыxml типатрибутаxml типзначенияjson ' + 'типканоническогоxml типкомпонентыxs типпроверкиxml типрезультатаdomxpath типузлаdom типузлаxml формаxml формапредставленияxs ' + 'форматдатыjson экранированиесимволовjson '; // v8 системные перечисления - система компоновки данных ==> class var v8_system_enums_data_composition_system = 'видсравнениякомпоновкиданных действиеобработкирасшифровкикомпоновкиданных направлениесортировкикомпоновкиданных ' + 'расположениевложенныхэлементоврезультатакомпоновкиданных расположениеитоговкомпоновкиданных расположениегруппировкикомпоновкиданных ' + 'расположениеполейгруппировкикомпоновкиданных расположениеполякомпоновкиданных расположениереквизитовкомпоновкиданных ' + 'расположениересурсовкомпоновкиданных типбухгалтерскогоостаткакомпоновкиданных типвыводатекстакомпоновкиданных ' + 'типгруппировкикомпоновкиданных типгруппыэлементовотборакомпоновкиданных типдополненияпериодакомпоновкиданных ' + 'типзаголовкаполейкомпоновкиданных типмакетагруппировкикомпоновкиданных типмакетаобластикомпоновкиданных типостаткакомпоновкиданных ' + 'типпериодакомпоновкиданных типразмещениятекстакомпоновкиданных типсвязинаборовданныхкомпоновкиданных типэлементарезультатакомпоновкиданных ' + 'расположениелегендыдиаграммыкомпоновкиданных типпримененияотборакомпоновкиданных режимотображенияэлементанастройкикомпоновкиданных ' + 'режимотображениянастроеккомпоновкиданных состояниеэлементанастройкикомпоновкиданных способвосстановлениянастроеккомпоновкиданных ' + 'режимкомпоновкирезультата использованиепараметракомпоновкиданных автопозицияресурсовкомпоновкиданных '+ 'вариантиспользованиягруппировкикомпоновкиданных расположениересурсоввдиаграммекомпоновкиданных фиксациякомпоновкиданных ' + 'использованиеусловногооформлениякомпоновкиданных '; // v8 системные перечисления - почта ==> class var v8_system_enums_email = 'важностьинтернетпочтовогосообщения обработкатекстаинтернетпочтовогосообщения способкодированияинтернетпочтовоговложения ' + 'способкодированиянеasciiсимволовинтернетпочтовогосообщения типтекстапочтовогосообщения протоколинтернетпочты ' + 'статусразборапочтовогосообщения '; // v8 системные перечисления - журнал регистрации ==> class var v8_system_enums_logbook = 'режимтранзакциизаписижурналарегистрации статустранзакциизаписижурналарегистрации уровеньжурналарегистрации '; // v8 системные перечисления - криптография ==> class var v8_system_enums_cryptography = 'расположениехранилищасертификатовкриптографии режимвключениясертификатовкриптографии режимпроверкисертификатакриптографии ' + 'типхранилищасертификатовкриптографии '; // v8 системные перечисления - ZIP ==> class var v8_system_enums_zip = 'кодировкаименфайловвzipфайле методсжатияzip методшифрованияzip режимвосстановленияпутейфайловzip режимобработкиподкаталоговzip ' + 'режимсохраненияпутейzip уровеньсжатияzip '; // v8 системные перечисления - // Блокировка данных, Фоновые задания, Автоматизированное тестирование, // Доставляемые уведомления, Встроенные покупки, Интернет, Работа с двоичными данными ==> class var v8_system_enums_other = 'звуковоеоповещение направлениепереходакстроке позициявпотоке порядокбайтов режимблокировкиданных режимуправленияблокировкойданных ' + 'сервисвстроенныхпокупок состояниефоновогозадания типподписчикадоставляемыхуведомлений уровеньиспользованиязащищенногосоединенияftp '; // v8 системные перечисления - схема запроса ==> class var v8_system_enums_request_schema = 'направлениепорядкасхемызапроса типдополненияпериодамисхемызапроса типконтрольнойточкисхемызапроса типобъединениясхемызапроса ' + 'типпараметрадоступнойтаблицысхемызапроса типсоединениясхемызапроса '; // v8 системные перечисления - свойства объектов метаданных ==> class var v8_system_enums_properties_of_metadata_objects = 'httpметод автоиспользованиеобщегореквизита автопрефиксномеразадачи вариантвстроенногоязыка видиерархии видрегистранакопления ' + 'видтаблицывнешнегоисточникаданных записьдвиженийприпроведении заполнениепоследовательностей индексирование ' + 'использованиебазыпланавидоврасчета использованиебыстроговыбора использованиеобщегореквизита использованиеподчинения ' + 'использованиеполнотекстовогопоиска использованиеразделяемыхданныхобщегореквизита использованиереквизита ' + 'назначениеиспользованияприложения назначениерасширенияконфигурации направлениепередачи обновлениепредопределенныхданных ' + 'оперативноепроведение основноепредставлениевидарасчета основноепредставлениевидахарактеристики основноепредставлениезадачи ' + 'основноепредставлениепланаобмена основноепредставлениесправочника основноепредставлениесчета перемещениеграницыприпроведении ' + 'периодичностьномерабизнеспроцесса периодичностьномерадокумента периодичностьрегистрарасчета периодичностьрегистрасведений ' + 'повторноеиспользованиевозвращаемыхзначений полнотекстовыйпоискпривводепостроке принадлежностьобъекта проведение ' + 'разделениеаутентификацииобщегореквизита разделениеданныхобщегореквизита разделениерасширенийконфигурацииобщегореквизита '+ 'режимавтонумерацииобъектов режимзаписирегистра режимиспользованиямодальности ' + 'режимиспользованиясинхронныхвызововрасширенийплатформыивнешнихкомпонент режимповторногоиспользованиясеансов ' + 'режимполученияданныхвыборапривводепостроке режимсовместимости режимсовместимостиинтерфейса ' + 'режимуправленияблокировкойданныхпоумолчанию сериикодовпланавидовхарактеристик сериикодовпланасчетов ' + 'сериикодовсправочника созданиепривводе способвыбора способпоискастрокипривводепостроке способредактирования ' + 'типданныхтаблицывнешнегоисточникаданных типкодапланавидоврасчета типкодасправочника типмакета типномерабизнеспроцесса ' + 'типномерадокумента типномеразадачи типформы удалениедвижений '; // v8 системные перечисления - разные ==> class var v8_system_enums_differents = 'важностьпроблемыприменениярасширенияконфигурации вариантинтерфейсаклиентскогоприложения вариантмасштабаформклиентскогоприложения ' + 'вариантосновногошрифтаклиентскогоприложения вариантстандартногопериода вариантстандартнойдатыначала видграницы видкартинки ' + 'видотображенияполнотекстовогопоиска видрамки видсравнения видцвета видчисловогозначения видшрифта допустимаядлина допустимыйзнак ' + 'использованиеbyteordermark использованиеметаданныхполнотекстовогопоиска источникрасширенийконфигурации клавиша кодвозвратадиалога ' + 'кодировкаxbase кодировкатекста направлениепоиска направлениесортировки обновлениепредопределенныхданных обновлениеприизмененииданных ' + 'отображениепанелиразделов проверказаполнения режимдиалогавопрос режимзапускаклиентскогоприложения режимокругления режимоткрытияформприложения ' + 'режимполнотекстовогопоиска скоростьклиентскогосоединения состояниевнешнегоисточникаданных состояниеобновленияконфигурациибазыданных ' + 'способвыборасертификатаwindows способкодированиястроки статуссообщения типвнешнейкомпоненты типплатформы типповеденияклавишиenter ' + 'типэлементаинформацииовыполненииобновленияконфигурациибазыданных уровеньизоляциитранзакций хешфункция частидаты'; // class: встроенные наборы значений, системные перечисления (содержат дочерние значения, обращения к которым через разыменование) var CLASS = v8_system_sets_of_values + v8_system_enums_interface + v8_system_enums_objects_properties + v8_system_enums_exchange_plans + v8_system_enums_tabular_document + v8_system_enums_sheduler + v8_system_enums_formatted_document + v8_system_enums_query + v8_system_enums_report_builder + v8_system_enums_files + v8_system_enums_query_builder + v8_system_enums_data_analysis + v8_system_enums_xml_json_xs_dom_xdto_ws + v8_system_enums_data_composition_system + v8_system_enums_email + v8_system_enums_logbook + v8_system_enums_cryptography + v8_system_enums_zip + v8_system_enums_other + v8_system_enums_request_schema + v8_system_enums_properties_of_metadata_objects + v8_system_enums_differents; // v8 общие объекты (у объектов есть конструктор, экземпляры создаются методом НОВЫЙ) ==> type var v8_shared_object = 'comобъект ftpсоединение httpзапрос httpсервисответ httpсоединение wsопределения wsпрокси xbase анализданных аннотацияxs ' + 'блокировкаданных буфердвоичныхданных включениеxs выражениекомпоновкиданных генераторслучайныхчисел географическаясхема ' + 'географическиекоординаты графическаясхема группамоделиxs данныерасшифровкикомпоновкиданных двоичныеданные дендрограмма ' + 'диаграмма диаграммаганта диалогвыборафайла диалогвыборацвета диалогвыборашрифта диалограсписаниярегламентногозадания ' + 'диалогредактированиястандартногопериода диапазон документdom документhtml документацияxs доставляемоеуведомление ' + 'записьdom записьfastinfoset записьhtml записьjson записьxml записьzipфайла записьданных записьтекста записьузловdom ' + 'запрос защищенноесоединениеopenssl значенияполейрасшифровкикомпоновкиданных извлечениетекста импортxs интернетпочта ' + 'интернетпочтовоесообщение интернетпочтовыйпрофиль интернетпрокси интернетсоединение информациядляприложенияxs ' + 'использованиеатрибутаxs использованиесобытияжурналарегистрации источникдоступныхнастроеккомпоновкиданных ' + 'итераторузловdom картинка квалификаторыдаты квалификаторыдвоичныхданных квалификаторыстроки квалификаторычисла ' + 'компоновщикмакетакомпоновкиданных компоновщикнастроеккомпоновкиданных конструктормакетаоформлениякомпоновкиданных ' + 'конструкторнастроеккомпоновкиданных конструкторформатнойстроки линия макеткомпоновкиданных макетобластикомпоновкиданных ' + 'макетоформлениякомпоновкиданных маскаxs менеджеркриптографии наборсхемxml настройкикомпоновкиданных настройкисериализацииjson ' + 'обработкакартинок обработкарасшифровкикомпоновкиданных обходдереваdom объявлениеатрибутаxs объявлениенотацииxs ' + 'объявлениеэлементаxs описаниеиспользованиясобытиядоступжурналарегистрации ' + 'описаниеиспользованиясобытияотказвдоступежурналарегистрации описаниеобработкирасшифровкикомпоновкиданных ' + 'описаниепередаваемогофайла описаниетипов определениегруппыатрибутовxs определениегруппымоделиxs ' + 'определениеограниченияидентичностиxs определениепростоготипаxs определениесоставноготипаxs определениетипадокументаdom ' + 'определенияxpathxs отборкомпоновкиданных пакетотображаемыхдокументов параметрвыбора параметркомпоновкиданных ' + 'параметрызаписиjson параметрызаписиxml параметрычтенияxml переопределениеxs планировщик полеанализаданных ' + 'полекомпоновкиданных построительdom построительзапроса построительотчета построительотчетаанализаданных ' + 'построительсхемxml поток потоквпамяти почта почтовоесообщение преобразованиеxsl преобразованиекканоническомуxml ' + 'процессорвыводарезультатакомпоновкиданныхвколлекциюзначений процессорвыводарезультатакомпоновкиданныхвтабличныйдокумент ' + 'процессоркомпоновкиданных разыменовательпространствименdom рамка расписаниерегламентногозадания расширенноеимяxml ' + 'результатчтенияданных своднаядиаграмма связьпараметравыбора связьпотипу связьпотипукомпоновкиданных сериализаторxdto ' + 'сертификатклиентаwindows сертификатклиентафайл сертификаткриптографии сертификатыудостоверяющихцентровwindows ' + 'сертификатыудостоверяющихцентровфайл сжатиеданных системнаяинформация сообщениепользователю сочетаниеклавиш ' + 'сравнениезначений стандартнаядатаначала стандартныйпериод схемаxml схемакомпоновкиданных табличныйдокумент ' + 'текстовыйдокумент тестируемоеприложение типданныхxml уникальныйидентификатор фабрикаxdto файл файловыйпоток ' + 'фасетдлиныxs фасетколичестваразрядовдробнойчастиxs фасетмаксимальноговключающегозначенияxs ' + 'фасетмаксимальногоисключающегозначенияxs фасетмаксимальнойдлиныxs фасетминимальноговключающегозначенияxs ' + 'фасетминимальногоисключающегозначенияxs фасетминимальнойдлиныxs фасетобразцаxs фасетобщегоколичестваразрядовxs ' + 'фасетперечисленияxs фасетпробельныхсимволовxs фильтрузловdom форматированнаястрока форматированныйдокумент ' + 'фрагментxs хешированиеданных хранилищезначения цвет чтениеfastinfoset чтениеhtml чтениеjson чтениеxml чтениеzipфайла ' + 'чтениеданных чтениетекста чтениеузловdom шрифт элементрезультатакомпоновкиданных '; // v8 универсальные коллекции значений ==> type var v8_universal_collection = 'comsafearray деревозначений массив соответствие списокзначений структура таблицазначений фиксированнаяструктура ' + 'фиксированноесоответствие фиксированныймассив '; // type : встроенные типы var TYPE = v8_shared_object + v8_universal_collection; // literal : примитивные типы var LITERAL = 'null истина ложь неопределено'; // number : числа var NUMBERS = hljs.inherit(hljs.NUMBER_MODE); // string : строки var STRINGS = { className: 'string', begin: '"|\\|', end: '"|$', contains: [{begin: '""'}] }; // number : даты var DATE = { begin: "'", end: "'", excludeBegin: true, excludeEnd: true, contains: [ { className: 'number', begin: '\\d{4}([\\.\\\\/:-]?\\d{2}){0,5}' } ] }; // comment : комментарии var COMMENTS = hljs.inherit(hljs.C_LINE_COMMENT_MODE); // meta : инструкции препроцессора, директивы компиляции var META = { className: 'meta', lexemes: UNDERSCORE_IDENT_RE, begin: '#|&', end: '$', keywords: {'meta-keyword': KEYWORD + METAKEYWORD}, contains: [ COMMENTS ] }; // symbol : метка goto var SYMBOL = { className: 'symbol', begin: '~', end: ';|:', excludeEnd: true }; // function : объявление процедур и функций var FUNCTION = { className: 'function', lexemes: UNDERSCORE_IDENT_RE, variants: [ {begin: 'процедура|функция', end: '\\)', keywords: 'процедура функция'}, {begin: 'конецпроцедуры|конецфункции', keywords: 'конецпроцедуры конецфункции'} ], contains: [ { begin: '\\(', end: '\\)', endsParent : true, contains: [ { className: 'params', lexemes: UNDERSCORE_IDENT_RE, begin: UNDERSCORE_IDENT_RE, end: ',', excludeEnd: true, endsWithParent: true, keywords: { keyword: 'знач', literal: LITERAL }, contains: [ NUMBERS, STRINGS, DATE ] }, COMMENTS ] }, hljs.inherit(hljs.TITLE_MODE, {begin: UNDERSCORE_IDENT_RE}) ] }; return { case_insensitive: true, lexemes: UNDERSCORE_IDENT_RE, keywords: { keyword: KEYWORD, built_in: BUILTIN, class: CLASS, type: TYPE, literal: LITERAL }, contains: [ META, FUNCTION, COMMENTS, SYMBOL, NUMBERS, STRINGS, DATE ] } }},{name:"abnf",create:/* Language: Augmented Backus-Naur Form Author: Alex McKibben */ function(hljs) { var regexes = { ruleDeclaration: "^[a-zA-Z][a-zA-Z0-9-]*", unexpectedChars: "[!@#$^&',?+~`|:]" }; var keywords = [ "ALPHA", "BIT", "CHAR", "CR", "CRLF", "CTL", "DIGIT", "DQUOTE", "HEXDIG", "HTAB", "LF", "LWSP", "OCTET", "SP", "VCHAR", "WSP" ]; var commentMode = hljs.COMMENT(";", "$"); var terminalBinaryMode = { className: "symbol", begin: /%b[0-1]+(-[0-1]+|(\.[0-1]+)+){0,1}/ }; var terminalDecimalMode = { className: "symbol", begin: /%d[0-9]+(-[0-9]+|(\.[0-9]+)+){0,1}/ }; var terminalHexadecimalMode = { className: "symbol", begin: /%x[0-9A-F]+(-[0-9A-F]+|(\.[0-9A-F]+)+){0,1}/, }; var caseSensitivityIndicatorMode = { className: "symbol", begin: /%[si]/ }; var ruleDeclarationMode = { begin: regexes.ruleDeclaration + '\\s*=', returnBegin: true, end: /=/, relevance: 0, contains: [{className: "attribute", begin: regexes.ruleDeclaration}] }; return { illegal: regexes.unexpectedChars, keywords: keywords.join(" "), contains: [ ruleDeclarationMode, commentMode, terminalBinaryMode, terminalDecimalMode, terminalHexadecimalMode, caseSensitivityIndicatorMode, hljs.QUOTE_STRING_MODE, hljs.NUMBER_MODE ] }; } },{name:"accesslog",create:/* Language: Access log Author: Oleg Efimov Description: Apache/Nginx Access Logs */ function(hljs) { return { contains: [ // IP { className: 'number', begin: '\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b' }, // Other numbers { className: 'number', begin: '\\b\\d+\\b', relevance: 0 }, // Requests { className: 'string', begin: '"(GET|POST|HEAD|PUT|DELETE|CONNECT|OPTIONS|PATCH|TRACE)', end: '"', keywords: 'GET POST HEAD PUT DELETE CONNECT OPTIONS PATCH TRACE', illegal: '\\n', relevance: 10 }, // Dates { className: 'string', begin: /\[/, end: /\]/, illegal: '\\n' }, // Strings { className: 'string', begin: '"', end: '"', illegal: '\\n' } ] }; } },{name:"actionscript",create:/* Language: ActionScript Author: Alexander Myadzel Category: scripting */ function(hljs) { var IDENT_RE = '[a-zA-Z_$][a-zA-Z0-9_$]*'; var IDENT_FUNC_RETURN_TYPE_RE = '([*]|[a-zA-Z_$][a-zA-Z0-9_$]*)'; var AS3_REST_ARG_MODE = { className: 'rest_arg', begin: '[.]{3}', end: IDENT_RE, relevance: 10 }; return { aliases: ['as'], keywords: { keyword: 'as break case catch class const continue default delete do dynamic each ' + 'else extends final finally for function get if implements import in include ' + 'instanceof interface internal is namespace native new override package private ' + 'protected public return set static super switch this throw try typeof use var void ' + 'while with', literal: 'true false null undefined' }, contains: [ hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.C_NUMBER_MODE, { className: 'class', beginKeywords: 'package', end: '{', contains: [hljs.TITLE_MODE] }, { className: 'class', beginKeywords: 'class interface', end: '{', excludeEnd: true, contains: [ { beginKeywords: 'extends implements' }, hljs.TITLE_MODE ] }, { className: 'meta', beginKeywords: 'import include', end: ';', keywords: {'meta-keyword': 'import include'} }, { className: 'function', beginKeywords: 'function', end: '[{;]', excludeEnd: true, illegal: '\\S', contains: [ hljs.TITLE_MODE, { className: 'params', begin: '\\(', end: '\\)', contains: [ hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, AS3_REST_ARG_MODE ] }, { begin: ':\\s*' + IDENT_FUNC_RETURN_TYPE_RE } ] }, hljs.METHOD_GUARD ], illegal: /#/ }; } },{name:"ada",create:/* Language: Ada Author: Lars Schulna Description: Ada is a general-purpose programming language that has great support for saftey critical and real-time applications. It has been developed by the DoD and thus has been used in military and safety-critical applications (like civil aviation). The first version appeared in the 80s, but it's still actively developed today with the newest standard being Ada2012. */ // We try to support full Ada2012 // // We highlight all appearances of types, keywords, literals (string, char, number, bool) // and titles (user defined function/procedure/package) // CSS classes are set accordingly // // Languages causing problems for language detection: // xml (broken by Foo : Bar type), elm (broken by Foo : Bar type), vbscript-html (broken by body keyword) // sql (ada default.txt has a lot of sql keywords) function(hljs) { // Regular expression for Ada numeric literals. // stolen form the VHDL highlighter // Decimal literal: var INTEGER_RE = '\\d(_|\\d)*'; var EXPONENT_RE = '[eE][-+]?' + INTEGER_RE; var DECIMAL_LITERAL_RE = INTEGER_RE + '(\\.' + INTEGER_RE + ')?' + '(' + EXPONENT_RE + ')?'; // Based literal: var BASED_INTEGER_RE = '\\w+'; var BASED_LITERAL_RE = INTEGER_RE + '#' + BASED_INTEGER_RE + '(\\.' + BASED_INTEGER_RE + ')?' + '#' + '(' + EXPONENT_RE + ')?'; var NUMBER_RE = '\\b(' + BASED_LITERAL_RE + '|' + DECIMAL_LITERAL_RE + ')'; // Identifier regex var ID_REGEX = '[A-Za-z](_?[A-Za-z0-9.])*'; // bad chars, only allowed in literals var BAD_CHARS = '[]{}%#\'\"' // Ada doesn't have block comments, only line comments var COMMENTS = hljs.COMMENT('--', '$'); // variable declarations of the form // Foo : Bar := Baz; // where only Bar will be highlighted var VAR_DECLS = { // TODO: These spaces are not required by the Ada syntax // however, I have yet to see handwritten Ada code where // someone does not put spaces around : begin: '\\s+:\\s+', end: '\\s*(:=|;|\\)|=>|$)', // endsWithParent: true, // returnBegin: true, illegal: BAD_CHARS, contains: [ { // workaround to avoid highlighting // named loops and declare blocks beginKeywords: 'loop for declare others', endsParent: true, }, { // properly highlight all modifiers className: 'keyword', beginKeywords: 'not null constant access function procedure in out aliased exception' }, { className: 'type', begin: ID_REGEX, endsParent: true, relevance: 0, } ] }; return { case_insensitive: true, keywords: { keyword: 'abort else new return abs elsif not reverse abstract end ' + 'accept entry select access exception of separate aliased exit or some ' + 'all others subtype and for out synchronized array function overriding ' + 'at tagged generic package task begin goto pragma terminate ' + 'body private then if procedure type case in protected constant interface ' + 'is raise use declare range delay limited record when delta loop rem while ' + 'digits renames with do mod requeue xor', literal: 'True False', }, contains: [ COMMENTS, // strings "foobar" { className: 'string', begin: /"/, end: /"/, contains: [{begin: /""/, relevance: 0}] }, // characters '' { // character literals always contain one char className: 'string', begin: /'.'/ }, { // number literals className: 'number', begin: NUMBER_RE, relevance: 0 }, { // Attributes className: 'symbol', begin: "'" + ID_REGEX, }, { // package definition, maybe inside generic className: 'title', begin: '(\\bwith\\s+)?(\\bprivate\\s+)?\\bpackage\\s+(\\bbody\\s+)?', end: '(is|$)', keywords: 'package body', excludeBegin: true, excludeEnd: true, illegal: BAD_CHARS }, { // function/procedure declaration/definition // maybe inside generic begin: '(\\b(with|overriding)\\s+)?\\b(function|procedure)\\s+', end: '(\\bis|\\bwith|\\brenames|\\)\\s*;)', keywords: 'overriding function procedure with is renames return', // we need to re-match the 'function' keyword, so that // the title mode below matches only exactly once returnBegin: true, contains: [ COMMENTS, { // name of the function/procedure className: 'title', begin: '(\\bwith\\s+)?\\b(function|procedure)\\s+', end: '(\\(|\\s+|$)', excludeBegin: true, excludeEnd: true, illegal: BAD_CHARS }, // 'self' // // parameter types VAR_DECLS, { // return type className: 'type', begin: '\\breturn\\s+', end: '(\\s+|;|$)', keywords: 'return', excludeBegin: true, excludeEnd: true, // we are done with functions endsParent: true, illegal: BAD_CHARS }, ] }, { // new type declarations // maybe inside generic className: 'type', begin: '\\b(sub)?type\\s+', end: '\\s+', keywords: 'type', excludeBegin: true, illegal: BAD_CHARS }, // see comment above the definition VAR_DECLS, // no markup // relevance boosters for small snippets // {begin: '\\s*=>\\s*'}, // {begin: '\\s*:=\\s*'}, // {begin: '\\s+:=\\s+'}, ] }; } },{name:"angelscript",create:/* Language: AngelScript Author: Melissa Geels Category: scripting */ function(hljs) { var builtInTypeMode = { className: 'built_in', begin: '\\b(void|bool|int|int8|int16|int32|int64|uint|uint8|uint16|uint32|uint64|string|ref|array|double|float|auto|dictionary)' }; var objectHandleMode = { className: 'symbol', begin: '[a-zA-Z0-9_]+@' }; var genericMode = { className: 'keyword', begin: '<', end: '>', contains: [ builtInTypeMode, objectHandleMode ] }; builtInTypeMode.contains = [ genericMode ]; objectHandleMode.contains = [ genericMode ]; return { aliases: [ 'asc' ], keywords: 'for in|0 break continue while do|0 return if else case switch namespace is cast ' + 'or and xor not get|0 in inout|10 out override set|0 private public const default|0 ' + 'final shared external mixin|10 enum typedef funcdef this super import from interface ' + 'abstract|0 try catch protected explicit', // avoid close detection with C# and JS illegal: '(^using\\s+[A-Za-z0-9_\\.]+;$|\\bfunction\s*[^\\(])', contains: [ { // 'strings' className: 'string', begin: '\'', end: '\'', illegal: '\\n', contains: [ hljs.BACKSLASH_ESCAPE ], relevance: 0 }, { // "strings" className: 'string', begin: '"', end: '"', illegal: '\\n', contains: [ hljs.BACKSLASH_ESCAPE ], relevance: 0 }, // """heredoc strings""" { className: 'string', begin: '"""', end: '"""' }, hljs.C_LINE_COMMENT_MODE, // single-line comments hljs.C_BLOCK_COMMENT_MODE, // comment blocks { // interface or namespace declaration beginKeywords: 'interface namespace', end: '{', illegal: '[;.\\-]', contains: [ { // interface or namespace name className: 'symbol', begin: '[a-zA-Z0-9_]+' } ] }, { // class declaration beginKeywords: 'class', end: '{', illegal: '[;.\\-]', contains: [ { // class name className: 'symbol', begin: '[a-zA-Z0-9_]+', contains: [ { begin: '[:,]\\s*', contains: [ { className: 'symbol', begin: '[a-zA-Z0-9_]+' } ] } ] } ] }, builtInTypeMode, // built-in types objectHandleMode, // object handles { // literals className: 'literal', begin: '\\b(null|true|false)' }, { // numbers className: 'number', begin: '(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?f?|\\.\\d+f?)([eE][-+]?\\d+f?)?)' } ] }; } },{name:"apache",create:/* Language: Apache Author: Ruslan Keba Contributors: Ivan Sagalaev Website: http://rukeba.com/ Description: language definition for Apache configuration files (httpd.conf & .htaccess) Category: common, config */ function(hljs) { var NUMBER = {className: 'number', begin: '[\\$%]\\d+'}; return { aliases: ['apacheconf'], case_insensitive: true, contains: [ hljs.HASH_COMMENT_MODE, {className: 'section', begin: ''}, { className: 'attribute', begin: /\w+/, relevance: 0, // keywords aren’t needed for highlighting per se, they only boost relevance // for a very generally defined mode (starts with a word, ends with line-end keywords: { nomarkup: 'order deny allow setenv rewriterule rewriteengine rewritecond documentroot ' + 'sethandler errordocument loadmodule options header listen serverroot ' + 'servername' }, starts: { end: /$/, relevance: 0, keywords: { literal: 'on off all' }, contains: [ { className: 'meta', begin: '\\s\\[', end: '\\]$' }, { className: 'variable', begin: '[\\$%]\\{', end: '\\}', contains: ['self', NUMBER] }, NUMBER, hljs.QUOTE_STRING_MODE ] } } ], illegal: /\S/ }; } },{name:"applescript",create:/* Language: AppleScript Authors: Nathan Grigg , Dr. Drang Category: scripting */ function(hljs) { var STRING = hljs.inherit(hljs.QUOTE_STRING_MODE, {illegal: ''}); var PARAMS = { className: 'params', begin: '\\(', end: '\\)', contains: ['self', hljs.C_NUMBER_MODE, STRING] }; var COMMENT_MODE_1 = hljs.COMMENT('--', '$'); var COMMENT_MODE_2 = hljs.COMMENT( '\\(\\*', '\\*\\)', { contains: ['self', COMMENT_MODE_1] //allow nesting } ); var COMMENTS = [ COMMENT_MODE_1, COMMENT_MODE_2, hljs.HASH_COMMENT_MODE ]; return { aliases: ['osascript'], keywords: { keyword: 'about above after against and around as at back before beginning ' + 'behind below beneath beside between but by considering ' + 'contain contains continue copy div does eighth else end equal ' + 'equals error every exit fifth first for fourth from front ' + 'get given global if ignoring in into is it its last local me ' + 'middle mod my ninth not of on onto or over prop property put ref ' + 'reference repeat returning script second set seventh since ' + 'sixth some tell tenth that the|0 then third through thru ' + 'timeout times to transaction try until where while whose with ' + 'without', literal: 'AppleScript false linefeed return pi quote result space tab true', built_in: 'alias application boolean class constant date file integer list ' + 'number real record string text ' + 'activate beep count delay launch log offset read round ' + 'run say summarize write ' + 'character characters contents day frontmost id item length ' + 'month name paragraph paragraphs rest reverse running time version ' + 'weekday word words year' }, contains: [ STRING, hljs.C_NUMBER_MODE, { className: 'built_in', begin: '\\b(clipboard info|the clipboard|info for|list (disks|folder)|' + 'mount volume|path to|(close|open for) access|(get|set) eof|' + 'current date|do shell script|get volume settings|random number|' + 'set volume|system attribute|system info|time to GMT|' + '(load|run|store) script|scripting components|' + 'ASCII (character|number)|localized string|' + 'choose (application|color|file|file name|' + 'folder|from list|remote application|URL)|' + 'display (alert|dialog))\\b|^\\s*return\\b' }, { className: 'literal', begin: '\\b(text item delimiters|current application|missing value)\\b' }, { className: 'keyword', begin: '\\b(apart from|aside from|instead of|out of|greater than|' + "isn't|(doesn't|does not) (equal|come before|come after|contain)|" + '(greater|less) than( or equal)?|(starts?|ends|begins?) with|' + 'contained by|comes (before|after)|a (ref|reference)|POSIX file|' + 'POSIX path|(date|time) string|quoted form)\\b' }, { beginKeywords: 'on', illegal: '[${=;\\n]', contains: [hljs.UNDERSCORE_TITLE_MODE, PARAMS] } ].concat(COMMENTS), illegal: '//|->|=>|\\[\\[' }; } },{name:"arcade",create:/* Language: ArcGIS Arcade Category: scripting Author: John Foster Description: ArcGIS Arcade is an expression language used in many Esri ArcGIS products such as Pro, Online, Server, Runtime, JavaScript, and Python */ function(hljs) { var IDENT_RE = '[A-Za-z_][0-9A-Za-z_]*'; var KEYWORDS = { keyword: 'if for while var new function do return void else break', literal: 'true false null undefined NaN Infinity PI BackSlash DoubleQuote ForwardSlash NewLine SingleQuote Tab', built_in: 'Abs Acos Area AreaGeodetic Asin Atan Atan2 Average Boolean Buffer BufferGeodetic ' + 'Ceil Centroid Clip Console Constrain Contains Cos Count Crosses Cut Date DateAdd ' + 'DateDiff Day Decode DefaultValue Dictionary Difference Disjoint Distance Distinct ' + 'DomainCode DomainName Equals Exp Extent Feature FeatureSet FeatureSetById FeatureSetByTitle ' + 'FeatureSetByUrl Filter First Floor Geometry Guid HasKey Hour IIf IndexOf Intersection ' + 'Intersects IsEmpty Length LengthGeodetic Log Max Mean Millisecond Min Minute Month ' + 'MultiPartToSinglePart Multipoint NextSequenceValue Now Number OrderBy Overlaps Point Polygon ' + 'Polyline Pow Random Relate Reverse Round Second SetGeometry Sin Sort Sqrt Stdev Sum ' + 'SymmetricDifference Tan Text Timestamp Today ToLocal Top Touches ToUTC TypeOf Union Variance ' + 'Weekday When Within Year ' }; var EXPRESSIONS; var SYMBOL = { className: 'symbol', begin: '\\$[feature|layer|map|value|view]+' }; var NUMBER = { className: 'number', variants: [ { begin: '\\b(0[bB][01]+)' }, { begin: '\\b(0[oO][0-7]+)' }, { begin: hljs.C_NUMBER_RE } ], relevance: 0 }; var SUBST = { className: 'subst', begin: '\\$\\{', end: '\\}', keywords: KEYWORDS, contains: [] // defined later }; var TEMPLATE_STRING = { className: 'string', begin: '`', end: '`', contains: [ hljs.BACKSLASH_ESCAPE, SUBST ] }; SUBST.contains = [ hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, TEMPLATE_STRING, NUMBER, hljs.REGEXP_MODE ]; var PARAMS_CONTAINS = SUBST.contains.concat([ hljs.C_BLOCK_COMMENT_MODE, hljs.C_LINE_COMMENT_MODE ]); return { aliases: ['arcade'], keywords: KEYWORDS, contains: [ hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, TEMPLATE_STRING, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, SYMBOL, NUMBER, { // object attr container begin: /[{,]\s*/, relevance: 0, contains: [ { begin: IDENT_RE + '\\s*:', returnBegin: true, relevance: 0, contains: [{className: 'attr', begin: IDENT_RE, relevance: 0}] } ] }, { // "value" container begin: '(' + hljs.RE_STARTERS_RE + '|\\b(return)\\b)\\s*', keywords: 'return', contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.REGEXP_MODE, { className: 'function', begin: '(\\(.*?\\)|' + IDENT_RE + ')\\s*=>', returnBegin: true, end: '\\s*=>', contains: [ { className: 'params', variants: [ { begin: IDENT_RE }, { begin: /\(\s*\)/, }, { begin: /\(/, end: /\)/, excludeBegin: true, excludeEnd: true, keywords: KEYWORDS, contains: PARAMS_CONTAINS } ] } ] } ], relevance: 0 }, { className: 'function', beginKeywords: 'function', end: /\{/, excludeEnd: true, contains: [ hljs.inherit(hljs.TITLE_MODE, {begin: IDENT_RE}), { className: 'params', begin: /\(/, end: /\)/, excludeBegin: true, excludeEnd: true, contains: PARAMS_CONTAINS } ], illegal: /\[|%/ }, { begin: /\$[(.]/ } ], illegal: /#(?!!)/ }; } },{name:"arduino",create:/* Language: Arduino Author: Stefania Mellai Description: The Arduino® Language is a superset of C++. This rules are designed to highlight the Arduino® source code. For info about language see http://www.arduino.cc. Requires: cpp.js */ function(hljs) { var CPP = hljs.getLanguage('cpp').exports; return { keywords: { keyword: 'boolean byte word string String array ' + CPP.keywords.keyword, built_in: 'setup loop while catch for if do goto try switch case else ' + 'default break continue return ' + 'KeyboardController MouseController SoftwareSerial ' + 'EthernetServer EthernetClient LiquidCrystal ' + 'RobotControl GSMVoiceCall EthernetUDP EsploraTFT ' + 'HttpClient RobotMotor WiFiClient GSMScanner ' + 'FileSystem Scheduler GSMServer YunClient YunServer ' + 'IPAddress GSMClient GSMModem Keyboard Ethernet ' + 'Console GSMBand Esplora Stepper Process ' + 'WiFiUDP GSM_SMS Mailbox USBHost Firmata PImage ' + 'Client Server GSMPIN FileIO Bridge Serial ' + 'EEPROM Stream Mouse Audio Servo File Task ' + 'GPRS WiFi Wire TFT GSM SPI SD ' + 'runShellCommandAsynchronously analogWriteResolution ' + 'retrieveCallingNumber printFirmwareVersion ' + 'analogReadResolution sendDigitalPortPair ' + 'noListenOnLocalhost readJoystickButton setFirmwareVersion ' + 'readJoystickSwitch scrollDisplayRight getVoiceCallStatus ' + 'scrollDisplayLeft writeMicroseconds delayMicroseconds ' + 'beginTransmission getSignalStrength runAsynchronously ' + 'getAsynchronously listenOnLocalhost getCurrentCarrier ' + 'readAccelerometer messageAvailable sendDigitalPorts ' + 'lineFollowConfig countryNameWrite runShellCommand ' + 'readStringUntil rewindDirectory readTemperature ' + 'setClockDivider readLightSensor endTransmission ' + 'analogReference detachInterrupt countryNameRead ' + 'attachInterrupt encryptionType readBytesUntil ' + 'robotNameWrite readMicrophone robotNameRead cityNameWrite ' + 'userNameWrite readJoystickY readJoystickX mouseReleased ' + 'openNextFile scanNetworks noInterrupts digitalWrite ' + 'beginSpeaker mousePressed isActionDone mouseDragged ' + 'displayLogos noAutoscroll addParameter remoteNumber ' + 'getModifiers keyboardRead userNameRead waitContinue ' + 'processInput parseCommand printVersion readNetworks ' + 'writeMessage blinkVersion cityNameRead readMessage ' + 'setDataMode parsePacket isListening setBitOrder ' + 'beginPacket isDirectory motorsWrite drawCompass ' + 'digitalRead clearScreen serialEvent rightToLeft ' + 'setTextSize leftToRight requestFrom keyReleased ' + 'compassRead analogWrite interrupts WiFiServer ' + 'disconnect playMelody parseFloat autoscroll ' + 'getPINUsed setPINUsed setTimeout sendAnalog ' + 'readSlider analogRead beginWrite createChar ' + 'motorsStop keyPressed tempoWrite readButton ' + 'subnetMask debugPrint macAddress writeGreen ' + 'randomSeed attachGPRS readString sendString ' + 'remotePort releaseAll mouseMoved background ' + 'getXChange getYChange answerCall getResult ' + 'voiceCall endPacket constrain getSocket writeJSON ' + 'getButton available connected findUntil readBytes ' + 'exitValue readGreen writeBlue startLoop IPAddress ' + 'isPressed sendSysex pauseMode gatewayIP setCursor ' + 'getOemKey tuneWrite noDisplay loadImage switchPIN ' + 'onRequest onReceive changePIN playFile noBuffer ' + 'parseInt overflow checkPIN knobRead beginTFT ' + 'bitClear updateIR bitWrite position writeRGB ' + 'highByte writeRed setSpeed readBlue noStroke ' + 'remoteIP transfer shutdown hangCall beginSMS ' + 'endWrite attached maintain noCursor checkReg ' + 'checkPUK shiftOut isValid shiftIn pulseIn ' + 'connect println localIP pinMode getIMEI ' + 'display noBlink process getBand running beginSD ' + 'drawBMP lowByte setBand release bitRead prepare ' + 'pointTo readRed setMode noFill remove listen ' + 'stroke detach attach noTone exists buffer ' + 'height bitSet circle config cursor random ' + 'IRread setDNS endSMS getKey micros ' + 'millis begin print write ready flush width ' + 'isPIN blink clear press mkdir rmdir close ' + 'point yield image BSSID click delay ' + 'read text move peek beep rect line open ' + 'seek fill size turn stop home find ' + 'step tone sqrt RSSI SSID ' + 'end bit tan cos sin pow map abs max ' + 'min get run put', literal: 'DIGITAL_MESSAGE FIRMATA_STRING ANALOG_MESSAGE ' + 'REPORT_DIGITAL REPORT_ANALOG INPUT_PULLUP ' + 'SET_PIN_MODE INTERNAL2V56 SYSTEM_RESET LED_BUILTIN ' + 'INTERNAL1V1 SYSEX_START INTERNAL EXTERNAL ' + 'DEFAULT OUTPUT INPUT HIGH LOW' }, contains: [ CPP.preprocessor, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, hljs.C_NUMBER_MODE ] }; } },{name:"armasm",create:/* Language: ARM Assembly Author: Dan Panzarella Description: ARM Assembly including Thumb and Thumb2 instructions Category: assembler */ function(hljs) { //local labels: %?[FB]?[AT]?\d{1,2}\w+ return { case_insensitive: true, aliases: ['arm'], lexemes: '\\.?' + hljs.IDENT_RE, keywords: { meta: //GNU preprocs '.2byte .4byte .align .ascii .asciz .balign .byte .code .data .else .end .endif .endm .endr .equ .err .exitm .extern .global .hword .if .ifdef .ifndef .include .irp .long .macro .rept .req .section .set .skip .space .text .word .arm .thumb .code16 .code32 .force_thumb .thumb_func .ltorg '+ //ARM directives 'ALIAS ALIGN ARM AREA ASSERT ATTR CN CODE CODE16 CODE32 COMMON CP DATA DCB DCD DCDU DCDO DCFD DCFDU DCI DCQ DCQU DCW DCWU DN ELIF ELSE END ENDFUNC ENDIF ENDP ENTRY EQU EXPORT EXPORTAS EXTERN FIELD FILL FUNCTION GBLA GBLL GBLS GET GLOBAL IF IMPORT INCBIN INCLUDE INFO KEEP LCLA LCLL LCLS LTORG MACRO MAP MEND MEXIT NOFP OPT PRESERVE8 PROC QN READONLY RELOC REQUIRE REQUIRE8 RLIST FN ROUT SETA SETL SETS SN SPACE SUBT THUMB THUMBX TTL WHILE WEND ', built_in: 'r0 r1 r2 r3 r4 r5 r6 r7 r8 r9 r10 r11 r12 r13 r14 r15 '+ //standard registers 'pc lr sp ip sl sb fp '+ //typical regs plus backward compatibility 'a1 a2 a3 a4 v1 v2 v3 v4 v5 v6 v7 v8 f0 f1 f2 f3 f4 f5 f6 f7 '+ //more regs and fp 'p0 p1 p2 p3 p4 p5 p6 p7 p8 p9 p10 p11 p12 p13 p14 p15 '+ //coprocessor regs 'c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 c10 c11 c12 c13 c14 c15 '+ //more coproc 'q0 q1 q2 q3 q4 q5 q6 q7 q8 q9 q10 q11 q12 q13 q14 q15 '+ //advanced SIMD NEON regs //program status registers 'cpsr_c cpsr_x cpsr_s cpsr_f cpsr_cx cpsr_cxs cpsr_xs cpsr_xsf cpsr_sf cpsr_cxsf '+ 'spsr_c spsr_x spsr_s spsr_f spsr_cx spsr_cxs spsr_xs spsr_xsf spsr_sf spsr_cxsf '+ //NEON and VFP registers 's0 s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 s11 s12 s13 s14 s15 '+ 's16 s17 s18 s19 s20 s21 s22 s23 s24 s25 s26 s27 s28 s29 s30 s31 '+ 'd0 d1 d2 d3 d4 d5 d6 d7 d8 d9 d10 d11 d12 d13 d14 d15 '+ 'd16 d17 d18 d19 d20 d21 d22 d23 d24 d25 d26 d27 d28 d29 d30 d31 ' + '{PC} {VAR} {TRUE} {FALSE} {OPT} {CONFIG} {ENDIAN} {CODESIZE} {CPU} {FPU} {ARCHITECTURE} {PCSTOREOFFSET} {ARMASM_VERSION} {INTER} {ROPI} {RWPI} {SWST} {NOSWST} . @' }, contains: [ { className: 'keyword', begin: '\\b('+ //mnemonics 'adc|'+ '(qd?|sh?|u[qh]?)?add(8|16)?|usada?8|(q|sh?|u[qh]?)?(as|sa)x|'+ 'and|adrl?|sbc|rs[bc]|asr|b[lx]?|blx|bxj|cbn?z|tb[bh]|bic|'+ 'bfc|bfi|[su]bfx|bkpt|cdp2?|clz|clrex|cmp|cmn|cpsi[ed]|cps|'+ 'setend|dbg|dmb|dsb|eor|isb|it[te]{0,3}|lsl|lsr|ror|rrx|'+ 'ldm(([id][ab])|f[ds])?|ldr((s|ex)?[bhd])?|movt?|mvn|mra|mar|'+ 'mul|[us]mull|smul[bwt][bt]|smu[as]d|smmul|smmla|'+ 'mla|umlaal|smlal?([wbt][bt]|d)|mls|smlsl?[ds]|smc|svc|sev|'+ 'mia([bt]{2}|ph)?|mrr?c2?|mcrr2?|mrs|msr|orr|orn|pkh(tb|bt)|rbit|'+ 'rev(16|sh)?|sel|[su]sat(16)?|nop|pop|push|rfe([id][ab])?|'+ 'stm([id][ab])?|str(ex)?[bhd]?|(qd?)?sub|(sh?|q|u[qh]?)?sub(8|16)|'+ '[su]xt(a?h|a?b(16)?)|srs([id][ab])?|swpb?|swi|smi|tst|teq|'+ 'wfe|wfi|yield'+ ')'+ '(eq|ne|cs|cc|mi|pl|vs|vc|hi|ls|ge|lt|gt|le|al|hs|lo)?'+ //condition codes '[sptrx]?' , //legal postfixes end: '\\s' }, hljs.COMMENT('[;@]', '$', {relevance: 0}), hljs.C_BLOCK_COMMENT_MODE, hljs.QUOTE_STRING_MODE, { className: 'string', begin: '\'', end: '[^\\\\]\'', relevance: 0 }, { className: 'title', begin: '\\|', end: '\\|', illegal: '\\n', relevance: 0 }, { className: 'number', variants: [ {begin: '[#$=]?0x[0-9a-f]+'}, //hex {begin: '[#$=]?0b[01]+'}, //bin {begin: '[#$=]\\d+'}, //literal {begin: '\\b\\d+'} //bare number ], relevance: 0 }, { className: 'symbol', variants: [ {begin: '^[a-z_\\.\\$][a-z0-9_\\.\\$]+'}, //ARM syntax {begin: '^\\s*[a-z_\\.\\$][a-z0-9_\\.\\$]+:'}, //GNU ARM syntax {begin: '[=#]\\w+' } //label reference ], relevance: 0 } ] }; } },{name:"asciidoc",create:/* Language: AsciiDoc Requires: xml.js Author: Dan Allen Website: http://google.com/profiles/dan.j.allen Description: A semantic, text-based document format that can be exported to HTML, DocBook and other backends. Category: markup */ function(hljs) { return { aliases: ['adoc'], contains: [ // block comment hljs.COMMENT( '^/{4,}\\n', '\\n/{4,}$', // can also be done as... //'^/{4,}$', //'^/{4,}$', { relevance: 10 } ), // line comment hljs.COMMENT( '^//', '$', { relevance: 0 } ), // title { className: 'title', begin: '^\\.\\w.*$' }, // example, admonition & sidebar blocks { begin: '^[=\\*]{4,}\\n', end: '\\n^[=\\*]{4,}$', relevance: 10 }, // headings { className: 'section', relevance: 10, variants: [ {begin: '^(={1,5}) .+?( \\1)?$'}, {begin: '^[^\\[\\]\\n]+?\\n[=\\-~\\^\\+]{2,}$'}, ] }, // document attributes { className: 'meta', begin: '^:.+?:', end: '\\s', excludeEnd: true, relevance: 10 }, // block attributes { className: 'meta', begin: '^\\[.+?\\]$', relevance: 0 }, // quoteblocks { className: 'quote', begin: '^_{4,}\\n', end: '\\n_{4,}$', relevance: 10 }, // listing and literal blocks { className: 'code', begin: '^[\\-\\.]{4,}\\n', end: '\\n[\\-\\.]{4,}$', relevance: 10 }, // passthrough blocks { begin: '^\\+{4,}\\n', end: '\\n\\+{4,}$', contains: [ { begin: '<', end: '>', subLanguage: 'xml', relevance: 0 } ], relevance: 10 }, // lists (can only capture indicators) { className: 'bullet', begin: '^(\\*+|\\-+|\\.+|[^\\n]+?::)\\s+' }, // admonition { className: 'symbol', begin: '^(NOTE|TIP|IMPORTANT|WARNING|CAUTION):\\s+', relevance: 10 }, // inline strong { className: 'strong', // must not follow a word character or be followed by an asterisk or space begin: '\\B\\*(?![\\*\\s])', end: '(\\n{2}|\\*)', // allow escaped asterisk followed by word char contains: [ { begin: '\\\\*\\w', relevance: 0 } ] }, // inline emphasis { className: 'emphasis', // must not follow a word character or be followed by a single quote or space begin: '\\B\'(?![\'\\s])', end: '(\\n{2}|\')', // allow escaped single quote followed by word char contains: [ { begin: '\\\\\'\\w', relevance: 0 } ], relevance: 0 }, // inline emphasis (alt) { className: 'emphasis', // must not follow a word character or be followed by an underline or space begin: '_(?![_\\s])', end: '(\\n{2}|_)', relevance: 0 }, // inline smart quotes { className: 'string', variants: [ {begin: "``.+?''"}, {begin: "`.+?'"} ] }, // inline code snippets (TODO should get same treatment as strong and emphasis) { className: 'code', begin: '(`.+?`|\\+.+?\\+)', relevance: 0 }, // indented literal block { className: 'code', begin: '^[ \\t]', end: '$', relevance: 0 }, // horizontal rules { begin: '^\'{3,}[ \\t]*$', relevance: 10 }, // images and links { begin: '(link:)?(http|https|ftp|file|irc|image:?):\\S+\\[.*?\\]', returnBegin: true, contains: [ { begin: '(link|image:?):', relevance: 0 }, { className: 'link', begin: '\\w', end: '[^\\[]+', relevance: 0 }, { className: 'string', begin: '\\[', end: '\\]', excludeBegin: true, excludeEnd: true, relevance: 0 } ], relevance: 10 } ] }; } },{name:"aspectj",create:/* Language: AspectJ Author: Hakan Ozler Description: Syntax Highlighting for the AspectJ Language which is a general-purpose aspect-oriented extension to the Java programming language. */ function (hljs) { var KEYWORDS = 'false synchronized int abstract float private char boolean static null if const ' + 'for true while long throw strictfp finally protected import native final return void ' + 'enum else extends implements break transient new catch instanceof byte super volatile case ' + 'assert short package default double public try this switch continue throws privileged ' + 'aspectOf adviceexecution proceed cflowbelow cflow initialization preinitialization ' + 'staticinitialization withincode target within execution getWithinTypeName handler ' + 'thisJoinPoint thisJoinPointStaticPart thisEnclosingJoinPointStaticPart declare parents '+ 'warning error soft precedence thisAspectInstance'; var SHORTKEYS = 'get set args call'; return { keywords : KEYWORDS, illegal : /<\/|#/, contains : [ hljs.COMMENT( '/\\*\\*', '\\*/', { relevance : 0, contains : [ { // eat up @'s in emails to prevent them to be recognized as doctags begin: /\w+@/, relevance: 0 }, { className : 'doctag', begin : '@[A-Za-z]+' } ] } ), hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, { className : 'class', beginKeywords : 'aspect', end : /[{;=]/, excludeEnd : true, illegal : /[:;"\[\]]/, contains : [ { beginKeywords : 'extends implements pertypewithin perthis pertarget percflowbelow percflow issingleton' }, hljs.UNDERSCORE_TITLE_MODE, { begin : /\([^\)]*/, end : /[)]+/, keywords : KEYWORDS + ' ' + SHORTKEYS, excludeEnd : false } ] }, { className : 'class', beginKeywords : 'class interface', end : /[{;=]/, excludeEnd : true, relevance: 0, keywords : 'class interface', illegal : /[:"\[\]]/, contains : [ {beginKeywords : 'extends implements'}, hljs.UNDERSCORE_TITLE_MODE ] }, { // AspectJ Constructs beginKeywords : 'pointcut after before around throwing returning', end : /[)]/, excludeEnd : false, illegal : /["\[\]]/, contains : [ { begin : hljs.UNDERSCORE_IDENT_RE + '\\s*\\(', returnBegin : true, contains : [hljs.UNDERSCORE_TITLE_MODE] } ] }, { begin : /[:]/, returnBegin : true, end : /[{;]/, relevance: 0, excludeEnd : false, keywords : KEYWORDS, illegal : /["\[\]]/, contains : [ { begin : hljs.UNDERSCORE_IDENT_RE + '\\s*\\(', keywords : KEYWORDS + ' ' + SHORTKEYS, relevance: 0 }, hljs.QUOTE_STRING_MODE ] }, { // this prevents 'new Name(...), or throw ...' from being recognized as a function definition beginKeywords : 'new throw', relevance : 0 }, { // the function class is a bit different for AspectJ compared to the Java language className : 'function', begin : /\w+ +\w+(\.)?\w+\s*\([^\)]*\)\s*((throws)[\w\s,]+)?[\{;]/, returnBegin : true, end : /[{;=]/, keywords : KEYWORDS, excludeEnd : true, contains : [ { begin : hljs.UNDERSCORE_IDENT_RE + '\\s*\\(', returnBegin : true, relevance: 0, contains : [hljs.UNDERSCORE_TITLE_MODE] }, { className : 'params', begin : /\(/, end : /\)/, relevance: 0, keywords : KEYWORDS, contains : [ hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, hljs.C_NUMBER_MODE, hljs.C_BLOCK_COMMENT_MODE ] }, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE ] }, hljs.C_NUMBER_MODE, { // annotation is also used in this language className : 'meta', begin : '@[A-Za-z]+' } ] }; } },{name:"autohotkey",create:/* Language: AutoHotkey Author: Seongwon Lee Description: AutoHotkey language definition Category: scripting */ function(hljs) { var BACKTICK_ESCAPE = { begin: '`[\\s\\S]' }; return { case_insensitive: true, aliases: [ 'ahk' ], keywords: { keyword: 'Break Continue Critical Exit ExitApp Gosub Goto New OnExit Pause return SetBatchLines SetTimer Suspend Thread Throw Until ahk_id ahk_class ahk_pid ahk_exe ahk_group', literal: 'true false NOT AND OR', built_in: 'ComSpec Clipboard ClipboardAll ErrorLevel', }, contains: [ BACKTICK_ESCAPE, hljs.inherit(hljs.QUOTE_STRING_MODE, {contains: [BACKTICK_ESCAPE]}), hljs.COMMENT(';', '$', {relevance: 0}), hljs.C_BLOCK_COMMENT_MODE, { className: 'number', begin: hljs.NUMBER_RE, relevance: 0 }, { className: 'variable', //subst would be the most accurate however fails the point of highlighting. variable is comparably the most accurate that actually has some effect begin: '%[a-zA-Z0-9#_$@]+%' }, { className: 'built_in', begin: '^\\s*\\w+\\s*(,|%)' //I don't really know if this is totally relevant }, { className: 'title', //symbol would be most accurate however is higlighted just like built_in and that makes up a lot of AutoHotkey code //meaning that it would fail to highlight anything variants: [ {begin: '^[^\\n";]+::(?!=)'}, {begin: '^[^\\n";]+:(?!=)', relevance: 0} // zero relevance as it catches a lot of things // followed by a single ':' in many languages ] }, { className: 'meta', begin: '^\\s*#\\w+', end:'$', relevance: 0 }, { className: 'built_in', begin: 'A_[a-zA-Z0-9]+' }, { // consecutive commas, not for highlighting but just for relevance begin: ',\\s*,' } ] } } },{name:"autoit",create:/* Language: AutoIt Author: Manh Tuan Description: AutoIt language definition Category: scripting */ function(hljs) { var KEYWORDS = 'ByRef Case Const ContinueCase ContinueLoop ' + 'Default Dim Do Else ElseIf EndFunc EndIf EndSelect ' + 'EndSwitch EndWith Enum Exit ExitLoop For Func ' + 'Global If In Local Next ReDim Return Select Static ' + 'Step Switch Then To Until Volatile WEnd While With', LITERAL = 'True False And Null Not Or', BUILT_IN = 'Abs ACos AdlibRegister AdlibUnRegister Asc AscW ASin Assign ATan AutoItSetOption AutoItWinGetTitle AutoItWinSetTitle Beep Binary BinaryLen BinaryMid BinaryToString BitAND BitNOT BitOR BitRotate BitShift BitXOR BlockInput Break Call CDTray Ceiling Chr ChrW ClipGet ClipPut ConsoleRead ConsoleWrite ConsoleWriteError ControlClick ControlCommand ControlDisable ControlEnable ControlFocus ControlGetFocus ControlGetHandle ControlGetPos ControlGetText ControlHide ControlListView ControlMove ControlSend ControlSetText ControlShow ControlTreeView Cos Dec DirCopy DirCreate DirGetSize DirMove DirRemove DllCall DllCallAddress DllCallbackFree DllCallbackGetPtr DllCallbackRegister DllClose DllOpen DllStructCreate DllStructGetData DllStructGetPtr DllStructGetSize DllStructSetData DriveGetDrive DriveGetFileSystem DriveGetLabel DriveGetSerial DriveGetType DriveMapAdd DriveMapDel DriveMapGet DriveSetLabel DriveSpaceFree DriveSpaceTotal DriveStatus EnvGet EnvSet EnvUpdate Eval Execute Exp FileChangeDir FileClose FileCopy FileCreateNTFSLink FileCreateShortcut FileDelete FileExists FileFindFirstFile FileFindNextFile FileFlush FileGetAttrib FileGetEncoding FileGetLongName FileGetPos FileGetShortcut FileGetShortName FileGetSize FileGetTime FileGetVersion FileInstall FileMove FileOpen FileOpenDialog FileRead FileReadLine FileReadToArray FileRecycle FileRecycleEmpty FileSaveDialog FileSelectFolder FileSetAttrib FileSetEnd FileSetPos FileSetTime FileWrite FileWriteLine Floor FtpSetProxy FuncName GUICreate GUICtrlCreateAvi GUICtrlCreateButton GUICtrlCreateCheckbox GUICtrlCreateCombo GUICtrlCreateContextMenu GUICtrlCreateDate GUICtrlCreateDummy GUICtrlCreateEdit GUICtrlCreateGraphic GUICtrlCreateGroup GUICtrlCreateIcon GUICtrlCreateInput GUICtrlCreateLabel GUICtrlCreateList GUICtrlCreateListView GUICtrlCreateListViewItem GUICtrlCreateMenu GUICtrlCreateMenuItem GUICtrlCreateMonthCal GUICtrlCreateObj GUICtrlCreatePic GUICtrlCreateProgress GUICtrlCreateRadio GUICtrlCreateSlider GUICtrlCreateTab GUICtrlCreateTabItem GUICtrlCreateTreeView GUICtrlCreateTreeViewItem GUICtrlCreateUpdown GUICtrlDelete GUICtrlGetHandle GUICtrlGetState GUICtrlRead GUICtrlRecvMsg GUICtrlRegisterListViewSort GUICtrlSendMsg GUICtrlSendToDummy GUICtrlSetBkColor GUICtrlSetColor GUICtrlSetCursor GUICtrlSetData GUICtrlSetDefBkColor GUICtrlSetDefColor GUICtrlSetFont GUICtrlSetGraphic GUICtrlSetImage GUICtrlSetLimit GUICtrlSetOnEvent GUICtrlSetPos GUICtrlSetResizing GUICtrlSetState GUICtrlSetStyle GUICtrlSetTip GUIDelete GUIGetCursorInfo GUIGetMsg GUIGetStyle GUIRegisterMsg GUISetAccelerators GUISetBkColor GUISetCoord GUISetCursor GUISetFont GUISetHelp GUISetIcon GUISetOnEvent GUISetState GUISetStyle GUIStartGroup GUISwitch Hex HotKeySet HttpSetProxy HttpSetUserAgent HWnd InetClose InetGet InetGetInfo InetGetSize InetRead IniDelete IniRead IniReadSection IniReadSectionNames IniRenameSection IniWrite IniWriteSection InputBox Int IsAdmin IsArray IsBinary IsBool IsDeclared IsDllStruct IsFloat IsFunc IsHWnd IsInt IsKeyword IsNumber IsObj IsPtr IsString Log MemGetStats Mod MouseClick MouseClickDrag MouseDown MouseGetCursor MouseGetPos MouseMove MouseUp MouseWheel MsgBox Number ObjCreate ObjCreateInterface ObjEvent ObjGet ObjName OnAutoItExitRegister OnAutoItExitUnRegister Ping PixelChecksum PixelGetColor PixelSearch ProcessClose ProcessExists ProcessGetStats ProcessList ProcessSetPriority ProcessWait ProcessWaitClose ProgressOff ProgressOn ProgressSet Ptr Random RegDelete RegEnumKey RegEnumVal RegRead RegWrite Round Run RunAs RunAsWait RunWait Send SendKeepActive SetError SetExtended ShellExecute ShellExecuteWait Shutdown Sin Sleep SoundPlay SoundSetWaveVolume SplashImageOn SplashOff SplashTextOn Sqrt SRandom StatusbarGetText StderrRead StdinWrite StdioClose StdoutRead String StringAddCR StringCompare StringFormat StringFromASCIIArray StringInStr StringIsAlNum StringIsAlpha StringIsASCII StringIsDigit StringIsFloat StringIsInt StringIsLower StringIsSpace StringIsUpper StringIsXDigit StringLeft StringLen StringLower StringMid StringRegExp StringRegExpReplace StringReplace StringReverse StringRight StringSplit StringStripCR StringStripWS StringToASCIIArray StringToBinary StringTrimLeft StringTrimRight StringUpper Tan TCPAccept TCPCloseSocket TCPConnect TCPListen TCPNameToIP TCPRecv TCPSend TCPShutdown, UDPShutdown TCPStartup, UDPStartup TimerDiff TimerInit ToolTip TrayCreateItem TrayCreateMenu TrayGetMsg TrayItemDelete TrayItemGetHandle TrayItemGetState TrayItemGetText TrayItemSetOnEvent TrayItemSetState TrayItemSetText TraySetClick TraySetIcon TraySetOnEvent TraySetPauseIcon TraySetState TraySetToolTip TrayTip UBound UDPBind UDPCloseSocket UDPOpen UDPRecv UDPSend VarGetType WinActivate WinActive WinClose WinExists WinFlash WinGetCaretPos WinGetClassList WinGetClientSize WinGetHandle WinGetPos WinGetProcess WinGetState WinGetText WinGetTitle WinKill WinList WinMenuSelectItem WinMinimizeAll WinMinimizeAllUndo WinMove WinSetOnTop WinSetState WinSetTitle WinSetTrans WinWait', COMMENT = { variants: [ hljs.COMMENT(';', '$', {relevance: 0}), hljs.COMMENT('#cs', '#ce'), hljs.COMMENT('#comments-start', '#comments-end') ] }, VARIABLE = { begin: '\\$[A-z0-9_]+' }, STRING = { className: 'string', variants: [{ begin: /"/, end: /"/, contains: [{ begin: /""/, relevance: 0 }] }, { begin: /'/, end: /'/, contains: [{ begin: /''/, relevance: 0 }] }] }, NUMBER = { variants: [hljs.BINARY_NUMBER_MODE, hljs.C_NUMBER_MODE] }, PREPROCESSOR = { className: 'meta', begin: '#', end: '$', keywords: {'meta-keyword': 'comments include include-once NoTrayIcon OnAutoItStartRegister pragma compile RequireAdmin'}, contains: [{ begin: /\\\n/, relevance: 0 }, { beginKeywords: 'include', keywords: {'meta-keyword': 'include'}, end: '$', contains: [ STRING, { className: 'meta-string', variants: [{ begin: '<', end: '>' }, { begin: /"/, end: /"/, contains: [{ begin: /""/, relevance: 0 }] }, { begin: /'/, end: /'/, contains: [{ begin: /''/, relevance: 0 }] }] } ] }, STRING, COMMENT ] }, CONSTANT = { className: 'symbol', // begin: '@', // end: '$', // keywords: 'AppDataCommonDir AppDataDir AutoItExe AutoItPID AutoItVersion AutoItX64 COM_EventObj CommonFilesDir Compiled ComputerName ComSpec CPUArch CR CRLF DesktopCommonDir DesktopDepth DesktopDir DesktopHeight DesktopRefresh DesktopWidth DocumentsCommonDir error exitCode exitMethod extended FavoritesCommonDir FavoritesDir GUI_CtrlHandle GUI_CtrlId GUI_DragFile GUI_DragId GUI_DropId GUI_WinHandle HomeDrive HomePath HomeShare HotKeyPressed HOUR IPAddress1 IPAddress2 IPAddress3 IPAddress4 KBLayout LF LocalAppDataDir LogonDNSDomain LogonDomain LogonServer MDAY MIN MON MSEC MUILang MyDocumentsDir NumParams OSArch OSBuild OSLang OSServicePack OSType OSVersion ProgramFilesDir ProgramsCommonDir ProgramsDir ScriptDir ScriptFullPath ScriptLineNumber ScriptName SEC StartMenuCommonDir StartMenuDir StartupCommonDir StartupDir SW_DISABLE SW_ENABLE SW_HIDE SW_LOCK SW_MAXIMIZE SW_MINIMIZE SW_RESTORE SW_SHOW SW_SHOWDEFAULT SW_SHOWMAXIMIZED SW_SHOWMINIMIZED SW_SHOWMINNOACTIVE SW_SHOWNA SW_SHOWNOACTIVATE SW_SHOWNORMAL SW_UNLOCK SystemDir TAB TempDir TRAY_ID TrayIconFlashing TrayIconVisible UserName UserProfileDir WDAY WindowsDir WorkingDir YDAY YEAR', // relevance: 5 begin: '@[A-z0-9_]+' }, FUNCTION = { className: 'function', beginKeywords: 'Func', end: '$', illegal: '\\$|\\[|%', contains: [ hljs.UNDERSCORE_TITLE_MODE, { className: 'params', begin: '\\(', end: '\\)', contains: [ VARIABLE, STRING, NUMBER ] } ] }; return { case_insensitive: true, illegal: /\/\*/, keywords: { keyword: KEYWORDS, built_in: BUILT_IN, literal: LITERAL }, contains: [ COMMENT, VARIABLE, STRING, NUMBER, PREPROCESSOR, CONSTANT, FUNCTION ] } } },{name:"avrasm",create:/* Language: AVR Assembler Author: Vladimir Ermakov Category: assembler */ function(hljs) { return { case_insensitive: true, lexemes: '\\.?' + hljs.IDENT_RE, keywords: { keyword: /* mnemonic */ 'adc add adiw and andi asr bclr bld brbc brbs brcc brcs break breq brge brhc brhs ' + 'brid brie brlo brlt brmi brne brpl brsh brtc brts brvc brvs bset bst call cbi cbr ' + 'clc clh cli cln clr cls clt clv clz com cp cpc cpi cpse dec eicall eijmp elpm eor ' + 'fmul fmuls fmulsu icall ijmp in inc jmp ld ldd ldi lds lpm lsl lsr mov movw mul ' + 'muls mulsu neg nop or ori out pop push rcall ret reti rjmp rol ror sbc sbr sbrc sbrs ' + 'sec seh sbi sbci sbic sbis sbiw sei sen ser ses set sev sez sleep spm st std sts sub ' + 'subi swap tst wdr', built_in: /* general purpose registers */ 'r0 r1 r2 r3 r4 r5 r6 r7 r8 r9 r10 r11 r12 r13 r14 r15 r16 r17 r18 r19 r20 r21 r22 ' + 'r23 r24 r25 r26 r27 r28 r29 r30 r31 x|0 xh xl y|0 yh yl z|0 zh zl ' + /* IO Registers (ATMega128) */ 'ucsr1c udr1 ucsr1a ucsr1b ubrr1l ubrr1h ucsr0c ubrr0h tccr3c tccr3a tccr3b tcnt3h ' + 'tcnt3l ocr3ah ocr3al ocr3bh ocr3bl ocr3ch ocr3cl icr3h icr3l etimsk etifr tccr1c ' + 'ocr1ch ocr1cl twcr twdr twar twsr twbr osccal xmcra xmcrb eicra spmcsr spmcr portg ' + 'ddrg ping portf ddrf sreg sph spl xdiv rampz eicrb eimsk gimsk gicr eifr gifr timsk ' + 'tifr mcucr mcucsr tccr0 tcnt0 ocr0 assr tccr1a tccr1b tcnt1h tcnt1l ocr1ah ocr1al ' + 'ocr1bh ocr1bl icr1h icr1l tccr2 tcnt2 ocr2 ocdr wdtcr sfior eearh eearl eedr eecr ' + 'porta ddra pina portb ddrb pinb portc ddrc pinc portd ddrd pind spdr spsr spcr udr0 ' + 'ucsr0a ucsr0b ubrr0l acsr admux adcsr adch adcl porte ddre pine pinf', meta: '.byte .cseg .db .def .device .dseg .dw .endmacro .equ .eseg .exit .include .list ' + '.listmac .macro .nolist .org .set' }, contains: [ hljs.C_BLOCK_COMMENT_MODE, hljs.COMMENT( ';', '$', { relevance: 0 } ), hljs.C_NUMBER_MODE, // 0x..., decimal, float hljs.BINARY_NUMBER_MODE, // 0b... { className: 'number', begin: '\\b(\\$[a-zA-Z0-9]+|0o[0-7]+)' // $..., 0o... }, hljs.QUOTE_STRING_MODE, { className: 'string', begin: '\'', end: '[^\\\\]\'', illegal: '[^\\\\][^\']' }, {className: 'symbol', begin: '^[A-Za-z0-9_.$]+:'}, {className: 'meta', begin: '#', end: '$'}, { // подстановка в «.macro» className: 'subst', begin: '@[0-9]+' } ] }; } },{name:"awk",create:/* Language: Awk Author: Matthew Daly Website: http://matthewdaly.co.uk/ Description: language definition for Awk scripts */ function(hljs) { var VARIABLE = { className: 'variable', variants: [ {begin: /\$[\w\d#@][\w\d_]*/}, {begin: /\$\{(.*?)}/} ] }; var KEYWORDS = 'BEGIN END if else while do for in break continue delete next nextfile function func exit|10'; var STRING = { className: 'string', contains: [hljs.BACKSLASH_ESCAPE], variants: [ { begin: /(u|b)?r?'''/, end: /'''/, relevance: 10 }, { begin: /(u|b)?r?"""/, end: /"""/, relevance: 10 }, { begin: /(u|r|ur)'/, end: /'/, relevance: 10 }, { begin: /(u|r|ur)"/, end: /"/, relevance: 10 }, { begin: /(b|br)'/, end: /'/ }, { begin: /(b|br)"/, end: /"/ }, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE ] }; return { keywords: { keyword: KEYWORDS }, contains: [ VARIABLE, STRING, hljs.REGEXP_MODE, hljs.HASH_COMMENT_MODE, hljs.NUMBER_MODE ] } } },{name:"axapta",create:/* Language: Axapta Author: Dmitri Roudakov Category: enterprise */ function(hljs) { return { keywords: 'false int abstract private char boolean static null if for true ' + 'while long throw finally protected final return void enum else ' + 'break new catch byte super case short default double public try this switch ' + 'continue reverse firstfast firstonly forupdate nofetch sum avg minof maxof count ' + 'order group by asc desc index hint like dispaly edit client server ttsbegin ' + 'ttscommit str real date container anytype common div mod', contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, hljs.C_NUMBER_MODE, { className: 'meta', begin: '#', end: '$' }, { className: 'class', beginKeywords: 'class interface', end: '{', excludeEnd: true, illegal: ':', contains: [ {beginKeywords: 'extends implements'}, hljs.UNDERSCORE_TITLE_MODE ] } ] }; } },{name:"bash",create:/* Language: Bash Author: vah Contributrors: Benjamin Pannell Category: common */ function(hljs) { var VAR = { className: 'variable', variants: [ {begin: /\$[\w\d#@][\w\d_]*/}, {begin: /\$\{(.*?)}/} ] }; var QUOTE_STRING = { className: 'string', begin: /"/, end: /"/, contains: [ hljs.BACKSLASH_ESCAPE, VAR, { className: 'variable', begin: /\$\(/, end: /\)/, contains: [hljs.BACKSLASH_ESCAPE] } ] }; var ESCAPED_QUOTE = { className: '', begin: /\\"/ }; var APOS_STRING = { className: 'string', begin: /'/, end: /'/ }; return { aliases: ['sh', 'zsh'], lexemes: /\b-?[a-z\._]+\b/, keywords: { keyword: 'if then else elif fi for while in do done case esac function', literal: 'true false', built_in: // Shell built-ins // http://www.gnu.org/software/bash/manual/html_node/Shell-Builtin-Commands.html 'break cd continue eval exec exit export getopts hash pwd readonly return shift test times ' + 'trap umask unset ' + // Bash built-ins 'alias bind builtin caller command declare echo enable help let local logout mapfile printf ' + 'read readarray source type typeset ulimit unalias ' + // Shell modifiers 'set shopt ' + // Zsh built-ins 'autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles ' + 'compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate ' + 'fc fg float functions getcap getln history integer jobs kill limit log noglob popd print ' + 'pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit ' + 'unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof ' + 'zpty zregexparse zsocket zstyle ztcp', _: '-ne -eq -lt -gt -f -d -e -s -l -a' // relevance booster }, contains: [ { className: 'meta', begin: /^#![^\n]+sh\s*$/, relevance: 10 }, { className: 'function', begin: /\w[\w\d_]*\s*\(\s*\)\s*\{/, returnBegin: true, contains: [hljs.inherit(hljs.TITLE_MODE, {begin: /\w[\w\d_]*/})], relevance: 0 }, hljs.HASH_COMMENT_MODE, QUOTE_STRING, ESCAPED_QUOTE, APOS_STRING, VAR ] }; } },{name:"basic",create:/* Language: Basic Author: Raphaël Assénat Description: Based on the BASIC reference from the Tandy 1000 guide */ function(hljs) { return { case_insensitive: true, illegal: '^\.', // Support explicitely typed variables that end with $%! or #. lexemes: '[a-zA-Z][a-zA-Z0-9_\$\%\!\#]*', keywords: { keyword: 'ABS ASC AND ATN AUTO|0 BEEP BLOAD|10 BSAVE|10 CALL CALLS CDBL CHAIN CHDIR CHR$|10 CINT CIRCLE ' + 'CLEAR CLOSE CLS COLOR COM COMMON CONT COS CSNG CSRLIN CVD CVI CVS DATA DATE$ ' + 'DEFDBL DEFINT DEFSNG DEFSTR DEF|0 SEG USR DELETE DIM DRAW EDIT END ENVIRON ENVIRON$ ' + 'EOF EQV ERASE ERDEV ERDEV$ ERL ERR ERROR EXP FIELD FILES FIX FOR|0 FRE GET GOSUB|10 GOTO ' + 'HEX$ IF|0 THEN ELSE|0 INKEY$ INP INPUT INPUT# INPUT$ INSTR IMP INT IOCTL IOCTL$ KEY ON ' + 'OFF LIST KILL LEFT$ LEN LET LINE LLIST LOAD LOC LOCATE LOF LOG LPRINT USING LSET ' + 'MERGE MID$ MKDIR MKD$ MKI$ MKS$ MOD NAME NEW NEXT NOISE NOT OCT$ ON OR PEN PLAY STRIG OPEN OPTION ' + 'BASE OUT PAINT PALETTE PCOPY PEEK PMAP POINT POKE POS PRINT PRINT] PSET PRESET ' + 'PUT RANDOMIZE READ REM RENUM RESET|0 RESTORE RESUME RETURN|0 RIGHT$ RMDIR RND RSET ' + 'RUN SAVE SCREEN SGN SHELL SIN SOUND SPACE$ SPC SQR STEP STICK STOP STR$ STRING$ SWAP ' + 'SYSTEM TAB TAN TIME$ TIMER TROFF TRON TO USR VAL VARPTR VARPTR$ VIEW WAIT WHILE ' + 'WEND WIDTH WINDOW WRITE XOR' }, contains: [ hljs.QUOTE_STRING_MODE, hljs.COMMENT('REM', '$', {relevance: 10}), hljs.COMMENT('\'', '$', {relevance: 0}), { // Match line numbers className: 'symbol', begin: '^[0-9]+\ ', relevance: 10 }, { // Match typed numeric constants (1000, 12.34!, 1.2e5, 1.5#, 1.2D2) className: 'number', begin: '\\b([0-9]+[0-9edED\.]*[#\!]?)', relevance: 0 }, { // Match hexadecimal numbers (&Hxxxx) className: 'number', begin: '(\&[hH][0-9a-fA-F]{1,4})' }, { // Match octal numbers (&Oxxxxxx) className: 'number', begin: '(\&[oO][0-7]{1,6})' } ] }; } },{name:"bnf",create:/* Language: Backus–Naur Form Author: Oleg Efimov */ function(hljs){ return { contains: [ // Attribute { className: 'attribute', begin: // }, // Specific { begin: /::=/, starts: { end: /$/, contains: [ { begin: // }, // Common hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE ] } } ] }; } },{name:"brainfuck",create:/* Language: Brainfuck Author: Evgeny Stepanischev */ function(hljs){ var LITERAL = { className: 'literal', begin: '[\\+\\-]', relevance: 0 }; return { aliases: ['bf'], contains: [ hljs.COMMENT( '[^\\[\\]\\.,\\+\\-<> \r\n]', '[\\[\\]\\.,\\+\\-<> \r\n]', { returnEnd: true, relevance: 0 } ), { className: 'title', begin: '[\\[\\]]', relevance: 0 }, { className: 'string', begin: '[\\.,]', relevance: 0 }, { // this mode works as the only relevance counter begin: /\+\+|\-\-/, returnBegin: true, contains: [LITERAL] }, LITERAL ] }; } },{name:"cal",create:/* Language: C/AL Author: Kenneth Fuglsang Christensen Description: Provides highlighting of Microsoft Dynamics NAV C/AL code files */ function(hljs) { var KEYWORDS = 'div mod in and or not xor asserterror begin case do downto else end exit for if of repeat then to ' + 'until while with var'; var LITERALS = 'false true'; var COMMENT_MODES = [ hljs.C_LINE_COMMENT_MODE, hljs.COMMENT( /\{/, /\}/, { relevance: 0 } ), hljs.COMMENT( /\(\*/, /\*\)/, { relevance: 10 } ) ]; var STRING = { className: 'string', begin: /'/, end: /'/, contains: [{begin: /''/}] }; var CHAR_STRING = { className: 'string', begin: /(#\d+)+/ }; var DATE = { className: 'number', begin: '\\b\\d+(\\.\\d+)?(DT|D|T)', relevance: 0 }; var DBL_QUOTED_VARIABLE = { className: 'string', // not a string technically but makes sense to be highlighted in the same style begin: '"', end: '"' }; var PROCEDURE = { className: 'function', beginKeywords: 'procedure', end: /[:;]/, keywords: 'procedure|10', contains: [ hljs.TITLE_MODE, { className: 'params', begin: /\(/, end: /\)/, keywords: KEYWORDS, contains: [STRING, CHAR_STRING] } ].concat(COMMENT_MODES) }; var OBJECT = { className: 'class', begin: 'OBJECT (Table|Form|Report|Dataport|Codeunit|XMLport|MenuSuite|Page|Query) (\\d+) ([^\\r\\n]+)', returnBegin: true, contains: [ hljs.TITLE_MODE, PROCEDURE ] }; return { case_insensitive: true, keywords: { keyword: KEYWORDS, literal: LITERALS }, illegal: /\/\*/, contains: [ STRING, CHAR_STRING, DATE, DBL_QUOTED_VARIABLE, hljs.NUMBER_MODE, OBJECT, PROCEDURE ] }; } },{name:"capnproto",create:/* Language: Cap’n Proto Author: Oleg Efimov Description: Cap’n Proto message definition format Category: protocols */ function(hljs) { return { aliases: ['capnp'], keywords: { keyword: 'struct enum interface union group import using const annotation extends in of on as with from fixed', built_in: 'Void Bool Int8 Int16 Int32 Int64 UInt8 UInt16 UInt32 UInt64 Float32 Float64 ' + 'Text Data AnyPointer AnyStruct Capability List', literal: 'true false' }, contains: [ hljs.QUOTE_STRING_MODE, hljs.NUMBER_MODE, hljs.HASH_COMMENT_MODE, { className: 'meta', begin: /@0x[\w\d]{16};/, illegal: /\n/ }, { className: 'symbol', begin: /@\d+\b/ }, { className: 'class', beginKeywords: 'struct enum', end: /\{/, illegal: /\n/, contains: [ hljs.inherit(hljs.TITLE_MODE, { starts: {endsWithParent: true, excludeEnd: true} // hack: eating everything after the first title }) ] }, { className: 'class', beginKeywords: 'interface', end: /\{/, illegal: /\n/, contains: [ hljs.inherit(hljs.TITLE_MODE, { starts: {endsWithParent: true, excludeEnd: true} // hack: eating everything after the first title }) ] } ] }; } },{name:"ceylon",create:/* Language: Ceylon Author: Lucas Werkmeister */ function(hljs) { // 2.3. Identifiers and keywords var KEYWORDS = 'assembly module package import alias class interface object given value ' + 'assign void function new of extends satisfies abstracts in out return ' + 'break continue throw assert dynamic if else switch case for while try ' + 'catch finally then let this outer super is exists nonempty'; // 7.4.1 Declaration Modifiers var DECLARATION_MODIFIERS = 'shared abstract formal default actual variable late native deprecated' + 'final sealed annotation suppressWarnings small'; // 7.4.2 Documentation var DOCUMENTATION = 'doc by license see throws tagged'; var SUBST = { className: 'subst', excludeBegin: true, excludeEnd: true, begin: /``/, end: /``/, keywords: KEYWORDS, relevance: 10 }; var EXPRESSIONS = [ { // verbatim string className: 'string', begin: '"""', end: '"""', relevance: 10 }, { // string literal or template className: 'string', begin: '"', end: '"', contains: [SUBST] }, { // character literal className: 'string', begin: "'", end: "'" }, { // numeric literal className: 'number', begin: '#[0-9a-fA-F_]+|\\$[01_]+|[0-9_]+(?:\\.[0-9_](?:[eE][+-]?\\d+)?)?[kMGTPmunpf]?', relevance: 0 } ]; SUBST.contains = EXPRESSIONS; return { keywords: { keyword: KEYWORDS + ' ' + DECLARATION_MODIFIERS, meta: DOCUMENTATION }, illegal: '\\$[^01]|#[^0-9a-fA-F]', contains: [ hljs.C_LINE_COMMENT_MODE, hljs.COMMENT('/\\*', '\\*/', {contains: ['self']}), { // compiler annotation className: 'meta', begin: '@[a-z]\\w*(?:\\:\"[^\"]*\")?' } ].concat(EXPRESSIONS) }; } },{name:"clean",create:/* Language: Clean Author: Camil Staps Category: functional Website: http://clean.cs.ru.nl */ function(hljs) { return { aliases: ['clean','icl','dcl'], keywords: { keyword: 'if let in with where case of class instance otherwise ' + 'implementation definition system module from import qualified as ' + 'special code inline foreign export ccall stdcall generic derive ' + 'infix infixl infixr', built_in: 'Int Real Char Bool', literal: 'True False' }, contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, hljs.C_NUMBER_MODE, {begin: '->|<-[|:]?|#!?|>>=|\\{\\||\\|\\}|:==|=:|<>'} // relevance booster ] }; } },{name:"clojure-repl",create:/* Language: Clojure REPL Description: Clojure REPL sessions Author: Ivan Sagalaev Requires: clojure.js Category: lisp */ function(hljs) { return { contains: [ { className: 'meta', begin: /^([\w.-]+|\s*#_)?=>/, starts: { end: /$/, subLanguage: 'clojure' } } ] } } },{name:"clojure",create:/* Language: Clojure Description: Clojure syntax (based on lisp.js) Author: mfornos Contributors: Martin Clausen Category: lisp */ function(hljs) { var keywords = { 'builtin-name': // Clojure keywords 'def defonce cond apply if-not if-let if not not= = < > <= >= == + / * - rem '+ 'quot neg? pos? delay? symbol? keyword? true? false? integer? empty? coll? list? '+ 'set? ifn? fn? associative? sequential? sorted? counted? reversible? number? decimal? '+ 'class? distinct? isa? float? rational? reduced? ratio? odd? even? char? seq? vector? '+ 'string? map? nil? contains? zero? instance? not-every? not-any? libspec? -> ->> .. . '+ 'inc compare do dotimes mapcat take remove take-while drop letfn drop-last take-last '+ 'drop-while while intern condp case reduced cycle split-at split-with repeat replicate '+ 'iterate range merge zipmap declare line-seq sort comparator sort-by dorun doall nthnext '+ 'nthrest partition eval doseq await await-for let agent atom send send-off release-pending-sends '+ 'add-watch mapv filterv remove-watch agent-error restart-agent set-error-handler error-handler '+ 'set-error-mode! error-mode shutdown-agents quote var fn loop recur throw try monitor-enter '+ 'monitor-exit defmacro defn defn- macroexpand macroexpand-1 for dosync and or '+ 'when when-not when-let comp juxt partial sequence memoize constantly complement identity assert '+ 'peek pop doto proxy defstruct first rest cons defprotocol cast coll deftype defrecord last butlast '+ 'sigs reify second ffirst fnext nfirst nnext defmulti defmethod meta with-meta ns in-ns create-ns import '+ 'refer keys select-keys vals key val rseq name namespace promise into transient persistent! conj! '+ 'assoc! dissoc! pop! disj! use class type num float double short byte boolean bigint biginteger '+ 'bigdec print-method print-dup throw-if printf format load compile get-in update-in pr pr-on newline '+ 'flush read slurp read-line subvec with-open memfn time re-find re-groups rand-int rand mod locking '+ 'assert-valid-fdecl alias resolve ref deref refset swap! reset! set-validator! compare-and-set! alter-meta! '+ 'reset-meta! commute get-validator alter ref-set ref-history-count ref-min-history ref-max-history ensure sync io! '+ 'new next conj set! to-array future future-call into-array aset gen-class reduce map filter find empty '+ 'hash-map hash-set sorted-map sorted-map-by sorted-set sorted-set-by vec vector seq flatten reverse assoc dissoc list '+ 'disj get union difference intersection extend extend-type extend-protocol int nth delay count concat chunk chunk-buffer '+ 'chunk-append chunk-first chunk-rest max min dec unchecked-inc-int unchecked-inc unchecked-dec-inc unchecked-dec unchecked-negate '+ 'unchecked-add-int unchecked-add unchecked-subtract-int unchecked-subtract chunk-next chunk-cons chunked-seq? prn vary-meta '+ 'lazy-seq spread list* str find-keyword keyword symbol gensym force rationalize' }; var SYMBOLSTART = 'a-zA-Z_\\-!.?+*=<>&#\''; var SYMBOL_RE = '[' + SYMBOLSTART + '][' + SYMBOLSTART + '0-9/;:]*'; var SIMPLE_NUMBER_RE = '[-+]?\\d+(\\.\\d+)?'; var SYMBOL = { begin: SYMBOL_RE, relevance: 0 }; var NUMBER = { className: 'number', begin: SIMPLE_NUMBER_RE, relevance: 0 }; var STRING = hljs.inherit(hljs.QUOTE_STRING_MODE, {illegal: null}); var COMMENT = hljs.COMMENT( ';', '$', { relevance: 0 } ); var LITERAL = { className: 'literal', begin: /\b(true|false|nil)\b/ }; var COLLECTION = { begin: '[\\[\\{]', end: '[\\]\\}]' }; var HINT = { className: 'comment', begin: '\\^' + SYMBOL_RE }; var HINT_COL = hljs.COMMENT('\\^\\{', '\\}'); var KEY = { className: 'symbol', begin: '[:]{1,2}' + SYMBOL_RE }; var LIST = { begin: '\\(', end: '\\)' }; var BODY = { endsWithParent: true, relevance: 0 }; var NAME = { keywords: keywords, lexemes: SYMBOL_RE, className: 'name', begin: SYMBOL_RE, starts: BODY }; var DEFAULT_CONTAINS = [LIST, STRING, HINT, HINT_COL, COMMENT, KEY, COLLECTION, NUMBER, LITERAL, SYMBOL]; LIST.contains = [hljs.COMMENT('comment', ''), NAME, BODY]; BODY.contains = DEFAULT_CONTAINS; COLLECTION.contains = DEFAULT_CONTAINS; HINT_COL.contains = [COLLECTION]; return { aliases: ['clj'], illegal: /\S/, contains: [LIST, STRING, HINT, HINT_COL, COMMENT, KEY, COLLECTION, NUMBER, LITERAL] } } },{name:"cmake",create:/* Language: CMake Description: CMake is an open-source cross-platform system for build automation. Author: Igor Kalnitsky Website: http://kalnitsky.org/ */ function(hljs) { return { aliases: ['cmake.in'], case_insensitive: true, keywords: { keyword: // scripting commands 'break cmake_host_system_information cmake_minimum_required cmake_parse_arguments ' + 'cmake_policy configure_file continue elseif else endforeach endfunction endif endmacro ' + 'endwhile execute_process file find_file find_library find_package find_path ' + 'find_program foreach function get_cmake_property get_directory_property ' + 'get_filename_component get_property if include include_guard list macro ' + 'mark_as_advanced math message option return separate_arguments ' + 'set_directory_properties set_property set site_name string unset variable_watch while ' + // project commands 'add_compile_definitions add_compile_options add_custom_command add_custom_target ' + 'add_definitions add_dependencies add_executable add_library add_link_options ' + 'add_subdirectory add_test aux_source_directory build_command create_test_sourcelist ' + 'define_property enable_language enable_testing export fltk_wrap_ui ' + 'get_source_file_property get_target_property get_test_property include_directories ' + 'include_external_msproject include_regular_expression install link_directories ' + 'link_libraries load_cache project qt_wrap_cpp qt_wrap_ui remove_definitions ' + 'set_source_files_properties set_target_properties set_tests_properties source_group ' + 'target_compile_definitions target_compile_features target_compile_options ' + 'target_include_directories target_link_directories target_link_libraries ' + 'target_link_options target_sources try_compile try_run ' + // CTest commands 'ctest_build ctest_configure ctest_coverage ctest_empty_binary_directory ctest_memcheck ' + 'ctest_read_custom_files ctest_run_script ctest_sleep ctest_start ctest_submit ' + 'ctest_test ctest_update ctest_upload ' + // deprecated commands 'build_name exec_program export_library_dependencies install_files install_programs ' + 'install_targets load_command make_directory output_required_files remove ' + 'subdir_depends subdirs use_mangled_mesa utility_source variable_requires write_file ' + 'qt5_use_modules qt5_use_package qt5_wrap_cpp ' + // core keywords 'on off true false and or not command policy target test exists is_newer_than ' + 'is_directory is_symlink is_absolute matches less greater equal less_equal ' + 'greater_equal strless strgreater strequal strless_equal strgreater_equal version_less ' + 'version_greater version_equal version_less_equal version_greater_equal in_list defined' }, contains: [ { className: 'variable', begin: '\\${', end: '}' }, hljs.HASH_COMMENT_MODE, hljs.QUOTE_STRING_MODE, hljs.NUMBER_MODE ] }; } },{name:"coffeescript",create:/* Language: CoffeeScript Author: Dmytrii Nagirniak Contributors: Oleg Efimov , Cédric Néhémie Description: CoffeeScript is a programming language that transcompiles to JavaScript. For info about language see http://coffeescript.org/ Category: common, scripting */ function(hljs) { var KEYWORDS = { keyword: // JS keywords 'in if for while finally new do return else break catch instanceof throw try this ' + 'switch continue typeof delete debugger super yield import export from as default await ' + // Coffee keywords 'then unless until loop of by when and or is isnt not', literal: // JS literals 'true false null undefined ' + // Coffee literals 'yes no on off', built_in: 'npm require console print module global window document' }; var JS_IDENT_RE = '[A-Za-z$_][0-9A-Za-z$_]*'; var SUBST = { className: 'subst', begin: /#\{/, end: /}/, keywords: KEYWORDS }; var EXPRESSIONS = [ hljs.BINARY_NUMBER_MODE, hljs.inherit(hljs.C_NUMBER_MODE, {starts: {end: '(\\s*/)?', relevance: 0}}), // a number tries to eat the following slash to prevent treating it as a regexp { className: 'string', variants: [ { begin: /'''/, end: /'''/, contains: [hljs.BACKSLASH_ESCAPE] }, { begin: /'/, end: /'/, contains: [hljs.BACKSLASH_ESCAPE] }, { begin: /"""/, end: /"""/, contains: [hljs.BACKSLASH_ESCAPE, SUBST] }, { begin: /"/, end: /"/, contains: [hljs.BACKSLASH_ESCAPE, SUBST] } ] }, { className: 'regexp', variants: [ { begin: '///', end: '///', contains: [SUBST, hljs.HASH_COMMENT_MODE] }, { begin: '//[gim]*', relevance: 0 }, { // regex can't start with space to parse x / 2 / 3 as two divisions // regex can't start with *, and it supports an "illegal" in the main mode begin: /\/(?![ *])(\\\/|.)*?\/[gim]*(?=\W|$)/ } ] }, { begin: '@' + JS_IDENT_RE // relevance booster }, { subLanguage: 'javascript', excludeBegin: true, excludeEnd: true, variants: [ { begin: '```', end: '```', }, { begin: '`', end: '`', } ] } ]; SUBST.contains = EXPRESSIONS; var TITLE = hljs.inherit(hljs.TITLE_MODE, {begin: JS_IDENT_RE}); var PARAMS_RE = '(\\(.*\\))?\\s*\\B[-=]>'; var PARAMS = { className: 'params', begin: '\\([^\\(]', returnBegin: true, /* We need another contained nameless mode to not have every nested pair of parens to be called "params" */ contains: [{ begin: /\(/, end: /\)/, keywords: KEYWORDS, contains: ['self'].concat(EXPRESSIONS) }] }; return { aliases: ['coffee', 'cson', 'iced'], keywords: KEYWORDS, illegal: /\/\*/, contains: EXPRESSIONS.concat([ hljs.COMMENT('###', '###'), hljs.HASH_COMMENT_MODE, { className: 'function', begin: '^\\s*' + JS_IDENT_RE + '\\s*=\\s*' + PARAMS_RE, end: '[-=]>', returnBegin: true, contains: [TITLE, PARAMS] }, { // anonymous function start begin: /[:\(,=]\s*/, relevance: 0, contains: [ { className: 'function', begin: PARAMS_RE, end: '[-=]>', returnBegin: true, contains: [PARAMS] } ] }, { className: 'class', beginKeywords: 'class', end: '$', illegal: /[:="\[\]]/, contains: [ { beginKeywords: 'extends', endsWithParent: true, illegal: /[:="\[\]]/, contains: [TITLE] }, TITLE ] }, { begin: JS_IDENT_RE + ':', end: ':', returnBegin: true, returnEnd: true, relevance: 0 } ]) }; } },{name:"coq",create:/* Language: Coq Author: Stephan Boyer Category: functional */ function(hljs) { return { keywords: { keyword: '_ as at cofix else end exists exists2 fix for forall fun if IF in let ' + 'match mod Prop return Set then Type using where with ' + 'Abort About Add Admit Admitted All Arguments Assumptions Axiom Back BackTo ' + 'Backtrack Bind Blacklist Canonical Cd Check Class Classes Close Coercion ' + 'Coercions CoFixpoint CoInductive Collection Combined Compute Conjecture ' + 'Conjectures Constant constr Constraint Constructors Context Corollary ' + 'CreateHintDb Cut Declare Defined Definition Delimit Dependencies Dependent' + 'Derive Drop eauto End Equality Eval Example Existential Existentials ' + 'Existing Export exporting Extern Extract Extraction Fact Field Fields File ' + 'Fixpoint Focus for From Function Functional Generalizable Global Goal Grab ' + 'Grammar Graph Guarded Heap Hint HintDb Hints Hypotheses Hypothesis ident ' + 'Identity If Immediate Implicit Import Include Inductive Infix Info Initial ' + 'Inline Inspect Instance Instances Intro Intros Inversion Inversion_clear ' + 'Language Left Lemma Let Libraries Library Load LoadPath Local Locate Ltac ML ' + 'Mode Module Modules Monomorphic Morphism Next NoInline Notation Obligation ' + 'Obligations Opaque Open Optimize Options Parameter Parameters Parametric ' + 'Path Paths pattern Polymorphic Preterm Print Printing Program Projections ' + 'Proof Proposition Pwd Qed Quit Rec Record Recursive Redirect Relation Remark ' + 'Remove Require Reserved Reset Resolve Restart Rewrite Right Ring Rings Save ' + 'Scheme Scope Scopes Script Search SearchAbout SearchHead SearchPattern ' + 'SearchRewrite Section Separate Set Setoid Show Solve Sorted Step Strategies ' + 'Strategy Structure SubClass Table Tables Tactic Term Test Theorem Time ' + 'Timeout Transparent Type Typeclasses Types Undelimit Undo Unfocus Unfocused ' + 'Unfold Universe Universes Unset Unshelve using Variable Variables Variant ' + 'Verbose Visibility where with', built_in: 'abstract absurd admit after apply as assert assumption at auto autorewrite ' + 'autounfold before bottom btauto by case case_eq cbn cbv change ' + 'classical_left classical_right clear clearbody cofix compare compute ' + 'congruence constr_eq constructor contradict contradiction cut cutrewrite ' + 'cycle decide decompose dependent destruct destruction dintuition ' + 'discriminate discrR do double dtauto eapply eassumption eauto ecase ' + 'econstructor edestruct ediscriminate eelim eexact eexists einduction ' + 'einjection eleft elim elimtype enough equality erewrite eright ' + 'esimplify_eq esplit evar exact exactly_once exfalso exists f_equal fail ' + 'field field_simplify field_simplify_eq first firstorder fix fold fourier ' + 'functional generalize generalizing gfail give_up has_evar hnf idtac in ' + 'induction injection instantiate intro intro_pattern intros intuition ' + 'inversion inversion_clear is_evar is_var lapply lazy left lia lra move ' + 'native_compute nia nsatz omega once pattern pose progress proof psatz quote ' + 'record red refine reflexivity remember rename repeat replace revert ' + 'revgoals rewrite rewrite_strat right ring ring_simplify rtauto set ' + 'setoid_reflexivity setoid_replace setoid_rewrite setoid_symmetry ' + 'setoid_transitivity shelve shelve_unifiable simpl simple simplify_eq solve ' + 'specialize split split_Rabs split_Rmult stepl stepr subst sum swap ' + 'symmetry tactic tauto time timeout top transitivity trivial try tryif ' + 'unfold unify until using vm_compute with' }, contains: [ hljs.QUOTE_STRING_MODE, hljs.COMMENT('\\(\\*', '\\*\\)'), hljs.C_NUMBER_MODE, { className: 'type', excludeBegin: true, begin: '\\|\\s*', end: '\\w+' }, {begin: /[-=]>/} // relevance booster ] }; } },{name:"cos",create:/* Language: Caché Object Script Author: Nikita Savchenko Category: enterprise, scripting */ function cos (hljs) { var STRINGS = { className: 'string', variants: [ { begin: '"', end: '"', contains: [{ // escaped begin: "\"\"", relevance: 0 }] } ] }; var NUMBERS = { className: "number", begin: "\\b(\\d+(\\.\\d*)?|\\.\\d+)", relevance: 0 }; var COS_KEYWORDS = 'property parameter class classmethod clientmethod extends as break ' + 'catch close continue do d|0 else elseif for goto halt hang h|0 if job ' + 'j|0 kill k|0 lock l|0 merge new open quit q|0 read r|0 return set s|0 ' + 'tcommit throw trollback try tstart use view while write w|0 xecute x|0 ' + 'zkill znspace zn ztrap zwrite zw zzdump zzwrite print zbreak zinsert ' + 'zload zprint zremove zsave zzprint mv mvcall mvcrt mvdim mvprint zquit ' + 'zsync ascii'; // registered function - no need in them due to all functions are highlighted, // but I'll just leave this here. //"$bit", "$bitcount", //"$bitfind", "$bitlogic", "$case", "$char", "$classmethod", "$classname", //"$compile", "$data", "$decimal", "$double", "$extract", "$factor", //"$find", "$fnumber", "$get", "$increment", "$inumber", "$isobject", //"$isvaliddouble", "$isvalidnum", "$justify", "$length", "$list", //"$listbuild", "$listdata", "$listfind", "$listfromstring", "$listget", //"$listlength", "$listnext", "$listsame", "$listtostring", "$listvalid", //"$locate", "$match", "$method", "$name", "$nconvert", "$next", //"$normalize", "$now", "$number", "$order", "$parameter", "$piece", //"$prefetchoff", "$prefetchon", "$property", "$qlength", "$qsubscript", //"$query", "$random", "$replace", "$reverse", "$sconvert", "$select", //"$sortbegin", "$sortend", "$stack", "$text", "$translate", "$view", //"$wascii", "$wchar", "$wextract", "$wfind", "$wiswide", "$wlength", //"$wreverse", "$xecute", "$zabs", "$zarccos", "$zarcsin", "$zarctan", //"$zcos", "$zcot", "$zcsc", "$zdate", "$zdateh", "$zdatetime", //"$zdatetimeh", "$zexp", "$zhex", "$zln", "$zlog", "$zpower", "$zsec", //"$zsin", "$zsqr", "$ztan", "$ztime", "$ztimeh", "$zboolean", //"$zconvert", "$zcrc", "$zcyc", "$zdascii", "$zdchar", "$zf", //"$ziswide", "$zlascii", "$zlchar", "$zname", "$zposition", "$zqascii", //"$zqchar", "$zsearch", "$zseek", "$zstrip", "$zwascii", "$zwchar", //"$zwidth", "$zwpack", "$zwbpack", "$zwunpack", "$zwbunpack", "$zzenkaku", //"$change", "$mv", "$mvat", "$mvfmt", "$mvfmts", "$mviconv", //"$mviconvs", "$mvinmat", "$mvlover", "$mvoconv", "$mvoconvs", "$mvraise", //"$mvtrans", "$mvv", "$mvname", "$zbitand", "$zbitcount", "$zbitfind", //"$zbitget", "$zbitlen", "$zbitnot", "$zbitor", "$zbitset", "$zbitstr", //"$zbitxor", "$zincrement", "$znext", "$zorder", "$zprevious", "$zsort", //"device", "$ecode", "$estack", "$etrap", "$halt", "$horolog", //"$io", "$job", "$key", "$namespace", "$principal", "$quit", "$roles", //"$storage", "$system", "$test", "$this", "$tlevel", "$username", //"$x", "$y", "$za", "$zb", "$zchild", "$zeof", "$zeos", "$zerror", //"$zhorolog", "$zio", "$zjob", "$zmode", "$znspace", "$zparent", "$zpi", //"$zpos", "$zreference", "$zstorage", "$ztimestamp", "$ztimezone", //"$ztrap", "$zversion" return { case_insensitive: true, aliases: ["cos", "cls"], keywords: COS_KEYWORDS, contains: [ NUMBERS, STRINGS, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, { className: "comment", begin: /;/, end: "$", relevance: 0 }, { // Functions and user-defined functions: write $ztime(60*60*3), $$myFunc(10), $$^Val(1) className: "built_in", begin: /(?:\$\$?|\.\.)\^?[a-zA-Z]+/ }, { // Macro command: quit $$$OK className: "built_in", begin: /\$\$\$[a-zA-Z]+/ }, { // Special (global) variables: write %request.Content; Built-in classes: %Library.Integer className: "built_in", begin: /%[a-z]+(?:\.[a-z]+)*/ }, { // Global variable: set ^globalName = 12 write ^globalName className: "symbol", begin: /\^%?[a-zA-Z][\w]*/ }, { // Some control constructions: do ##class(Package.ClassName).Method(), ##super() className: "keyword", begin: /##class|##super|#define|#dim/ }, // sub-languages: are not fully supported by hljs by 11/15/2015 // left for the future implementation. { begin: /&sql\(/, end: /\)/, excludeBegin: true, excludeEnd: true, subLanguage: "sql" }, { begin: /&(js|jscript|javascript)/, excludeBegin: true, excludeEnd: true, subLanguage: "javascript" }, { // this brakes first and last tag, but this is the only way to embed a valid html begin: /&html<\s*\s*>/, subLanguage: "xml" } ] }; } },{name:"crmsh",create:/* Language: crmsh Author: Kristoffer Gronlund Website: http://crmsh.github.io Description: Syntax Highlighting for the crmsh DSL Category: config */ function(hljs) { var RESOURCES = 'primitive rsc_template'; var COMMANDS = 'group clone ms master location colocation order fencing_topology ' + 'rsc_ticket acl_target acl_group user role ' + 'tag xml'; var PROPERTY_SETS = 'property rsc_defaults op_defaults'; var KEYWORDS = 'params meta operations op rule attributes utilization'; var OPERATORS = 'read write deny defined not_defined in_range date spec in ' + 'ref reference attribute type xpath version and or lt gt tag ' + 'lte gte eq ne \\'; var TYPES = 'number string'; var LITERALS = 'Master Started Slave Stopped start promote demote stop monitor true false'; return { aliases: ['crm', 'pcmk'], case_insensitive: true, keywords: { keyword: KEYWORDS + ' ' + OPERATORS + ' ' + TYPES, literal: LITERALS }, contains: [ hljs.HASH_COMMENT_MODE, { beginKeywords: 'node', starts: { end: '\\s*([\\w_-]+:)?', starts: { className: 'title', end: '\\s*[\\$\\w_][\\w_-]*' } } }, { beginKeywords: RESOURCES, starts: { className: 'title', end: '\\s*[\\$\\w_][\\w_-]*', starts: { end: '\\s*@?[\\w_][\\w_\\.:-]*' } } }, { begin: '\\b(' + COMMANDS.split(' ').join('|') + ')\\s+', keywords: COMMANDS, starts: { className: 'title', end: '[\\$\\w_][\\w_-]*' } }, { beginKeywords: PROPERTY_SETS, starts: { className: 'title', end: '\\s*([\\w_-]+:)?' } }, hljs.QUOTE_STRING_MODE, { className: 'meta', begin: '(ocf|systemd|service|lsb):[\\w_:-]+', relevance: 0 }, { className: 'number', begin: '\\b\\d+(\\.\\d+)?(ms|s|h|m)?', relevance: 0 }, { className: 'literal', begin: '[-]?(infinity|inf)', relevance: 0 }, { className: 'attr', begin: /([A-Za-z\$_\#][\w_-]+)=/, relevance: 0 }, { className: 'tag', begin: '', relevance: 0 } ] }; } },{name:"crystal",create:/* Language: Crystal Author: TSUYUSATO Kitsune */ function(hljs) { var INT_SUFFIX = '(_*[ui](8|16|32|64|128))?'; var FLOAT_SUFFIX = '(_*f(32|64))?'; var CRYSTAL_IDENT_RE = '[a-zA-Z_]\\w*[!?=]?'; var CRYSTAL_METHOD_RE = '[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~|]|//|//=|&[-+*]=?|&\\*\\*|\\[\\][=?]?'; var CRYSTAL_PATH_RE = '[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?'; var CRYSTAL_KEYWORDS = { keyword: 'abstract alias annotation as as? asm begin break case class def do else elsif end ensure enum extend for fun if ' + 'include instance_sizeof is_a? lib macro module next nil? of out pointerof private protected rescue responds_to? ' + 'return require select self sizeof struct super then type typeof union uninitialized unless until verbatim when while with yield ' + '__DIR__ __END_LINE__ __FILE__ __LINE__', literal: 'false nil true' }; var SUBST = { className: 'subst', begin: '#{', end: '}', keywords: CRYSTAL_KEYWORDS }; var EXPANSION = { className: 'template-variable', variants: [ {begin: '\\{\\{', end: '\\}\\}'}, {begin: '\\{%', end: '%\\}'} ], keywords: CRYSTAL_KEYWORDS }; function recursiveParen(begin, end) { var contains = [{begin: begin, end: end}]; contains[0].contains = contains; return contains; } var STRING = { className: 'string', contains: [hljs.BACKSLASH_ESCAPE, SUBST], variants: [ {begin: /'/, end: /'/}, {begin: /"/, end: /"/}, {begin: /`/, end: /`/}, {begin: '%[Qwi]?\\(', end: '\\)', contains: recursiveParen('\\(', '\\)')}, {begin: '%[Qwi]?\\[', end: '\\]', contains: recursiveParen('\\[', '\\]')}, {begin: '%[Qwi]?{', end: '}', contains: recursiveParen('{', '}')}, {begin: '%[Qwi]?<', end: '>', contains: recursiveParen('<', '>')}, {begin: '%[Qwi]?\\|', end: '\\|'}, {begin: /<<-\w+$/, end: /^\s*\w+$/}, ], relevance: 0, }; var Q_STRING = { className: 'string', variants: [ {begin: '%q\\(', end: '\\)', contains: recursiveParen('\\(', '\\)')}, {begin: '%q\\[', end: '\\]', contains: recursiveParen('\\[', '\\]')}, {begin: '%q{', end: '}', contains: recursiveParen('{', '}')}, {begin: '%q<', end: '>', contains: recursiveParen('<', '>')}, {begin: '%q\\|', end: '\\|'}, {begin: /<<-'\w+'$/, end: /^\s*\w+$/}, ], relevance: 0, }; var REGEXP = { begin: '(?!%})(' + hljs.RE_STARTERS_RE + '|\\n|\\b(case|if|select|unless|until|when|while)\\b)\\s*', keywords: 'case if select unless until when while', contains: [ { className: 'regexp', contains: [hljs.BACKSLASH_ESCAPE, SUBST], variants: [ {begin: '//[a-z]*', relevance: 0}, {begin: '/(?!\\/)', end: '/[a-z]*'}, ] } ], relevance: 0 }; var REGEXP2 = { className: 'regexp', contains: [hljs.BACKSLASH_ESCAPE, SUBST], variants: [ {begin: '%r\\(', end: '\\)', contains: recursiveParen('\\(', '\\)')}, {begin: '%r\\[', end: '\\]', contains: recursiveParen('\\[', '\\]')}, {begin: '%r{', end: '}', contains: recursiveParen('{', '}')}, {begin: '%r<', end: '>', contains: recursiveParen('<', '>')}, {begin: '%r\\|', end: '\\|'}, ], relevance: 0 }; var ATTRIBUTE = { className: 'meta', begin: '@\\[', end: '\\]', contains: [ hljs.inherit(hljs.QUOTE_STRING_MODE, {className: 'meta-string'}) ] }; var CRYSTAL_DEFAULT_CONTAINS = [ EXPANSION, STRING, Q_STRING, REGEXP2, REGEXP, ATTRIBUTE, hljs.HASH_COMMENT_MODE, { className: 'class', beginKeywords: 'class module struct', end: '$|;', illegal: /=/, contains: [ hljs.HASH_COMMENT_MODE, hljs.inherit(hljs.TITLE_MODE, {begin: CRYSTAL_PATH_RE}), {begin: '<'} // relevance booster for inheritance ] }, { className: 'class', beginKeywords: 'lib enum union', end: '$|;', illegal: /=/, contains: [ hljs.HASH_COMMENT_MODE, hljs.inherit(hljs.TITLE_MODE, {begin: CRYSTAL_PATH_RE}), ], relevance: 10 }, { beginKeywords: 'annotation', end: '$|;', illegal: /=/, contains: [ hljs.HASH_COMMENT_MODE, hljs.inherit(hljs.TITLE_MODE, {begin: CRYSTAL_PATH_RE}), ], relevance: 10 }, { className: 'function', beginKeywords: 'def', end: /\B\b/, contains: [ hljs.inherit(hljs.TITLE_MODE, { begin: CRYSTAL_METHOD_RE, endsParent: true }) ] }, { className: 'function', beginKeywords: 'fun macro', end: /\B\b/, contains: [ hljs.inherit(hljs.TITLE_MODE, { begin: CRYSTAL_METHOD_RE, endsParent: true }) ], relevance: 5 }, { className: 'symbol', begin: hljs.UNDERSCORE_IDENT_RE + '(\\!|\\?)?:', relevance: 0 }, { className: 'symbol', begin: ':', contains: [STRING, {begin: CRYSTAL_METHOD_RE}], relevance: 0 }, { className: 'number', variants: [ { begin: '\\b0b([01_]+)' + INT_SUFFIX }, { begin: '\\b0o([0-7_]+)' + INT_SUFFIX }, { begin: '\\b0x([A-Fa-f0-9_]+)' + INT_SUFFIX }, { begin: '\\b([1-9][0-9_]*[0-9]|[0-9])(\\.[0-9][0-9_]*)?([eE]_*[-+]?[0-9_]*)?' + FLOAT_SUFFIX + '(?!_)' }, { begin: '\\b([1-9][0-9_]*|0)' + INT_SUFFIX } ], relevance: 0 } ]; SUBST.contains = CRYSTAL_DEFAULT_CONTAINS; EXPANSION.contains = CRYSTAL_DEFAULT_CONTAINS.slice(1); // without EXPANSION return { aliases: ['cr'], lexemes: CRYSTAL_IDENT_RE, keywords: CRYSTAL_KEYWORDS, contains: CRYSTAL_DEFAULT_CONTAINS }; } },{name:"cs",create:/* Language: C# Author: Jason Diamond Contributor: Nicolas LLOBERA , Pieter Vantorre Category: common */ function(hljs) { var KEYWORDS = { keyword: // Normal keywords. 'abstract as base bool break byte case catch char checked const continue decimal ' + 'default delegate do double enum event explicit extern finally fixed float ' + 'for foreach goto if implicit in int interface internal is lock long nameof ' + 'object operator out override params private protected public readonly ref sbyte ' + 'sealed short sizeof stackalloc static string struct switch this try typeof ' + 'uint ulong unchecked unsafe ushort using virtual void volatile while ' + // Contextual keywords. 'add alias ascending async await by descending dynamic equals from get global group into join ' + 'let on orderby partial remove select set value var where yield', literal: 'null false true' }; var NUMBERS = { className: 'number', variants: [ { begin: '\\b(0b[01\']+)' }, { begin: '(-?)\\b([\\d\']+(\\.[\\d\']*)?|\\.[\\d\']+)(u|U|l|L|ul|UL|f|F|b|B)' }, { begin: '(-?)(\\b0[xX][a-fA-F0-9\']+|(\\b[\\d\']+(\\.[\\d\']*)?|\\.[\\d\']+)([eE][-+]?[\\d\']+)?)' } ], relevance: 0 }; var VERBATIM_STRING = { className: 'string', begin: '@"', end: '"', contains: [{begin: '""'}] }; var VERBATIM_STRING_NO_LF = hljs.inherit(VERBATIM_STRING, {illegal: /\n/}); var SUBST = { className: 'subst', begin: '{', end: '}', keywords: KEYWORDS }; var SUBST_NO_LF = hljs.inherit(SUBST, {illegal: /\n/}); var INTERPOLATED_STRING = { className: 'string', begin: /\$"/, end: '"', illegal: /\n/, contains: [{begin: '{{'}, {begin: '}}'}, hljs.BACKSLASH_ESCAPE, SUBST_NO_LF] }; var INTERPOLATED_VERBATIM_STRING = { className: 'string', begin: /\$@"/, end: '"', contains: [{begin: '{{'}, {begin: '}}'}, {begin: '""'}, SUBST] }; var INTERPOLATED_VERBATIM_STRING_NO_LF = hljs.inherit(INTERPOLATED_VERBATIM_STRING, { illegal: /\n/, contains: [{begin: '{{'}, {begin: '}}'}, {begin: '""'}, SUBST_NO_LF] }); SUBST.contains = [ INTERPOLATED_VERBATIM_STRING, INTERPOLATED_STRING, VERBATIM_STRING, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, NUMBERS, hljs.C_BLOCK_COMMENT_MODE ]; SUBST_NO_LF.contains = [ INTERPOLATED_VERBATIM_STRING_NO_LF, INTERPOLATED_STRING, VERBATIM_STRING_NO_LF, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, NUMBERS, hljs.inherit(hljs.C_BLOCK_COMMENT_MODE, {illegal: /\n/}) ]; var STRING = { variants: [ INTERPOLATED_VERBATIM_STRING, INTERPOLATED_STRING, VERBATIM_STRING, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE ] }; var TYPE_IDENT_RE = hljs.IDENT_RE + '(<' + hljs.IDENT_RE + '(\\s*,\\s*' + hljs.IDENT_RE + ')*>)?(\\[\\])?'; return { aliases: ['csharp', 'c#'], keywords: KEYWORDS, illegal: /::/, contains: [ hljs.COMMENT( '///', '$', { returnBegin: true, contains: [ { className: 'doctag', variants: [ { begin: '///', relevance: 0 }, { begin: '' }, { begin: '' } ] } ] } ), hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, { className: 'meta', begin: '#', end: '$', keywords: { 'meta-keyword': 'if else elif endif define undef warning error line region endregion pragma checksum' } }, STRING, NUMBERS, { beginKeywords: 'class interface', end: /[{;=]/, illegal: /[^\s:,]/, contains: [ hljs.TITLE_MODE, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE ] }, { beginKeywords: 'namespace', end: /[{;=]/, illegal: /[^\s:]/, contains: [ hljs.inherit(hljs.TITLE_MODE, {begin: '[a-zA-Z](\\.?\\w)*'}), hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE ] }, { // [Attributes("")] className: 'meta', begin: '^\\s*\\[', excludeBegin: true, end: '\\]', excludeEnd: true, contains: [ {className: 'meta-string', begin: /"/, end: /"/} ] }, { // Expression keywords prevent 'keyword Name(...)' from being // recognized as a function definition beginKeywords: 'new return throw await else', relevance: 0 }, { className: 'function', begin: '(' + TYPE_IDENT_RE + '\\s+)+' + hljs.IDENT_RE + '\\s*\\(', returnBegin: true, end: /\s*[{;=]/, excludeEnd: true, keywords: KEYWORDS, contains: [ { begin: hljs.IDENT_RE + '\\s*\\(', returnBegin: true, contains: [hljs.TITLE_MODE], relevance: 0 }, { className: 'params', begin: /\(/, end: /\)/, excludeBegin: true, excludeEnd: true, keywords: KEYWORDS, relevance: 0, contains: [ STRING, NUMBERS, hljs.C_BLOCK_COMMENT_MODE ] }, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE ] } ] }; } },{name:"csp",create:/* Language: CSP Description: Content Security Policy definition highlighting Author: Taras vim: ts=2 sw=2 st=2 */ function(hljs) { return { case_insensitive: false, lexemes: '[a-zA-Z][a-zA-Z0-9_-]*', keywords: { keyword: 'base-uri child-src connect-src default-src font-src form-action' + ' frame-ancestors frame-src img-src media-src object-src plugin-types' + ' report-uri sandbox script-src style-src', }, contains: [ { className: 'string', begin: "'", end: "'" }, { className: 'attribute', begin: '^Content', end: ':', excludeEnd: true, }, ] }; } },{name:"css",create:/* Language: CSS Category: common, css */ function(hljs) { var IDENT_RE = '[a-zA-Z-][a-zA-Z0-9_-]*'; var RULE = { begin: /(?:[A-Z\_\.\-]+|--[a-zA-Z0-9_-]+)\s*:/, returnBegin: true, end: ';', endsWithParent: true, contains: [ { className: 'attribute', begin: /\S/, end: ':', excludeEnd: true, starts: { endsWithParent: true, excludeEnd: true, contains: [ { begin: /[\w-]+\(/, returnBegin: true, contains: [ { className: 'built_in', begin: /[\w-]+/ }, { begin: /\(/, end: /\)/, contains: [ hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE ] } ] }, hljs.CSS_NUMBER_MODE, hljs.QUOTE_STRING_MODE, hljs.APOS_STRING_MODE, hljs.C_BLOCK_COMMENT_MODE, { className: 'number', begin: '#[0-9A-Fa-f]+' }, { className: 'meta', begin: '!important' } ] } } ] }; return { case_insensitive: true, illegal: /[=\/|'\$]/, contains: [ hljs.C_BLOCK_COMMENT_MODE, { className: 'selector-id', begin: /#[A-Za-z0-9_-]+/ }, { className: 'selector-class', begin: /\.[A-Za-z0-9_-]+/ }, { className: 'selector-attr', begin: /\[/, end: /\]/, illegal: '$' }, { className: 'selector-pseudo', begin: /:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/ }, { begin: '@(font-face|page)', lexemes: '[a-z-]+', keywords: 'font-face page' }, { begin: '@', end: '[{;]', // at_rule eating first "{" is a good thing // because it doesn’t let it to be parsed as // a rule set but instead drops parser into // the default mode which is how it should be. illegal: /:/, // break on Less variables @var: ... contains: [ { className: 'keyword', begin: /\w+/ }, { begin: /\s/, endsWithParent: true, excludeEnd: true, relevance: 0, contains: [ hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, hljs.CSS_NUMBER_MODE ] } ] }, { className: 'selector-tag', begin: IDENT_RE, relevance: 0 }, { begin: '{', end: '}', illegal: /\S/, contains: [ hljs.C_BLOCK_COMMENT_MODE, RULE, ] } ] }; } },{name:"d",create:/* Language: D Author: Aleksandar Ruzicic Description: D is a language with C-like syntax and static typing. It pragmatically combines efficiency, control, and modeling power, with safety and programmer productivity. Version: 1.0a Date: 2012-04-08 */ /** * Known issues: * * - invalid hex string literals will be recognized as a double quoted strings * but 'x' at the beginning of string will not be matched * * - delimited string literals are not checked for matching end delimiter * (not possible to do with js regexp) * * - content of token string is colored as a string (i.e. no keyword coloring inside a token string) * also, content of token string is not validated to contain only valid D tokens * * - special token sequence rule is not strictly following D grammar (anything following #line * up to the end of line is matched as special token sequence) */ function(hljs) { /** * Language keywords * * @type {Object} */ var D_KEYWORDS = { keyword: 'abstract alias align asm assert auto body break byte case cast catch class ' + 'const continue debug default delete deprecated do else enum export extern final ' + 'finally for foreach foreach_reverse|10 goto if immutable import in inout int ' + 'interface invariant is lazy macro mixin module new nothrow out override package ' + 'pragma private protected public pure ref return scope shared static struct ' + 'super switch synchronized template this throw try typedef typeid typeof union ' + 'unittest version void volatile while with __FILE__ __LINE__ __gshared|10 ' + '__thread __traits __DATE__ __EOF__ __TIME__ __TIMESTAMP__ __VENDOR__ __VERSION__', built_in: 'bool cdouble cent cfloat char creal dchar delegate double dstring float function ' + 'idouble ifloat ireal long real short string ubyte ucent uint ulong ushort wchar ' + 'wstring', literal: 'false null true' }; /** * Number literal regexps * * @type {String} */ var decimal_integer_re = '(0|[1-9][\\d_]*)', decimal_integer_nosus_re = '(0|[1-9][\\d_]*|\\d[\\d_]*|[\\d_]+?\\d)', binary_integer_re = '0[bB][01_]+', hexadecimal_digits_re = '([\\da-fA-F][\\da-fA-F_]*|_[\\da-fA-F][\\da-fA-F_]*)', hexadecimal_integer_re = '0[xX]' + hexadecimal_digits_re, decimal_exponent_re = '([eE][+-]?' + decimal_integer_nosus_re + ')', decimal_float_re = '(' + decimal_integer_nosus_re + '(\\.\\d*|' + decimal_exponent_re + ')|' + '\\d+\\.' + decimal_integer_nosus_re + decimal_integer_nosus_re + '|' + '\\.' + decimal_integer_re + decimal_exponent_re + '?' + ')', hexadecimal_float_re = '(0[xX](' + hexadecimal_digits_re + '\\.' + hexadecimal_digits_re + '|'+ '\\.?' + hexadecimal_digits_re + ')[pP][+-]?' + decimal_integer_nosus_re + ')', integer_re = '(' + decimal_integer_re + '|' + binary_integer_re + '|' + hexadecimal_integer_re + ')', float_re = '(' + hexadecimal_float_re + '|' + decimal_float_re + ')'; /** * Escape sequence supported in D string and character literals * * @type {String} */ var escape_sequence_re = '\\\\(' + '[\'"\\?\\\\abfnrtv]|' + // common escapes 'u[\\dA-Fa-f]{4}|' + // four hex digit unicode codepoint '[0-7]{1,3}|' + // one to three octal digit ascii char code 'x[\\dA-Fa-f]{2}|' + // two hex digit ascii char code 'U[\\dA-Fa-f]{8}' + // eight hex digit unicode codepoint ')|' + '&[a-zA-Z\\d]{2,};'; // named character entity /** * D integer number literals * * @type {Object} */ var D_INTEGER_MODE = { className: 'number', begin: '\\b' + integer_re + '(L|u|U|Lu|LU|uL|UL)?', relevance: 0 }; /** * [D_FLOAT_MODE description] * @type {Object} */ var D_FLOAT_MODE = { className: 'number', begin: '\\b(' + float_re + '([fF]|L|i|[fF]i|Li)?|' + integer_re + '(i|[fF]i|Li)' + ')', relevance: 0 }; /** * D character literal * * @type {Object} */ var D_CHARACTER_MODE = { className: 'string', begin: '\'(' + escape_sequence_re + '|.)', end: '\'', illegal: '.' }; /** * D string escape sequence * * @type {Object} */ var D_ESCAPE_SEQUENCE = { begin: escape_sequence_re, relevance: 0 }; /** * D double quoted string literal * * @type {Object} */ var D_STRING_MODE = { className: 'string', begin: '"', contains: [D_ESCAPE_SEQUENCE], end: '"[cwd]?' }; /** * D wysiwyg and delimited string literals * * @type {Object} */ var D_WYSIWYG_DELIMITED_STRING_MODE = { className: 'string', begin: '[rq]"', end: '"[cwd]?', relevance: 5 }; /** * D alternate wysiwyg string literal * * @type {Object} */ var D_ALTERNATE_WYSIWYG_STRING_MODE = { className: 'string', begin: '`', end: '`[cwd]?' }; /** * D hexadecimal string literal * * @type {Object} */ var D_HEX_STRING_MODE = { className: 'string', begin: 'x"[\\da-fA-F\\s\\n\\r]*"[cwd]?', relevance: 10 }; /** * D delimited string literal * * @type {Object} */ var D_TOKEN_STRING_MODE = { className: 'string', begin: 'q"\\{', end: '\\}"' }; /** * Hashbang support * * @type {Object} */ var D_HASHBANG_MODE = { className: 'meta', begin: '^#!', end: '$', relevance: 5 }; /** * D special token sequence * * @type {Object} */ var D_SPECIAL_TOKEN_SEQUENCE_MODE = { className: 'meta', begin: '#(line)', end: '$', relevance: 5 }; /** * D attributes * * @type {Object} */ var D_ATTRIBUTE_MODE = { className: 'keyword', begin: '@[a-zA-Z_][a-zA-Z_\\d]*' }; /** * D nesting comment * * @type {Object} */ var D_NESTING_COMMENT_MODE = hljs.COMMENT( '\\/\\+', '\\+\\/', { contains: ['self'], relevance: 10 } ); return { lexemes: hljs.UNDERSCORE_IDENT_RE, keywords: D_KEYWORDS, contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, D_NESTING_COMMENT_MODE, D_HEX_STRING_MODE, D_STRING_MODE, D_WYSIWYG_DELIMITED_STRING_MODE, D_ALTERNATE_WYSIWYG_STRING_MODE, D_TOKEN_STRING_MODE, D_FLOAT_MODE, D_INTEGER_MODE, D_CHARACTER_MODE, D_HASHBANG_MODE, D_SPECIAL_TOKEN_SEQUENCE_MODE, D_ATTRIBUTE_MODE ] }; } },{name:"dart",create:/* Language: Dart Requires: markdown.js Author: Maxim Dikun Description: Dart a modern, object-oriented language developed by Google. For more information see https://www.dartlang.org/ Category: scripting */ function (hljs) { var SUBST = { className: 'subst', variants: [ {begin: '\\$[A-Za-z0-9_]+'} ], }; var BRACED_SUBST = { className: 'subst', variants: [ {begin: '\\${', end: '}'}, ], keywords: 'true false null this is new super', }; var STRING = { className: 'string', variants: [ { begin: 'r\'\'\'', end: '\'\'\'' }, { begin: 'r"""', end: '"""' }, { begin: 'r\'', end: '\'', illegal: '\\n' }, { begin: 'r"', end: '"', illegal: '\\n' }, { begin: '\'\'\'', end: '\'\'\'', contains: [hljs.BACKSLASH_ESCAPE, SUBST, BRACED_SUBST] }, { begin: '"""', end: '"""', contains: [hljs.BACKSLASH_ESCAPE, SUBST, BRACED_SUBST] }, { begin: '\'', end: '\'', illegal: '\\n', contains: [hljs.BACKSLASH_ESCAPE, SUBST, BRACED_SUBST] }, { begin: '"', end: '"', illegal: '\\n', contains: [hljs.BACKSLASH_ESCAPE, SUBST, BRACED_SUBST] } ] }; BRACED_SUBST.contains = [ hljs.C_NUMBER_MODE, STRING ]; var KEYWORDS = { keyword: 'assert async await break case catch class const continue default do else enum extends false final ' + 'finally for if in is new null rethrow return super switch sync this throw true try var void while with yield ' + 'abstract as dynamic export external factory get implements import library operator part set static typedef', built_in: // dart:core 'print Comparable DateTime Duration Function Iterable Iterator List Map Match Null Object Pattern RegExp Set ' + 'Stopwatch String StringBuffer StringSink Symbol Type Uri bool double int num ' + // dart:html 'document window querySelector querySelectorAll Element ElementList' }; return { keywords: KEYWORDS, contains: [ STRING, hljs.COMMENT( '/\\*\\*', '\\*/', { subLanguage: 'markdown' } ), hljs.COMMENT( '///', '$', { subLanguage: 'markdown' } ), hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, { className: 'class', beginKeywords: 'class interface', end: '{', excludeEnd: true, contains: [ { beginKeywords: 'extends implements' }, hljs.UNDERSCORE_TITLE_MODE ] }, hljs.C_NUMBER_MODE, { className: 'meta', begin: '@[A-Za-z]+' }, { begin: '=>' // No markup, just a relevance booster } ] } } },{name:"delphi",create:/* Language: Delphi */ function(hljs) { var KEYWORDS = 'exports register file shl array record property for mod while set ally label uses raise not ' + 'stored class safecall var interface or private static exit index inherited to else stdcall ' + 'override shr asm far resourcestring finalization packed virtual out and protected library do ' + 'xorwrite goto near function end div overload object unit begin string on inline repeat until ' + 'destructor write message program with read initialization except default nil if case cdecl in ' + 'downto threadvar of try pascal const external constructor type public then implementation ' + 'finally published procedure absolute reintroduce operator as is abstract alias assembler ' + 'bitpacked break continue cppdecl cvar enumerator experimental platform deprecated ' + 'unimplemented dynamic export far16 forward generic helper implements interrupt iochecks ' + 'local name nodefault noreturn nostackframe oldfpccall otherwise saveregisters softfloat ' + 'specialize strict unaligned varargs '; var COMMENT_MODES = [ hljs.C_LINE_COMMENT_MODE, hljs.COMMENT(/\{/, /\}/, {relevance: 0}), hljs.COMMENT(/\(\*/, /\*\)/, {relevance: 10}) ]; var DIRECTIVE = { className: 'meta', variants: [ {begin: /\{\$/, end: /\}/}, {begin: /\(\*\$/, end: /\*\)/} ] }; var STRING = { className: 'string', begin: /'/, end: /'/, contains: [{begin: /''/}] }; var CHAR_STRING = { className: 'string', begin: /(#\d+)+/ }; var CLASS = { begin: hljs.IDENT_RE + '\\s*=\\s*class\\s*\\(', returnBegin: true, contains: [ hljs.TITLE_MODE ] }; var FUNCTION = { className: 'function', beginKeywords: 'function constructor destructor procedure', end: /[:;]/, keywords: 'function constructor|10 destructor|10 procedure|10', contains: [ hljs.TITLE_MODE, { className: 'params', begin: /\(/, end: /\)/, keywords: KEYWORDS, contains: [STRING, CHAR_STRING, DIRECTIVE].concat(COMMENT_MODES) }, DIRECTIVE ].concat(COMMENT_MODES) }; return { aliases: ['dpr', 'dfm', 'pas', 'pascal', 'freepascal', 'lazarus', 'lpr', 'lfm'], case_insensitive: true, keywords: KEYWORDS, illegal: /"|\$[G-Zg-z]|\/\*|<\/|\|/, contains: [ STRING, CHAR_STRING, hljs.NUMBER_MODE, CLASS, FUNCTION, DIRECTIVE ].concat(COMMENT_MODES) }; } },{name:"diff",create:/* Language: Diff Description: Unified and context diff Author: Vasily Polovnyov Category: common */ function(hljs) { return { aliases: ['patch'], contains: [ { className: 'meta', relevance: 10, variants: [ {begin: /^@@ +\-\d+,\d+ +\+\d+,\d+ +@@$/}, {begin: /^\*\*\* +\d+,\d+ +\*\*\*\*$/}, {begin: /^\-\-\- +\d+,\d+ +\-\-\-\-$/} ] }, { className: 'comment', variants: [ {begin: /Index: /, end: /$/}, {begin: /={3,}/, end: /$/}, {begin: /^\-{3}/, end: /$/}, {begin: /^\*{3} /, end: /$/}, {begin: /^\+{3}/, end: /$/}, {begin: /\*{5}/, end: /\*{5}$/} ] }, { className: 'addition', begin: '^\\+', end: '$' }, { className: 'deletion', begin: '^\\-', end: '$' }, { className: 'addition', begin: '^\\!', end: '$' } ] }; } },{name:"django",create:/* Language: Django Requires: xml.js Author: Ivan Sagalaev Contributors: Ilya Baryshev Category: template */ function(hljs) { var FILTER = { begin: /\|[A-Za-z]+:?/, keywords: { name: 'truncatewords removetags linebreaksbr yesno get_digit timesince random striptags ' + 'filesizeformat escape linebreaks length_is ljust rjust cut urlize fix_ampersands ' + 'title floatformat capfirst pprint divisibleby add make_list unordered_list urlencode ' + 'timeuntil urlizetrunc wordcount stringformat linenumbers slice date dictsort ' + 'dictsortreversed default_if_none pluralize lower join center default ' + 'truncatewords_html upper length phone2numeric wordwrap time addslashes slugify first ' + 'escapejs force_escape iriencode last safe safeseq truncatechars localize unlocalize ' + 'localtime utc timezone' }, contains: [ hljs.QUOTE_STRING_MODE, hljs.APOS_STRING_MODE ] }; return { aliases: ['jinja'], case_insensitive: true, subLanguage: 'xml', contains: [ hljs.COMMENT(/\{%\s*comment\s*%}/, /\{%\s*endcomment\s*%}/), hljs.COMMENT(/\{#/, /#}/), { className: 'template-tag', begin: /\{%/, end: /%}/, contains: [ { className: 'name', begin: /\w+/, keywords: { name: 'comment endcomment load templatetag ifchanged endifchanged if endif firstof for ' + 'endfor ifnotequal endifnotequal widthratio extends include spaceless ' + 'endspaceless regroup ifequal endifequal ssi now with cycle url filter ' + 'endfilter debug block endblock else autoescape endautoescape csrf_token empty elif ' + 'endwith static trans blocktrans endblocktrans get_static_prefix get_media_prefix ' + 'plural get_current_language language get_available_languages ' + 'get_current_language_bidi get_language_info get_language_info_list localize ' + 'endlocalize localtime endlocaltime timezone endtimezone get_current_timezone ' + 'verbatim' }, starts: { endsWithParent: true, keywords: 'in by as', contains: [FILTER], relevance: 0 } } ] }, { className: 'template-variable', begin: /\{\{/, end: /}}/, contains: [FILTER] } ] }; } },{name:"dns",create:/* Language: DNS Zone file Author: Tim Schumacher Category: config */ function(hljs) { return { aliases: ['bind', 'zone'], keywords: { keyword: 'IN A AAAA AFSDB APL CAA CDNSKEY CDS CERT CNAME DHCID DLV DNAME DNSKEY DS HIP IPSECKEY KEY KX ' + 'LOC MX NAPTR NS NSEC NSEC3 NSEC3PARAM PTR RRSIG RP SIG SOA SRV SSHFP TA TKEY TLSA TSIG TXT' }, contains: [ hljs.COMMENT(';', '$', {relevance: 0}), { className: 'meta', begin: /^\$(TTL|GENERATE|INCLUDE|ORIGIN)\b/ }, // IPv6 { className: 'number', begin: '((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))\\b' }, // IPv4 { className: 'number', begin: '((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\b' }, hljs.inherit(hljs.NUMBER_MODE, {begin: /\b\d+[dhwm]?/}) ] }; } },{name:"dockerfile",create:/* Language: Dockerfile Requires: bash.js Author: Alexis Hénaut Description: language definition for Dockerfile files Category: config */ function(hljs) { return { aliases: ['docker'], case_insensitive: true, keywords: 'from maintainer expose env arg user onbuild stopsignal', contains: [ hljs.HASH_COMMENT_MODE, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, hljs.NUMBER_MODE, { beginKeywords: 'run cmd entrypoint volume add copy workdir label healthcheck shell', starts: { end: /[^\\]$/, subLanguage: 'bash' } } ], illegal: ' Contributors: Anton Kochkov */ function(hljs) { var COMMENT = hljs.COMMENT( /^\s*@?rem\b/, /$/, { relevance: 10 } ); var LABEL = { className: 'symbol', begin: '^\\s*[A-Za-z._?][A-Za-z0-9_$#@~.?]*(:|\\s+label)', relevance: 0 }; return { aliases: ['bat', 'cmd'], case_insensitive: true, illegal: /\/\*/, keywords: { keyword: 'if else goto for in do call exit not exist errorlevel defined ' + 'equ neq lss leq gtr geq', built_in: 'prn nul lpt3 lpt2 lpt1 con com4 com3 com2 com1 aux ' + 'shift cd dir echo setlocal endlocal set pause copy ' + 'append assoc at attrib break cacls cd chcp chdir chkdsk chkntfs cls cmd color ' + 'comp compact convert date dir diskcomp diskcopy doskey erase fs ' + 'find findstr format ftype graftabl help keyb label md mkdir mode more move path ' + 'pause print popd pushd promt rd recover rem rename replace restore rmdir shift' + 'sort start subst time title tree type ver verify vol ' + // winutils 'ping net ipconfig taskkill xcopy ren del' }, contains: [ { className: 'variable', begin: /%%[^ ]|%[^ ]+?%|![^ ]+?!/ }, { className: 'function', begin: LABEL.begin, end: 'goto:eof', contains: [ hljs.inherit(hljs.TITLE_MODE, {begin: '([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*'}), COMMENT ] }, { className: 'number', begin: '\\b\\d+', relevance: 0 }, COMMENT ] }; } },{name:"dsconfig",create:/* Language: dsconfig Description: dsconfig batch configuration language for LDAP directory servers Contributors: Jacob Childress Category: enterprise, config */ function(hljs) { var QUOTED_PROPERTY = { className: 'string', begin: /"/, end: /"/ }; var APOS_PROPERTY = { className: 'string', begin: /'/, end: /'/ }; var UNQUOTED_PROPERTY = { className: 'string', begin: '[\\w-?]+:\\w+', end: '\\W', relevance: 0 }; var VALUELESS_PROPERTY = { className: 'string', begin: '\\w+-?\\w+', end: '\\W', relevance: 0 }; return { keywords: 'dsconfig', contains: [ { className: 'keyword', begin: '^dsconfig', end: '\\s', excludeEnd: true, relevance: 10 }, { className: 'built_in', begin: '(list|create|get|set|delete)-(\\w+)', end: '\\s', excludeEnd: true, illegal: '!@#$%^&*()', relevance: 10 }, { className: 'built_in', begin: '--(\\w+)', end: '\\s', excludeEnd: true }, QUOTED_PROPERTY, APOS_PROPERTY, UNQUOTED_PROPERTY, VALUELESS_PROPERTY, hljs.HASH_COMMENT_MODE ] }; } },{name:"dts",create:/* Language: Device Tree Description: *.dts files used in the Linux kernel Author: Martin Braun , Moritz Fischer Category: config */ function(hljs) { var STRINGS = { className: 'string', variants: [ hljs.inherit(hljs.QUOTE_STRING_MODE, { begin: '((u8?|U)|L)?"' }), { begin: '(u8?|U)?R"', end: '"', contains: [hljs.BACKSLASH_ESCAPE] }, { begin: '\'\\\\?.', end: '\'', illegal: '.' } ] }; var NUMBERS = { className: 'number', variants: [ { begin: '\\b(\\d+(\\.\\d*)?|\\.\\d+)(u|U|l|L|ul|UL|f|F)' }, { begin: hljs.C_NUMBER_RE } ], relevance: 0 }; var PREPROCESSOR = { className: 'meta', begin: '#', end: '$', keywords: {'meta-keyword': 'if else elif endif define undef ifdef ifndef'}, contains: [ { begin: /\\\n/, relevance: 0 }, { beginKeywords: 'include', end: '$', keywords: {'meta-keyword': 'include'}, contains: [ hljs.inherit(STRINGS, {className: 'meta-string'}), { className: 'meta-string', begin: '<', end: '>', illegal: '\\n' } ] }, STRINGS, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE ] }; var DTS_REFERENCE = { className: 'variable', begin: '\\&[a-z\\d_]*\\b' }; var DTS_KEYWORD = { className: 'meta-keyword', begin: '/[a-z][a-z\\d-]*/' }; var DTS_LABEL = { className: 'symbol', begin: '^\\s*[a-zA-Z_][a-zA-Z\\d_]*:' }; var DTS_CELL_PROPERTY = { className: 'params', begin: '<', end: '>', contains: [ NUMBERS, DTS_REFERENCE ] }; var DTS_NODE = { className: 'class', begin: /[a-zA-Z_][a-zA-Z\d_@]*\s{/, end: /[{;=]/, returnBegin: true, excludeEnd: true }; var DTS_ROOT_NODE = { className: 'class', begin: '/\\s*{', end: '};', relevance: 10, contains: [ DTS_REFERENCE, DTS_KEYWORD, DTS_LABEL, DTS_NODE, DTS_CELL_PROPERTY, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, NUMBERS, STRINGS ] }; return { keywords: "", contains: [ DTS_ROOT_NODE, DTS_REFERENCE, DTS_KEYWORD, DTS_LABEL, DTS_NODE, DTS_CELL_PROPERTY, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, NUMBERS, STRINGS, PREPROCESSOR, { begin: hljs.IDENT_RE + '::', keywords: "" } ] }; } },{name:"dust",create:/* Language: Dust Requires: xml.js Author: Michael Allen Description: Matcher for dust.js templates. Category: template */ function(hljs) { var EXPRESSION_KEYWORDS = 'if eq ne lt lte gt gte select default math sep'; return { aliases: ['dst'], case_insensitive: true, subLanguage: 'xml', contains: [ { className: 'template-tag', begin: /\{[#\/]/, end: /\}/, illegal: /;/, contains: [ { className: 'name', begin: /[a-zA-Z\.-]+/, starts: { endsWithParent: true, relevance: 0, contains: [ hljs.QUOTE_STRING_MODE ] } } ] }, { className: 'template-variable', begin: /\{/, end: /\}/, illegal: /;/, keywords: EXPRESSION_KEYWORDS } ] }; } },{name:"ebnf",create:/* Language: Extended Backus-Naur Form Author: Alex McKibben */ function(hljs) { var commentMode = hljs.COMMENT(/\(\*/, /\*\)/); var nonTerminalMode = { className: "attribute", begin: /^[ ]*[a-zA-Z][a-zA-Z-]*([\s-]+[a-zA-Z][a-zA-Z]*)*/ }; var specialSequenceMode = { className: "meta", begin: /\?.*\?/ }; var ruleBodyMode = { begin: /=/, end: /;/, contains: [ commentMode, specialSequenceMode, // terminals hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE ] }; return { illegal: /\S/, contains: [ commentMode, nonTerminalMode, ruleBodyMode ] }; } },{name:"elixir",create:/* Language: Elixir Author: Josh Adams Description: language definition for Elixir source code files (.ex and .exs). Based on ruby language support. Category: functional */ function(hljs) { var ELIXIR_IDENT_RE = '[a-zA-Z_][a-zA-Z0-9_.]*(\\!|\\?)?'; var ELIXIR_METHOD_RE = '[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?'; var ELIXIR_KEYWORDS = 'and false then defined module in return redo retry end for true self when ' + 'next until do begin unless nil break not case cond alias while ensure or ' + 'include use alias fn quote require import with|0'; var SUBST = { className: 'subst', begin: '#\\{', end: '}', lexemes: ELIXIR_IDENT_RE, keywords: ELIXIR_KEYWORDS }; var STRING = { className: 'string', contains: [hljs.BACKSLASH_ESCAPE, SUBST], variants: [ { begin: /'/, end: /'/ }, { begin: /"/, end: /"/ } ] }; var FUNCTION = { className: 'function', beginKeywords: 'def defp defmacro', end: /\B\b/, // the mode is ended by the title contains: [ hljs.inherit(hljs.TITLE_MODE, { begin: ELIXIR_IDENT_RE, endsParent: true }) ] }; var CLASS = hljs.inherit(FUNCTION, { className: 'class', beginKeywords: 'defimpl defmodule defprotocol defrecord', end: /\bdo\b|$|;/ }); var ELIXIR_DEFAULT_CONTAINS = [ STRING, hljs.HASH_COMMENT_MODE, CLASS, FUNCTION, { begin: '::' }, { className: 'symbol', begin: ':(?![\\s:])', contains: [STRING, {begin: ELIXIR_METHOD_RE}], relevance: 0 }, { className: 'symbol', begin: ELIXIR_IDENT_RE + ':(?!:)', relevance: 0 }, { className: 'number', begin: '(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b', relevance: 0 }, { className: 'variable', begin: '(\\$\\W)|((\\$|\\@\\@?)(\\w+))' }, { begin: '->' }, { // regexp container begin: '(' + hljs.RE_STARTERS_RE + ')\\s*', contains: [ hljs.HASH_COMMENT_MODE, { className: 'regexp', illegal: '\\n', contains: [hljs.BACKSLASH_ESCAPE, SUBST], variants: [ { begin: '/', end: '/[a-z]*' }, { begin: '%r\\[', end: '\\][a-z]*' } ] } ], relevance: 0 } ]; SUBST.contains = ELIXIR_DEFAULT_CONTAINS; return { lexemes: ELIXIR_IDENT_RE, keywords: ELIXIR_KEYWORDS, contains: ELIXIR_DEFAULT_CONTAINS }; } },{name:"elm",create:/* Language: Elm Author: Janis Voigtlaender Category: functional */ function(hljs) { var COMMENT = { variants: [ hljs.COMMENT('--', '$'), hljs.COMMENT( '{-', '-}', { contains: ['self'] } ) ] }; var CONSTRUCTOR = { className: 'type', begin: '\\b[A-Z][\\w\']*', // TODO: other constructors (built-in, infix). relevance: 0 }; var LIST = { begin: '\\(', end: '\\)', illegal: '"', contains: [ {className: 'type', begin: '\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?'}, COMMENT ] }; var RECORD = { begin: '{', end: '}', contains: LIST.contains }; var CHARACTER = { className: 'string', begin: '\'\\\\?.', end: '\'', illegal: '.' }; return { keywords: 'let in if then else case of where module import exposing ' + 'type alias as infix infixl infixr port effect command subscription', contains: [ // Top-level constructions. { beginKeywords: 'port effect module', end: 'exposing', keywords: 'port effect module where command subscription exposing', contains: [LIST, COMMENT], illegal: '\\W\\.|;' }, { begin: 'import', end: '$', keywords: 'import as exposing', contains: [LIST, COMMENT], illegal: '\\W\\.|;' }, { begin: 'type', end: '$', keywords: 'type alias', contains: [CONSTRUCTOR, LIST, RECORD, COMMENT] }, { beginKeywords: 'infix infixl infixr', end: '$', contains: [hljs.C_NUMBER_MODE, COMMENT] }, { begin: 'port', end: '$', keywords: 'port', contains: [COMMENT] }, // Literals and names. CHARACTER, hljs.QUOTE_STRING_MODE, hljs.C_NUMBER_MODE, CONSTRUCTOR, hljs.inherit(hljs.TITLE_MODE, {begin: '^[_a-z][\\w\']*'}), COMMENT, {begin: '->|<-'} // No markup, relevance booster ], illegal: /;/ }; } },{name:"erb",create:/* Language: ERB (Embedded Ruby) Requires: xml.js, ruby.js Author: Lucas Mazza Contributors: Kassio Borges Description: "Bridge" language defining fragments of Ruby in HTML within <% .. %> Category: template */ function(hljs) { return { subLanguage: 'xml', contains: [ hljs.COMMENT('<%#', '%>'), { begin: '<%[%=-]?', end: '[%-]?%>', subLanguage: 'ruby', excludeBegin: true, excludeEnd: true } ] }; } },{name:"erlang-repl",create:/* Language: Erlang REPL Author: Sergey Ignatov Category: functional */ function(hljs) { return { keywords: { built_in: 'spawn spawn_link self', keyword: 'after and andalso|10 band begin bnot bor bsl bsr bxor case catch cond div end fun if ' + 'let not of or orelse|10 query receive rem try when xor' }, contains: [ { className: 'meta', begin: '^[0-9]+> ', relevance: 10 }, hljs.COMMENT('%', '$'), { className: 'number', begin: '\\b(\\d+#[a-fA-F0-9]+|\\d+(\\.\\d+)?([eE][-+]?\\d+)?)', relevance: 0 }, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, { begin: '\\?(::)?([A-Z]\\w*(::)?)+' }, { begin: '->' }, { begin: 'ok' }, { begin: '!' }, { begin: '(\\b[a-z\'][a-zA-Z0-9_\']*:[a-z\'][a-zA-Z0-9_\']*)|(\\b[a-z\'][a-zA-Z0-9_\']*)', relevance: 0 }, { begin: '[A-Z][a-zA-Z0-9_\']*', relevance: 0 } ] }; } },{name:"erlang",create:/* Language: Erlang Description: Erlang is a general-purpose functional language, with strict evaluation, single assignment, and dynamic typing. Author: Nikolay Zakharov , Dmitry Kovega Category: functional */ function(hljs) { var BASIC_ATOM_RE = '[a-z\'][a-zA-Z0-9_\']*'; var FUNCTION_NAME_RE = '(' + BASIC_ATOM_RE + ':' + BASIC_ATOM_RE + '|' + BASIC_ATOM_RE + ')'; var ERLANG_RESERVED = { keyword: 'after and andalso|10 band begin bnot bor bsl bzr bxor case catch cond div end fun if ' + 'let not of orelse|10 query receive rem try when xor', literal: 'false true' }; var COMMENT = hljs.COMMENT('%', '$'); var NUMBER = { className: 'number', begin: '\\b(\\d+#[a-fA-F0-9]+|\\d+(\\.\\d+)?([eE][-+]?\\d+)?)', relevance: 0 }; var NAMED_FUN = { begin: 'fun\\s+' + BASIC_ATOM_RE + '/\\d+' }; var FUNCTION_CALL = { begin: FUNCTION_NAME_RE + '\\(', end: '\\)', returnBegin: true, relevance: 0, contains: [ { begin: FUNCTION_NAME_RE, relevance: 0 }, { begin: '\\(', end: '\\)', endsWithParent: true, returnEnd: true, relevance: 0 // "contains" defined later } ] }; var TUPLE = { begin: '{', end: '}', relevance: 0 // "contains" defined later }; var VAR1 = { begin: '\\b_([A-Z][A-Za-z0-9_]*)?', relevance: 0 }; var VAR2 = { begin: '[A-Z][a-zA-Z0-9_]*', relevance: 0 }; var RECORD_ACCESS = { begin: '#' + hljs.UNDERSCORE_IDENT_RE, relevance: 0, returnBegin: true, contains: [ { begin: '#' + hljs.UNDERSCORE_IDENT_RE, relevance: 0 }, { begin: '{', end: '}', relevance: 0 // "contains" defined later } ] }; var BLOCK_STATEMENTS = { beginKeywords: 'fun receive if try case', end: 'end', keywords: ERLANG_RESERVED }; BLOCK_STATEMENTS.contains = [ COMMENT, NAMED_FUN, hljs.inherit(hljs.APOS_STRING_MODE, {className: ''}), BLOCK_STATEMENTS, FUNCTION_CALL, hljs.QUOTE_STRING_MODE, NUMBER, TUPLE, VAR1, VAR2, RECORD_ACCESS ]; var BASIC_MODES = [ COMMENT, NAMED_FUN, BLOCK_STATEMENTS, FUNCTION_CALL, hljs.QUOTE_STRING_MODE, NUMBER, TUPLE, VAR1, VAR2, RECORD_ACCESS ]; FUNCTION_CALL.contains[1].contains = BASIC_MODES; TUPLE.contains = BASIC_MODES; RECORD_ACCESS.contains[1].contains = BASIC_MODES; var PARAMS = { className: 'params', begin: '\\(', end: '\\)', contains: BASIC_MODES }; return { aliases: ['erl'], keywords: ERLANG_RESERVED, illegal: '(', returnBegin: true, illegal: '\\(|#|//|/\\*|\\\\|:|;', contains: [ PARAMS, hljs.inherit(hljs.TITLE_MODE, {begin: BASIC_ATOM_RE}) ], starts: { end: ';|\\.', keywords: ERLANG_RESERVED, contains: BASIC_MODES } }, COMMENT, { begin: '^-', end: '\\.', relevance: 0, excludeEnd: true, returnBegin: true, lexemes: '-' + hljs.IDENT_RE, keywords: '-module -record -undef -export -ifdef -ifndef -author -copyright -doc -vsn ' + '-import -include -include_lib -compile -define -else -endif -file -behaviour ' + '-behavior -spec', contains: [PARAMS] }, NUMBER, hljs.QUOTE_STRING_MODE, RECORD_ACCESS, VAR1, VAR2, TUPLE, {begin: /\.$/} // relevance booster ] }; } },{name:"excel",create:/* Language: Excel Author: Victor Zhou Description: Excel formulae */ function(hljs) { return { aliases: ['xlsx', 'xls'], case_insensitive: true, lexemes: /[a-zA-Z][\w\.]*/, // built-in functions imported from https://web.archive.org/web/20160513042710/https://support.office.com/en-us/article/Excel-functions-alphabetical-b3944572-255d-4efb-bb96-c6d90033e188 keywords: { built_in: 'ABS ACCRINT ACCRINTM ACOS ACOSH ACOT ACOTH AGGREGATE ADDRESS AMORDEGRC AMORLINC AND ARABIC AREAS ASC ASIN ASINH ATAN ATAN2 ATANH AVEDEV AVERAGE AVERAGEA AVERAGEIF AVERAGEIFS BAHTTEXT BASE BESSELI BESSELJ BESSELK BESSELY BETADIST BETA.DIST BETAINV BETA.INV BIN2DEC BIN2HEX BIN2OCT BINOMDIST BINOM.DIST BINOM.DIST.RANGE BINOM.INV BITAND BITLSHIFT BITOR BITRSHIFT BITXOR CALL CEILING CEILING.MATH CEILING.PRECISE CELL CHAR CHIDIST CHIINV CHITEST CHISQ.DIST CHISQ.DIST.RT CHISQ.INV CHISQ.INV.RT CHISQ.TEST CHOOSE CLEAN CODE COLUMN COLUMNS COMBIN COMBINA COMPLEX CONCAT CONCATENATE CONFIDENCE CONFIDENCE.NORM CONFIDENCE.T CONVERT CORREL COS COSH COT COTH COUNT COUNTA COUNTBLANK COUNTIF COUNTIFS COUPDAYBS COUPDAYS COUPDAYSNC COUPNCD COUPNUM COUPPCD COVAR COVARIANCE.P COVARIANCE.S CRITBINOM CSC CSCH CUBEKPIMEMBER CUBEMEMBER CUBEMEMBERPROPERTY CUBERANKEDMEMBER CUBESET CUBESETCOUNT CUBEVALUE CUMIPMT CUMPRINC DATE DATEDIF DATEVALUE DAVERAGE DAY DAYS DAYS360 DB DBCS DCOUNT DCOUNTA DDB DEC2BIN DEC2HEX DEC2OCT DECIMAL DEGREES DELTA DEVSQ DGET DISC DMAX DMIN DOLLAR DOLLARDE DOLLARFR DPRODUCT DSTDEV DSTDEVP DSUM DURATION DVAR DVARP EDATE EFFECT ENCODEURL EOMONTH ERF ERF.PRECISE ERFC ERFC.PRECISE ERROR.TYPE EUROCONVERT EVEN EXACT EXP EXPON.DIST EXPONDIST FACT FACTDOUBLE FALSE|0 F.DIST FDIST F.DIST.RT FILTERXML FIND FINDB F.INV F.INV.RT FINV FISHER FISHERINV FIXED FLOOR FLOOR.MATH FLOOR.PRECISE FORECAST FORECAST.ETS FORECAST.ETS.CONFINT FORECAST.ETS.SEASONALITY FORECAST.ETS.STAT FORECAST.LINEAR FORMULATEXT FREQUENCY F.TEST FTEST FV FVSCHEDULE GAMMA GAMMA.DIST GAMMADIST GAMMA.INV GAMMAINV GAMMALN GAMMALN.PRECISE GAUSS GCD GEOMEAN GESTEP GETPIVOTDATA GROWTH HARMEAN HEX2BIN HEX2DEC HEX2OCT HLOOKUP HOUR HYPERLINK HYPGEOM.DIST HYPGEOMDIST IF|0 IFERROR IFNA IFS IMABS IMAGINARY IMARGUMENT IMCONJUGATE IMCOS IMCOSH IMCOT IMCSC IMCSCH IMDIV IMEXP IMLN IMLOG10 IMLOG2 IMPOWER IMPRODUCT IMREAL IMSEC IMSECH IMSIN IMSINH IMSQRT IMSUB IMSUM IMTAN INDEX INDIRECT INFO INT INTERCEPT INTRATE IPMT IRR ISBLANK ISERR ISERROR ISEVEN ISFORMULA ISLOGICAL ISNA ISNONTEXT ISNUMBER ISODD ISREF ISTEXT ISO.CEILING ISOWEEKNUM ISPMT JIS KURT LARGE LCM LEFT LEFTB LEN LENB LINEST LN LOG LOG10 LOGEST LOGINV LOGNORM.DIST LOGNORMDIST LOGNORM.INV LOOKUP LOWER MATCH MAX MAXA MAXIFS MDETERM MDURATION MEDIAN MID MIDBs MIN MINIFS MINA MINUTE MINVERSE MIRR MMULT MOD MODE MODE.MULT MODE.SNGL MONTH MROUND MULTINOMIAL MUNIT N NA NEGBINOM.DIST NEGBINOMDIST NETWORKDAYS NETWORKDAYS.INTL NOMINAL NORM.DIST NORMDIST NORMINV NORM.INV NORM.S.DIST NORMSDIST NORM.S.INV NORMSINV NOT NOW NPER NPV NUMBERVALUE OCT2BIN OCT2DEC OCT2HEX ODD ODDFPRICE ODDFYIELD ODDLPRICE ODDLYIELD OFFSET OR PDURATION PEARSON PERCENTILE.EXC PERCENTILE.INC PERCENTILE PERCENTRANK.EXC PERCENTRANK.INC PERCENTRANK PERMUT PERMUTATIONA PHI PHONETIC PI PMT POISSON.DIST POISSON POWER PPMT PRICE PRICEDISC PRICEMAT PROB PRODUCT PROPER PV QUARTILE QUARTILE.EXC QUARTILE.INC QUOTIENT RADIANS RAND RANDBETWEEN RANK.AVG RANK.EQ RANK RATE RECEIVED REGISTER.ID REPLACE REPLACEB REPT RIGHT RIGHTB ROMAN ROUND ROUNDDOWN ROUNDUP ROW ROWS RRI RSQ RTD SEARCH SEARCHB SEC SECH SECOND SERIESSUM SHEET SHEETS SIGN SIN SINH SKEW SKEW.P SLN SLOPE SMALL SQL.REQUEST SQRT SQRTPI STANDARDIZE STDEV STDEV.P STDEV.S STDEVA STDEVP STDEVPA STEYX SUBSTITUTE SUBTOTAL SUM SUMIF SUMIFS SUMPRODUCT SUMSQ SUMX2MY2 SUMX2PY2 SUMXMY2 SWITCH SYD T TAN TANH TBILLEQ TBILLPRICE TBILLYIELD T.DIST T.DIST.2T T.DIST.RT TDIST TEXT TEXTJOIN TIME TIMEVALUE T.INV T.INV.2T TINV TODAY TRANSPOSE TREND TRIM TRIMMEAN TRUE|0 TRUNC T.TEST TTEST TYPE UNICHAR UNICODE UPPER VALUE VAR VAR.P VAR.S VARA VARP VARPA VDB VLOOKUP WEBSERVICE WEEKDAY WEEKNUM WEIBULL WEIBULL.DIST WORKDAY WORKDAY.INTL XIRR XNPV XOR YEAR YEARFRAC YIELD YIELDDISC YIELDMAT Z.TEST ZTEST' }, contains: [ { /* matches a beginning equal sign found in Excel formula examples */ begin: /^=/, end: /[^=]/, returnEnd: true, illegal: /=/, /* only allow single equal sign at front of line */ relevance: 10 }, /* technically, there can be more than 2 letters in column names, but this prevents conflict with some keywords */ { /* matches a reference to a single cell */ className: 'symbol', begin: /\b[A-Z]{1,2}\d+\b/, end: /[^\d]/, excludeEnd: true, relevance: 0 }, { /* matches a reference to a range of cells */ className: 'symbol', begin: /[A-Z]{0,2}\d*:[A-Z]{0,2}\d*/, relevance: 0 }, hljs.BACKSLASH_ESCAPE, hljs.QUOTE_STRING_MODE, { className: 'number', begin: hljs.NUMBER_RE + '(%)?', relevance: 0 }, /* Excel formula comments are done by putting the comment in a function call to N() */ hljs.COMMENT(/\bN\(/,/\)/, { excludeBegin: true, excludeEnd: true, illegal: /\n/ }) ] }; } },{name:"fix",create:/* Language: FIX Author: Brent Bradbury */ function(hljs) { return { contains: [ { begin: /[^\u2401\u0001]+/, end: /[\u2401\u0001]/, excludeEnd: true, returnBegin: true, returnEnd: false, contains: [ { begin: /([^\u2401\u0001=]+)/, end: /=([^\u2401\u0001=]+)/, returnEnd: true, returnBegin: false, className: 'attr' }, { begin: /=/, end: /([\u2401\u0001])/, excludeEnd: true, excludeBegin: true, className: 'string' }] }], case_insensitive: true }; } },{name:"flix",create:/* Language: Flix Category: functional Author: Magnus Madsen */ function (hljs) { var CHAR = { className: 'string', begin: /'(.|\\[xXuU][a-zA-Z0-9]+)'/ }; var STRING = { className: 'string', variants: [ { begin: '"', end: '"' } ] }; var NAME = { className: 'title', begin: /[^0-9\n\t "'(),.`{}\[\]:;][^\n\t "'(),.`{}\[\]:;]+|[^0-9\n\t "'(),.`{}\[\]:;=]/ }; var METHOD = { className: 'function', beginKeywords: 'def', end: /[:={\[(\n;]/, excludeEnd: true, contains: [NAME] }; return { keywords: { literal: 'true false', keyword: 'case class def else enum if impl import in lat rel index let match namespace switch type yield with' }, contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, CHAR, STRING, METHOD, hljs.C_NUMBER_MODE ] }; } },{name:"fortran",create:/* Language: Fortran Author: Anthony Scemama Category: scientific */ function(hljs) { var PARAMS = { className: 'params', begin: '\\(', end: '\\)' }; var F_KEYWORDS = { literal: '.False. .True.', keyword: 'kind do while private call intrinsic where elsewhere ' + 'type endtype endmodule endselect endinterface end enddo endif if forall endforall only contains default return stop then ' + 'public subroutine|10 function program .and. .or. .not. .le. .eq. .ge. .gt. .lt. ' + 'goto save else use module select case ' + 'access blank direct exist file fmt form formatted iostat name named nextrec number opened rec recl sequential status unformatted unit ' + 'continue format pause cycle exit ' + 'c_null_char c_alert c_backspace c_form_feed flush wait decimal round iomsg ' + 'synchronous nopass non_overridable pass protected volatile abstract extends import ' + 'non_intrinsic value deferred generic final enumerator class associate bind enum ' + 'c_int c_short c_long c_long_long c_signed_char c_size_t c_int8_t c_int16_t c_int32_t c_int64_t c_int_least8_t c_int_least16_t ' + 'c_int_least32_t c_int_least64_t c_int_fast8_t c_int_fast16_t c_int_fast32_t c_int_fast64_t c_intmax_t C_intptr_t c_float c_double ' + 'c_long_double c_float_complex c_double_complex c_long_double_complex c_bool c_char c_null_ptr c_null_funptr ' + 'c_new_line c_carriage_return c_horizontal_tab c_vertical_tab iso_c_binding c_loc c_funloc c_associated c_f_pointer ' + 'c_ptr c_funptr iso_fortran_env character_storage_size error_unit file_storage_size input_unit iostat_end iostat_eor ' + 'numeric_storage_size output_unit c_f_procpointer ieee_arithmetic ieee_support_underflow_control ' + 'ieee_get_underflow_mode ieee_set_underflow_mode newunit contiguous recursive ' + 'pad position action delim readwrite eor advance nml interface procedure namelist include sequence elemental pure ' + 'integer real character complex logical dimension allocatable|10 parameter ' + 'external implicit|10 none double precision assign intent optional pointer ' + 'target in out common equivalence data', built_in: 'alog alog10 amax0 amax1 amin0 amin1 amod cabs ccos cexp clog csin csqrt dabs dacos dasin datan datan2 dcos dcosh ddim dexp dint ' + 'dlog dlog10 dmax1 dmin1 dmod dnint dsign dsin dsinh dsqrt dtan dtanh float iabs idim idint idnint ifix isign max0 max1 min0 min1 sngl ' + 'algama cdabs cdcos cdexp cdlog cdsin cdsqrt cqabs cqcos cqexp cqlog cqsin cqsqrt dcmplx dconjg derf derfc dfloat dgamma dimag dlgama ' + 'iqint qabs qacos qasin qatan qatan2 qcmplx qconjg qcos qcosh qdim qerf qerfc qexp qgamma qimag qlgama qlog qlog10 qmax1 qmin1 qmod ' + 'qnint qsign qsin qsinh qsqrt qtan qtanh abs acos aimag aint anint asin atan atan2 char cmplx conjg cos cosh exp ichar index int log ' + 'log10 max min nint sign sin sinh sqrt tan tanh print write dim lge lgt lle llt mod nullify allocate deallocate ' + 'adjustl adjustr all allocated any associated bit_size btest ceiling count cshift date_and_time digits dot_product ' + 'eoshift epsilon exponent floor fraction huge iand ibclr ibits ibset ieor ior ishft ishftc lbound len_trim matmul ' + 'maxexponent maxloc maxval merge minexponent minloc minval modulo mvbits nearest pack present product ' + 'radix random_number random_seed range repeat reshape rrspacing scale scan selected_int_kind selected_real_kind ' + 'set_exponent shape size spacing spread sum system_clock tiny transpose trim ubound unpack verify achar iachar transfer ' + 'dble entry dprod cpu_time command_argument_count get_command get_command_argument get_environment_variable is_iostat_end ' + 'ieee_arithmetic ieee_support_underflow_control ieee_get_underflow_mode ieee_set_underflow_mode ' + 'is_iostat_eor move_alloc new_line selected_char_kind same_type_as extends_type_of' + 'acosh asinh atanh bessel_j0 bessel_j1 bessel_jn bessel_y0 bessel_y1 bessel_yn erf erfc erfc_scaled gamma log_gamma hypot norm2 ' + 'atomic_define atomic_ref execute_command_line leadz trailz storage_size merge_bits ' + 'bge bgt ble blt dshiftl dshiftr findloc iall iany iparity image_index lcobound ucobound maskl maskr ' + 'num_images parity popcnt poppar shifta shiftl shiftr this_image' }; return { case_insensitive: true, aliases: ['f90', 'f95'], keywords: F_KEYWORDS, illegal: /\/\*/, contains: [ hljs.inherit(hljs.APOS_STRING_MODE, {className: 'string', relevance: 0}), hljs.inherit(hljs.QUOTE_STRING_MODE, {className: 'string', relevance: 0}), { className: 'function', beginKeywords: 'subroutine function program', illegal: '[${=\\n]', contains: [hljs.UNDERSCORE_TITLE_MODE, PARAMS] }, hljs.COMMENT('!', '$', {relevance: 0}), { className: 'number', begin: '(?=\\b|\\+|\\-|\\.)(?=\\.\\d|\\d)(?:\\d+)?(?:\\.?\\d*)(?:[de][+-]?\\d+)?\\b\\.?', relevance: 0 } ] }; } },{name:"fsharp",create:/* Language: F# Author: Jonas Follesø Contributors: Troy Kershaw , Henrik Feldt Category: functional */ function(hljs) { var TYPEPARAM = { begin: '<', end: '>', contains: [ hljs.inherit(hljs.TITLE_MODE, {begin: /'[a-zA-Z0-9_]+/}) ] }; return { aliases: ['fs'], keywords: 'abstract and as assert base begin class default delegate do done ' + 'downcast downto elif else end exception extern false finally for ' + 'fun function global if in inherit inline interface internal lazy let ' + 'match member module mutable namespace new null of open or ' + 'override private public rec return sig static struct then to ' + 'true try type upcast use val void when while with yield', illegal: /\/\*/, contains: [ { // monad builder keywords (matches before non-bang kws) className: 'keyword', begin: /\b(yield|return|let|do)!/ }, { className: 'string', begin: '@"', end: '"', contains: [{begin: '""'}] }, { className: 'string', begin: '"""', end: '"""' }, hljs.COMMENT('\\(\\*', '\\*\\)'), { className: 'class', beginKeywords: 'type', end: '\\(|=|$', excludeEnd: true, contains: [ hljs.UNDERSCORE_TITLE_MODE, TYPEPARAM ] }, { className: 'meta', begin: '\\[<', end: '>\\]', relevance: 10 }, { className: 'symbol', begin: '\\B(\'[A-Za-z])\\b', contains: [hljs.BACKSLASH_ESCAPE] }, hljs.C_LINE_COMMENT_MODE, hljs.inherit(hljs.QUOTE_STRING_MODE, {illegal: null}), hljs.C_NUMBER_MODE ] }; } },{name:"gams",create: /* Language: GAMS Author: Stefan Bechert Contributors: Oleg Efimov , Mikko Kouhia Description: The General Algebraic Modeling System language Category: scientific */ function (hljs) { var KEYWORDS = { 'keyword': 'abort acronym acronyms alias all and assign binary card diag display ' + 'else eq file files for free ge gt if integer le loop lt maximizing ' + 'minimizing model models ne negative no not option options or ord ' + 'positive prod put putpage puttl repeat sameas semicont semiint smax ' + 'smin solve sos1 sos2 sum system table then until using while xor yes', 'literal': 'eps inf na', 'built-in': 'abs arccos arcsin arctan arctan2 Beta betaReg binomial ceil centropy ' + 'cos cosh cvPower div div0 eDist entropy errorf execSeed exp fact ' + 'floor frac gamma gammaReg log logBeta logGamma log10 log2 mapVal max ' + 'min mod ncpCM ncpF ncpVUpow ncpVUsin normal pi poly power ' + 'randBinomial randLinear randTriangle round rPower sigmoid sign ' + 'signPower sin sinh slexp sllog10 slrec sqexp sqlog10 sqr sqrec sqrt ' + 'tan tanh trunc uniform uniformInt vcPower bool_and bool_eqv bool_imp ' + 'bool_not bool_or bool_xor ifThen rel_eq rel_ge rel_gt rel_le rel_lt ' + 'rel_ne gday gdow ghour gleap gmillisec gminute gmonth gsecond gyear ' + 'jdate jnow jstart jtime errorLevel execError gamsRelease gamsVersion ' + 'handleCollect handleDelete handleStatus handleSubmit heapFree ' + 'heapLimit heapSize jobHandle jobKill jobStatus jobTerminate ' + 'licenseLevel licenseStatus maxExecError sleep timeClose timeComp ' + 'timeElapsed timeExec timeStart' }; var PARAMS = { className: 'params', begin: /\(/, end: /\)/, excludeBegin: true, excludeEnd: true, }; var SYMBOLS = { className: 'symbol', variants: [ {begin: /\=[lgenxc]=/}, {begin: /\$/}, ] }; var QSTR = { // One-line quoted comment string className: 'comment', variants: [ {begin: '\'', end: '\''}, {begin: '"', end: '"'}, ], illegal: '\\n', contains: [hljs.BACKSLASH_ESCAPE] }; var ASSIGNMENT = { begin: '/', end: '/', keywords: KEYWORDS, contains: [ QSTR, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.QUOTE_STRING_MODE, hljs.APOS_STRING_MODE, hljs.C_NUMBER_MODE, ], }; var DESCTEXT = { // Parameter/set/variable description text begin: /[a-z][a-z0-9_]*(\([a-z0-9_, ]*\))?[ \t]+/, excludeBegin: true, end: '$', endsWithParent: true, contains: [ QSTR, ASSIGNMENT, { className: 'comment', begin: /([ ]*[a-z0-9&#*=?@>\\<:\-,()$\[\]_.{}!+%^]+)+/, relevance: 0 }, ], }; return { aliases: ['gms'], case_insensitive: true, keywords: KEYWORDS, contains: [ hljs.COMMENT(/^\$ontext/, /^\$offtext/), { className: 'meta', begin: '^\\$[a-z0-9]+', end: '$', returnBegin: true, contains: [ { className: 'meta-keyword', begin: '^\\$[a-z0-9]+', } ] }, hljs.COMMENT('^\\*', '$'), hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.QUOTE_STRING_MODE, hljs.APOS_STRING_MODE, // Declarations { beginKeywords: 'set sets parameter parameters variable variables ' + 'scalar scalars equation equations', end: ';', contains: [ hljs.COMMENT('^\\*', '$'), hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.QUOTE_STRING_MODE, hljs.APOS_STRING_MODE, ASSIGNMENT, DESCTEXT, ] }, { // table environment beginKeywords: 'table', end: ';', returnBegin: true, contains: [ { // table header row beginKeywords: 'table', end: '$', contains: [DESCTEXT], }, hljs.COMMENT('^\\*', '$'), hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.QUOTE_STRING_MODE, hljs.APOS_STRING_MODE, hljs.C_NUMBER_MODE, // Table does not contain DESCTEXT or ASSIGNMENT ] }, // Function definitions { className: 'function', begin: /^[a-z][a-z0-9_,\-+' ()$]+\.{2}/, returnBegin: true, contains: [ { // Function title className: 'title', begin: /^[a-z0-9_]+/, }, PARAMS, SYMBOLS, ], }, hljs.C_NUMBER_MODE, SYMBOLS, ] }; } },{name:"gauss",create:/* Language: GAUSS Author: Matt Evans Category: scientific Description: GAUSS Mathematical and Statistical language */ function(hljs) { var KEYWORDS = { keyword: 'bool break call callexe checkinterrupt clear clearg closeall cls comlog compile ' + 'continue create debug declare delete disable dlibrary dllcall do dos ed edit else ' + 'elseif enable end endfor endif endp endo errorlog errorlogat expr external fn ' + 'for format goto gosub graph if keyword let lib library line load loadarray loadexe ' + 'loadf loadk loadm loadp loads loadx local locate loopnextindex lprint lpwidth lshow ' + 'matrix msym ndpclex new open output outwidth plot plotsym pop prcsn print ' + 'printdos proc push retp return rndcon rndmod rndmult rndseed run save saveall screen ' + 'scroll setarray show sparse stop string struct system trace trap threadfor ' + 'threadendfor threadbegin threadjoin threadstat threadend until use while winprint ' + 'ne ge le gt lt and xor or not eq eqv', built_in: 'abs acf aconcat aeye amax amean AmericanBinomCall AmericanBinomCall_Greeks AmericanBinomCall_ImpVol ' + 'AmericanBinomPut AmericanBinomPut_Greeks AmericanBinomPut_ImpVol AmericanBSCall AmericanBSCall_Greeks ' + 'AmericanBSCall_ImpVol AmericanBSPut AmericanBSPut_Greeks AmericanBSPut_ImpVol amin amult annotationGetDefaults ' + 'annotationSetBkd annotationSetFont annotationSetLineColor annotationSetLineStyle annotationSetLineThickness ' + 'annualTradingDays arccos arcsin areshape arrayalloc arrayindex arrayinit arraytomat asciiload asclabel astd ' + 'astds asum atan atan2 atranspose axmargin balance band bandchol bandcholsol bandltsol bandrv bandsolpd bar ' + 'base10 begwind besselj bessely beta box boxcox cdfBeta cdfBetaInv cdfBinomial cdfBinomialInv cdfBvn cdfBvn2 ' + 'cdfBvn2e cdfCauchy cdfCauchyInv cdfChic cdfChii cdfChinc cdfChincInv cdfExp cdfExpInv cdfFc cdfFnc cdfFncInv ' + 'cdfGam cdfGenPareto cdfHyperGeo cdfLaplace cdfLaplaceInv cdfLogistic cdfLogisticInv cdfmControlCreate cdfMvn ' + 'cdfMvn2e cdfMvnce cdfMvne cdfMvt2e cdfMvtce cdfMvte cdfN cdfN2 cdfNc cdfNegBinomial cdfNegBinomialInv cdfNi ' + 'cdfPoisson cdfPoissonInv cdfRayleigh cdfRayleighInv cdfTc cdfTci cdfTnc cdfTvn cdfWeibull cdfWeibullInv cdir ' + 'ceil ChangeDir chdir chiBarSquare chol choldn cholsol cholup chrs close code cols colsf combinate combinated ' + 'complex con cond conj cons ConScore contour conv convertsatostr convertstrtosa corrm corrms corrvc corrx corrxs ' + 'cos cosh counts countwts crossprd crout croutp csrcol csrlin csvReadM csvReadSA cumprodc cumsumc curve cvtos ' + 'datacreate datacreatecomplex datalist dataload dataloop dataopen datasave date datestr datestring datestrymd ' + 'dayinyr dayofweek dbAddDatabase dbClose dbCommit dbCreateQuery dbExecQuery dbGetConnectOptions dbGetDatabaseName ' + 'dbGetDriverName dbGetDrivers dbGetHostName dbGetLastErrorNum dbGetLastErrorText dbGetNumericalPrecPolicy ' + 'dbGetPassword dbGetPort dbGetTableHeaders dbGetTables dbGetUserName dbHasFeature dbIsDriverAvailable dbIsOpen ' + 'dbIsOpenError dbOpen dbQueryBindValue dbQueryClear dbQueryCols dbQueryExecPrepared dbQueryFetchAllM dbQueryFetchAllSA ' + 'dbQueryFetchOneM dbQueryFetchOneSA dbQueryFinish dbQueryGetBoundValue dbQueryGetBoundValues dbQueryGetField ' + 'dbQueryGetLastErrorNum dbQueryGetLastErrorText dbQueryGetLastInsertID dbQueryGetLastQuery dbQueryGetPosition ' + 'dbQueryIsActive dbQueryIsForwardOnly dbQueryIsNull dbQueryIsSelect dbQueryIsValid dbQueryPrepare dbQueryRows ' + 'dbQuerySeek dbQuerySeekFirst dbQuerySeekLast dbQuerySeekNext dbQuerySeekPrevious dbQuerySetForwardOnly ' + 'dbRemoveDatabase dbRollback dbSetConnectOptions dbSetDatabaseName dbSetHostName dbSetNumericalPrecPolicy ' + 'dbSetPort dbSetUserName dbTransaction DeleteFile delif delrows denseToSp denseToSpRE denToZero design det detl ' + 'dfft dffti diag diagrv digamma doswin DOSWinCloseall DOSWinOpen dotfeq dotfeqmt dotfge dotfgemt dotfgt dotfgtmt ' + 'dotfle dotflemt dotflt dotfltmt dotfne dotfnemt draw drop dsCreate dstat dstatmt dstatmtControlCreate dtdate dtday ' + 'dttime dttodtv dttostr dttoutc dtvnormal dtvtodt dtvtoutc dummy dummybr dummydn eig eigh eighv eigv elapsedTradingDays ' + 'endwind envget eof eqSolve eqSolvemt eqSolvemtControlCreate eqSolvemtOutCreate eqSolveset erf erfc erfccplx erfcplx error ' + 'etdays ethsec etstr EuropeanBinomCall EuropeanBinomCall_Greeks EuropeanBinomCall_ImpVol EuropeanBinomPut ' + 'EuropeanBinomPut_Greeks EuropeanBinomPut_ImpVol EuropeanBSCall EuropeanBSCall_Greeks EuropeanBSCall_ImpVol ' + 'EuropeanBSPut EuropeanBSPut_Greeks EuropeanBSPut_ImpVol exctsmpl exec execbg exp extern eye fcheckerr fclearerr feq ' + 'feqmt fflush fft ffti fftm fftmi fftn fge fgemt fgets fgetsa fgetsat fgetst fgt fgtmt fileinfo filesa fle flemt ' + 'floor flt fltmt fmod fne fnemt fonts fopen formatcv formatnv fputs fputst fseek fstrerror ftell ftocv ftos ftostrC ' + 'gamma gammacplx gammaii gausset gdaAppend gdaCreate gdaDStat gdaDStatMat gdaGetIndex gdaGetName gdaGetNames gdaGetOrders ' + 'gdaGetType gdaGetTypes gdaGetVarInfo gdaIsCplx gdaLoad gdaPack gdaRead gdaReadByIndex gdaReadSome gdaReadSparse ' + 'gdaReadStruct gdaReportVarInfo gdaSave gdaUpdate gdaUpdateAndPack gdaVars gdaWrite gdaWrite32 gdaWriteSome getarray ' + 'getdims getf getGAUSShome getmatrix getmatrix4D getname getnamef getNextTradingDay getNextWeekDay getnr getorders ' + 'getpath getPreviousTradingDay getPreviousWeekDay getRow getscalar3D getscalar4D getTrRow getwind glm gradcplx gradMT ' + 'gradMTm gradMTT gradMTTm gradp graphprt graphset hasimag header headermt hess hessMT hessMTg hessMTgw hessMTm ' + 'hessMTmw hessMTT hessMTTg hessMTTgw hessMTTm hessMTw hessp hist histf histp hsec imag indcv indexcat indices indices2 ' + 'indicesf indicesfn indnv indsav integrate1d integrateControlCreate intgrat2 intgrat3 inthp1 inthp2 inthp3 inthp4 ' + 'inthpControlCreate intquad1 intquad2 intquad3 intrleav intrleavsa intrsect intsimp inv invpd invswp iscplx iscplxf ' + 'isden isinfnanmiss ismiss key keyav keyw lag lag1 lagn lapEighb lapEighi lapEighvb lapEighvi lapgEig lapgEigh lapgEighv ' + 'lapgEigv lapgSchur lapgSvdcst lapgSvds lapgSvdst lapSvdcusv lapSvds lapSvdusv ldlp ldlsol linSolve listwise ln lncdfbvn ' + 'lncdfbvn2 lncdfmvn lncdfn lncdfn2 lncdfnc lnfact lngammacplx lnpdfmvn lnpdfmvt lnpdfn lnpdft loadd loadstruct loadwind ' + 'loess loessmt loessmtControlCreate log loglog logx logy lower lowmat lowmat1 ltrisol lu lusol machEpsilon make makevars ' + 'makewind margin matalloc matinit mattoarray maxbytes maxc maxindc maxv maxvec mbesselei mbesselei0 mbesselei1 mbesseli ' + 'mbesseli0 mbesseli1 meanc median mergeby mergevar minc minindc minv miss missex missrv moment momentd movingave ' + 'movingaveExpwgt movingaveWgt nextindex nextn nextnevn nextwind ntos null null1 numCombinations ols olsmt olsmtControlCreate ' + 'olsqr olsqr2 olsqrmt ones optn optnevn orth outtyp pacf packedToSp packr parse pause pdfCauchy pdfChi pdfExp pdfGenPareto ' + 'pdfHyperGeo pdfLaplace pdfLogistic pdfn pdfPoisson pdfRayleigh pdfWeibull pi pinv pinvmt plotAddArrow plotAddBar plotAddBox ' + 'plotAddHist plotAddHistF plotAddHistP plotAddPolar plotAddScatter plotAddShape plotAddTextbox plotAddTS plotAddXY plotArea ' + 'plotBar plotBox plotClearLayout plotContour plotCustomLayout plotGetDefaults plotHist plotHistF plotHistP plotLayout ' + 'plotLogLog plotLogX plotLogY plotOpenWindow plotPolar plotSave plotScatter plotSetAxesPen plotSetBar plotSetBarFill ' + 'plotSetBarStacked plotSetBkdColor plotSetFill plotSetGrid plotSetLegend plotSetLineColor plotSetLineStyle plotSetLineSymbol ' + 'plotSetLineThickness plotSetNewWindow plotSetTitle plotSetWhichYAxis plotSetXAxisShow plotSetXLabel plotSetXRange ' + 'plotSetXTicInterval plotSetXTicLabel plotSetYAxisShow plotSetYLabel plotSetYRange plotSetZAxisShow plotSetZLabel ' + 'plotSurface plotTS plotXY polar polychar polyeval polygamma polyint polymake polymat polymroot polymult polyroot ' + 'pqgwin previousindex princomp printfm printfmt prodc psi putarray putf putvals pvCreate pvGetIndex pvGetParNames ' + 'pvGetParVector pvLength pvList pvPack pvPacki pvPackm pvPackmi pvPacks pvPacksi pvPacksm pvPacksmi pvPutParVector ' + 'pvTest pvUnpack QNewton QNewtonmt QNewtonmtControlCreate QNewtonmtOutCreate QNewtonSet QProg QProgmt QProgmtInCreate ' + 'qqr qqre qqrep qr qre qrep qrsol qrtsol qtyr qtyre qtyrep quantile quantiled qyr qyre qyrep qz rank rankindx readr ' + 'real reclassify reclassifyCuts recode recserar recsercp recserrc rerun rescale reshape rets rev rfft rffti rfftip rfftn ' + 'rfftnp rfftp rndBernoulli rndBeta rndBinomial rndCauchy rndChiSquare rndCon rndCreateState rndExp rndGamma rndGeo rndGumbel ' + 'rndHyperGeo rndi rndKMbeta rndKMgam rndKMi rndKMn rndKMnb rndKMp rndKMu rndKMvm rndLaplace rndLCbeta rndLCgam rndLCi rndLCn ' + 'rndLCnb rndLCp rndLCu rndLCvm rndLogNorm rndMTu rndMVn rndMVt rndn rndnb rndNegBinomial rndp rndPoisson rndRayleigh ' + 'rndStateSkip rndu rndvm rndWeibull rndWishart rotater round rows rowsf rref sampleData satostrC saved saveStruct savewind ' + 'scale scale3d scalerr scalinfnanmiss scalmiss schtoc schur searchsourcepath seekr select selif seqa seqm setdif setdifsa ' + 'setvars setvwrmode setwind shell shiftr sin singleindex sinh sleep solpd sortc sortcc sortd sorthc sorthcc sortind ' + 'sortindc sortmc sortr sortrc spBiconjGradSol spChol spConjGradSol spCreate spDenseSubmat spDiagRvMat spEigv spEye spLDL ' + 'spline spLU spNumNZE spOnes spreadSheetReadM spreadSheetReadSA spreadSheetWrite spScale spSubmat spToDense spTrTDense ' + 'spTScalar spZeros sqpSolve sqpSolveMT sqpSolveMTControlCreate sqpSolveMTlagrangeCreate sqpSolveMToutCreate sqpSolveSet ' + 'sqrt statements stdc stdsc stocv stof strcombine strindx strlen strput strrindx strsect strsplit strsplitPad strtodt ' + 'strtof strtofcplx strtriml strtrimr strtrunc strtruncl strtruncpad strtruncr submat subscat substute subvec sumc sumr ' + 'surface svd svd1 svd2 svdcusv svds svdusv sysstate tab tan tanh tempname ' + 'time timedt timestr timeutc title tkf2eps tkf2ps tocart todaydt toeplitz token topolar trapchk ' + 'trigamma trimr trunc type typecv typef union unionsa uniqindx uniqindxsa unique uniquesa upmat upmat1 upper utctodt ' + 'utctodtv utrisol vals varCovMS varCovXS varget vargetl varmall varmares varput varputl vartypef vcm vcms vcx vcxs ' + 'vec vech vecr vector vget view viewxyz vlist vnamecv volume vput vread vtypecv wait waitc walkindex where window ' + 'writer xlabel xlsGetSheetCount xlsGetSheetSize xlsGetSheetTypes xlsMakeRange xlsReadM xlsReadSA xlsWrite xlsWriteM ' + 'xlsWriteSA xpnd xtics xy xyz ylabel ytics zeros zeta zlabel ztics cdfEmpirical dot h5create h5open h5read h5readAttribute ' + 'h5write h5writeAttribute ldl plotAddErrorBar plotAddSurface plotCDFEmpirical plotSetColormap plotSetContourLabels ' + 'plotSetLegendFont plotSetTextInterpreter plotSetXTicCount plotSetYTicCount plotSetZLevels powerm strjoin sylvester ' + 'strtrim', literal: 'DB_AFTER_LAST_ROW DB_ALL_TABLES DB_BATCH_OPERATIONS DB_BEFORE_FIRST_ROW DB_BLOB DB_EVENT_NOTIFICATIONS ' + 'DB_FINISH_QUERY DB_HIGH_PRECISION DB_LAST_INSERT_ID DB_LOW_PRECISION_DOUBLE DB_LOW_PRECISION_INT32 ' + 'DB_LOW_PRECISION_INT64 DB_LOW_PRECISION_NUMBERS DB_MULTIPLE_RESULT_SETS DB_NAMED_PLACEHOLDERS ' + 'DB_POSITIONAL_PLACEHOLDERS DB_PREPARED_QUERIES DB_QUERY_SIZE DB_SIMPLE_LOCKING DB_SYSTEM_TABLES DB_TABLES ' + 'DB_TRANSACTIONS DB_UNICODE DB_VIEWS __STDIN __STDOUT __STDERR __FILE_DIR' }; var AT_COMMENT_MODE = hljs.COMMENT('@', '@'); var PREPROCESSOR = { className: 'meta', begin: '#', end: '$', keywords: {'meta-keyword': 'define definecs|10 undef ifdef ifndef iflight ifdllcall ifmac ifos2win ifunix else endif lineson linesoff srcfile srcline'}, contains: [ { begin: /\\\n/, relevance: 0 }, { beginKeywords: 'include', end: '$', keywords: {'meta-keyword': 'include'}, contains: [ { className: 'meta-string', begin: '"', end: '"', illegal: '\\n' } ] }, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, AT_COMMENT_MODE, ] }; var STRUCT_TYPE = { begin: /\bstruct\s+/, end: /\s/, keywords: "struct", contains: [ { className: "type", begin: hljs.UNDERSCORE_IDENT_RE, relevance: 0, }, ], }; // only for definitions var PARSE_PARAMS = [ { className: 'params', begin: /\(/, end: /\)/, excludeBegin: true, excludeEnd: true, endsWithParent: true, relevance: 0, contains: [ { // dots className: 'literal', begin: /\.\.\./, }, hljs.C_NUMBER_MODE, hljs.C_BLOCK_COMMENT_MODE, AT_COMMENT_MODE, STRUCT_TYPE, ] } ]; var FUNCTION_DEF = { className: "title", begin: hljs.UNDERSCORE_IDENT_RE, relevance: 0, }; var DEFINITION = function (beginKeywords, end, inherits) { var mode = hljs.inherit( { className: "function", beginKeywords: beginKeywords, end: end, excludeEnd: true, contains: [].concat(PARSE_PARAMS), }, inherits || {} ); mode.contains.push(FUNCTION_DEF); mode.contains.push(hljs.C_NUMBER_MODE); mode.contains.push(hljs.C_BLOCK_COMMENT_MODE); mode.contains.push(AT_COMMENT_MODE); return mode; }; var BUILT_IN_REF = { // these are explicitly named internal function calls className: 'built_in', begin: '\\b(' + KEYWORDS.built_in.split(' ').join('|') + ')\\b', }; var STRING_REF = { className: 'string', begin: '"', end: '"', contains: [hljs.BACKSLASH_ESCAPE], relevance: 0, }; var FUNCTION_REF = { //className: "fn_ref", begin: hljs.UNDERSCORE_IDENT_RE + '\\s*\\(', returnBegin: true, keywords: KEYWORDS, relevance: 0, contains: [ { beginKeywords: KEYWORDS.keyword, }, BUILT_IN_REF, { // ambiguously named function calls get a relevance of 0 className: 'built_in', begin: hljs.UNDERSCORE_IDENT_RE, relevance: 0, }, ], }; var FUNCTION_REF_PARAMS = { //className: "fn_ref_params", begin: /\(/, end: /\)/, relevance: 0, keywords: { built_in: KEYWORDS.built_in, literal: KEYWORDS.literal }, contains: [ hljs.C_NUMBER_MODE, hljs.C_BLOCK_COMMENT_MODE, AT_COMMENT_MODE, BUILT_IN_REF, FUNCTION_REF, STRING_REF, 'self', ], }; FUNCTION_REF.contains.push(FUNCTION_REF_PARAMS); return { aliases: ['gss'], case_insensitive: true, // language is case-insensitive keywords: KEYWORDS, illegal: /(\{[%#]|[%#]\}| <- )/, contains: [ hljs.C_NUMBER_MODE, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, AT_COMMENT_MODE, STRING_REF, PREPROCESSOR, { className: 'keyword', begin: /\bexternal (matrix|string|array|sparse matrix|struct|proc|keyword|fn)/, }, DEFINITION('proc keyword', ';'), DEFINITION('fn', '='), { beginKeywords: 'for threadfor', end: /;/, //end: /\(/, relevance: 0, contains: [ hljs.C_BLOCK_COMMENT_MODE, AT_COMMENT_MODE, FUNCTION_REF_PARAMS, ], }, { // custom method guard // excludes method names from keyword processing variants: [ { begin: hljs.UNDERSCORE_IDENT_RE + '\\.' + hljs.UNDERSCORE_IDENT_RE, }, { begin: hljs.UNDERSCORE_IDENT_RE + '\\s*=', }, ], relevance: 0, }, FUNCTION_REF, STRUCT_TYPE, ] }; } },{name:"gcode",create:/* Language: G-code (ISO 6983) Contributors: Adam Joseph Cook Description: G-code syntax highlighter for Fanuc and other common CNC machine tool controls. */ function(hljs) { var GCODE_IDENT_RE = '[A-Z_][A-Z0-9_.]*'; var GCODE_CLOSE_RE = '\\%'; var GCODE_KEYWORDS = 'IF DO WHILE ENDWHILE CALL ENDIF SUB ENDSUB GOTO REPEAT ENDREPEAT ' + 'EQ LT GT NE GE LE OR XOR'; var GCODE_START = { className: 'meta', begin: '([O])([0-9]+)' }; var GCODE_CODE = [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.COMMENT(/\(/, /\)/), hljs.inherit(hljs.C_NUMBER_MODE, {begin: '([-+]?([0-9]*\\.?[0-9]+\\.?))|' + hljs.C_NUMBER_RE}), hljs.inherit(hljs.APOS_STRING_MODE, {illegal: null}), hljs.inherit(hljs.QUOTE_STRING_MODE, {illegal: null}), { className: 'name', begin: '([G])([0-9]+\\.?[0-9]?)' }, { className: 'name', begin: '([M])([0-9]+\\.?[0-9]?)' }, { className: 'attr', begin: '(VC|VS|#)', end: '(\\d+)' }, { className: 'attr', begin: '(VZOFX|VZOFY|VZOFZ)' }, { className: 'built_in', begin: '(ATAN|ABS|ACOS|ASIN|SIN|COS|EXP|FIX|FUP|ROUND|LN|TAN)(\\[)', end: '([-+]?([0-9]*\\.?[0-9]+\\.?))(\\])' }, { className: 'symbol', variants: [ { begin: 'N', end: '\\d+', illegal: '\\W' } ] } ]; return { aliases: ['nc'], // Some implementations (CNC controls) of G-code are interoperable with uppercase and lowercase letters seamlessly. // However, most prefer all uppercase and uppercase is customary. case_insensitive: true, lexemes: GCODE_IDENT_RE, keywords: GCODE_KEYWORDS, contains: [ { className: 'meta', begin: GCODE_CLOSE_RE }, GCODE_START ].concat(GCODE_CODE) }; } },{name:"gherkin",create:/* Language: Gherkin Author: Sam Pikesley (@pikesley) Description: Gherkin (Cucumber etc) */ function (hljs) { return { aliases: ['feature'], keywords: 'Feature Background Ability Business\ Need Scenario Scenarios Scenario\ Outline Scenario\ Template Examples Given And Then But When', contains: [ { className: 'symbol', begin: '\\*', relevance: 0 }, { className: 'meta', begin: '@[^@\\s]+' }, { begin: '\\|', end: '\\|\\w*$', contains: [ { className: 'string', begin: '[^|]+' } ] }, { className: 'variable', begin: '<', end: '>' }, hljs.HASH_COMMENT_MODE, { className: 'string', begin: '"""', end: '"""' }, hljs.QUOTE_STRING_MODE ] }; } },{name:"glsl",create:/* Language: GLSL Description: OpenGL Shading Language Author: Sergey Tikhomirov Category: graphics */ function(hljs) { return { keywords: { keyword: // Statements 'break continue discard do else for if return while switch case default ' + // Qualifiers 'attribute binding buffer ccw centroid centroid varying coherent column_major const cw ' + 'depth_any depth_greater depth_less depth_unchanged early_fragment_tests equal_spacing ' + 'flat fractional_even_spacing fractional_odd_spacing highp in index inout invariant ' + 'invocations isolines layout line_strip lines lines_adjacency local_size_x local_size_y ' + 'local_size_z location lowp max_vertices mediump noperspective offset origin_upper_left ' + 'out packed patch pixel_center_integer point_mode points precise precision quads r11f_g11f_b10f '+ 'r16 r16_snorm r16f r16i r16ui r32f r32i r32ui r8 r8_snorm r8i r8ui readonly restrict ' + 'rg16 rg16_snorm rg16f rg16i rg16ui rg32f rg32i rg32ui rg8 rg8_snorm rg8i rg8ui rgb10_a2 ' + 'rgb10_a2ui rgba16 rgba16_snorm rgba16f rgba16i rgba16ui rgba32f rgba32i rgba32ui rgba8 ' + 'rgba8_snorm rgba8i rgba8ui row_major sample shared smooth std140 std430 stream triangle_strip ' + 'triangles triangles_adjacency uniform varying vertices volatile writeonly', type: 'atomic_uint bool bvec2 bvec3 bvec4 dmat2 dmat2x2 dmat2x3 dmat2x4 dmat3 dmat3x2 dmat3x3 ' + 'dmat3x4 dmat4 dmat4x2 dmat4x3 dmat4x4 double dvec2 dvec3 dvec4 float iimage1D iimage1DArray ' + 'iimage2D iimage2DArray iimage2DMS iimage2DMSArray iimage2DRect iimage3D iimageBuffer' + 'iimageCube iimageCubeArray image1D image1DArray image2D image2DArray image2DMS image2DMSArray ' + 'image2DRect image3D imageBuffer imageCube imageCubeArray int isampler1D isampler1DArray ' + 'isampler2D isampler2DArray isampler2DMS isampler2DMSArray isampler2DRect isampler3D ' + 'isamplerBuffer isamplerCube isamplerCubeArray ivec2 ivec3 ivec4 mat2 mat2x2 mat2x3 ' + 'mat2x4 mat3 mat3x2 mat3x3 mat3x4 mat4 mat4x2 mat4x3 mat4x4 sampler1D sampler1DArray ' + 'sampler1DArrayShadow sampler1DShadow sampler2D sampler2DArray sampler2DArrayShadow ' + 'sampler2DMS sampler2DMSArray sampler2DRect sampler2DRectShadow sampler2DShadow sampler3D ' + 'samplerBuffer samplerCube samplerCubeArray samplerCubeArrayShadow samplerCubeShadow ' + 'image1D uimage1DArray uimage2D uimage2DArray uimage2DMS uimage2DMSArray uimage2DRect ' + 'uimage3D uimageBuffer uimageCube uimageCubeArray uint usampler1D usampler1DArray ' + 'usampler2D usampler2DArray usampler2DMS usampler2DMSArray usampler2DRect usampler3D ' + 'samplerBuffer usamplerCube usamplerCubeArray uvec2 uvec3 uvec4 vec2 vec3 vec4 void', built_in: // Constants 'gl_MaxAtomicCounterBindings gl_MaxAtomicCounterBufferSize gl_MaxClipDistances gl_MaxClipPlanes ' + 'gl_MaxCombinedAtomicCounterBuffers gl_MaxCombinedAtomicCounters gl_MaxCombinedImageUniforms ' + 'gl_MaxCombinedImageUnitsAndFragmentOutputs gl_MaxCombinedTextureImageUnits gl_MaxComputeAtomicCounterBuffers ' + 'gl_MaxComputeAtomicCounters gl_MaxComputeImageUniforms gl_MaxComputeTextureImageUnits ' + 'gl_MaxComputeUniformComponents gl_MaxComputeWorkGroupCount gl_MaxComputeWorkGroupSize ' + 'gl_MaxDrawBuffers gl_MaxFragmentAtomicCounterBuffers gl_MaxFragmentAtomicCounters ' + 'gl_MaxFragmentImageUniforms gl_MaxFragmentInputComponents gl_MaxFragmentInputVectors ' + 'gl_MaxFragmentUniformComponents gl_MaxFragmentUniformVectors gl_MaxGeometryAtomicCounterBuffers ' + 'gl_MaxGeometryAtomicCounters gl_MaxGeometryImageUniforms gl_MaxGeometryInputComponents ' + 'gl_MaxGeometryOutputComponents gl_MaxGeometryOutputVertices gl_MaxGeometryTextureImageUnits ' + 'gl_MaxGeometryTotalOutputComponents gl_MaxGeometryUniformComponents gl_MaxGeometryVaryingComponents ' + 'gl_MaxImageSamples gl_MaxImageUnits gl_MaxLights gl_MaxPatchVertices gl_MaxProgramTexelOffset ' + 'gl_MaxTessControlAtomicCounterBuffers gl_MaxTessControlAtomicCounters gl_MaxTessControlImageUniforms ' + 'gl_MaxTessControlInputComponents gl_MaxTessControlOutputComponents gl_MaxTessControlTextureImageUnits ' + 'gl_MaxTessControlTotalOutputComponents gl_MaxTessControlUniformComponents ' + 'gl_MaxTessEvaluationAtomicCounterBuffers gl_MaxTessEvaluationAtomicCounters ' + 'gl_MaxTessEvaluationImageUniforms gl_MaxTessEvaluationInputComponents gl_MaxTessEvaluationOutputComponents ' + 'gl_MaxTessEvaluationTextureImageUnits gl_MaxTessEvaluationUniformComponents ' + 'gl_MaxTessGenLevel gl_MaxTessPatchComponents gl_MaxTextureCoords gl_MaxTextureImageUnits ' + 'gl_MaxTextureUnits gl_MaxVaryingComponents gl_MaxVaryingFloats gl_MaxVaryingVectors ' + 'gl_MaxVertexAtomicCounterBuffers gl_MaxVertexAtomicCounters gl_MaxVertexAttribs gl_MaxVertexImageUniforms ' + 'gl_MaxVertexOutputComponents gl_MaxVertexOutputVectors gl_MaxVertexTextureImageUnits ' + 'gl_MaxVertexUniformComponents gl_MaxVertexUniformVectors gl_MaxViewports gl_MinProgramTexelOffset ' + // Variables 'gl_BackColor gl_BackLightModelProduct gl_BackLightProduct gl_BackMaterial ' + 'gl_BackSecondaryColor gl_ClipDistance gl_ClipPlane gl_ClipVertex gl_Color ' + 'gl_DepthRange gl_EyePlaneQ gl_EyePlaneR gl_EyePlaneS gl_EyePlaneT gl_Fog gl_FogCoord ' + 'gl_FogFragCoord gl_FragColor gl_FragCoord gl_FragData gl_FragDepth gl_FrontColor ' + 'gl_FrontFacing gl_FrontLightModelProduct gl_FrontLightProduct gl_FrontMaterial ' + 'gl_FrontSecondaryColor gl_GlobalInvocationID gl_InstanceID gl_InvocationID gl_Layer gl_LightModel ' + 'gl_LightSource gl_LocalInvocationID gl_LocalInvocationIndex gl_ModelViewMatrix ' + 'gl_ModelViewMatrixInverse gl_ModelViewMatrixInverseTranspose gl_ModelViewMatrixTranspose ' + 'gl_ModelViewProjectionMatrix gl_ModelViewProjectionMatrixInverse gl_ModelViewProjectionMatrixInverseTranspose ' + 'gl_ModelViewProjectionMatrixTranspose gl_MultiTexCoord0 gl_MultiTexCoord1 gl_MultiTexCoord2 ' + 'gl_MultiTexCoord3 gl_MultiTexCoord4 gl_MultiTexCoord5 gl_MultiTexCoord6 gl_MultiTexCoord7 ' + 'gl_Normal gl_NormalMatrix gl_NormalScale gl_NumSamples gl_NumWorkGroups gl_ObjectPlaneQ ' + 'gl_ObjectPlaneR gl_ObjectPlaneS gl_ObjectPlaneT gl_PatchVerticesIn gl_Point gl_PointCoord ' + 'gl_PointSize gl_Position gl_PrimitiveID gl_PrimitiveIDIn gl_ProjectionMatrix gl_ProjectionMatrixInverse ' + 'gl_ProjectionMatrixInverseTranspose gl_ProjectionMatrixTranspose gl_SampleID gl_SampleMask ' + 'gl_SampleMaskIn gl_SamplePosition gl_SecondaryColor gl_TessCoord gl_TessLevelInner gl_TessLevelOuter ' + 'gl_TexCoord gl_TextureEnvColor gl_TextureMatrix gl_TextureMatrixInverse gl_TextureMatrixInverseTranspose ' + 'gl_TextureMatrixTranspose gl_Vertex gl_VertexID gl_ViewportIndex gl_WorkGroupID gl_WorkGroupSize gl_in gl_out ' + // Functions 'EmitStreamVertex EmitVertex EndPrimitive EndStreamPrimitive abs acos acosh all any asin ' + 'asinh atan atanh atomicAdd atomicAnd atomicCompSwap atomicCounter atomicCounterDecrement ' + 'atomicCounterIncrement atomicExchange atomicMax atomicMin atomicOr atomicXor barrier ' + 'bitCount bitfieldExtract bitfieldInsert bitfieldReverse ceil clamp cos cosh cross ' + 'dFdx dFdy degrees determinant distance dot equal exp exp2 faceforward findLSB findMSB ' + 'floatBitsToInt floatBitsToUint floor fma fract frexp ftransform fwidth greaterThan ' + 'greaterThanEqual groupMemoryBarrier imageAtomicAdd imageAtomicAnd imageAtomicCompSwap ' + 'imageAtomicExchange imageAtomicMax imageAtomicMin imageAtomicOr imageAtomicXor imageLoad ' + 'imageSize imageStore imulExtended intBitsToFloat interpolateAtCentroid interpolateAtOffset ' + 'interpolateAtSample inverse inversesqrt isinf isnan ldexp length lessThan lessThanEqual log ' + 'log2 matrixCompMult max memoryBarrier memoryBarrierAtomicCounter memoryBarrierBuffer ' + 'memoryBarrierImage memoryBarrierShared min mix mod modf noise1 noise2 noise3 noise4 ' + 'normalize not notEqual outerProduct packDouble2x32 packHalf2x16 packSnorm2x16 packSnorm4x8 ' + 'packUnorm2x16 packUnorm4x8 pow radians reflect refract round roundEven shadow1D shadow1DLod ' + 'shadow1DProj shadow1DProjLod shadow2D shadow2DLod shadow2DProj shadow2DProjLod sign sin sinh ' + 'smoothstep sqrt step tan tanh texelFetch texelFetchOffset texture texture1D texture1DLod ' + 'texture1DProj texture1DProjLod texture2D texture2DLod texture2DProj texture2DProjLod ' + 'texture3D texture3DLod texture3DProj texture3DProjLod textureCube textureCubeLod ' + 'textureGather textureGatherOffset textureGatherOffsets textureGrad textureGradOffset ' + 'textureLod textureLodOffset textureOffset textureProj textureProjGrad textureProjGradOffset ' + 'textureProjLod textureProjLodOffset textureProjOffset textureQueryLevels textureQueryLod ' + 'textureSize transpose trunc uaddCarry uintBitsToFloat umulExtended unpackDouble2x32 ' + 'unpackHalf2x16 unpackSnorm2x16 unpackSnorm4x8 unpackUnorm2x16 unpackUnorm4x8 usubBorrow', literal: 'true false' }, illegal: '"', contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.C_NUMBER_MODE, { className: 'meta', begin: '#', end: '$' } ] }; } },{name:"gml",create:/* Language: GML Author: Meseta Description: Game Maker Language for GameMaker Studio 2 Category: scripting */ function(hljs) { var GML_KEYWORDS = { keywords: 'begin end if then else while do for break continue with until ' + 'repeat exit and or xor not return mod div switch case default var ' + 'globalvar enum #macro #region #endregion', built_in: 'is_real is_string is_array is_undefined is_int32 is_int64 ' + 'is_ptr is_vec3 is_vec4 is_matrix is_bool typeof ' + 'variable_global_exists variable_global_get variable_global_set ' + 'variable_instance_exists variable_instance_get variable_instance_set ' + 'variable_instance_get_names array_length_1d array_length_2d ' + 'array_height_2d array_equals array_create array_copy random ' + 'random_range irandom irandom_range random_set_seed random_get_seed ' + 'randomize randomise choose abs round floor ceil sign frac sqrt sqr ' + 'exp ln log2 log10 sin cos tan arcsin arccos arctan arctan2 dsin dcos ' + 'dtan darcsin darccos darctan darctan2 degtorad radtodeg power logn ' + 'min max mean median clamp lerp dot_product dot_product_3d ' + 'dot_product_normalised dot_product_3d_normalised ' + 'dot_product_normalized dot_product_3d_normalized math_set_epsilon ' + 'math_get_epsilon angle_difference point_distance_3d point_distance ' + 'point_direction lengthdir_x lengthdir_y real string int64 ptr ' + 'string_format chr ansi_char ord string_length string_byte_length ' + 'string_pos string_copy string_char_at string_ord_at string_byte_at ' + 'string_set_byte_at string_delete string_insert string_lower ' + 'string_upper string_repeat string_letters string_digits ' + 'string_lettersdigits string_replace string_replace_all string_count ' + 'string_hash_to_newline clipboard_has_text clipboard_set_text ' + 'clipboard_get_text date_current_datetime date_create_datetime ' + 'date_valid_datetime date_inc_year date_inc_month date_inc_week ' + 'date_inc_day date_inc_hour date_inc_minute date_inc_second ' + 'date_get_year date_get_month date_get_week date_get_day ' + 'date_get_hour date_get_minute date_get_second date_get_weekday ' + 'date_get_day_of_year date_get_hour_of_year date_get_minute_of_year ' + 'date_get_second_of_year date_year_span date_month_span ' + 'date_week_span date_day_span date_hour_span date_minute_span ' + 'date_second_span date_compare_datetime date_compare_date ' + 'date_compare_time date_date_of date_time_of date_datetime_string ' + 'date_date_string date_time_string date_days_in_month ' + 'date_days_in_year date_leap_year date_is_today date_set_timezone ' + 'date_get_timezone game_set_speed game_get_speed motion_set ' + 'motion_add place_free place_empty place_meeting place_snapped ' + 'move_random move_snap move_towards_point move_contact_solid ' + 'move_contact_all move_outside_solid move_outside_all ' + 'move_bounce_solid move_bounce_all move_wrap distance_to_point ' + 'distance_to_object position_empty position_meeting path_start ' + 'path_end mp_linear_step mp_potential_step mp_linear_step_object ' + 'mp_potential_step_object mp_potential_settings mp_linear_path ' + 'mp_potential_path mp_linear_path_object mp_potential_path_object ' + 'mp_grid_create mp_grid_destroy mp_grid_clear_all mp_grid_clear_cell ' + 'mp_grid_clear_rectangle mp_grid_add_cell mp_grid_get_cell ' + 'mp_grid_add_rectangle mp_grid_add_instances mp_grid_path ' + 'mp_grid_draw mp_grid_to_ds_grid collision_point collision_rectangle ' + 'collision_circle collision_ellipse collision_line ' + 'collision_point_list collision_rectangle_list collision_circle_list ' + 'collision_ellipse_list collision_line_list instance_position_list ' + 'instance_place_list point_in_rectangle ' + 'point_in_triangle point_in_circle rectangle_in_rectangle ' + 'rectangle_in_triangle rectangle_in_circle instance_find ' + 'instance_exists instance_number instance_position instance_nearest ' + 'instance_furthest instance_place instance_create_depth ' + 'instance_create_layer instance_copy instance_change instance_destroy ' + 'position_destroy position_change instance_id_get ' + 'instance_deactivate_all instance_deactivate_object ' + 'instance_deactivate_region instance_activate_all ' + 'instance_activate_object instance_activate_region room_goto ' + 'room_goto_previous room_goto_next room_previous room_next ' + 'room_restart game_end game_restart game_load game_save ' + 'game_save_buffer game_load_buffer event_perform event_user ' + 'event_perform_object event_inherited show_debug_message ' + 'show_debug_overlay debug_event debug_get_callstack alarm_get ' + 'alarm_set font_texture_page_size keyboard_set_map keyboard_get_map ' + 'keyboard_unset_map keyboard_check keyboard_check_pressed ' + 'keyboard_check_released keyboard_check_direct keyboard_get_numlock ' + 'keyboard_set_numlock keyboard_key_press keyboard_key_release ' + 'keyboard_clear io_clear mouse_check_button ' + 'mouse_check_button_pressed mouse_check_button_released ' + 'mouse_wheel_up mouse_wheel_down mouse_clear draw_self draw_sprite ' + 'draw_sprite_pos draw_sprite_ext draw_sprite_stretched ' + 'draw_sprite_stretched_ext draw_sprite_tiled draw_sprite_tiled_ext ' + 'draw_sprite_part draw_sprite_part_ext draw_sprite_general draw_clear ' + 'draw_clear_alpha draw_point draw_line draw_line_width draw_rectangle ' + 'draw_roundrect draw_roundrect_ext draw_triangle draw_circle ' + 'draw_ellipse draw_set_circle_precision draw_arrow draw_button ' + 'draw_path draw_healthbar draw_getpixel draw_getpixel_ext ' + 'draw_set_colour draw_set_color draw_set_alpha draw_get_colour ' + 'draw_get_color draw_get_alpha merge_colour make_colour_rgb ' + 'make_colour_hsv colour_get_red colour_get_green colour_get_blue ' + 'colour_get_hue colour_get_saturation colour_get_value merge_color ' + 'make_color_rgb make_color_hsv color_get_red color_get_green ' + 'color_get_blue color_get_hue color_get_saturation color_get_value ' + 'merge_color screen_save screen_save_part draw_set_font ' + 'draw_set_halign draw_set_valign draw_text draw_text_ext string_width ' + 'string_height string_width_ext string_height_ext ' + 'draw_text_transformed draw_text_ext_transformed draw_text_colour ' + 'draw_text_ext_colour draw_text_transformed_colour ' + 'draw_text_ext_transformed_colour draw_text_color draw_text_ext_color ' + 'draw_text_transformed_color draw_text_ext_transformed_color ' + 'draw_point_colour draw_line_colour draw_line_width_colour ' + 'draw_rectangle_colour draw_roundrect_colour ' + 'draw_roundrect_colour_ext draw_triangle_colour draw_circle_colour ' + 'draw_ellipse_colour draw_point_color draw_line_color ' + 'draw_line_width_color draw_rectangle_color draw_roundrect_color ' + 'draw_roundrect_color_ext draw_triangle_color draw_circle_color ' + 'draw_ellipse_color draw_primitive_begin draw_vertex ' + 'draw_vertex_colour draw_vertex_color draw_primitive_end ' + 'sprite_get_uvs font_get_uvs sprite_get_texture font_get_texture ' + 'texture_get_width texture_get_height texture_get_uvs ' + 'draw_primitive_begin_texture draw_vertex_texture ' + 'draw_vertex_texture_colour draw_vertex_texture_color ' + 'texture_global_scale surface_create surface_create_ext ' + 'surface_resize surface_free surface_exists surface_get_width ' + 'surface_get_height surface_get_texture surface_set_target ' + 'surface_set_target_ext surface_reset_target surface_depth_disable ' + 'surface_get_depth_disable draw_surface draw_surface_stretched ' + 'draw_surface_tiled draw_surface_part draw_surface_ext ' + 'draw_surface_stretched_ext draw_surface_tiled_ext ' + 'draw_surface_part_ext draw_surface_general surface_getpixel ' + 'surface_getpixel_ext surface_save surface_save_part surface_copy ' + 'surface_copy_part application_surface_draw_enable ' + 'application_get_position application_surface_enable ' + 'application_surface_is_enabled display_get_width display_get_height ' + 'display_get_orientation display_get_gui_width display_get_gui_height ' + 'display_reset display_mouse_get_x display_mouse_get_y ' + 'display_mouse_set display_set_ui_visibility ' + 'window_set_fullscreen window_get_fullscreen ' + 'window_set_caption window_set_min_width window_set_max_width ' + 'window_set_min_height window_set_max_height window_get_visible_rects ' + 'window_get_caption window_set_cursor window_get_cursor ' + 'window_set_colour window_get_colour window_set_color ' + 'window_get_color window_set_position window_set_size ' + 'window_set_rectangle window_center window_get_x window_get_y ' + 'window_get_width window_get_height window_mouse_get_x ' + 'window_mouse_get_y window_mouse_set window_view_mouse_get_x ' + 'window_view_mouse_get_y window_views_mouse_get_x ' + 'window_views_mouse_get_y audio_listener_position ' + 'audio_listener_velocity audio_listener_orientation ' + 'audio_emitter_position audio_emitter_create audio_emitter_free ' + 'audio_emitter_exists audio_emitter_pitch audio_emitter_velocity ' + 'audio_emitter_falloff audio_emitter_gain audio_play_sound ' + 'audio_play_sound_on audio_play_sound_at audio_stop_sound ' + 'audio_resume_music audio_music_is_playing audio_resume_sound ' + 'audio_pause_sound audio_pause_music audio_channel_num ' + 'audio_sound_length audio_get_type audio_falloff_set_model ' + 'audio_play_music audio_stop_music audio_master_gain audio_music_gain ' + 'audio_sound_gain audio_sound_pitch audio_stop_all audio_resume_all ' + 'audio_pause_all audio_is_playing audio_is_paused audio_exists ' + 'audio_sound_set_track_position audio_sound_get_track_position ' + 'audio_emitter_get_gain audio_emitter_get_pitch audio_emitter_get_x ' + 'audio_emitter_get_y audio_emitter_get_z audio_emitter_get_vx ' + 'audio_emitter_get_vy audio_emitter_get_vz ' + 'audio_listener_set_position audio_listener_set_velocity ' + 'audio_listener_set_orientation audio_listener_get_data ' + 'audio_set_master_gain audio_get_master_gain audio_sound_get_gain ' + 'audio_sound_get_pitch audio_get_name audio_sound_set_track_position ' + 'audio_sound_get_track_position audio_create_stream ' + 'audio_destroy_stream audio_create_sync_group ' + 'audio_destroy_sync_group audio_play_in_sync_group ' + 'audio_start_sync_group audio_stop_sync_group audio_pause_sync_group ' + 'audio_resume_sync_group audio_sync_group_get_track_pos ' + 'audio_sync_group_debug audio_sync_group_is_playing audio_debug ' + 'audio_group_load audio_group_unload audio_group_is_loaded ' + 'audio_group_load_progress audio_group_name audio_group_stop_all ' + 'audio_group_set_gain audio_create_buffer_sound ' + 'audio_free_buffer_sound audio_create_play_queue ' + 'audio_free_play_queue audio_queue_sound audio_get_recorder_count ' + 'audio_get_recorder_info audio_start_recording audio_stop_recording ' + 'audio_sound_get_listener_mask audio_emitter_get_listener_mask ' + 'audio_get_listener_mask audio_sound_set_listener_mask ' + 'audio_emitter_set_listener_mask audio_set_listener_mask ' + 'audio_get_listener_count audio_get_listener_info audio_system ' + 'show_message show_message_async clickable_add clickable_add_ext ' + 'clickable_change clickable_change_ext clickable_delete ' + 'clickable_exists clickable_set_style show_question ' + 'show_question_async get_integer get_string get_integer_async ' + 'get_string_async get_login_async get_open_filename get_save_filename ' + 'get_open_filename_ext get_save_filename_ext show_error ' + 'highscore_clear highscore_add highscore_value highscore_name ' + 'draw_highscore sprite_exists sprite_get_name sprite_get_number ' + 'sprite_get_width sprite_get_height sprite_get_xoffset ' + 'sprite_get_yoffset sprite_get_bbox_left sprite_get_bbox_right ' + 'sprite_get_bbox_top sprite_get_bbox_bottom sprite_save ' + 'sprite_save_strip sprite_set_cache_size sprite_set_cache_size_ext ' + 'sprite_get_tpe sprite_prefetch sprite_prefetch_multi sprite_flush ' + 'sprite_flush_multi sprite_set_speed sprite_get_speed_type ' + 'sprite_get_speed font_exists font_get_name font_get_fontname ' + 'font_get_bold font_get_italic font_get_first font_get_last ' + 'font_get_size font_set_cache_size path_exists path_get_name ' + 'path_get_length path_get_time path_get_kind path_get_closed ' + 'path_get_precision path_get_number path_get_point_x path_get_point_y ' + 'path_get_point_speed path_get_x path_get_y path_get_speed ' + 'script_exists script_get_name timeline_add timeline_delete ' + 'timeline_clear timeline_exists timeline_get_name ' + 'timeline_moment_clear timeline_moment_add_script timeline_size ' + 'timeline_max_moment object_exists object_get_name object_get_sprite ' + 'object_get_solid object_get_visible object_get_persistent ' + 'object_get_mask object_get_parent object_get_physics ' + 'object_is_ancestor room_exists room_get_name sprite_set_offset ' + 'sprite_duplicate sprite_assign sprite_merge sprite_add ' + 'sprite_replace sprite_create_from_surface sprite_add_from_surface ' + 'sprite_delete sprite_set_alpha_from_sprite sprite_collision_mask ' + 'font_add_enable_aa font_add_get_enable_aa font_add font_add_sprite ' + 'font_add_sprite_ext font_replace font_replace_sprite ' + 'font_replace_sprite_ext font_delete path_set_kind path_set_closed ' + 'path_set_precision path_add path_assign path_duplicate path_append ' + 'path_delete path_add_point path_insert_point path_change_point ' + 'path_delete_point path_clear_points path_reverse path_mirror ' + 'path_flip path_rotate path_rescale path_shift script_execute ' + 'object_set_sprite object_set_solid object_set_visible ' + 'object_set_persistent object_set_mask room_set_width room_set_height ' + 'room_set_persistent room_set_background_colour ' + 'room_set_background_color room_set_view room_set_viewport ' + 'room_get_viewport room_set_view_enabled room_add room_duplicate ' + 'room_assign room_instance_add room_instance_clear room_get_camera ' + 'room_set_camera asset_get_index asset_get_type ' + 'file_text_open_from_string file_text_open_read file_text_open_write ' + 'file_text_open_append file_text_close file_text_write_string ' + 'file_text_write_real file_text_writeln file_text_read_string ' + 'file_text_read_real file_text_readln file_text_eof file_text_eoln ' + 'file_exists file_delete file_rename file_copy directory_exists ' + 'directory_create directory_destroy file_find_first file_find_next ' + 'file_find_close file_attributes filename_name filename_path ' + 'filename_dir filename_drive filename_ext filename_change_ext ' + 'file_bin_open file_bin_rewrite file_bin_close file_bin_position ' + 'file_bin_size file_bin_seek file_bin_write_byte file_bin_read_byte ' + 'parameter_count parameter_string environment_get_variable ' + 'ini_open_from_string ini_open ini_close ini_read_string ' + 'ini_read_real ini_write_string ini_write_real ini_key_exists ' + 'ini_section_exists ini_key_delete ini_section_delete ' + 'ds_set_precision ds_exists ds_stack_create ds_stack_destroy ' + 'ds_stack_clear ds_stack_copy ds_stack_size ds_stack_empty ' + 'ds_stack_push ds_stack_pop ds_stack_top ds_stack_write ds_stack_read ' + 'ds_queue_create ds_queue_destroy ds_queue_clear ds_queue_copy ' + 'ds_queue_size ds_queue_empty ds_queue_enqueue ds_queue_dequeue ' + 'ds_queue_head ds_queue_tail ds_queue_write ds_queue_read ' + 'ds_list_create ds_list_destroy ds_list_clear ds_list_copy ' + 'ds_list_size ds_list_empty ds_list_add ds_list_insert ' + 'ds_list_replace ds_list_delete ds_list_find_index ds_list_find_value ' + 'ds_list_mark_as_list ds_list_mark_as_map ds_list_sort ' + 'ds_list_shuffle ds_list_write ds_list_read ds_list_set ds_map_create ' + 'ds_map_destroy ds_map_clear ds_map_copy ds_map_size ds_map_empty ' + 'ds_map_add ds_map_add_list ds_map_add_map ds_map_replace ' + 'ds_map_replace_map ds_map_replace_list ds_map_delete ds_map_exists ' + 'ds_map_find_value ds_map_find_previous ds_map_find_next ' + 'ds_map_find_first ds_map_find_last ds_map_write ds_map_read ' + 'ds_map_secure_save ds_map_secure_load ds_map_secure_load_buffer ' + 'ds_map_secure_save_buffer ds_map_set ds_priority_create ' + 'ds_priority_destroy ds_priority_clear ds_priority_copy ' + 'ds_priority_size ds_priority_empty ds_priority_add ' + 'ds_priority_change_priority ds_priority_find_priority ' + 'ds_priority_delete_value ds_priority_delete_min ds_priority_find_min ' + 'ds_priority_delete_max ds_priority_find_max ds_priority_write ' + 'ds_priority_read ds_grid_create ds_grid_destroy ds_grid_copy ' + 'ds_grid_resize ds_grid_width ds_grid_height ds_grid_clear ' + 'ds_grid_set ds_grid_add ds_grid_multiply ds_grid_set_region ' + 'ds_grid_add_region ds_grid_multiply_region ds_grid_set_disk ' + 'ds_grid_add_disk ds_grid_multiply_disk ds_grid_set_grid_region ' + 'ds_grid_add_grid_region ds_grid_multiply_grid_region ds_grid_get ' + 'ds_grid_get_sum ds_grid_get_max ds_grid_get_min ds_grid_get_mean ' + 'ds_grid_get_disk_sum ds_grid_get_disk_min ds_grid_get_disk_max ' + 'ds_grid_get_disk_mean ds_grid_value_exists ds_grid_value_x ' + 'ds_grid_value_y ds_grid_value_disk_exists ds_grid_value_disk_x ' + 'ds_grid_value_disk_y ds_grid_shuffle ds_grid_write ds_grid_read ' + 'ds_grid_sort ds_grid_set ds_grid_get effect_create_below ' + 'effect_create_above effect_clear part_type_create part_type_destroy ' + 'part_type_exists part_type_clear part_type_shape part_type_sprite ' + 'part_type_size part_type_scale part_type_orientation part_type_life ' + 'part_type_step part_type_death part_type_speed part_type_direction ' + 'part_type_gravity part_type_colour1 part_type_colour2 ' + 'part_type_colour3 part_type_colour_mix part_type_colour_rgb ' + 'part_type_colour_hsv part_type_color1 part_type_color2 ' + 'part_type_color3 part_type_color_mix part_type_color_rgb ' + 'part_type_color_hsv part_type_alpha1 part_type_alpha2 ' + 'part_type_alpha3 part_type_blend part_system_create ' + 'part_system_create_layer part_system_destroy part_system_exists ' + 'part_system_clear part_system_draw_order part_system_depth ' + 'part_system_position part_system_automatic_update ' + 'part_system_automatic_draw part_system_update part_system_drawit ' + 'part_system_get_layer part_system_layer part_particles_create ' + 'part_particles_create_colour part_particles_create_color ' + 'part_particles_clear part_particles_count part_emitter_create ' + 'part_emitter_destroy part_emitter_destroy_all part_emitter_exists ' + 'part_emitter_clear part_emitter_region part_emitter_burst ' + 'part_emitter_stream external_call external_define external_free ' + 'window_handle window_device matrix_get matrix_set ' + 'matrix_build_identity matrix_build matrix_build_lookat ' + 'matrix_build_projection_ortho matrix_build_projection_perspective ' + 'matrix_build_projection_perspective_fov matrix_multiply ' + 'matrix_transform_vertex matrix_stack_push matrix_stack_pop ' + 'matrix_stack_multiply matrix_stack_set matrix_stack_clear ' + 'matrix_stack_top matrix_stack_is_empty browser_input_capture ' + 'os_get_config os_get_info os_get_language os_get_region ' + 'os_lock_orientation display_get_dpi_x display_get_dpi_y ' + 'display_set_gui_size display_set_gui_maximise ' + 'display_set_gui_maximize device_mouse_dbclick_enable ' + 'display_set_timing_method display_get_timing_method ' + 'display_set_sleep_margin display_get_sleep_margin virtual_key_add ' + 'virtual_key_hide virtual_key_delete virtual_key_show ' + 'draw_enable_drawevent draw_enable_swf_aa draw_set_swf_aa_level ' + 'draw_get_swf_aa_level draw_texture_flush draw_flush ' + 'gpu_set_blendenable gpu_set_ztestenable gpu_set_zfunc ' + 'gpu_set_zwriteenable gpu_set_lightingenable gpu_set_fog ' + 'gpu_set_cullmode gpu_set_blendmode gpu_set_blendmode_ext ' + 'gpu_set_blendmode_ext_sepalpha gpu_set_colorwriteenable ' + 'gpu_set_colourwriteenable gpu_set_alphatestenable ' + 'gpu_set_alphatestref gpu_set_alphatestfunc gpu_set_texfilter ' + 'gpu_set_texfilter_ext gpu_set_texrepeat gpu_set_texrepeat_ext ' + 'gpu_set_tex_filter gpu_set_tex_filter_ext gpu_set_tex_repeat ' + 'gpu_set_tex_repeat_ext gpu_set_tex_mip_filter ' + 'gpu_set_tex_mip_filter_ext gpu_set_tex_mip_bias ' + 'gpu_set_tex_mip_bias_ext gpu_set_tex_min_mip gpu_set_tex_min_mip_ext ' + 'gpu_set_tex_max_mip gpu_set_tex_max_mip_ext gpu_set_tex_max_aniso ' + 'gpu_set_tex_max_aniso_ext gpu_set_tex_mip_enable ' + 'gpu_set_tex_mip_enable_ext gpu_get_blendenable gpu_get_ztestenable ' + 'gpu_get_zfunc gpu_get_zwriteenable gpu_get_lightingenable ' + 'gpu_get_fog gpu_get_cullmode gpu_get_blendmode gpu_get_blendmode_ext ' + 'gpu_get_blendmode_ext_sepalpha gpu_get_blendmode_src ' + 'gpu_get_blendmode_dest gpu_get_blendmode_srcalpha ' + 'gpu_get_blendmode_destalpha gpu_get_colorwriteenable ' + 'gpu_get_colourwriteenable gpu_get_alphatestenable ' + 'gpu_get_alphatestref gpu_get_alphatestfunc gpu_get_texfilter ' + 'gpu_get_texfilter_ext gpu_get_texrepeat gpu_get_texrepeat_ext ' + 'gpu_get_tex_filter gpu_get_tex_filter_ext gpu_get_tex_repeat ' + 'gpu_get_tex_repeat_ext gpu_get_tex_mip_filter ' + 'gpu_get_tex_mip_filter_ext gpu_get_tex_mip_bias ' + 'gpu_get_tex_mip_bias_ext gpu_get_tex_min_mip gpu_get_tex_min_mip_ext ' + 'gpu_get_tex_max_mip gpu_get_tex_max_mip_ext gpu_get_tex_max_aniso ' + 'gpu_get_tex_max_aniso_ext gpu_get_tex_mip_enable ' + 'gpu_get_tex_mip_enable_ext gpu_push_state gpu_pop_state ' + 'gpu_get_state gpu_set_state draw_light_define_ambient ' + 'draw_light_define_direction draw_light_define_point ' + 'draw_light_enable draw_set_lighting draw_light_get_ambient ' + 'draw_light_get draw_get_lighting shop_leave_rating url_get_domain ' + 'url_open url_open_ext url_open_full get_timer achievement_login ' + 'achievement_logout achievement_post achievement_increment ' + 'achievement_post_score achievement_available ' + 'achievement_show_achievements achievement_show_leaderboards ' + 'achievement_load_friends achievement_load_leaderboard ' + 'achievement_send_challenge achievement_load_progress ' + 'achievement_reset achievement_login_status achievement_get_pic ' + 'achievement_show_challenge_notifications achievement_get_challenges ' + 'achievement_event achievement_show achievement_get_info ' + 'cloud_file_save cloud_string_save cloud_synchronise ads_enable ' + 'ads_disable ads_setup ads_engagement_launch ads_engagement_available ' + 'ads_engagement_active ads_event ads_event_preload ' + 'ads_set_reward_callback ads_get_display_height ads_get_display_width ' + 'ads_move ads_interstitial_available ads_interstitial_display ' + 'device_get_tilt_x device_get_tilt_y device_get_tilt_z ' + 'device_is_keypad_open device_mouse_check_button ' + 'device_mouse_check_button_pressed device_mouse_check_button_released ' + 'device_mouse_x device_mouse_y device_mouse_raw_x device_mouse_raw_y ' + 'device_mouse_x_to_gui device_mouse_y_to_gui iap_activate iap_status ' + 'iap_enumerate_products iap_restore_all iap_acquire iap_consume ' + 'iap_product_details iap_purchase_details facebook_init ' + 'facebook_login facebook_status facebook_graph_request ' + 'facebook_dialog facebook_logout facebook_launch_offerwall ' + 'facebook_post_message facebook_send_invite facebook_user_id ' + 'facebook_accesstoken facebook_check_permission ' + 'facebook_request_read_permissions ' + 'facebook_request_publish_permissions gamepad_is_supported ' + 'gamepad_get_device_count gamepad_is_connected ' + 'gamepad_get_description gamepad_get_button_threshold ' + 'gamepad_set_button_threshold gamepad_get_axis_deadzone ' + 'gamepad_set_axis_deadzone gamepad_button_count gamepad_button_check ' + 'gamepad_button_check_pressed gamepad_button_check_released ' + 'gamepad_button_value gamepad_axis_count gamepad_axis_value ' + 'gamepad_set_vibration gamepad_set_colour gamepad_set_color ' + 'os_is_paused window_has_focus code_is_compiled http_get ' + 'http_get_file http_post_string http_request json_encode json_decode ' + 'zip_unzip load_csv base64_encode base64_decode md5_string_unicode ' + 'md5_string_utf8 md5_file os_is_network_connected sha1_string_unicode ' + 'sha1_string_utf8 sha1_file os_powersave_enable analytics_event ' + 'analytics_event_ext win8_livetile_tile_notification ' + 'win8_livetile_tile_clear win8_livetile_badge_notification ' + 'win8_livetile_badge_clear win8_livetile_queue_enable ' + 'win8_secondarytile_pin win8_secondarytile_badge_notification ' + 'win8_secondarytile_delete win8_livetile_notification_begin ' + 'win8_livetile_notification_secondary_begin ' + 'win8_livetile_notification_expiry win8_livetile_notification_tag ' + 'win8_livetile_notification_text_add ' + 'win8_livetile_notification_image_add win8_livetile_notification_end ' + 'win8_appbar_enable win8_appbar_add_element ' + 'win8_appbar_remove_element win8_settingscharm_add_entry ' + 'win8_settingscharm_add_html_entry win8_settingscharm_add_xaml_entry ' + 'win8_settingscharm_set_xaml_property ' + 'win8_settingscharm_get_xaml_property win8_settingscharm_remove_entry ' + 'win8_share_image win8_share_screenshot win8_share_file ' + 'win8_share_url win8_share_text win8_search_enable ' + 'win8_search_disable win8_search_add_suggestions ' + 'win8_device_touchscreen_available win8_license_initialize_sandbox ' + 'win8_license_trial_version winphone_license_trial_version ' + 'winphone_tile_title winphone_tile_count winphone_tile_back_title ' + 'winphone_tile_back_content winphone_tile_back_content_wide ' + 'winphone_tile_front_image winphone_tile_front_image_small ' + 'winphone_tile_front_image_wide winphone_tile_back_image ' + 'winphone_tile_back_image_wide winphone_tile_background_colour ' + 'winphone_tile_background_color winphone_tile_icon_image ' + 'winphone_tile_small_icon_image winphone_tile_wide_content ' + 'winphone_tile_cycle_images winphone_tile_small_background_image ' + 'physics_world_create physics_world_gravity ' + 'physics_world_update_speed physics_world_update_iterations ' + 'physics_world_draw_debug physics_pause_enable physics_fixture_create ' + 'physics_fixture_set_kinematic physics_fixture_set_density ' + 'physics_fixture_set_awake physics_fixture_set_restitution ' + 'physics_fixture_set_friction physics_fixture_set_collision_group ' + 'physics_fixture_set_sensor physics_fixture_set_linear_damping ' + 'physics_fixture_set_angular_damping physics_fixture_set_circle_shape ' + 'physics_fixture_set_box_shape physics_fixture_set_edge_shape ' + 'physics_fixture_set_polygon_shape physics_fixture_set_chain_shape ' + 'physics_fixture_add_point physics_fixture_bind ' + 'physics_fixture_bind_ext physics_fixture_delete physics_apply_force ' + 'physics_apply_impulse physics_apply_angular_impulse ' + 'physics_apply_local_force physics_apply_local_impulse ' + 'physics_apply_torque physics_mass_properties physics_draw_debug ' + 'physics_test_overlap physics_remove_fixture physics_set_friction ' + 'physics_set_density physics_set_restitution physics_get_friction ' + 'physics_get_density physics_get_restitution ' + 'physics_joint_distance_create physics_joint_rope_create ' + 'physics_joint_revolute_create physics_joint_prismatic_create ' + 'physics_joint_pulley_create physics_joint_wheel_create ' + 'physics_joint_weld_create physics_joint_friction_create ' + 'physics_joint_gear_create physics_joint_enable_motor ' + 'physics_joint_get_value physics_joint_set_value physics_joint_delete ' + 'physics_particle_create physics_particle_delete ' + 'physics_particle_delete_region_circle ' + 'physics_particle_delete_region_box ' + 'physics_particle_delete_region_poly physics_particle_set_flags ' + 'physics_particle_set_category_flags physics_particle_draw ' + 'physics_particle_draw_ext physics_particle_count ' + 'physics_particle_get_data physics_particle_get_data_particle ' + 'physics_particle_group_begin physics_particle_group_circle ' + 'physics_particle_group_box physics_particle_group_polygon ' + 'physics_particle_group_add_point physics_particle_group_end ' + 'physics_particle_group_join physics_particle_group_delete ' + 'physics_particle_group_count physics_particle_group_get_data ' + 'physics_particle_group_get_mass physics_particle_group_get_inertia ' + 'physics_particle_group_get_centre_x ' + 'physics_particle_group_get_centre_y physics_particle_group_get_vel_x ' + 'physics_particle_group_get_vel_y physics_particle_group_get_ang_vel ' + 'physics_particle_group_get_x physics_particle_group_get_y ' + 'physics_particle_group_get_angle physics_particle_set_group_flags ' + 'physics_particle_get_group_flags physics_particle_get_max_count ' + 'physics_particle_get_radius physics_particle_get_density ' + 'physics_particle_get_damping physics_particle_get_gravity_scale ' + 'physics_particle_set_max_count physics_particle_set_radius ' + 'physics_particle_set_density physics_particle_set_damping ' + 'physics_particle_set_gravity_scale network_create_socket ' + 'network_create_socket_ext network_create_server ' + 'network_create_server_raw network_connect network_connect_raw ' + 'network_send_packet network_send_raw network_send_broadcast ' + 'network_send_udp network_send_udp_raw network_set_timeout ' + 'network_set_config network_resolve network_destroy buffer_create ' + 'buffer_write buffer_read buffer_seek buffer_get_surface ' + 'buffer_set_surface buffer_delete buffer_exists buffer_get_type ' + 'buffer_get_alignment buffer_poke buffer_peek buffer_save ' + 'buffer_save_ext buffer_load buffer_load_ext buffer_load_partial ' + 'buffer_copy buffer_fill buffer_get_size buffer_tell buffer_resize ' + 'buffer_md5 buffer_sha1 buffer_base64_encode buffer_base64_decode ' + 'buffer_base64_decode_ext buffer_sizeof buffer_get_address ' + 'buffer_create_from_vertex_buffer ' + 'buffer_create_from_vertex_buffer_ext buffer_copy_from_vertex_buffer ' + 'buffer_async_group_begin buffer_async_group_option ' + 'buffer_async_group_end buffer_load_async buffer_save_async ' + 'gml_release_mode gml_pragma steam_activate_overlay ' + 'steam_is_overlay_enabled steam_is_overlay_activated ' + 'steam_get_persona_name steam_initialised ' + 'steam_is_cloud_enabled_for_app steam_is_cloud_enabled_for_account ' + 'steam_file_persisted steam_get_quota_total steam_get_quota_free ' + 'steam_file_write steam_file_write_file steam_file_read ' + 'steam_file_delete steam_file_exists steam_file_size steam_file_share ' + 'steam_is_screenshot_requested steam_send_screenshot ' + 'steam_is_user_logged_on steam_get_user_steam_id steam_user_owns_dlc ' + 'steam_user_installed_dlc steam_set_achievement steam_get_achievement ' + 'steam_clear_achievement steam_set_stat_int steam_set_stat_float ' + 'steam_set_stat_avg_rate steam_get_stat_int steam_get_stat_float ' + 'steam_get_stat_avg_rate steam_reset_all_stats ' + 'steam_reset_all_stats_achievements steam_stats_ready ' + 'steam_create_leaderboard steam_upload_score steam_upload_score_ext ' + 'steam_download_scores_around_user steam_download_scores ' + 'steam_download_friends_scores steam_upload_score_buffer ' + 'steam_upload_score_buffer_ext steam_current_game_language ' + 'steam_available_languages steam_activate_overlay_browser ' + 'steam_activate_overlay_user steam_activate_overlay_store ' + 'steam_get_user_persona_name steam_get_app_id ' + 'steam_get_user_account_id steam_ugc_download steam_ugc_create_item ' + 'steam_ugc_start_item_update steam_ugc_set_item_title ' + 'steam_ugc_set_item_description steam_ugc_set_item_visibility ' + 'steam_ugc_set_item_tags steam_ugc_set_item_content ' + 'steam_ugc_set_item_preview steam_ugc_submit_item_update ' + 'steam_ugc_get_item_update_progress steam_ugc_subscribe_item ' + 'steam_ugc_unsubscribe_item steam_ugc_num_subscribed_items ' + 'steam_ugc_get_subscribed_items steam_ugc_get_item_install_info ' + 'steam_ugc_get_item_update_info steam_ugc_request_item_details ' + 'steam_ugc_create_query_user steam_ugc_create_query_user_ex ' + 'steam_ugc_create_query_all steam_ugc_create_query_all_ex ' + 'steam_ugc_query_set_cloud_filename_filter ' + 'steam_ugc_query_set_match_any_tag steam_ugc_query_set_search_text ' + 'steam_ugc_query_set_ranked_by_trend_days ' + 'steam_ugc_query_add_required_tag steam_ugc_query_add_excluded_tag ' + 'steam_ugc_query_set_return_long_description ' + 'steam_ugc_query_set_return_total_only ' + 'steam_ugc_query_set_allow_cached_response steam_ugc_send_query ' + 'shader_set shader_get_name shader_reset shader_current ' + 'shader_is_compiled shader_get_sampler_index shader_get_uniform ' + 'shader_set_uniform_i shader_set_uniform_i_array shader_set_uniform_f ' + 'shader_set_uniform_f_array shader_set_uniform_matrix ' + 'shader_set_uniform_matrix_array shader_enable_corner_id ' + 'texture_set_stage texture_get_texel_width texture_get_texel_height ' + 'shaders_are_supported vertex_format_begin vertex_format_end ' + 'vertex_format_delete vertex_format_add_position ' + 'vertex_format_add_position_3d vertex_format_add_colour ' + 'vertex_format_add_color vertex_format_add_normal ' + 'vertex_format_add_texcoord vertex_format_add_textcoord ' + 'vertex_format_add_custom vertex_create_buffer ' + 'vertex_create_buffer_ext vertex_delete_buffer vertex_begin ' + 'vertex_end vertex_position vertex_position_3d vertex_colour ' + 'vertex_color vertex_argb vertex_texcoord vertex_normal vertex_float1 ' + 'vertex_float2 vertex_float3 vertex_float4 vertex_ubyte4 ' + 'vertex_submit vertex_freeze vertex_get_number vertex_get_buffer_size ' + 'vertex_create_buffer_from_buffer ' + 'vertex_create_buffer_from_buffer_ext push_local_notification ' + 'push_get_first_local_notification push_get_next_local_notification ' + 'push_cancel_local_notification skeleton_animation_set ' + 'skeleton_animation_get skeleton_animation_mix ' + 'skeleton_animation_set_ext skeleton_animation_get_ext ' + 'skeleton_animation_get_duration skeleton_animation_get_frames ' + 'skeleton_animation_clear skeleton_skin_set skeleton_skin_get ' + 'skeleton_attachment_set skeleton_attachment_get ' + 'skeleton_attachment_create skeleton_collision_draw_set ' + 'skeleton_bone_data_get skeleton_bone_data_set ' + 'skeleton_bone_state_get skeleton_bone_state_set skeleton_get_minmax ' + 'skeleton_get_num_bounds skeleton_get_bounds ' + 'skeleton_animation_get_frame skeleton_animation_set_frame ' + 'draw_skeleton draw_skeleton_time draw_skeleton_instance ' + 'draw_skeleton_collision skeleton_animation_list skeleton_skin_list ' + 'skeleton_slot_data layer_get_id layer_get_id_at_depth ' + 'layer_get_depth layer_create layer_destroy layer_destroy_instances ' + 'layer_add_instance layer_has_instance layer_set_visible ' + 'layer_get_visible layer_exists layer_x layer_y layer_get_x ' + 'layer_get_y layer_hspeed layer_vspeed layer_get_hspeed ' + 'layer_get_vspeed layer_script_begin layer_script_end layer_shader ' + 'layer_get_script_begin layer_get_script_end layer_get_shader ' + 'layer_set_target_room layer_get_target_room layer_reset_target_room ' + 'layer_get_all layer_get_all_elements layer_get_name layer_depth ' + 'layer_get_element_layer layer_get_element_type layer_element_move ' + 'layer_force_draw_depth layer_is_draw_depth_forced ' + 'layer_get_forced_depth layer_background_get_id ' + 'layer_background_exists layer_background_create ' + 'layer_background_destroy layer_background_visible ' + 'layer_background_change layer_background_sprite ' + 'layer_background_htiled layer_background_vtiled ' + 'layer_background_stretch layer_background_yscale ' + 'layer_background_xscale layer_background_blend ' + 'layer_background_alpha layer_background_index layer_background_speed ' + 'layer_background_get_visible layer_background_get_sprite ' + 'layer_background_get_htiled layer_background_get_vtiled ' + 'layer_background_get_stretch layer_background_get_yscale ' + 'layer_background_get_xscale layer_background_get_blend ' + 'layer_background_get_alpha layer_background_get_index ' + 'layer_background_get_speed layer_sprite_get_id layer_sprite_exists ' + 'layer_sprite_create layer_sprite_destroy layer_sprite_change ' + 'layer_sprite_index layer_sprite_speed layer_sprite_xscale ' + 'layer_sprite_yscale layer_sprite_angle layer_sprite_blend ' + 'layer_sprite_alpha layer_sprite_x layer_sprite_y ' + 'layer_sprite_get_sprite layer_sprite_get_index ' + 'layer_sprite_get_speed layer_sprite_get_xscale ' + 'layer_sprite_get_yscale layer_sprite_get_angle ' + 'layer_sprite_get_blend layer_sprite_get_alpha layer_sprite_get_x ' + 'layer_sprite_get_y layer_tilemap_get_id layer_tilemap_exists ' + 'layer_tilemap_create layer_tilemap_destroy tilemap_tileset tilemap_x ' + 'tilemap_y tilemap_set tilemap_set_at_pixel tilemap_get_tileset ' + 'tilemap_get_tile_width tilemap_get_tile_height tilemap_get_width ' + 'tilemap_get_height tilemap_get_x tilemap_get_y tilemap_get ' + 'tilemap_get_at_pixel tilemap_get_cell_x_at_pixel ' + 'tilemap_get_cell_y_at_pixel tilemap_clear draw_tilemap draw_tile ' + 'tilemap_set_global_mask tilemap_get_global_mask tilemap_set_mask ' + 'tilemap_get_mask tilemap_get_frame tile_set_empty tile_set_index ' + 'tile_set_flip tile_set_mirror tile_set_rotate tile_get_empty ' + 'tile_get_index tile_get_flip tile_get_mirror tile_get_rotate ' + 'layer_tile_exists layer_tile_create layer_tile_destroy ' + 'layer_tile_change layer_tile_xscale layer_tile_yscale ' + 'layer_tile_blend layer_tile_alpha layer_tile_x layer_tile_y ' + 'layer_tile_region layer_tile_visible layer_tile_get_sprite ' + 'layer_tile_get_xscale layer_tile_get_yscale layer_tile_get_blend ' + 'layer_tile_get_alpha layer_tile_get_x layer_tile_get_y ' + 'layer_tile_get_region layer_tile_get_visible ' + 'layer_instance_get_instance instance_activate_layer ' + 'instance_deactivate_layer camera_create camera_create_view ' + 'camera_destroy camera_apply camera_get_active camera_get_default ' + 'camera_set_default camera_set_view_mat camera_set_proj_mat ' + 'camera_set_update_script camera_set_begin_script ' + 'camera_set_end_script camera_set_view_pos camera_set_view_size ' + 'camera_set_view_speed camera_set_view_border camera_set_view_angle ' + 'camera_set_view_target camera_get_view_mat camera_get_proj_mat ' + 'camera_get_update_script camera_get_begin_script ' + 'camera_get_end_script camera_get_view_x camera_get_view_y ' + 'camera_get_view_width camera_get_view_height camera_get_view_speed_x ' + 'camera_get_view_speed_y camera_get_view_border_x ' + 'camera_get_view_border_y camera_get_view_angle ' + 'camera_get_view_target view_get_camera view_get_visible ' + 'view_get_xport view_get_yport view_get_wport view_get_hport ' + 'view_get_surface_id view_set_camera view_set_visible view_set_xport ' + 'view_set_yport view_set_wport view_set_hport view_set_surface_id ' + 'gesture_drag_time gesture_drag_distance gesture_flick_speed ' + 'gesture_double_tap_time gesture_double_tap_distance ' + 'gesture_pinch_distance gesture_pinch_angle_towards ' + 'gesture_pinch_angle_away gesture_rotate_time gesture_rotate_angle ' + 'gesture_tap_count gesture_get_drag_time gesture_get_drag_distance ' + 'gesture_get_flick_speed gesture_get_double_tap_time ' + 'gesture_get_double_tap_distance gesture_get_pinch_distance ' + 'gesture_get_pinch_angle_towards gesture_get_pinch_angle_away ' + 'gesture_get_rotate_time gesture_get_rotate_angle ' + 'gesture_get_tap_count keyboard_virtual_show keyboard_virtual_hide ' + 'keyboard_virtual_status keyboard_virtual_height', literal: 'self other all noone global local undefined pointer_invalid ' + 'pointer_null path_action_stop path_action_restart ' + 'path_action_continue path_action_reverse true false pi GM_build_date ' + 'GM_version GM_runtime_version timezone_local timezone_utc ' + 'gamespeed_fps gamespeed_microseconds ev_create ev_destroy ev_step ' + 'ev_alarm ev_keyboard ev_mouse ev_collision ev_other ev_draw ' + 'ev_draw_begin ev_draw_end ev_draw_pre ev_draw_post ev_keypress ' + 'ev_keyrelease ev_trigger ev_left_button ev_right_button ' + 'ev_middle_button ev_no_button ev_left_press ev_right_press ' + 'ev_middle_press ev_left_release ev_right_release ev_middle_release ' + 'ev_mouse_enter ev_mouse_leave ev_mouse_wheel_up ev_mouse_wheel_down ' + 'ev_global_left_button ev_global_right_button ev_global_middle_button ' + 'ev_global_left_press ev_global_right_press ev_global_middle_press ' + 'ev_global_left_release ev_global_right_release ' + 'ev_global_middle_release ev_joystick1_left ev_joystick1_right ' + 'ev_joystick1_up ev_joystick1_down ev_joystick1_button1 ' + 'ev_joystick1_button2 ev_joystick1_button3 ev_joystick1_button4 ' + 'ev_joystick1_button5 ev_joystick1_button6 ev_joystick1_button7 ' + 'ev_joystick1_button8 ev_joystick2_left ev_joystick2_right ' + 'ev_joystick2_up ev_joystick2_down ev_joystick2_button1 ' + 'ev_joystick2_button2 ev_joystick2_button3 ev_joystick2_button4 ' + 'ev_joystick2_button5 ev_joystick2_button6 ev_joystick2_button7 ' + 'ev_joystick2_button8 ev_outside ev_boundary ev_game_start ' + 'ev_game_end ev_room_start ev_room_end ev_no_more_lives ' + 'ev_animation_end ev_end_of_path ev_no_more_health ev_close_button ' + 'ev_user0 ev_user1 ev_user2 ev_user3 ev_user4 ev_user5 ev_user6 ' + 'ev_user7 ev_user8 ev_user9 ev_user10 ev_user11 ev_user12 ev_user13 ' + 'ev_user14 ev_user15 ev_step_normal ev_step_begin ev_step_end ev_gui ' + 'ev_gui_begin ev_gui_end ev_cleanup ev_gesture ev_gesture_tap ' + 'ev_gesture_double_tap ev_gesture_drag_start ev_gesture_dragging ' + 'ev_gesture_drag_end ev_gesture_flick ev_gesture_pinch_start ' + 'ev_gesture_pinch_in ev_gesture_pinch_out ev_gesture_pinch_end ' + 'ev_gesture_rotate_start ev_gesture_rotating ev_gesture_rotate_end ' + 'ev_global_gesture_tap ev_global_gesture_double_tap ' + 'ev_global_gesture_drag_start ev_global_gesture_dragging ' + 'ev_global_gesture_drag_end ev_global_gesture_flick ' + 'ev_global_gesture_pinch_start ev_global_gesture_pinch_in ' + 'ev_global_gesture_pinch_out ev_global_gesture_pinch_end ' + 'ev_global_gesture_rotate_start ev_global_gesture_rotating ' + 'ev_global_gesture_rotate_end vk_nokey vk_anykey vk_enter vk_return ' + 'vk_shift vk_control vk_alt vk_escape vk_space vk_backspace vk_tab ' + 'vk_pause vk_printscreen vk_left vk_right vk_up vk_down vk_home ' + 'vk_end vk_delete vk_insert vk_pageup vk_pagedown vk_f1 vk_f2 vk_f3 ' + 'vk_f4 vk_f5 vk_f6 vk_f7 vk_f8 vk_f9 vk_f10 vk_f11 vk_f12 vk_numpad0 ' + 'vk_numpad1 vk_numpad2 vk_numpad3 vk_numpad4 vk_numpad5 vk_numpad6 ' + 'vk_numpad7 vk_numpad8 vk_numpad9 vk_divide vk_multiply vk_subtract ' + 'vk_add vk_decimal vk_lshift vk_lcontrol vk_lalt vk_rshift ' + 'vk_rcontrol vk_ralt mb_any mb_none mb_left mb_right mb_middle ' + 'c_aqua c_black c_blue c_dkgray c_fuchsia c_gray c_green c_lime ' + 'c_ltgray c_maroon c_navy c_olive c_purple c_red c_silver c_teal ' + 'c_white c_yellow c_orange fa_left fa_center fa_right fa_top ' + 'fa_middle fa_bottom pr_pointlist pr_linelist pr_linestrip ' + 'pr_trianglelist pr_trianglestrip pr_trianglefan bm_complex bm_normal ' + 'bm_add bm_max bm_subtract bm_zero bm_one bm_src_colour ' + 'bm_inv_src_colour bm_src_color bm_inv_src_color bm_src_alpha ' + 'bm_inv_src_alpha bm_dest_alpha bm_inv_dest_alpha bm_dest_colour ' + 'bm_inv_dest_colour bm_dest_color bm_inv_dest_color bm_src_alpha_sat ' + 'tf_point tf_linear tf_anisotropic mip_off mip_on mip_markedonly ' + 'audio_falloff_none audio_falloff_inverse_distance ' + 'audio_falloff_inverse_distance_clamped audio_falloff_linear_distance ' + 'audio_falloff_linear_distance_clamped ' + 'audio_falloff_exponent_distance ' + 'audio_falloff_exponent_distance_clamped audio_old_system ' + 'audio_new_system audio_mono audio_stereo audio_3d cr_default cr_none ' + 'cr_arrow cr_cross cr_beam cr_size_nesw cr_size_ns cr_size_nwse ' + 'cr_size_we cr_uparrow cr_hourglass cr_drag cr_appstart cr_handpoint ' + 'cr_size_all spritespeed_framespersecond ' + 'spritespeed_framespergameframe asset_object asset_unknown ' + 'asset_sprite asset_sound asset_room asset_path asset_script ' + 'asset_font asset_timeline asset_tiles asset_shader fa_readonly ' + 'fa_hidden fa_sysfile fa_volumeid fa_directory fa_archive ' + 'ds_type_map ds_type_list ds_type_stack ds_type_queue ds_type_grid ' + 'ds_type_priority ef_explosion ef_ring ef_ellipse ef_firework ' + 'ef_smoke ef_smokeup ef_star ef_spark ef_flare ef_cloud ef_rain ' + 'ef_snow pt_shape_pixel pt_shape_disk pt_shape_square pt_shape_line ' + 'pt_shape_star pt_shape_circle pt_shape_ring pt_shape_sphere ' + 'pt_shape_flare pt_shape_spark pt_shape_explosion pt_shape_cloud ' + 'pt_shape_smoke pt_shape_snow ps_distr_linear ps_distr_gaussian ' + 'ps_distr_invgaussian ps_shape_rectangle ps_shape_ellipse ' + 'ps_shape_diamond ps_shape_line ty_real ty_string dll_cdecl ' + 'dll_stdcall matrix_view matrix_projection matrix_world os_win32 ' + 'os_windows os_macosx os_ios os_android os_symbian os_linux ' + 'os_unknown os_winphone os_tizen os_win8native ' + 'os_wiiu os_3ds os_psvita os_bb10 os_ps4 os_xboxone ' + 'os_ps3 os_xbox360 os_uwp os_tvos os_switch ' + 'browser_not_a_browser browser_unknown browser_ie browser_firefox ' + 'browser_chrome browser_safari browser_safari_mobile browser_opera ' + 'browser_tizen browser_edge browser_windows_store browser_ie_mobile ' + 'device_ios_unknown device_ios_iphone device_ios_iphone_retina ' + 'device_ios_ipad device_ios_ipad_retina device_ios_iphone5 ' + 'device_ios_iphone6 device_ios_iphone6plus device_emulator ' + 'device_tablet display_landscape display_landscape_flipped ' + 'display_portrait display_portrait_flipped tm_sleep tm_countvsyncs ' + 'of_challenge_win of_challen ge_lose of_challenge_tie ' + 'leaderboard_type_number leaderboard_type_time_mins_secs ' + 'cmpfunc_never cmpfunc_less cmpfunc_equal cmpfunc_lessequal ' + 'cmpfunc_greater cmpfunc_notequal cmpfunc_greaterequal cmpfunc_always ' + 'cull_noculling cull_clockwise cull_counterclockwise lighttype_dir ' + 'lighttype_point iap_ev_storeload iap_ev_product iap_ev_purchase ' + 'iap_ev_consume iap_ev_restore iap_storeload_ok iap_storeload_failed ' + 'iap_status_uninitialised iap_status_unavailable iap_status_loading ' + 'iap_status_available iap_status_processing iap_status_restoring ' + 'iap_failed iap_unavailable iap_available iap_purchased iap_canceled ' + 'iap_refunded fb_login_default fb_login_fallback_to_webview ' + 'fb_login_no_fallback_to_webview fb_login_forcing_webview ' + 'fb_login_use_system_account fb_login_forcing_safari ' + 'phy_joint_anchor_1_x phy_joint_anchor_1_y phy_joint_anchor_2_x ' + 'phy_joint_anchor_2_y phy_joint_reaction_force_x ' + 'phy_joint_reaction_force_y phy_joint_reaction_torque ' + 'phy_joint_motor_speed phy_joint_angle phy_joint_motor_torque ' + 'phy_joint_max_motor_torque phy_joint_translation phy_joint_speed ' + 'phy_joint_motor_force phy_joint_max_motor_force phy_joint_length_1 ' + 'phy_joint_length_2 phy_joint_damping_ratio phy_joint_frequency ' + 'phy_joint_lower_angle_limit phy_joint_upper_angle_limit ' + 'phy_joint_angle_limits phy_joint_max_length phy_joint_max_torque ' + 'phy_joint_max_force phy_debug_render_aabb ' + 'phy_debug_render_collision_pairs phy_debug_render_coms ' + 'phy_debug_render_core_shapes phy_debug_render_joints ' + 'phy_debug_render_obb phy_debug_render_shapes ' + 'phy_particle_flag_water phy_particle_flag_zombie ' + 'phy_particle_flag_wall phy_particle_flag_spring ' + 'phy_particle_flag_elastic phy_particle_flag_viscous ' + 'phy_particle_flag_powder phy_particle_flag_tensile ' + 'phy_particle_flag_colourmixing phy_particle_flag_colormixing ' + 'phy_particle_group_flag_solid phy_particle_group_flag_rigid ' + 'phy_particle_data_flag_typeflags phy_particle_data_flag_position ' + 'phy_particle_data_flag_velocity phy_particle_data_flag_colour ' + 'phy_particle_data_flag_color phy_particle_data_flag_category ' + 'achievement_our_info achievement_friends_info ' + 'achievement_leaderboard_info achievement_achievement_info ' + 'achievement_filter_all_players achievement_filter_friends_only ' + 'achievement_filter_favorites_only ' + 'achievement_type_achievement_challenge ' + 'achievement_type_score_challenge achievement_pic_loaded ' + 'achievement_show_ui achievement_show_profile ' + 'achievement_show_leaderboard achievement_show_achievement ' + 'achievement_show_bank achievement_show_friend_picker ' + 'achievement_show_purchase_prompt network_socket_tcp ' + 'network_socket_udp network_socket_bluetooth network_type_connect ' + 'network_type_disconnect network_type_data ' + 'network_type_non_blocking_connect network_config_connect_timeout ' + 'network_config_use_non_blocking_socket ' + 'network_config_enable_reliable_udp ' + 'network_config_disable_reliable_udp buffer_fixed buffer_grow ' + 'buffer_wrap buffer_fast buffer_vbuffer buffer_network buffer_u8 ' + 'buffer_s8 buffer_u16 buffer_s16 buffer_u32 buffer_s32 buffer_u64 ' + 'buffer_f16 buffer_f32 buffer_f64 buffer_bool buffer_text ' + 'buffer_string buffer_surface_copy buffer_seek_start ' + 'buffer_seek_relative buffer_seek_end ' + 'buffer_generalerror buffer_outofspace buffer_outofbounds ' + 'buffer_invalidtype text_type button_type input_type ANSI_CHARSET ' + 'DEFAULT_CHARSET EASTEUROPE_CHARSET RUSSIAN_CHARSET SYMBOL_CHARSET ' + 'SHIFTJIS_CHARSET HANGEUL_CHARSET GB2312_CHARSET CHINESEBIG5_CHARSET ' + 'JOHAB_CHARSET HEBREW_CHARSET ARABIC_CHARSET GREEK_CHARSET ' + 'TURKISH_CHARSET VIETNAMESE_CHARSET THAI_CHARSET MAC_CHARSET ' + 'BALTIC_CHARSET OEM_CHARSET gp_face1 gp_face2 gp_face3 gp_face4 ' + 'gp_shoulderl gp_shoulderr gp_shoulderlb gp_shoulderrb gp_select ' + 'gp_start gp_stickl gp_stickr gp_padu gp_padd gp_padl gp_padr ' + 'gp_axislh gp_axislv gp_axisrh gp_axisrv ov_friends ov_community ' + 'ov_players ov_settings ov_gamegroup ov_achievements lb_sort_none ' + 'lb_sort_ascending lb_sort_descending lb_disp_none lb_disp_numeric ' + 'lb_disp_time_sec lb_disp_time_ms ugc_result_success ' + 'ugc_filetype_community ugc_filetype_microtrans ugc_visibility_public ' + 'ugc_visibility_friends_only ugc_visibility_private ' + 'ugc_query_RankedByVote ugc_query_RankedByPublicationDate ' + 'ugc_query_AcceptedForGameRankedByAcceptanceDate ' + 'ugc_query_RankedByTrend ' + 'ugc_query_FavoritedByFriendsRankedByPublicationDate ' + 'ugc_query_CreatedByFriendsRankedByPublicationDate ' + 'ugc_query_RankedByNumTimesReported ' + 'ugc_query_CreatedByFollowedUsersRankedByPublicationDate ' + 'ugc_query_NotYetRated ugc_query_RankedByTotalVotesAsc ' + 'ugc_query_RankedByVotesUp ugc_query_RankedByTextSearch ' + 'ugc_sortorder_CreationOrderDesc ugc_sortorder_CreationOrderAsc ' + 'ugc_sortorder_TitleAsc ugc_sortorder_LastUpdatedDesc ' + 'ugc_sortorder_SubscriptionDateDesc ugc_sortorder_VoteScoreDesc ' + 'ugc_sortorder_ForModeration ugc_list_Published ugc_list_VotedOn ' + 'ugc_list_VotedUp ugc_list_VotedDown ugc_list_WillVoteLater ' + 'ugc_list_Favorited ugc_list_Subscribed ugc_list_UsedOrPlayed ' + 'ugc_list_Followed ugc_match_Items ugc_match_Items_Mtx ' + 'ugc_match_Items_ReadyToUse ugc_match_Collections ugc_match_Artwork ' + 'ugc_match_Videos ugc_match_Screenshots ugc_match_AllGuides ' + 'ugc_match_WebGuides ugc_match_IntegratedGuides ' + 'ugc_match_UsableInGame ugc_match_ControllerBindings ' + 'vertex_usage_position vertex_usage_colour vertex_usage_color ' + 'vertex_usage_normal vertex_usage_texcoord vertex_usage_textcoord ' + 'vertex_usage_blendweight vertex_usage_blendindices ' + 'vertex_usage_psize vertex_usage_tangent vertex_usage_binormal ' + 'vertex_usage_fog vertex_usage_depth vertex_usage_sample ' + 'vertex_type_float1 vertex_type_float2 vertex_type_float3 ' + 'vertex_type_float4 vertex_type_colour vertex_type_color ' + 'vertex_type_ubyte4 layerelementtype_undefined ' + 'layerelementtype_background layerelementtype_instance ' + 'layerelementtype_oldtilemap layerelementtype_sprite ' + 'layerelementtype_tilemap layerelementtype_particlesystem ' + 'layerelementtype_tile tile_rotate tile_flip tile_mirror ' + 'tile_index_mask kbv_type_default kbv_type_ascii kbv_type_url ' + 'kbv_type_email kbv_type_numbers kbv_type_phone kbv_type_phone_name ' + 'kbv_returnkey_default kbv_returnkey_go kbv_returnkey_google ' + 'kbv_returnkey_join kbv_returnkey_next kbv_returnkey_route ' + 'kbv_returnkey_search kbv_returnkey_send kbv_returnkey_yahoo ' + 'kbv_returnkey_done kbv_returnkey_continue kbv_returnkey_emergency ' + 'kbv_autocapitalize_none kbv_autocapitalize_words ' + 'kbv_autocapitalize_sentences kbv_autocapitalize_characters', symbol: 'argument_relative argument argument0 argument1 argument2 ' + 'argument3 argument4 argument5 argument6 argument7 argument8 ' + 'argument9 argument10 argument11 argument12 argument13 argument14 ' + 'argument15 argument_count x y xprevious yprevious xstart ystart ' + 'hspeed vspeed direction speed friction gravity gravity_direction ' + 'path_index path_position path_positionprevious path_speed ' + 'path_scale path_orientation path_endaction object_index id solid ' + 'persistent mask_index instance_count instance_id room_speed fps ' + 'fps_real current_time current_year current_month current_day ' + 'current_weekday current_hour current_minute current_second alarm ' + 'timeline_index timeline_position timeline_speed timeline_running ' + 'timeline_loop room room_first room_last room_width room_height ' + 'room_caption room_persistent score lives health show_score ' + 'show_lives show_health caption_score caption_lives caption_health ' + 'event_type event_number event_object event_action ' + 'application_surface gamemaker_pro gamemaker_registered ' + 'gamemaker_version error_occurred error_last debug_mode ' + 'keyboard_key keyboard_lastkey keyboard_lastchar keyboard_string ' + 'mouse_x mouse_y mouse_button mouse_lastbutton cursor_sprite ' + 'visible sprite_index sprite_width sprite_height sprite_xoffset ' + 'sprite_yoffset image_number image_index image_speed depth ' + 'image_xscale image_yscale image_angle image_alpha image_blend ' + 'bbox_left bbox_right bbox_top bbox_bottom layer background_colour ' + 'background_showcolour background_color background_showcolor ' + 'view_enabled view_current view_visible view_xview view_yview ' + 'view_wview view_hview view_xport view_yport view_wport view_hport ' + 'view_angle view_hborder view_vborder view_hspeed view_vspeed ' + 'view_object view_surface_id view_camera game_id game_display_name ' + 'game_project_name game_save_id working_directory temp_directory ' + 'program_directory browser_width browser_height os_type os_device ' + 'os_browser os_version display_aa async_load delta_time ' + 'webgl_enabled event_data iap_data phy_rotation phy_position_x ' + 'phy_position_y phy_angular_velocity phy_linear_velocity_x ' + 'phy_linear_velocity_y phy_speed_x phy_speed_y phy_speed ' + 'phy_angular_damping phy_linear_damping phy_bullet ' + 'phy_fixed_rotation phy_active phy_mass phy_inertia phy_com_x ' + 'phy_com_y phy_dynamic phy_kinematic phy_sleeping ' + 'phy_collision_points phy_collision_x phy_collision_y ' + 'phy_col_normal_x phy_col_normal_y phy_position_xprevious ' + 'phy_position_yprevious' }; return { aliases: ['gml', 'GML'], case_insensitive: false, // language is case-insensitive keywords: GML_KEYWORDS, contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, hljs.C_NUMBER_MODE ] }; } },{name:"go",create:/* Language: Go Author: Stephan Kountso aka StepLg Contributors: Evgeny Stepanischev Description: Google go language (golang). For info about language see http://golang.org/ Category: system */ function(hljs) { var GO_KEYWORDS = { keyword: 'break default func interface select case map struct chan else goto package switch ' + 'const fallthrough if range type continue for import return var go defer ' + 'bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 ' + 'uint16 uint32 uint64 int uint uintptr rune', literal: 'true false iota nil', built_in: 'append cap close complex copy imag len make new panic print println real recover delete' }; return { aliases: ['golang'], keywords: GO_KEYWORDS, illegal: ' Description: a lightweight dynamic language for the JVM, see http://golo-lang.org/ */ function(hljs) { return { keywords: { keyword: 'println readln print import module function local return let var ' + 'while for foreach times in case when match with break continue ' + 'augment augmentation each find filter reduce ' + 'if then else otherwise try catch finally raise throw orIfNull ' + 'DynamicObject|10 DynamicVariable struct Observable map set vector list array', literal: 'true false null' }, contains: [ hljs.HASH_COMMENT_MODE, hljs.QUOTE_STRING_MODE, hljs.C_NUMBER_MODE, { className: 'meta', begin: '@[A-Za-z]+' } ] } } },{name:"gradle",create:/* Language: Gradle Author: Damian Mee Website: http://meeDamian.com */ function(hljs) { return { case_insensitive: true, keywords: { keyword: 'task project allprojects subprojects artifacts buildscript configurations ' + 'dependencies repositories sourceSets description delete from into include ' + 'exclude source classpath destinationDir includes options sourceCompatibility ' + 'targetCompatibility group flatDir doLast doFirst flatten todir fromdir ant ' + 'def abstract break case catch continue default do else extends final finally ' + 'for if implements instanceof native new private protected public return static ' + 'switch synchronized throw throws transient try volatile while strictfp package ' + 'import false null super this true antlrtask checkstyle codenarc copy boolean ' + 'byte char class double float int interface long short void compile runTime ' + 'file fileTree abs any append asList asWritable call collect compareTo count ' + 'div dump each eachByte eachFile eachLine every find findAll flatten getAt ' + 'getErr getIn getOut getText grep immutable inject inspect intersect invokeMethods ' + 'isCase join leftShift minus multiply newInputStream newOutputStream newPrintWriter ' + 'newReader newWriter next plus pop power previous print println push putAt read ' + 'readBytes readLines reverse reverseEach round size sort splitEachLine step subMap ' + 'times toInteger toList tokenize upto waitForOrKill withPrintWriter withReader ' + 'withStream withWriter withWriterAppend write writeLine' }, contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, hljs.NUMBER_MODE, hljs.REGEXP_MODE ] } } },{name:"groovy",create:/* Language: Groovy Author: Guillaume Laforge Website: http://glaforge.appspot.com Description: Groovy programming language implementation inspired from Vsevolod's Java mode */ function(hljs) { return { keywords: { literal : 'true false null', keyword: 'byte short char int long boolean float double void ' + // groovy specific keywords 'def as in assert trait ' + // common keywords with Java 'super this abstract static volatile transient public private protected synchronized final ' + 'class interface enum if else for while switch case break default continue ' + 'throw throws try catch finally implements extends new import package return instanceof' }, contains: [ hljs.COMMENT( '/\\*\\*', '\\*/', { relevance : 0, contains : [ { // eat up @'s in emails to prevent them to be recognized as doctags begin: /\w+@/, relevance: 0 }, { className : 'doctag', begin : '@[A-Za-z]+' } ] } ), hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, { className: 'string', begin: '"""', end: '"""' }, { className: 'string', begin: "'''", end: "'''" }, { className: 'string', begin: "\\$/", end: "/\\$", relevance: 10 }, hljs.APOS_STRING_MODE, { className: 'regexp', begin: /~?\/[^\/\n]+\//, contains: [ hljs.BACKSLASH_ESCAPE ] }, hljs.QUOTE_STRING_MODE, { className: 'meta', begin: "^#!/usr/bin/env", end: '$', illegal: '\n' }, hljs.BINARY_NUMBER_MODE, { className: 'class', beginKeywords: 'class interface trait enum', end: '{', illegal: ':', contains: [ {beginKeywords: 'extends implements'}, hljs.UNDERSCORE_TITLE_MODE ] }, hljs.C_NUMBER_MODE, { className: 'meta', begin: '@[A-Za-z]+' }, { // highlight map keys and named parameters as strings className: 'string', begin: /[^\?]{0}[A-Za-z0-9_$]+ *:/ }, { // catch middle element of the ternary operator // to avoid highlight it as a label, named parameter, or map key begin: /\?/, end: /\:/ }, { // highlight labeled statements className: 'symbol', begin: '^\\s*[A-Za-z0-9_$]+:', relevance: 0 } ], illegal: /#|<\// } } },{name:"haml",create:/* Language: Haml Requires: ruby.js Author: Dan Allen Website: http://google.com/profiles/dan.j.allen Category: template */ // TODO support filter tags like :javascript, support inline HTML function(hljs) { return { case_insensitive: true, contains: [ { className: 'meta', begin: '^!!!( (5|1\\.1|Strict|Frameset|Basic|Mobile|RDFa|XML\\b.*))?$', relevance: 10 }, // FIXME these comments should be allowed to span indented lines hljs.COMMENT( '^\\s*(!=#|=#|-#|/).*$', false, { relevance: 0 } ), { begin: '^\\s*(-|=|!=)(?!#)', starts: { end: '\\n', subLanguage: 'ruby' } }, { className: 'tag', begin: '^\\s*%', contains: [ { className: 'selector-tag', begin: '\\w+' }, { className: 'selector-id', begin: '#[\\w-]+' }, { className: 'selector-class', begin: '\\.[\\w-]+' }, { begin: '{\\s*', end: '\\s*}', contains: [ { begin: ':\\w+\\s*=>', end: ',\\s+', returnBegin: true, endsWithParent: true, contains: [ { className: 'attr', begin: ':\\w+' }, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, { begin: '\\w+', relevance: 0 } ] } ] }, { begin: '\\(\\s*', end: '\\s*\\)', excludeEnd: true, contains: [ { begin: '\\w+\\s*=', end: '\\s+', returnBegin: true, endsWithParent: true, contains: [ { className: 'attr', begin: '\\w+', relevance: 0 }, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, { begin: '\\w+', relevance: 0 } ] } ] } ] }, { begin: '^\\s*[=~]\\s*' }, { begin: '#{', starts: { end: '}', subLanguage: 'ruby' } } ] }; } },{name:"handlebars",create:/* Language: Handlebars Requires: xml.js Author: Robin Ward Description: Matcher for Handlebars as well as EmberJS additions. Category: template */ function(hljs) { var BUILT_INS = {'builtin-name': 'each in with if else unless bindattr action collection debugger log outlet template unbound view yield'}; return { aliases: ['hbs', 'html.hbs', 'html.handlebars'], case_insensitive: true, subLanguage: 'xml', contains: [ hljs.COMMENT('{{!(--)?', '(--)?}}'), { className: 'template-tag', begin: /\{\{[#\/]/, end: /\}\}/, contains: [ { className: 'name', begin: /[a-zA-Z\.-]+/, keywords: BUILT_INS, starts: { endsWithParent: true, relevance: 0, contains: [ hljs.QUOTE_STRING_MODE ] } } ] }, { className: 'template-variable', begin: /\{\{/, end: /\}\}/, keywords: BUILT_INS } ] }; } },{name:"haskell",create:/* Language: Haskell Author: Jeremy Hull Contributors: Zena Treep Category: functional */ function(hljs) { var COMMENT = { variants: [ hljs.COMMENT('--', '$'), hljs.COMMENT( '{-', '-}', { contains: ['self'] } ) ] }; var PRAGMA = { className: 'meta', begin: '{-#', end: '#-}' }; var PREPROCESSOR = { className: 'meta', begin: '^#', end: '$' }; var CONSTRUCTOR = { className: 'type', begin: '\\b[A-Z][\\w\']*', // TODO: other constructors (build-in, infix). relevance: 0 }; var LIST = { begin: '\\(', end: '\\)', illegal: '"', contains: [ PRAGMA, PREPROCESSOR, {className: 'type', begin: '\\b[A-Z][\\w]*(\\((\\.\\.|,|\\w+)\\))?'}, hljs.inherit(hljs.TITLE_MODE, {begin: '[_a-z][\\w\']*'}), COMMENT ] }; var RECORD = { begin: '{', end: '}', contains: LIST.contains }; return { aliases: ['hs'], keywords: 'let in if then else case of where do module import hiding ' + 'qualified type data newtype deriving class instance as default ' + 'infix infixl infixr foreign export ccall stdcall cplusplus ' + 'jvm dotnet safe unsafe family forall mdo proc rec', contains: [ // Top-level constructions. { beginKeywords: 'module', end: 'where', keywords: 'module where', contains: [LIST, COMMENT], illegal: '\\W\\.|;' }, { begin: '\\bimport\\b', end: '$', keywords: 'import qualified as hiding', contains: [LIST, COMMENT], illegal: '\\W\\.|;' }, { className: 'class', begin: '^(\\s*)?(class|instance)\\b', end: 'where', keywords: 'class family instance where', contains: [CONSTRUCTOR, LIST, COMMENT] }, { className: 'class', begin: '\\b(data|(new)?type)\\b', end: '$', keywords: 'data family type newtype deriving', contains: [PRAGMA, CONSTRUCTOR, LIST, RECORD, COMMENT] }, { beginKeywords: 'default', end: '$', contains: [CONSTRUCTOR, LIST, COMMENT] }, { beginKeywords: 'infix infixl infixr', end: '$', contains: [hljs.C_NUMBER_MODE, COMMENT] }, { begin: '\\bforeign\\b', end: '$', keywords: 'foreign import export ccall stdcall cplusplus jvm ' + 'dotnet safe unsafe', contains: [CONSTRUCTOR, hljs.QUOTE_STRING_MODE, COMMENT] }, { className: 'meta', begin: '#!\\/usr\\/bin\\/env\ runhaskell', end: '$' }, // "Whitespaces". PRAGMA, PREPROCESSOR, // Literals and names. // TODO: characters. hljs.QUOTE_STRING_MODE, hljs.C_NUMBER_MODE, CONSTRUCTOR, hljs.inherit(hljs.TITLE_MODE, {begin: '^[_a-z][\\w\']*'}), COMMENT, {begin: '->|<-'} // No markup, relevance booster ] }; } },{name:"haxe",create:/* Language: Haxe Author: Christopher Kaster (Based on the actionscript.js language file by Alexander Myadzel) Contributors: Kenton Hamaluik */ function(hljs) { var IDENT_RE = '[a-zA-Z_$][a-zA-Z0-9_$]*'; var IDENT_FUNC_RETURN_TYPE_RE = '([*]|[a-zA-Z_$][a-zA-Z0-9_$]*)'; var HAXE_BASIC_TYPES = 'Int Float String Bool Dynamic Void Array '; return { aliases: ['hx'], keywords: { keyword: 'break case cast catch continue default do dynamic else enum extern ' + 'for function here if import in inline never new override package private get set ' + 'public return static super switch this throw trace try typedef untyped using var while ' + HAXE_BASIC_TYPES, built_in: 'trace this', literal: 'true false null _' }, contains: [ { className: 'string', // interpolate-able strings begin: '\'', end: '\'', contains: [ hljs.BACKSLASH_ESCAPE, { className: 'subst', // interpolation begin: '\\$\\{', end: '\\}' }, { className: 'subst', // interpolation begin: '\\$', end: '\\W}' } ] }, hljs.QUOTE_STRING_MODE, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.C_NUMBER_MODE, { className: 'meta', // compiler meta begin: '@:', end: '$' }, { className: 'meta', // compiler conditionals begin: '#', end: '$', keywords: {'meta-keyword': 'if else elseif end error'} }, { className: 'type', // function types begin: ':[ \t]*', end: '[^A-Za-z0-9_ \t\\->]', excludeBegin: true, excludeEnd: true, relevance: 0 }, { className: 'type', // types begin: ':[ \t]*', end: '\\W', excludeBegin: true, excludeEnd: true }, { className: 'type', // instantiation begin: 'new *', end: '\\W', excludeBegin: true, excludeEnd: true }, { className: 'class', // enums beginKeywords: 'enum', end: '\\{', contains: [ hljs.TITLE_MODE ] }, { className: 'class', // abstracts beginKeywords: 'abstract', end: '[\\{$]', contains: [ { className: 'type', begin: '\\(', end: '\\)', excludeBegin: true, excludeEnd: true }, { className: 'type', begin: 'from +', end: '\\W', excludeBegin: true, excludeEnd: true }, { className: 'type', begin: 'to +', end: '\\W', excludeBegin: true, excludeEnd: true }, hljs.TITLE_MODE ], keywords: { keyword: 'abstract from to' } }, { className: 'class', // classes begin: '\\b(class|interface) +', end: '[\\{$]', excludeEnd: true, keywords: 'class interface', contains: [ { className: 'keyword', begin: '\\b(extends|implements) +', keywords: 'extends implements', contains: [ { className: 'type', begin: hljs.IDENT_RE, relevance: 0 } ] }, hljs.TITLE_MODE ] }, { className: 'function', beginKeywords: 'function', end: '\\(', excludeEnd: true, illegal: '\\S', contains: [ hljs.TITLE_MODE ] } ], illegal: /<\// }; } },{name:"hsp",create:/* Language: HSP Author: prince Website: http://prince.webcrow.jp/ Category: scripting */ function(hljs) { return { case_insensitive: true, lexemes: /[\w\._]+/, keywords: 'goto gosub return break repeat loop continue wait await dim sdim foreach dimtype dup dupptr end stop newmod delmod mref run exgoto on mcall assert logmes newlab resume yield onexit onerror onkey onclick oncmd exist delete mkdir chdir dirlist bload bsave bcopy memfile if else poke wpoke lpoke getstr chdpm memexpand memcpy memset notesel noteadd notedel noteload notesave randomize noteunsel noteget split strrep setease button chgdisp exec dialog mmload mmplay mmstop mci pset pget syscolor mes print title pos circle cls font sysfont objsize picload color palcolor palette redraw width gsel gcopy gzoom gmode bmpsave hsvcolor getkey listbox chkbox combox input mesbox buffer screen bgscr mouse objsel groll line clrobj boxf objprm objmode stick grect grotate gsquare gradf objimage objskip objenable celload celdiv celput newcom querycom delcom cnvstow comres axobj winobj sendmsg comevent comevarg sarrayconv callfunc cnvwtos comevdisp libptr system hspstat hspver stat cnt err strsize looplev sublev iparam wparam lparam refstr refdval int rnd strlen length length2 length3 length4 vartype gettime peek wpeek lpeek varptr varuse noteinfo instr abs limit getease str strmid strf getpath strtrim sin cos tan atan sqrt double absf expf logf limitf powf geteasef mousex mousey mousew hwnd hinstance hdc ginfo objinfo dirinfo sysinfo thismod __hspver__ __hsp30__ __date__ __time__ __line__ __file__ _debug __hspdef__ and or xor not screen_normal screen_palette screen_hide screen_fixedsize screen_tool screen_frame gmode_gdi gmode_mem gmode_rgb0 gmode_alpha gmode_rgb0alpha gmode_add gmode_sub gmode_pixela ginfo_mx ginfo_my ginfo_act ginfo_sel ginfo_wx1 ginfo_wy1 ginfo_wx2 ginfo_wy2 ginfo_vx ginfo_vy ginfo_sizex ginfo_sizey ginfo_winx ginfo_winy ginfo_mesx ginfo_mesy ginfo_r ginfo_g ginfo_b ginfo_paluse ginfo_dispx ginfo_dispy ginfo_cx ginfo_cy ginfo_intid ginfo_newid ginfo_sx ginfo_sy objinfo_mode objinfo_bmscr objinfo_hwnd notemax notesize dir_cur dir_exe dir_win dir_sys dir_cmdline dir_desktop dir_mydoc dir_tv font_normal font_bold font_italic font_underline font_strikeout font_antialias objmode_normal objmode_guifont objmode_usefont gsquare_grad msgothic msmincho do until while wend for next _break _continue switch case default swbreak swend ddim ldim alloc m_pi rad2deg deg2rad ease_linear ease_quad_in ease_quad_out ease_quad_inout ease_cubic_in ease_cubic_out ease_cubic_inout ease_quartic_in ease_quartic_out ease_quartic_inout ease_bounce_in ease_bounce_out ease_bounce_inout ease_shake_in ease_shake_out ease_shake_inout ease_loop', contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.QUOTE_STRING_MODE, hljs.APOS_STRING_MODE, { // multi-line string className: 'string', begin: '{"', end: '"}', contains: [hljs.BACKSLASH_ESCAPE] }, hljs.COMMENT(';', '$', {relevance: 0}), { // pre-processor className: 'meta', begin: '#', end: '$', keywords: {'meta-keyword': 'addion cfunc cmd cmpopt comfunc const defcfunc deffunc define else endif enum epack func global if ifdef ifndef include modcfunc modfunc modinit modterm module pack packopt regcmd runtime undef usecom uselib'}, contains: [ hljs.inherit(hljs.QUOTE_STRING_MODE, {className: 'meta-string'}), hljs.NUMBER_MODE, hljs.C_NUMBER_MODE, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE ] }, { // label className: 'symbol', begin: '^\\*(\\w+|@)' }, hljs.NUMBER_MODE, hljs.C_NUMBER_MODE ] }; } },{name:"htmlbars",create:/* Language: HTMLBars Requires: xml.js Author: Michael Johnston Description: Matcher for HTMLBars Category: template */ function(hljs) { var BUILT_INS = 'action collection component concat debugger each each-in else get hash if input link-to loc log mut outlet partial query-params render textarea unbound unless with yield view'; var ATTR_ASSIGNMENT = { illegal: /\}\}/, begin: /[a-zA-Z0-9_]+=/, returnBegin: true, relevance: 0, contains: [ { className: 'attr', begin: /[a-zA-Z0-9_]+/ } ] }; var SUB_EXPR = { illegal: /\}\}/, begin: /\)/, end: /\)/, contains: [ { begin: /[a-zA-Z\.\-]+/, keywords: {built_in: BUILT_INS}, starts: { endsWithParent: true, relevance: 0, contains: [ hljs.QUOTE_STRING_MODE, ] } } ] }; var TAG_INNARDS = { endsWithParent: true, relevance: 0, keywords: {keyword: 'as', built_in: BUILT_INS}, contains: [ hljs.QUOTE_STRING_MODE, ATTR_ASSIGNMENT, hljs.NUMBER_MODE ] }; return { case_insensitive: true, subLanguage: 'xml', contains: [ hljs.COMMENT('{{!(--)?', '(--)?}}'), { className: 'template-tag', begin: /\{\{[#\/]/, end: /\}\}/, contains: [ { className: 'name', begin: /[a-zA-Z\.\-]+/, keywords: {'builtin-name': BUILT_INS}, starts: TAG_INNARDS } ] }, { className: 'template-variable', begin: /\{\{[a-zA-Z][a-zA-Z\-]+/, end: /\}\}/, keywords: {keyword: 'as', built_in: BUILT_INS}, contains: [ hljs.QUOTE_STRING_MODE ] } ] }; } },{name:"http",create:/* Language: HTTP Description: HTTP request and response headers with automatic body highlighting Author: Ivan Sagalaev Category: common, protocols */ function(hljs) { var VERSION = 'HTTP/[0-9\\.]+'; return { aliases: ['https'], illegal: '\\S', contains: [ { begin: '^' + VERSION, end: '$', contains: [{className: 'number', begin: '\\b\\d{3}\\b'}] }, { begin: '^[A-Z]+ (.*?) ' + VERSION + '$', returnBegin: true, end: '$', contains: [ { className: 'string', begin: ' ', end: ' ', excludeBegin: true, excludeEnd: true }, { begin: VERSION }, { className: 'keyword', begin: '[A-Z]+' } ] }, { className: 'attribute', begin: '^\\w', end: ': ', excludeEnd: true, illegal: '\\n|\\s|=', starts: {end: '$', relevance: 0} }, { begin: '\\n\\n', starts: {subLanguage: [], endsWithParent: true} } ] }; } },{name:"hy",create:/* Language: Hy Description: Hy syntax (based on clojure.js) Author: Sergey Sobko Category: lisp */ function(hljs) { var keywords = { 'builtin-name': // keywords '!= % %= & &= * ** **= *= *map ' + '+ += , --build-class-- --import-- -= . / // //= ' + '/= < << <<= <= = > >= >> >>= ' + '@ @= ^ ^= abs accumulate all and any ap-compose ' + 'ap-dotimes ap-each ap-each-while ap-filter ap-first ap-if ap-last ap-map ap-map-when ap-pipe ' + 'ap-reduce ap-reject apply as-> ascii assert assoc bin break butlast ' + 'callable calling-module-name car case cdr chain chr coll? combinations compile ' + 'compress cond cons cons? continue count curry cut cycle dec ' + 'def default-method defclass defmacro defmacro-alias defmacro/g! defmain defmethod defmulti defn ' + 'defn-alias defnc defnr defreader defseq del delattr delete-route dict-comp dir ' + 'disassemble dispatch-reader-macro distinct divmod do doto drop drop-last drop-while empty? ' + 'end-sequence eval eval-and-compile eval-when-compile even? every? except exec filter first ' + 'flatten float? fn fnc fnr for for* format fraction genexpr ' + 'gensym get getattr global globals group-by hasattr hash hex id ' + 'identity if if* if-not if-python2 import in inc input instance? ' + 'integer integer-char? integer? interleave interpose is is-coll is-cons is-empty is-even ' + 'is-every is-float is-instance is-integer is-integer-char is-iterable is-iterator is-keyword is-neg is-none ' + 'is-not is-numeric is-odd is-pos is-string is-symbol is-zero isinstance islice issubclass ' + 'iter iterable? iterate iterator? keyword keyword? lambda last len let ' + 'lif lif-not list* list-comp locals loop macro-error macroexpand macroexpand-1 macroexpand-all ' + 'map max merge-with method-decorator min multi-decorator multicombinations name neg? next ' + 'none? nonlocal not not-in not? nth numeric? oct odd? open ' + 'or ord partition permutations pos? post-route postwalk pow prewalk print ' + 'product profile/calls profile/cpu put-route quasiquote quote raise range read read-str ' + 'recursive-replace reduce remove repeat repeatedly repr require rest round route ' + 'route-with-methods rwm second seq set-comp setattr setv some sorted string ' + 'string? sum switch symbol? take take-nth take-while tee try unless ' + 'unquote unquote-splicing vars walk when while with with* with-decorator with-gensyms ' + 'xi xor yield yield-from zero? zip zip-longest | |= ~' }; var SYMBOLSTART = 'a-zA-Z_\\-!.?+*=<>&#\''; var SYMBOL_RE = '[' + SYMBOLSTART + '][' + SYMBOLSTART + '0-9/;:]*'; var SIMPLE_NUMBER_RE = '[-+]?\\d+(\\.\\d+)?'; var SHEBANG = { className: 'meta', begin: '^#!', end: '$' }; var SYMBOL = { begin: SYMBOL_RE, relevance: 0 }; var NUMBER = { className: 'number', begin: SIMPLE_NUMBER_RE, relevance: 0 }; var STRING = hljs.inherit(hljs.QUOTE_STRING_MODE, {illegal: null}); var COMMENT = hljs.COMMENT( ';', '$', { relevance: 0 } ); var LITERAL = { className: 'literal', begin: /\b([Tt]rue|[Ff]alse|nil|None)\b/ }; var COLLECTION = { begin: '[\\[\\{]', end: '[\\]\\}]' }; var HINT = { className: 'comment', begin: '\\^' + SYMBOL_RE }; var HINT_COL = hljs.COMMENT('\\^\\{', '\\}'); var KEY = { className: 'symbol', begin: '[:]{1,2}' + SYMBOL_RE }; var LIST = { begin: '\\(', end: '\\)' }; var BODY = { endsWithParent: true, relevance: 0 }; var NAME = { keywords: keywords, lexemes: SYMBOL_RE, className: 'name', begin: SYMBOL_RE, starts: BODY }; var DEFAULT_CONTAINS = [LIST, STRING, HINT, HINT_COL, COMMENT, KEY, COLLECTION, NUMBER, LITERAL, SYMBOL]; LIST.contains = [hljs.COMMENT('comment', ''), NAME, BODY]; BODY.contains = DEFAULT_CONTAINS; COLLECTION.contains = DEFAULT_CONTAINS; return { aliases: ['hylang'], illegal: /\S/, contains: [SHEBANG, LIST, STRING, HINT, HINT_COL, COMMENT, KEY, COLLECTION, NUMBER, LITERAL] } } },{name:"inform7",create:/* Language: Inform 7 Author: Bruno Dias Description: Language definition for Inform 7, a DSL for writing parser interactive fiction. */ function(hljs) { var START_BRACKET = '\\['; var END_BRACKET = '\\]'; return { aliases: ['i7'], case_insensitive: true, keywords: { // Some keywords more or less unique to I7, for relevance. keyword: // kind: 'thing room person man woman animal container ' + 'supporter backdrop door ' + // characteristic: 'scenery open closed locked inside gender ' + // verb: 'is are say understand ' + // misc keyword: 'kind of rule' }, contains: [ { className: 'string', begin: '"', end: '"', relevance: 0, contains: [ { className: 'subst', begin: START_BRACKET, end: END_BRACKET } ] }, { className: 'section', begin: /^(Volume|Book|Part|Chapter|Section|Table)\b/, end: '$' }, { // Rule definition // This is here for relevance. begin: /^(Check|Carry out|Report|Instead of|To|Rule|When|Before|After)\b/, end: ':', contains: [ { //Rule name begin: '\\(This', end: '\\)' } ] }, { className: 'comment', begin: START_BRACKET, end: END_BRACKET, contains: ['self'] } ] }; } },{name:"ini",create:/* Language: Ini, TOML Contributors: Guillaume Gomez Category: common, config */ function(hljs) { var STRING = { className: "string", contains: [hljs.BACKSLASH_ESCAPE], variants: [ { begin: "'''", end: "'''", relevance: 10 }, { begin: '"""', end: '"""', relevance: 10 }, { begin: '"', end: '"' }, { begin: "'", end: "'" } ] }; return { aliases: ['toml'], case_insensitive: true, illegal: /\S/, contains: [ hljs.COMMENT(';', '$'), hljs.HASH_COMMENT_MODE, { className: 'section', begin: /^\s*\[+/, end: /\]+/ }, { begin: /^[a-z0-9\[\]_\.-]+\s*=\s*/, end: '$', returnBegin: true, contains: [ { className: 'attr', begin: /[a-z0-9\[\]_\.-]+/ }, { begin: /=/, endsWithParent: true, relevance: 0, contains: [ hljs.COMMENT(';', '$'), hljs.HASH_COMMENT_MODE, { className: 'literal', begin: /\bon|off|true|false|yes|no\b/ }, { className: 'variable', variants: [ {begin: /\$[\w\d"][\w\d_]*/}, {begin: /\$\{(.*?)}/} ] }, STRING, { className: 'number', begin: /([\+\-]+)?[\d]+_[\d_]+/ }, hljs.NUMBER_MODE ] } ] } ] }; } },{name:"irpf90",create:/* Language: IRPF90 Author: Anthony Scemama Description: IRPF90 is an open-source Fortran code generator : http://irpf90.ups-tlse.fr Category: scientific */ function(hljs) { var PARAMS = { className: 'params', begin: '\\(', end: '\\)' }; var F_KEYWORDS = { literal: '.False. .True.', keyword: 'kind do while private call intrinsic where elsewhere ' + 'type endtype endmodule endselect endinterface end enddo endif if forall endforall only contains default return stop then ' + 'public subroutine|10 function program .and. .or. .not. .le. .eq. .ge. .gt. .lt. ' + 'goto save else use module select case ' + 'access blank direct exist file fmt form formatted iostat name named nextrec number opened rec recl sequential status unformatted unit ' + 'continue format pause cycle exit ' + 'c_null_char c_alert c_backspace c_form_feed flush wait decimal round iomsg ' + 'synchronous nopass non_overridable pass protected volatile abstract extends import ' + 'non_intrinsic value deferred generic final enumerator class associate bind enum ' + 'c_int c_short c_long c_long_long c_signed_char c_size_t c_int8_t c_int16_t c_int32_t c_int64_t c_int_least8_t c_int_least16_t ' + 'c_int_least32_t c_int_least64_t c_int_fast8_t c_int_fast16_t c_int_fast32_t c_int_fast64_t c_intmax_t C_intptr_t c_float c_double ' + 'c_long_double c_float_complex c_double_complex c_long_double_complex c_bool c_char c_null_ptr c_null_funptr ' + 'c_new_line c_carriage_return c_horizontal_tab c_vertical_tab iso_c_binding c_loc c_funloc c_associated c_f_pointer ' + 'c_ptr c_funptr iso_fortran_env character_storage_size error_unit file_storage_size input_unit iostat_end iostat_eor ' + 'numeric_storage_size output_unit c_f_procpointer ieee_arithmetic ieee_support_underflow_control ' + 'ieee_get_underflow_mode ieee_set_underflow_mode newunit contiguous recursive ' + 'pad position action delim readwrite eor advance nml interface procedure namelist include sequence elemental pure ' + 'integer real character complex logical dimension allocatable|10 parameter ' + 'external implicit|10 none double precision assign intent optional pointer ' + 'target in out common equivalence data ' + // IRPF90 special keywords 'begin_provider &begin_provider end_provider begin_shell end_shell begin_template end_template subst assert touch ' + 'soft_touch provide no_dep free irp_if irp_else irp_endif irp_write irp_read', built_in: 'alog alog10 amax0 amax1 amin0 amin1 amod cabs ccos cexp clog csin csqrt dabs dacos dasin datan datan2 dcos dcosh ddim dexp dint ' + 'dlog dlog10 dmax1 dmin1 dmod dnint dsign dsin dsinh dsqrt dtan dtanh float iabs idim idint idnint ifix isign max0 max1 min0 min1 sngl ' + 'algama cdabs cdcos cdexp cdlog cdsin cdsqrt cqabs cqcos cqexp cqlog cqsin cqsqrt dcmplx dconjg derf derfc dfloat dgamma dimag dlgama ' + 'iqint qabs qacos qasin qatan qatan2 qcmplx qconjg qcos qcosh qdim qerf qerfc qexp qgamma qimag qlgama qlog qlog10 qmax1 qmin1 qmod ' + 'qnint qsign qsin qsinh qsqrt qtan qtanh abs acos aimag aint anint asin atan atan2 char cmplx conjg cos cosh exp ichar index int log ' + 'log10 max min nint sign sin sinh sqrt tan tanh print write dim lge lgt lle llt mod nullify allocate deallocate ' + 'adjustl adjustr all allocated any associated bit_size btest ceiling count cshift date_and_time digits dot_product ' + 'eoshift epsilon exponent floor fraction huge iand ibclr ibits ibset ieor ior ishft ishftc lbound len_trim matmul ' + 'maxexponent maxloc maxval merge minexponent minloc minval modulo mvbits nearest pack present product ' + 'radix random_number random_seed range repeat reshape rrspacing scale scan selected_int_kind selected_real_kind ' + 'set_exponent shape size spacing spread sum system_clock tiny transpose trim ubound unpack verify achar iachar transfer ' + 'dble entry dprod cpu_time command_argument_count get_command get_command_argument get_environment_variable is_iostat_end ' + 'ieee_arithmetic ieee_support_underflow_control ieee_get_underflow_mode ieee_set_underflow_mode ' + 'is_iostat_eor move_alloc new_line selected_char_kind same_type_as extends_type_of' + 'acosh asinh atanh bessel_j0 bessel_j1 bessel_jn bessel_y0 bessel_y1 bessel_yn erf erfc erfc_scaled gamma log_gamma hypot norm2 ' + 'atomic_define atomic_ref execute_command_line leadz trailz storage_size merge_bits ' + 'bge bgt ble blt dshiftl dshiftr findloc iall iany iparity image_index lcobound ucobound maskl maskr ' + 'num_images parity popcnt poppar shifta shiftl shiftr this_image ' + // IRPF90 special built_ins 'IRP_ALIGN irp_here' }; return { case_insensitive: true, keywords: F_KEYWORDS, illegal: /\/\*/, contains: [ hljs.inherit(hljs.APOS_STRING_MODE, {className: 'string', relevance: 0}), hljs.inherit(hljs.QUOTE_STRING_MODE, {className: 'string', relevance: 0}), { className: 'function', beginKeywords: 'subroutine function program', illegal: '[${=\\n]', contains: [hljs.UNDERSCORE_TITLE_MODE, PARAMS] }, hljs.COMMENT('!', '$', {relevance: 0}), hljs.COMMENT('begin_doc', 'end_doc', {relevance: 10}), { className: 'number', begin: '(?=\\b|\\+|\\-|\\.)(?=\\.\\d|\\d)(?:\\d+)?(?:\\.?\\d*)(?:[de][+-]?\\d+)?\\b\\.?', relevance: 0 } ] }; } },{name:"isbl",create:/* Language: ISBL Author: Dmitriy Tarasov Description: built-in language DIRECTUM Category: enterprise */ function(hljs) { // Определение идентификаторов var UNDERSCORE_IDENT_RE = "[A-Za-zА-Яа-яёЁ_!][A-Za-zА-Яа-яёЁ_0-9]*"; // Определение имен функций var FUNCTION_NAME_IDENT_RE = "[A-Za-zА-Яа-яёЁ_][A-Za-zА-Яа-яёЁ_0-9]*"; // keyword : ключевые слова var KEYWORD = "and и else иначе endexcept endfinally endforeach конецвсе endif конецесли endwhile конецпока " + "except exitfor finally foreach все if если in в not не or или try while пока "; // SYSRES Constants var sysres_constants = "SYSRES_CONST_ACCES_RIGHT_TYPE_EDIT " + "SYSRES_CONST_ACCES_RIGHT_TYPE_FULL " + "SYSRES_CONST_ACCES_RIGHT_TYPE_VIEW " + "SYSRES_CONST_ACCESS_MODE_REQUISITE_CODE " + "SYSRES_CONST_ACCESS_NO_ACCESS_VIEW " + "SYSRES_CONST_ACCESS_NO_ACCESS_VIEW_CODE " + "SYSRES_CONST_ACCESS_RIGHTS_ADD_REQUISITE_CODE " + "SYSRES_CONST_ACCESS_RIGHTS_ADD_REQUISITE_YES_CODE " + "SYSRES_CONST_ACCESS_RIGHTS_CHANGE_REQUISITE_CODE " + "SYSRES_CONST_ACCESS_RIGHTS_CHANGE_REQUISITE_YES_CODE " + "SYSRES_CONST_ACCESS_RIGHTS_DELETE_REQUISITE_CODE " + "SYSRES_CONST_ACCESS_RIGHTS_DELETE_REQUISITE_YES_CODE " + "SYSRES_CONST_ACCESS_RIGHTS_EXECUTE_REQUISITE_CODE " + "SYSRES_CONST_ACCESS_RIGHTS_EXECUTE_REQUISITE_YES_CODE " + "SYSRES_CONST_ACCESS_RIGHTS_NO_ACCESS_REQUISITE_CODE " + "SYSRES_CONST_ACCESS_RIGHTS_NO_ACCESS_REQUISITE_YES_CODE " + "SYSRES_CONST_ACCESS_RIGHTS_RATIFY_REQUISITE_CODE " + "SYSRES_CONST_ACCESS_RIGHTS_RATIFY_REQUISITE_YES_CODE " + "SYSRES_CONST_ACCESS_RIGHTS_REQUISITE_CODE " + "SYSRES_CONST_ACCESS_RIGHTS_VIEW " + "SYSRES_CONST_ACCESS_RIGHTS_VIEW_CODE " + "SYSRES_CONST_ACCESS_RIGHTS_VIEW_REQUISITE_CODE " + "SYSRES_CONST_ACCESS_RIGHTS_VIEW_REQUISITE_YES_CODE " + "SYSRES_CONST_ACCESS_TYPE_CHANGE " + "SYSRES_CONST_ACCESS_TYPE_CHANGE_CODE " + "SYSRES_CONST_ACCESS_TYPE_EXISTS " + "SYSRES_CONST_ACCESS_TYPE_EXISTS_CODE " + "SYSRES_CONST_ACCESS_TYPE_FULL " + "SYSRES_CONST_ACCESS_TYPE_FULL_CODE " + "SYSRES_CONST_ACCESS_TYPE_VIEW " + "SYSRES_CONST_ACCESS_TYPE_VIEW_CODE " + "SYSRES_CONST_ACTION_TYPE_ABORT " + "SYSRES_CONST_ACTION_TYPE_ACCEPT " + "SYSRES_CONST_ACTION_TYPE_ACCESS_RIGHTS " + "SYSRES_CONST_ACTION_TYPE_ADD_ATTACHMENT " + "SYSRES_CONST_ACTION_TYPE_CHANGE_CARD " + "SYSRES_CONST_ACTION_TYPE_CHANGE_KIND " + "SYSRES_CONST_ACTION_TYPE_CHANGE_STORAGE " + "SYSRES_CONST_ACTION_TYPE_CONTINUE " + "SYSRES_CONST_ACTION_TYPE_COPY " + "SYSRES_CONST_ACTION_TYPE_CREATE " + "SYSRES_CONST_ACTION_TYPE_CREATE_VERSION " + "SYSRES_CONST_ACTION_TYPE_DELETE " + "SYSRES_CONST_ACTION_TYPE_DELETE_ATTACHMENT " + "SYSRES_CONST_ACTION_TYPE_DELETE_VERSION " + "SYSRES_CONST_ACTION_TYPE_DISABLE_DELEGATE_ACCESS_RIGHTS " + "SYSRES_CONST_ACTION_TYPE_ENABLE_DELEGATE_ACCESS_RIGHTS " + "SYSRES_CONST_ACTION_TYPE_ENCRYPTION_BY_CERTIFICATE " + "SYSRES_CONST_ACTION_TYPE_ENCRYPTION_BY_CERTIFICATE_AND_PASSWORD " + "SYSRES_CONST_ACTION_TYPE_ENCRYPTION_BY_PASSWORD " + "SYSRES_CONST_ACTION_TYPE_EXPORT_WITH_LOCK " + "SYSRES_CONST_ACTION_TYPE_EXPORT_WITHOUT_LOCK " + "SYSRES_CONST_ACTION_TYPE_IMPORT_WITH_UNLOCK " + "SYSRES_CONST_ACTION_TYPE_IMPORT_WITHOUT_UNLOCK " + "SYSRES_CONST_ACTION_TYPE_LIFE_CYCLE_STAGE " + "SYSRES_CONST_ACTION_TYPE_LOCK " + "SYSRES_CONST_ACTION_TYPE_LOCK_FOR_SERVER " + "SYSRES_CONST_ACTION_TYPE_LOCK_MODIFY " + "SYSRES_CONST_ACTION_TYPE_MARK_AS_READED " + "SYSRES_CONST_ACTION_TYPE_MARK_AS_UNREADED " + "SYSRES_CONST_ACTION_TYPE_MODIFY " + "SYSRES_CONST_ACTION_TYPE_MODIFY_CARD " + "SYSRES_CONST_ACTION_TYPE_MOVE_TO_ARCHIVE " + "SYSRES_CONST_ACTION_TYPE_OFF_ENCRYPTION " + "SYSRES_CONST_ACTION_TYPE_PASSWORD_CHANGE " + "SYSRES_CONST_ACTION_TYPE_PERFORM " + "SYSRES_CONST_ACTION_TYPE_RECOVER_FROM_LOCAL_COPY " + "SYSRES_CONST_ACTION_TYPE_RESTART " + "SYSRES_CONST_ACTION_TYPE_RESTORE_FROM_ARCHIVE " + "SYSRES_CONST_ACTION_TYPE_REVISION " + "SYSRES_CONST_ACTION_TYPE_SEND_BY_MAIL " + "SYSRES_CONST_ACTION_TYPE_SIGN " + "SYSRES_CONST_ACTION_TYPE_START " + "SYSRES_CONST_ACTION_TYPE_UNLOCK " + "SYSRES_CONST_ACTION_TYPE_UNLOCK_FROM_SERVER " + "SYSRES_CONST_ACTION_TYPE_VERSION_STATE " + "SYSRES_CONST_ACTION_TYPE_VERSION_VISIBILITY " + "SYSRES_CONST_ACTION_TYPE_VIEW " + "SYSRES_CONST_ACTION_TYPE_VIEW_SHADOW_COPY " + "SYSRES_CONST_ACTION_TYPE_WORKFLOW_DESCRIPTION_MODIFY " + "SYSRES_CONST_ACTION_TYPE_WRITE_HISTORY " + "SYSRES_CONST_ACTIVE_VERSION_STATE_PICK_VALUE " + "SYSRES_CONST_ADD_REFERENCE_MODE_NAME " + "SYSRES_CONST_ADDITION_REQUISITE_CODE " + "SYSRES_CONST_ADDITIONAL_PARAMS_REQUISITE_CODE " + "SYSRES_CONST_ADITIONAL_JOB_END_DATE_REQUISITE_NAME " + "SYSRES_CONST_ADITIONAL_JOB_READ_REQUISITE_NAME " + "SYSRES_CONST_ADITIONAL_JOB_START_DATE_REQUISITE_NAME " + "SYSRES_CONST_ADITIONAL_JOB_STATE_REQUISITE_NAME " + "SYSRES_CONST_ADMINISTRATION_HISTORY_ADDING_USER_TO_GROUP_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_ADDING_USER_TO_GROUP_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_CREATION_COMP_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_CREATION_COMP_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_CREATION_GROUP_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_CREATION_GROUP_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_CREATION_USER_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_CREATION_USER_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_DATABASE_USER_CREATION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_DATABASE_USER_CREATION_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_DATABASE_USER_DELETION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_DATABASE_USER_DELETION_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_COMP_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_COMP_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_GROUP_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_GROUP_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_USER_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_USER_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_USER_FROM_GROUP_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_DELETION_USER_FROM_GROUP_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_FILTERER_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_FILTERER_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_FILTERER_RESTRICTION_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_FILTERER_RESTRICTION_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_PRIVILEGE_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_PRIVILEGE_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_RIGHTS_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_GRANTING_RIGHTS_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_IS_MAIN_SERVER_CHANGED_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_IS_MAIN_SERVER_CHANGED_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_IS_PUBLIC_CHANGED_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_IS_PUBLIC_CHANGED_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_FILTERER_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_FILTERER_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_FILTERER_RESTRICTION_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_FILTERER_RESTRICTION_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_PRIVILEGE_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_PRIVILEGE_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_RIGHTS_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_REMOVING_RIGHTS_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_SERVER_LOGIN_CREATION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_SERVER_LOGIN_CREATION_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_SERVER_LOGIN_DELETION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_SERVER_LOGIN_DELETION_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_CATEGORY_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_CATEGORY_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_COMP_TITLE_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_COMP_TITLE_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_FULL_NAME_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_FULL_NAME_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_GROUP_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_GROUP_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_PARENT_GROUP_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_PARENT_GROUP_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_USER_AUTH_TYPE_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_USER_AUTH_TYPE_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_USER_LOGIN_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_USER_LOGIN_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_USER_STATUS_ACTION " + "SYSRES_CONST_ADMINISTRATION_HISTORY_UPDATING_USER_STATUS_ACTION_CODE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_USER_PASSWORD_CHANGE " + "SYSRES_CONST_ADMINISTRATION_HISTORY_USER_PASSWORD_CHANGE_ACTION " + "SYSRES_CONST_ALL_ACCEPT_CONDITION_RUS " + "SYSRES_CONST_ALL_USERS_GROUP " + "SYSRES_CONST_ALL_USERS_GROUP_NAME " + "SYSRES_CONST_ALL_USERS_SERVER_GROUP_NAME " + "SYSRES_CONST_ALLOWED_ACCESS_TYPE_CODE " + "SYSRES_CONST_ALLOWED_ACCESS_TYPE_NAME " + "SYSRES_CONST_APP_VIEWER_TYPE_REQUISITE_CODE " + "SYSRES_CONST_APPROVING_SIGNATURE_NAME " + "SYSRES_CONST_APPROVING_SIGNATURE_REQUISITE_CODE " + "SYSRES_CONST_ASSISTANT_SUBSTITUE_TYPE " + "SYSRES_CONST_ASSISTANT_SUBSTITUE_TYPE_CODE " + "SYSRES_CONST_ATTACH_TYPE_COMPONENT_TOKEN " + "SYSRES_CONST_ATTACH_TYPE_DOC " + "SYSRES_CONST_ATTACH_TYPE_EDOC " + "SYSRES_CONST_ATTACH_TYPE_FOLDER " + "SYSRES_CONST_ATTACH_TYPE_JOB " + "SYSRES_CONST_ATTACH_TYPE_REFERENCE " + "SYSRES_CONST_ATTACH_TYPE_TASK " + "SYSRES_CONST_AUTH_ENCODED_PASSWORD " + "SYSRES_CONST_AUTH_ENCODED_PASSWORD_CODE " + "SYSRES_CONST_AUTH_NOVELL " + "SYSRES_CONST_AUTH_PASSWORD " + "SYSRES_CONST_AUTH_PASSWORD_CODE " + "SYSRES_CONST_AUTH_WINDOWS " + "SYSRES_CONST_AUTHENTICATING_SIGNATURE_NAME " + "SYSRES_CONST_AUTHENTICATING_SIGNATURE_REQUISITE_CODE " + "SYSRES_CONST_AUTO_ENUM_METHOD_FLAG " + "SYSRES_CONST_AUTO_NUMERATION_CODE " + "SYSRES_CONST_AUTO_STRONG_ENUM_METHOD_FLAG " + "SYSRES_CONST_AUTOTEXT_NAME_REQUISITE_CODE " + "SYSRES_CONST_AUTOTEXT_TEXT_REQUISITE_CODE " + "SYSRES_CONST_AUTOTEXT_USAGE_ALL " + "SYSRES_CONST_AUTOTEXT_USAGE_ALL_CODE " + "SYSRES_CONST_AUTOTEXT_USAGE_SIGN " + "SYSRES_CONST_AUTOTEXT_USAGE_SIGN_CODE " + "SYSRES_CONST_AUTOTEXT_USAGE_WORK " + "SYSRES_CONST_AUTOTEXT_USAGE_WORK_CODE " + "SYSRES_CONST_AUTOTEXT_USE_ANYWHERE_CODE " + "SYSRES_CONST_AUTOTEXT_USE_ON_SIGNING_CODE " + "SYSRES_CONST_AUTOTEXT_USE_ON_WORK_CODE " + "SYSRES_CONST_BEGIN_DATE_REQUISITE_CODE " + "SYSRES_CONST_BLACK_LIFE_CYCLE_STAGE_FONT_COLOR " + "SYSRES_CONST_BLUE_LIFE_CYCLE_STAGE_FONT_COLOR " + "SYSRES_CONST_BTN_PART " + "SYSRES_CONST_CALCULATED_ROLE_TYPE_CODE " + "SYSRES_CONST_CALL_TYPE_VARIABLE_BUTTON_VALUE " + "SYSRES_CONST_CALL_TYPE_VARIABLE_PROGRAM_VALUE " + "SYSRES_CONST_CANCEL_MESSAGE_FUNCTION_RESULT " + "SYSRES_CONST_CARD_PART " + "SYSRES_CONST_CARD_REFERENCE_MODE_NAME " + "SYSRES_CONST_CERTIFICATE_TYPE_REQUISITE_ENCRYPT_VALUE " + "SYSRES_CONST_CERTIFICATE_TYPE_REQUISITE_SIGN_AND_ENCRYPT_VALUE " + "SYSRES_CONST_CERTIFICATE_TYPE_REQUISITE_SIGN_VALUE " + "SYSRES_CONST_CHECK_PARAM_VALUE_DATE_PARAM_TYPE " + "SYSRES_CONST_CHECK_PARAM_VALUE_FLOAT_PARAM_TYPE " + "SYSRES_CONST_CHECK_PARAM_VALUE_INTEGER_PARAM_TYPE " + "SYSRES_CONST_CHECK_PARAM_VALUE_PICK_PARAM_TYPE " + "SYSRES_CONST_CHECK_PARAM_VALUE_REEFRENCE_PARAM_TYPE " + "SYSRES_CONST_CLOSED_RECORD_FLAG_VALUE_FEMININE " + "SYSRES_CONST_CLOSED_RECORD_FLAG_VALUE_MASCULINE " + "SYSRES_CONST_CODE_COMPONENT_TYPE_ADMIN " + "SYSRES_CONST_CODE_COMPONENT_TYPE_DEVELOPER " + "SYSRES_CONST_CODE_COMPONENT_TYPE_DOCS " + "SYSRES_CONST_CODE_COMPONENT_TYPE_EDOC_CARDS " + "SYSRES_CONST_CODE_COMPONENT_TYPE_EXTERNAL_EXECUTABLE " + "SYSRES_CONST_CODE_COMPONENT_TYPE_OTHER " + "SYSRES_CONST_CODE_COMPONENT_TYPE_REFERENCE " + "SYSRES_CONST_CODE_COMPONENT_TYPE_REPORT " + "SYSRES_CONST_CODE_COMPONENT_TYPE_SCRIPT " + "SYSRES_CONST_CODE_COMPONENT_TYPE_URL " + "SYSRES_CONST_CODE_REQUISITE_ACCESS " + "SYSRES_CONST_CODE_REQUISITE_CODE " + "SYSRES_CONST_CODE_REQUISITE_COMPONENT " + "SYSRES_CONST_CODE_REQUISITE_DESCRIPTION " + "SYSRES_CONST_CODE_REQUISITE_EXCLUDE_COMPONENT " + "SYSRES_CONST_CODE_REQUISITE_RECORD " + "SYSRES_CONST_COMMENT_REQ_CODE " + "SYSRES_CONST_COMMON_SETTINGS_REQUISITE_CODE " + "SYSRES_CONST_COMP_CODE_GRD " + "SYSRES_CONST_COMPONENT_GROUP_TYPE_REQUISITE_CODE " + "SYSRES_CONST_COMPONENT_TYPE_ADMIN_COMPONENTS " + "SYSRES_CONST_COMPONENT_TYPE_DEVELOPER_COMPONENTS " + "SYSRES_CONST_COMPONENT_TYPE_DOCS " + "SYSRES_CONST_COMPONENT_TYPE_EDOC_CARDS " + "SYSRES_CONST_COMPONENT_TYPE_EDOCS " + "SYSRES_CONST_COMPONENT_TYPE_EXTERNAL_EXECUTABLE " + "SYSRES_CONST_COMPONENT_TYPE_OTHER " + "SYSRES_CONST_COMPONENT_TYPE_REFERENCE_TYPES " + "SYSRES_CONST_COMPONENT_TYPE_REFERENCES " + "SYSRES_CONST_COMPONENT_TYPE_REPORTS " + "SYSRES_CONST_COMPONENT_TYPE_SCRIPTS " + "SYSRES_CONST_COMPONENT_TYPE_URL " + "SYSRES_CONST_COMPONENTS_REMOTE_SERVERS_VIEW_CODE " + "SYSRES_CONST_CONDITION_BLOCK_DESCRIPTION " + "SYSRES_CONST_CONST_FIRM_STATUS_COMMON " + "SYSRES_CONST_CONST_FIRM_STATUS_INDIVIDUAL " + "SYSRES_CONST_CONST_NEGATIVE_VALUE " + "SYSRES_CONST_CONST_POSITIVE_VALUE " + "SYSRES_CONST_CONST_SERVER_STATUS_DONT_REPLICATE " + "SYSRES_CONST_CONST_SERVER_STATUS_REPLICATE " + "SYSRES_CONST_CONTENTS_REQUISITE_CODE " + "SYSRES_CONST_DATA_TYPE_BOOLEAN " + "SYSRES_CONST_DATA_TYPE_DATE " + "SYSRES_CONST_DATA_TYPE_FLOAT " + "SYSRES_CONST_DATA_TYPE_INTEGER " + "SYSRES_CONST_DATA_TYPE_PICK " + "SYSRES_CONST_DATA_TYPE_REFERENCE " + "SYSRES_CONST_DATA_TYPE_STRING " + "SYSRES_CONST_DATA_TYPE_TEXT " + "SYSRES_CONST_DATA_TYPE_VARIANT " + "SYSRES_CONST_DATE_CLOSE_REQ_CODE " + "SYSRES_CONST_DATE_FORMAT_DATE_ONLY_CHAR " + "SYSRES_CONST_DATE_OPEN_REQ_CODE " + "SYSRES_CONST_DATE_REQUISITE " + "SYSRES_CONST_DATE_REQUISITE_CODE " + "SYSRES_CONST_DATE_REQUISITE_NAME " + "SYSRES_CONST_DATE_REQUISITE_TYPE " + "SYSRES_CONST_DATE_TYPE_CHAR " + "SYSRES_CONST_DATETIME_FORMAT_VALUE " + "SYSRES_CONST_DEA_ACCESS_RIGHTS_ACTION_CODE " + "SYSRES_CONST_DESCRIPTION_LOCALIZE_ID_REQUISITE_CODE " + "SYSRES_CONST_DESCRIPTION_REQUISITE_CODE " + "SYSRES_CONST_DET1_PART " + "SYSRES_CONST_DET2_PART " + "SYSRES_CONST_DET3_PART " + "SYSRES_CONST_DET4_PART " + "SYSRES_CONST_DET5_PART " + "SYSRES_CONST_DET6_PART " + "SYSRES_CONST_DETAIL_DATASET_KEY_REQUISITE_CODE " + "SYSRES_CONST_DETAIL_PICK_REQUISITE_CODE " + "SYSRES_CONST_DETAIL_REQ_CODE " + "SYSRES_CONST_DO_NOT_USE_ACCESS_TYPE_CODE " + "SYSRES_CONST_DO_NOT_USE_ACCESS_TYPE_NAME " + "SYSRES_CONST_DO_NOT_USE_ON_VIEW_ACCESS_TYPE_CODE " + "SYSRES_CONST_DO_NOT_USE_ON_VIEW_ACCESS_TYPE_NAME " + "SYSRES_CONST_DOCUMENT_STORAGES_CODE " + "SYSRES_CONST_DOCUMENT_TEMPLATES_TYPE_NAME " + "SYSRES_CONST_DOUBLE_REQUISITE_CODE " + "SYSRES_CONST_EDITOR_CLOSE_FILE_OBSERV_TYPE_CODE " + "SYSRES_CONST_EDITOR_CLOSE_PROCESS_OBSERV_TYPE_CODE " + "SYSRES_CONST_EDITOR_TYPE_REQUISITE_CODE " + "SYSRES_CONST_EDITORS_APPLICATION_NAME_REQUISITE_CODE " + "SYSRES_CONST_EDITORS_CREATE_SEVERAL_PROCESSES_REQUISITE_CODE " + "SYSRES_CONST_EDITORS_EXTENSION_REQUISITE_CODE " + "SYSRES_CONST_EDITORS_OBSERVER_BY_PROCESS_TYPE " + "SYSRES_CONST_EDITORS_REFERENCE_CODE " + "SYSRES_CONST_EDITORS_REPLACE_SPEC_CHARS_REQUISITE_CODE " + "SYSRES_CONST_EDITORS_USE_PLUGINS_REQUISITE_CODE " + "SYSRES_CONST_EDITORS_VIEW_DOCUMENT_OPENED_TO_EDIT_CODE " + "SYSRES_CONST_EDOC_CARD_TYPE_REQUISITE_CODE " + "SYSRES_CONST_EDOC_CARD_TYPES_LINK_REQUISITE_CODE " + "SYSRES_CONST_EDOC_CERTIFICATE_AND_PASSWORD_ENCODE_CODE " + "SYSRES_CONST_EDOC_CERTIFICATE_ENCODE_CODE " + "SYSRES_CONST_EDOC_DATE_REQUISITE_CODE " + "SYSRES_CONST_EDOC_KIND_REFERENCE_CODE " + "SYSRES_CONST_EDOC_KINDS_BY_TEMPLATE_ACTION_CODE " + "SYSRES_CONST_EDOC_MANAGE_ACCESS_CODE " + "SYSRES_CONST_EDOC_NONE_ENCODE_CODE " + "SYSRES_CONST_EDOC_NUMBER_REQUISITE_CODE " + "SYSRES_CONST_EDOC_PASSWORD_ENCODE_CODE " + "SYSRES_CONST_EDOC_READONLY_ACCESS_CODE " + "SYSRES_CONST_EDOC_SHELL_LIFE_TYPE_VIEW_VALUE " + "SYSRES_CONST_EDOC_SIZE_RESTRICTION_PRIORITY_REQUISITE_CODE " + "SYSRES_CONST_EDOC_STORAGE_CHECK_ACCESS_RIGHTS_REQUISITE_CODE " + "SYSRES_CONST_EDOC_STORAGE_COMPUTER_NAME_REQUISITE_CODE " + "SYSRES_CONST_EDOC_STORAGE_DATABASE_NAME_REQUISITE_CODE " + "SYSRES_CONST_EDOC_STORAGE_EDIT_IN_STORAGE_REQUISITE_CODE " + "SYSRES_CONST_EDOC_STORAGE_LOCAL_PATH_REQUISITE_CODE " + "SYSRES_CONST_EDOC_STORAGE_SHARED_SOURCE_NAME_REQUISITE_CODE " + "SYSRES_CONST_EDOC_TEMPLATE_REQUISITE_CODE " + "SYSRES_CONST_EDOC_TYPES_REFERENCE_CODE " + "SYSRES_CONST_EDOC_VERSION_ACTIVE_STAGE_CODE " + "SYSRES_CONST_EDOC_VERSION_DESIGN_STAGE_CODE " + "SYSRES_CONST_EDOC_VERSION_OBSOLETE_STAGE_CODE " + "SYSRES_CONST_EDOC_WRITE_ACCES_CODE " + "SYSRES_CONST_EDOCUMENT_CARD_REQUISITES_REFERENCE_CODE_SELECTED_REQUISITE " + "SYSRES_CONST_ENCODE_CERTIFICATE_TYPE_CODE " + "SYSRES_CONST_END_DATE_REQUISITE_CODE " + "SYSRES_CONST_ENUMERATION_TYPE_REQUISITE_CODE " + "SYSRES_CONST_EXECUTE_ACCESS_RIGHTS_TYPE_CODE " + "SYSRES_CONST_EXECUTIVE_FILE_STORAGE_TYPE " + "SYSRES_CONST_EXIST_CONST " + "SYSRES_CONST_EXIST_VALUE " + "SYSRES_CONST_EXPORT_LOCK_TYPE_ASK " + "SYSRES_CONST_EXPORT_LOCK_TYPE_WITH_LOCK " + "SYSRES_CONST_EXPORT_LOCK_TYPE_WITHOUT_LOCK " + "SYSRES_CONST_EXPORT_VERSION_TYPE_ASK " + "SYSRES_CONST_EXPORT_VERSION_TYPE_LAST " + "SYSRES_CONST_EXPORT_VERSION_TYPE_LAST_ACTIVE " + "SYSRES_CONST_EXTENSION_REQUISITE_CODE " + "SYSRES_CONST_FILTER_NAME_REQUISITE_CODE " + "SYSRES_CONST_FILTER_REQUISITE_CODE " + "SYSRES_CONST_FILTER_TYPE_COMMON_CODE " + "SYSRES_CONST_FILTER_TYPE_COMMON_NAME " + "SYSRES_CONST_FILTER_TYPE_USER_CODE " + "SYSRES_CONST_FILTER_TYPE_USER_NAME " + "SYSRES_CONST_FILTER_VALUE_REQUISITE_NAME " + "SYSRES_CONST_FLOAT_NUMBER_FORMAT_CHAR " + "SYSRES_CONST_FLOAT_REQUISITE_TYPE " + "SYSRES_CONST_FOLDER_AUTHOR_VALUE " + "SYSRES_CONST_FOLDER_KIND_ANY_OBJECTS " + "SYSRES_CONST_FOLDER_KIND_COMPONENTS " + "SYSRES_CONST_FOLDER_KIND_EDOCS " + "SYSRES_CONST_FOLDER_KIND_JOBS " + "SYSRES_CONST_FOLDER_KIND_TASKS " + "SYSRES_CONST_FOLDER_TYPE_COMMON " + "SYSRES_CONST_FOLDER_TYPE_COMPONENT " + "SYSRES_CONST_FOLDER_TYPE_FAVORITES " + "SYSRES_CONST_FOLDER_TYPE_INBOX " + "SYSRES_CONST_FOLDER_TYPE_OUTBOX " + "SYSRES_CONST_FOLDER_TYPE_QUICK_LAUNCH " + "SYSRES_CONST_FOLDER_TYPE_SEARCH " + "SYSRES_CONST_FOLDER_TYPE_SHORTCUTS " + "SYSRES_CONST_FOLDER_TYPE_USER " + "SYSRES_CONST_FROM_DICTIONARY_ENUM_METHOD_FLAG " + "SYSRES_CONST_FULL_SUBSTITUTE_TYPE " + "SYSRES_CONST_FULL_SUBSTITUTE_TYPE_CODE " + "SYSRES_CONST_FUNCTION_CANCEL_RESULT " + "SYSRES_CONST_FUNCTION_CATEGORY_SYSTEM " + "SYSRES_CONST_FUNCTION_CATEGORY_USER " + "SYSRES_CONST_FUNCTION_FAILURE_RESULT " + "SYSRES_CONST_FUNCTION_SAVE_RESULT " + "SYSRES_CONST_GENERATED_REQUISITE " + "SYSRES_CONST_GREEN_LIFE_CYCLE_STAGE_FONT_COLOR " + "SYSRES_CONST_GROUP_ACCOUNT_TYPE_VALUE_CODE " + "SYSRES_CONST_GROUP_CATEGORY_NORMAL_CODE " + "SYSRES_CONST_GROUP_CATEGORY_NORMAL_NAME " + "SYSRES_CONST_GROUP_CATEGORY_SERVICE_CODE " + "SYSRES_CONST_GROUP_CATEGORY_SERVICE_NAME " + "SYSRES_CONST_GROUP_COMMON_CATEGORY_FIELD_VALUE " + "SYSRES_CONST_GROUP_FULL_NAME_REQUISITE_CODE " + "SYSRES_CONST_GROUP_NAME_REQUISITE_CODE " + "SYSRES_CONST_GROUP_RIGHTS_T_REQUISITE_CODE " + "SYSRES_CONST_GROUP_SERVER_CODES_REQUISITE_CODE " + "SYSRES_CONST_GROUP_SERVER_NAME_REQUISITE_CODE " + "SYSRES_CONST_GROUP_SERVICE_CATEGORY_FIELD_VALUE " + "SYSRES_CONST_GROUP_USER_REQUISITE_CODE " + "SYSRES_CONST_GROUPS_REFERENCE_CODE " + "SYSRES_CONST_GROUPS_REQUISITE_CODE " + "SYSRES_CONST_HIDDEN_MODE_NAME " + "SYSRES_CONST_HIGH_LVL_REQUISITE_CODE " + "SYSRES_CONST_HISTORY_ACTION_CREATE_CODE " + "SYSRES_CONST_HISTORY_ACTION_DELETE_CODE " + "SYSRES_CONST_HISTORY_ACTION_EDIT_CODE " + "SYSRES_CONST_HOUR_CHAR " + "SYSRES_CONST_ID_REQUISITE_CODE " + "SYSRES_CONST_IDSPS_REQUISITE_CODE " + "SYSRES_CONST_IMAGE_MODE_COLOR " + "SYSRES_CONST_IMAGE_MODE_GREYSCALE " + "SYSRES_CONST_IMAGE_MODE_MONOCHROME " + "SYSRES_CONST_IMPORTANCE_HIGH " + "SYSRES_CONST_IMPORTANCE_LOW " + "SYSRES_CONST_IMPORTANCE_NORMAL " + "SYSRES_CONST_IN_DESIGN_VERSION_STATE_PICK_VALUE " + "SYSRES_CONST_INCOMING_WORK_RULE_TYPE_CODE " + "SYSRES_CONST_INT_REQUISITE " + "SYSRES_CONST_INT_REQUISITE_TYPE " + "SYSRES_CONST_INTEGER_NUMBER_FORMAT_CHAR " + "SYSRES_CONST_INTEGER_TYPE_CHAR " + "SYSRES_CONST_IS_GENERATED_REQUISITE_NEGATIVE_VALUE " + "SYSRES_CONST_IS_PUBLIC_ROLE_REQUISITE_CODE " + "SYSRES_CONST_IS_REMOTE_USER_NEGATIVE_VALUE " + "SYSRES_CONST_IS_REMOTE_USER_POSITIVE_VALUE " + "SYSRES_CONST_IS_STORED_REQUISITE_NEGATIVE_VALUE " + "SYSRES_CONST_IS_STORED_REQUISITE_STORED_VALUE " + "SYSRES_CONST_ITALIC_LIFE_CYCLE_STAGE_DRAW_STYLE " + "SYSRES_CONST_JOB_BLOCK_DESCRIPTION " + "SYSRES_CONST_JOB_KIND_CONTROL_JOB " + "SYSRES_CONST_JOB_KIND_JOB " + "SYSRES_CONST_JOB_KIND_NOTICE " + "SYSRES_CONST_JOB_STATE_ABORTED " + "SYSRES_CONST_JOB_STATE_COMPLETE " + "SYSRES_CONST_JOB_STATE_WORKING " + "SYSRES_CONST_KIND_REQUISITE_CODE " + "SYSRES_CONST_KIND_REQUISITE_NAME " + "SYSRES_CONST_KINDS_CREATE_SHADOW_COPIES_REQUISITE_CODE " + "SYSRES_CONST_KINDS_DEFAULT_EDOC_LIFE_STAGE_REQUISITE_CODE " + "SYSRES_CONST_KINDS_EDOC_ALL_TEPLATES_ALLOWED_REQUISITE_CODE " + "SYSRES_CONST_KINDS_EDOC_ALLOW_LIFE_CYCLE_STAGE_CHANGING_REQUISITE_CODE " + "SYSRES_CONST_KINDS_EDOC_ALLOW_MULTIPLE_ACTIVE_VERSIONS_REQUISITE_CODE " + "SYSRES_CONST_KINDS_EDOC_SHARE_ACCES_RIGHTS_BY_DEFAULT_CODE " + "SYSRES_CONST_KINDS_EDOC_TEMPLATE_REQUISITE_CODE " + "SYSRES_CONST_KINDS_EDOC_TYPE_REQUISITE_CODE " + "SYSRES_CONST_KINDS_SIGNERS_REQUISITES_CODE " + "SYSRES_CONST_KOD_INPUT_TYPE " + "SYSRES_CONST_LAST_UPDATE_DATE_REQUISITE_CODE " + "SYSRES_CONST_LIFE_CYCLE_START_STAGE_REQUISITE_CODE " + "SYSRES_CONST_LILAC_LIFE_CYCLE_STAGE_FONT_COLOR " + "SYSRES_CONST_LINK_OBJECT_KIND_COMPONENT " + "SYSRES_CONST_LINK_OBJECT_KIND_DOCUMENT " + "SYSRES_CONST_LINK_OBJECT_KIND_EDOC " + "SYSRES_CONST_LINK_OBJECT_KIND_FOLDER " + "SYSRES_CONST_LINK_OBJECT_KIND_JOB " + "SYSRES_CONST_LINK_OBJECT_KIND_REFERENCE " + "SYSRES_CONST_LINK_OBJECT_KIND_TASK " + "SYSRES_CONST_LINK_REF_TYPE_REQUISITE_CODE " + "SYSRES_CONST_LIST_REFERENCE_MODE_NAME " + "SYSRES_CONST_LOCALIZATION_DICTIONARY_MAIN_VIEW_CODE " + "SYSRES_CONST_MAIN_VIEW_CODE " + "SYSRES_CONST_MANUAL_ENUM_METHOD_FLAG " + "SYSRES_CONST_MASTER_COMP_TYPE_REQUISITE_CODE " + "SYSRES_CONST_MASTER_TABLE_REC_ID_REQUISITE_CODE " + "SYSRES_CONST_MAXIMIZED_MODE_NAME " + "SYSRES_CONST_ME_VALUE " + "SYSRES_CONST_MESSAGE_ATTENTION_CAPTION " + "SYSRES_CONST_MESSAGE_CONFIRMATION_CAPTION " + "SYSRES_CONST_MESSAGE_ERROR_CAPTION " + "SYSRES_CONST_MESSAGE_INFORMATION_CAPTION " + "SYSRES_CONST_MINIMIZED_MODE_NAME " + "SYSRES_CONST_MINUTE_CHAR " + "SYSRES_CONST_MODULE_REQUISITE_CODE " + "SYSRES_CONST_MONITORING_BLOCK_DESCRIPTION " + "SYSRES_CONST_MONTH_FORMAT_VALUE " + "SYSRES_CONST_NAME_LOCALIZE_ID_REQUISITE_CODE " + "SYSRES_CONST_NAME_REQUISITE_CODE " + "SYSRES_CONST_NAME_SINGULAR_REQUISITE_CODE " + "SYSRES_CONST_NAMEAN_INPUT_TYPE " + "SYSRES_CONST_NEGATIVE_PICK_VALUE " + "SYSRES_CONST_NEGATIVE_VALUE " + "SYSRES_CONST_NO " + "SYSRES_CONST_NO_PICK_VALUE " + "SYSRES_CONST_NO_SIGNATURE_REQUISITE_CODE " + "SYSRES_CONST_NO_VALUE " + "SYSRES_CONST_NONE_ACCESS_RIGHTS_TYPE_CODE " + "SYSRES_CONST_NONOPERATING_RECORD_FLAG_VALUE " + "SYSRES_CONST_NONOPERATING_RECORD_FLAG_VALUE_MASCULINE " + "SYSRES_CONST_NORMAL_ACCESS_RIGHTS_TYPE_CODE " + "SYSRES_CONST_NORMAL_LIFE_CYCLE_STAGE_DRAW_STYLE " + "SYSRES_CONST_NORMAL_MODE_NAME " + "SYSRES_CONST_NOT_ALLOWED_ACCESS_TYPE_CODE " + "SYSRES_CONST_NOT_ALLOWED_ACCESS_TYPE_NAME " + "SYSRES_CONST_NOTE_REQUISITE_CODE " + "SYSRES_CONST_NOTICE_BLOCK_DESCRIPTION " + "SYSRES_CONST_NUM_REQUISITE " + "SYSRES_CONST_NUM_STR_REQUISITE_CODE " + "SYSRES_CONST_NUMERATION_AUTO_NOT_STRONG " + "SYSRES_CONST_NUMERATION_AUTO_STRONG " + "SYSRES_CONST_NUMERATION_FROM_DICTONARY " + "SYSRES_CONST_NUMERATION_MANUAL " + "SYSRES_CONST_NUMERIC_TYPE_CHAR " + "SYSRES_CONST_NUMREQ_REQUISITE_CODE " + "SYSRES_CONST_OBSOLETE_VERSION_STATE_PICK_VALUE " + "SYSRES_CONST_OPERATING_RECORD_FLAG_VALUE " + "SYSRES_CONST_OPERATING_RECORD_FLAG_VALUE_CODE " + "SYSRES_CONST_OPERATING_RECORD_FLAG_VALUE_FEMININE " + "SYSRES_CONST_OPERATING_RECORD_FLAG_VALUE_MASCULINE " + "SYSRES_CONST_OPTIONAL_FORM_COMP_REQCODE_PREFIX " + "SYSRES_CONST_ORANGE_LIFE_CYCLE_STAGE_FONT_COLOR " + "SYSRES_CONST_ORIGINALREF_REQUISITE_CODE " + "SYSRES_CONST_OURFIRM_REF_CODE " + "SYSRES_CONST_OURFIRM_REQUISITE_CODE " + "SYSRES_CONST_OURFIRM_VAR " + "SYSRES_CONST_OUTGOING_WORK_RULE_TYPE_CODE " + "SYSRES_CONST_PICK_NEGATIVE_RESULT " + "SYSRES_CONST_PICK_POSITIVE_RESULT " + "SYSRES_CONST_PICK_REQUISITE " + "SYSRES_CONST_PICK_REQUISITE_TYPE " + "SYSRES_CONST_PICK_TYPE_CHAR " + "SYSRES_CONST_PLAN_STATUS_REQUISITE_CODE " + "SYSRES_CONST_PLATFORM_VERSION_COMMENT " + "SYSRES_CONST_PLUGINS_SETTINGS_DESCRIPTION_REQUISITE_CODE " + "SYSRES_CONST_POSITIVE_PICK_VALUE " + "SYSRES_CONST_POWER_TO_CREATE_ACTION_CODE " + "SYSRES_CONST_POWER_TO_SIGN_ACTION_CODE " + "SYSRES_CONST_PRIORITY_REQUISITE_CODE " + "SYSRES_CONST_QUALIFIED_TASK_TYPE " + "SYSRES_CONST_QUALIFIED_TASK_TYPE_CODE " + "SYSRES_CONST_RECSTAT_REQUISITE_CODE " + "SYSRES_CONST_RED_LIFE_CYCLE_STAGE_FONT_COLOR " + "SYSRES_CONST_REF_ID_T_REF_TYPE_REQUISITE_CODE " + "SYSRES_CONST_REF_REQUISITE " + "SYSRES_CONST_REF_REQUISITE_TYPE " + "SYSRES_CONST_REF_REQUISITES_REFERENCE_CODE_SELECTED_REQUISITE " + "SYSRES_CONST_REFERENCE_RECORD_HISTORY_CREATE_ACTION_CODE " + "SYSRES_CONST_REFERENCE_RECORD_HISTORY_DELETE_ACTION_CODE " + "SYSRES_CONST_REFERENCE_RECORD_HISTORY_MODIFY_ACTION_CODE " + "SYSRES_CONST_REFERENCE_TYPE_CHAR " + "SYSRES_CONST_REFERENCE_TYPE_REQUISITE_NAME " + "SYSRES_CONST_REFERENCES_ADD_PARAMS_REQUISITE_CODE " + "SYSRES_CONST_REFERENCES_DISPLAY_REQUISITE_REQUISITE_CODE " + "SYSRES_CONST_REMOTE_SERVER_STATUS_WORKING " + "SYSRES_CONST_REMOTE_SERVER_TYPE_MAIN " + "SYSRES_CONST_REMOTE_SERVER_TYPE_SECONDARY " + "SYSRES_CONST_REMOTE_USER_FLAG_VALUE_CODE " + "SYSRES_CONST_REPORT_APP_EDITOR_INTERNAL " + "SYSRES_CONST_REPORT_BASE_REPORT_ID_REQUISITE_CODE " + "SYSRES_CONST_REPORT_BASE_REPORT_REQUISITE_CODE " + "SYSRES_CONST_REPORT_SCRIPT_REQUISITE_CODE " + "SYSRES_CONST_REPORT_TEMPLATE_REQUISITE_CODE " + "SYSRES_CONST_REPORT_VIEWER_CODE_REQUISITE_CODE " + "SYSRES_CONST_REQ_ALLOW_COMPONENT_DEFAULT_VALUE " + "SYSRES_CONST_REQ_ALLOW_RECORD_DEFAULT_VALUE " + "SYSRES_CONST_REQ_ALLOW_SERVER_COMPONENT_DEFAULT_VALUE " + "SYSRES_CONST_REQ_MODE_AVAILABLE_CODE " + "SYSRES_CONST_REQ_MODE_EDIT_CODE " + "SYSRES_CONST_REQ_MODE_HIDDEN_CODE " + "SYSRES_CONST_REQ_MODE_NOT_AVAILABLE_CODE " + "SYSRES_CONST_REQ_MODE_VIEW_CODE " + "SYSRES_CONST_REQ_NUMBER_REQUISITE_CODE " + "SYSRES_CONST_REQ_SECTION_VALUE " + "SYSRES_CONST_REQ_TYPE_VALUE " + "SYSRES_CONST_REQUISITE_FORMAT_BY_UNIT " + "SYSRES_CONST_REQUISITE_FORMAT_DATE_FULL " + "SYSRES_CONST_REQUISITE_FORMAT_DATE_TIME " + "SYSRES_CONST_REQUISITE_FORMAT_LEFT " + "SYSRES_CONST_REQUISITE_FORMAT_RIGHT " + "SYSRES_CONST_REQUISITE_FORMAT_WITHOUT_UNIT " + "SYSRES_CONST_REQUISITE_NUMBER_REQUISITE_CODE " + "SYSRES_CONST_REQUISITE_SECTION_ACTIONS " + "SYSRES_CONST_REQUISITE_SECTION_BUTTON " + "SYSRES_CONST_REQUISITE_SECTION_BUTTONS " + "SYSRES_CONST_REQUISITE_SECTION_CARD " + "SYSRES_CONST_REQUISITE_SECTION_TABLE " + "SYSRES_CONST_REQUISITE_SECTION_TABLE10 " + "SYSRES_CONST_REQUISITE_SECTION_TABLE11 " + "SYSRES_CONST_REQUISITE_SECTION_TABLE12 " + "SYSRES_CONST_REQUISITE_SECTION_TABLE13 " + "SYSRES_CONST_REQUISITE_SECTION_TABLE14 " + "SYSRES_CONST_REQUISITE_SECTION_TABLE15 " + "SYSRES_CONST_REQUISITE_SECTION_TABLE16 " + "SYSRES_CONST_REQUISITE_SECTION_TABLE17 " + "SYSRES_CONST_REQUISITE_SECTION_TABLE18 " + "SYSRES_CONST_REQUISITE_SECTION_TABLE19 " + "SYSRES_CONST_REQUISITE_SECTION_TABLE2 " + "SYSRES_CONST_REQUISITE_SECTION_TABLE20 " + "SYSRES_CONST_REQUISITE_SECTION_TABLE21 " + "SYSRES_CONST_REQUISITE_SECTION_TABLE22 " + "SYSRES_CONST_REQUISITE_SECTION_TABLE23 " + "SYSRES_CONST_REQUISITE_SECTION_TABLE24 " + "SYSRES_CONST_REQUISITE_SECTION_TABLE3 " + "SYSRES_CONST_REQUISITE_SECTION_TABLE4 " + "SYSRES_CONST_REQUISITE_SECTION_TABLE5 " + "SYSRES_CONST_REQUISITE_SECTION_TABLE6 " + "SYSRES_CONST_REQUISITE_SECTION_TABLE7 " + "SYSRES_CONST_REQUISITE_SECTION_TABLE8 " + "SYSRES_CONST_REQUISITE_SECTION_TABLE9 " + "SYSRES_CONST_REQUISITES_PSEUDOREFERENCE_REQUISITE_NUMBER_REQUISITE_CODE " + "SYSRES_CONST_RIGHT_ALIGNMENT_CODE " + "SYSRES_CONST_ROLES_REFERENCE_CODE " + "SYSRES_CONST_ROUTE_STEP_AFTER_RUS " + "SYSRES_CONST_ROUTE_STEP_AND_CONDITION_RUS " + "SYSRES_CONST_ROUTE_STEP_OR_CONDITION_RUS " + "SYSRES_CONST_ROUTE_TYPE_COMPLEX " + "SYSRES_CONST_ROUTE_TYPE_PARALLEL " + "SYSRES_CONST_ROUTE_TYPE_SERIAL " + "SYSRES_CONST_SBDATASETDESC_NEGATIVE_VALUE " + "SYSRES_CONST_SBDATASETDESC_POSITIVE_VALUE " + "SYSRES_CONST_SBVIEWSDESC_POSITIVE_VALUE " + "SYSRES_CONST_SCRIPT_BLOCK_DESCRIPTION " + "SYSRES_CONST_SEARCH_BY_TEXT_REQUISITE_CODE " + "SYSRES_CONST_SEARCHES_COMPONENT_CONTENT " + "SYSRES_CONST_SEARCHES_CRITERIA_ACTION_NAME " + "SYSRES_CONST_SEARCHES_EDOC_CONTENT " + "SYSRES_CONST_SEARCHES_FOLDER_CONTENT " + "SYSRES_CONST_SEARCHES_JOB_CONTENT " + "SYSRES_CONST_SEARCHES_REFERENCE_CODE " + "SYSRES_CONST_SEARCHES_TASK_CONTENT " + "SYSRES_CONST_SECOND_CHAR " + "SYSRES_CONST_SECTION_REQUISITE_ACTIONS_VALUE " + "SYSRES_CONST_SECTION_REQUISITE_CARD_VALUE " + "SYSRES_CONST_SECTION_REQUISITE_CODE " + "SYSRES_CONST_SECTION_REQUISITE_DETAIL_1_VALUE " + "SYSRES_CONST_SECTION_REQUISITE_DETAIL_2_VALUE " + "SYSRES_CONST_SECTION_REQUISITE_DETAIL_3_VALUE " + "SYSRES_CONST_SECTION_REQUISITE_DETAIL_4_VALUE " + "SYSRES_CONST_SECTION_REQUISITE_DETAIL_5_VALUE " + "SYSRES_CONST_SECTION_REQUISITE_DETAIL_6_VALUE " + "SYSRES_CONST_SELECT_REFERENCE_MODE_NAME " + "SYSRES_CONST_SELECT_TYPE_SELECTABLE " + "SYSRES_CONST_SELECT_TYPE_SELECTABLE_ONLY_CHILD " + "SYSRES_CONST_SELECT_TYPE_SELECTABLE_WITH_CHILD " + "SYSRES_CONST_SELECT_TYPE_UNSLECTABLE " + "SYSRES_CONST_SERVER_TYPE_MAIN " + "SYSRES_CONST_SERVICE_USER_CATEGORY_FIELD_VALUE " + "SYSRES_CONST_SETTINGS_USER_REQUISITE_CODE " + "SYSRES_CONST_SIGNATURE_AND_ENCODE_CERTIFICATE_TYPE_CODE " + "SYSRES_CONST_SIGNATURE_CERTIFICATE_TYPE_CODE " + "SYSRES_CONST_SINGULAR_TITLE_REQUISITE_CODE " + "SYSRES_CONST_SQL_SERVER_AUTHENTIFICATION_FLAG_VALUE_CODE " + "SYSRES_CONST_SQL_SERVER_ENCODE_AUTHENTIFICATION_FLAG_VALUE_CODE " + "SYSRES_CONST_STANDART_ROUTE_REFERENCE_CODE " + "SYSRES_CONST_STANDART_ROUTE_REFERENCE_COMMENT_REQUISITE_CODE " + "SYSRES_CONST_STANDART_ROUTES_GROUPS_REFERENCE_CODE " + "SYSRES_CONST_STATE_REQ_NAME " + "SYSRES_CONST_STATE_REQUISITE_ACTIVE_VALUE " + "SYSRES_CONST_STATE_REQUISITE_CLOSED_VALUE " + "SYSRES_CONST_STATE_REQUISITE_CODE " + "SYSRES_CONST_STATIC_ROLE_TYPE_CODE " + "SYSRES_CONST_STATUS_PLAN_DEFAULT_VALUE " + "SYSRES_CONST_STATUS_VALUE_AUTOCLEANING " + "SYSRES_CONST_STATUS_VALUE_BLUE_SQUARE " + "SYSRES_CONST_STATUS_VALUE_COMPLETE " + "SYSRES_CONST_STATUS_VALUE_GREEN_SQUARE " + "SYSRES_CONST_STATUS_VALUE_ORANGE_SQUARE " + "SYSRES_CONST_STATUS_VALUE_PURPLE_SQUARE " + "SYSRES_CONST_STATUS_VALUE_RED_SQUARE " + "SYSRES_CONST_STATUS_VALUE_SUSPEND " + "SYSRES_CONST_STATUS_VALUE_YELLOW_SQUARE " + "SYSRES_CONST_STDROUTE_SHOW_TO_USERS_REQUISITE_CODE " + "SYSRES_CONST_STORAGE_TYPE_FILE " + "SYSRES_CONST_STORAGE_TYPE_SQL_SERVER " + "SYSRES_CONST_STR_REQUISITE " + "SYSRES_CONST_STRIKEOUT_LIFE_CYCLE_STAGE_DRAW_STYLE " + "SYSRES_CONST_STRING_FORMAT_LEFT_ALIGN_CHAR " + "SYSRES_CONST_STRING_FORMAT_RIGHT_ALIGN_CHAR " + "SYSRES_CONST_STRING_REQUISITE_CODE " + "SYSRES_CONST_STRING_REQUISITE_TYPE " + "SYSRES_CONST_STRING_TYPE_CHAR " + "SYSRES_CONST_SUBSTITUTES_PSEUDOREFERENCE_CODE " + "SYSRES_CONST_SUBTASK_BLOCK_DESCRIPTION " + "SYSRES_CONST_SYSTEM_SETTING_CURRENT_USER_PARAM_VALUE " + "SYSRES_CONST_SYSTEM_SETTING_EMPTY_VALUE_PARAM_VALUE " + "SYSRES_CONST_SYSTEM_VERSION_COMMENT " + "SYSRES_CONST_TASK_ACCESS_TYPE_ALL " + "SYSRES_CONST_TASK_ACCESS_TYPE_ALL_MEMBERS " + "SYSRES_CONST_TASK_ACCESS_TYPE_MANUAL " + "SYSRES_CONST_TASK_ENCODE_TYPE_CERTIFICATION " + "SYSRES_CONST_TASK_ENCODE_TYPE_CERTIFICATION_AND_PASSWORD " + "SYSRES_CONST_TASK_ENCODE_TYPE_NONE " + "SYSRES_CONST_TASK_ENCODE_TYPE_PASSWORD " + "SYSRES_CONST_TASK_ROUTE_ALL_CONDITION " + "SYSRES_CONST_TASK_ROUTE_AND_CONDITION " + "SYSRES_CONST_TASK_ROUTE_OR_CONDITION " + "SYSRES_CONST_TASK_STATE_ABORTED " + "SYSRES_CONST_TASK_STATE_COMPLETE " + "SYSRES_CONST_TASK_STATE_CONTINUED " + "SYSRES_CONST_TASK_STATE_CONTROL " + "SYSRES_CONST_TASK_STATE_INIT " + "SYSRES_CONST_TASK_STATE_WORKING " + "SYSRES_CONST_TASK_TITLE " + "SYSRES_CONST_TASK_TYPES_GROUPS_REFERENCE_CODE " + "SYSRES_CONST_TASK_TYPES_REFERENCE_CODE " + "SYSRES_CONST_TEMPLATES_REFERENCE_CODE " + "SYSRES_CONST_TEST_DATE_REQUISITE_NAME " + "SYSRES_CONST_TEST_DEV_DATABASE_NAME " + "SYSRES_CONST_TEST_DEV_SYSTEM_CODE " + "SYSRES_CONST_TEST_EDMS_DATABASE_NAME " + "SYSRES_CONST_TEST_EDMS_MAIN_CODE " + "SYSRES_CONST_TEST_EDMS_MAIN_DB_NAME " + "SYSRES_CONST_TEST_EDMS_SECOND_CODE " + "SYSRES_CONST_TEST_EDMS_SECOND_DB_NAME " + "SYSRES_CONST_TEST_EDMS_SYSTEM_CODE " + "SYSRES_CONST_TEST_NUMERIC_REQUISITE_NAME " + "SYSRES_CONST_TEXT_REQUISITE " + "SYSRES_CONST_TEXT_REQUISITE_CODE " + "SYSRES_CONST_TEXT_REQUISITE_TYPE " + "SYSRES_CONST_TEXT_TYPE_CHAR " + "SYSRES_CONST_TYPE_CODE_REQUISITE_CODE " + "SYSRES_CONST_TYPE_REQUISITE_CODE " + "SYSRES_CONST_UNDEFINED_LIFE_CYCLE_STAGE_FONT_COLOR " + "SYSRES_CONST_UNITS_SECTION_ID_REQUISITE_CODE " + "SYSRES_CONST_UNITS_SECTION_REQUISITE_CODE " + "SYSRES_CONST_UNOPERATING_RECORD_FLAG_VALUE_CODE " + "SYSRES_CONST_UNSTORED_DATA_REQUISITE_CODE " + "SYSRES_CONST_UNSTORED_DATA_REQUISITE_NAME " + "SYSRES_CONST_USE_ACCESS_TYPE_CODE " + "SYSRES_CONST_USE_ACCESS_TYPE_NAME " + "SYSRES_CONST_USER_ACCOUNT_TYPE_VALUE_CODE " + "SYSRES_CONST_USER_ADDITIONAL_INFORMATION_REQUISITE_CODE " + "SYSRES_CONST_USER_AND_GROUP_ID_FROM_PSEUDOREFERENCE_REQUISITE_CODE " + "SYSRES_CONST_USER_CATEGORY_NORMAL " + "SYSRES_CONST_USER_CERTIFICATE_REQUISITE_CODE " + "SYSRES_CONST_USER_CERTIFICATE_STATE_REQUISITE_CODE " + "SYSRES_CONST_USER_CERTIFICATE_SUBJECT_NAME_REQUISITE_CODE " + "SYSRES_CONST_USER_CERTIFICATE_THUMBPRINT_REQUISITE_CODE " + "SYSRES_CONST_USER_COMMON_CATEGORY " + "SYSRES_CONST_USER_COMMON_CATEGORY_CODE " + "SYSRES_CONST_USER_FULL_NAME_REQUISITE_CODE " + "SYSRES_CONST_USER_GROUP_TYPE_REQUISITE_CODE " + "SYSRES_CONST_USER_LOGIN_REQUISITE_CODE " + "SYSRES_CONST_USER_REMOTE_CONTROLLER_REQUISITE_CODE " + "SYSRES_CONST_USER_REMOTE_SYSTEM_REQUISITE_CODE " + "SYSRES_CONST_USER_RIGHTS_T_REQUISITE_CODE " + "SYSRES_CONST_USER_SERVER_NAME_REQUISITE_CODE " + "SYSRES_CONST_USER_SERVICE_CATEGORY " + "SYSRES_CONST_USER_SERVICE_CATEGORY_CODE " + "SYSRES_CONST_USER_STATUS_ADMINISTRATOR_CODE " + "SYSRES_CONST_USER_STATUS_ADMINISTRATOR_NAME " + "SYSRES_CONST_USER_STATUS_DEVELOPER_CODE " + "SYSRES_CONST_USER_STATUS_DEVELOPER_NAME " + "SYSRES_CONST_USER_STATUS_DISABLED_CODE " + "SYSRES_CONST_USER_STATUS_DISABLED_NAME " + "SYSRES_CONST_USER_STATUS_SYSTEM_DEVELOPER_CODE " + "SYSRES_CONST_USER_STATUS_USER_CODE " + "SYSRES_CONST_USER_STATUS_USER_NAME " + "SYSRES_CONST_USER_STATUS_USER_NAME_DEPRECATED " + "SYSRES_CONST_USER_TYPE_FIELD_VALUE_USER " + "SYSRES_CONST_USER_TYPE_REQUISITE_CODE " + "SYSRES_CONST_USERS_CONTROLLER_REQUISITE_CODE " + "SYSRES_CONST_USERS_IS_MAIN_SERVER_REQUISITE_CODE " + "SYSRES_CONST_USERS_REFERENCE_CODE " + "SYSRES_CONST_USERS_REGISTRATION_CERTIFICATES_ACTION_NAME " + "SYSRES_CONST_USERS_REQUISITE_CODE " + "SYSRES_CONST_USERS_SYSTEM_REQUISITE_CODE " + "SYSRES_CONST_USERS_USER_ACCESS_RIGHTS_TYPR_REQUISITE_CODE " + "SYSRES_CONST_USERS_USER_AUTHENTICATION_REQUISITE_CODE " + "SYSRES_CONST_USERS_USER_COMPONENT_REQUISITE_CODE " + "SYSRES_CONST_USERS_USER_GROUP_REQUISITE_CODE " + "SYSRES_CONST_USERS_VIEW_CERTIFICATES_ACTION_NAME " + "SYSRES_CONST_VIEW_DEFAULT_CODE " + "SYSRES_CONST_VIEW_DEFAULT_NAME " + "SYSRES_CONST_VIEWER_REQUISITE_CODE " + "SYSRES_CONST_WAITING_BLOCK_DESCRIPTION " + "SYSRES_CONST_WIZARD_FORM_LABEL_TEST_STRING " + "SYSRES_CONST_WIZARD_QUERY_PARAM_HEIGHT_ETALON_STRING " + "SYSRES_CONST_WIZARD_REFERENCE_COMMENT_REQUISITE_CODE " + "SYSRES_CONST_WORK_RULES_DESCRIPTION_REQUISITE_CODE " + "SYSRES_CONST_WORK_TIME_CALENDAR_REFERENCE_CODE " + "SYSRES_CONST_WORK_WORKFLOW_HARD_ROUTE_TYPE_VALUE " + "SYSRES_CONST_WORK_WORKFLOW_HARD_ROUTE_TYPE_VALUE_CODE " + "SYSRES_CONST_WORK_WORKFLOW_HARD_ROUTE_TYPE_VALUE_CODE_RUS " + "SYSRES_CONST_WORK_WORKFLOW_SOFT_ROUTE_TYPE_VALUE_CODE_RUS " + "SYSRES_CONST_WORKFLOW_ROUTE_TYPR_HARD " + "SYSRES_CONST_WORKFLOW_ROUTE_TYPR_SOFT " + "SYSRES_CONST_XML_ENCODING " + "SYSRES_CONST_XREC_STAT_REQUISITE_CODE " + "SYSRES_CONST_XRECID_FIELD_NAME " + "SYSRES_CONST_YES " + "SYSRES_CONST_YES_NO_2_REQUISITE_CODE " + "SYSRES_CONST_YES_NO_REQUISITE_CODE " + "SYSRES_CONST_YES_NO_T_REF_TYPE_REQUISITE_CODE " + "SYSRES_CONST_YES_PICK_VALUE " + "SYSRES_CONST_YES_VALUE "; // Base constant var base_constants = "CR FALSE nil NO_VALUE NULL TAB TRUE YES_VALUE "; // Base group name var base_group_name_constants = "ADMINISTRATORS_GROUP_NAME CUSTOMIZERS_GROUP_NAME DEVELOPERS_GROUP_NAME SERVICE_USERS_GROUP_NAME "; // Decision block properties var decision_block_properties_constants = "DECISION_BLOCK_FIRST_OPERAND_PROPERTY DECISION_BLOCK_NAME_PROPERTY DECISION_BLOCK_OPERATION_PROPERTY " + "DECISION_BLOCK_RESULT_TYPE_PROPERTY DECISION_BLOCK_SECOND_OPERAND_PROPERTY "; // File extension var file_extension_constants = "ANY_FILE_EXTENTION COMPRESSED_DOCUMENT_EXTENSION EXTENDED_DOCUMENT_EXTENSION " + "SHORT_COMPRESSED_DOCUMENT_EXTENSION SHORT_EXTENDED_DOCUMENT_EXTENSION "; // Job block properties var job_block_properties_constants = "JOB_BLOCK_ABORT_DEADLINE_PROPERTY " + "JOB_BLOCK_AFTER_FINISH_EVENT " + "JOB_BLOCK_AFTER_QUERY_PARAMETERS_EVENT " + "JOB_BLOCK_ATTACHMENT_PROPERTY " + "JOB_BLOCK_ATTACHMENTS_RIGHTS_GROUP_PROPERTY " + "JOB_BLOCK_ATTACHMENTS_RIGHTS_TYPE_PROPERTY " + "JOB_BLOCK_BEFORE_QUERY_PARAMETERS_EVENT " + "JOB_BLOCK_BEFORE_START_EVENT " + "JOB_BLOCK_CREATED_JOBS_PROPERTY " + "JOB_BLOCK_DEADLINE_PROPERTY " + "JOB_BLOCK_EXECUTION_RESULTS_PROPERTY " + "JOB_BLOCK_IS_PARALLEL_PROPERTY " + "JOB_BLOCK_IS_RELATIVE_ABORT_DEADLINE_PROPERTY " + "JOB_BLOCK_IS_RELATIVE_DEADLINE_PROPERTY " + "JOB_BLOCK_JOB_TEXT_PROPERTY " + "JOB_BLOCK_NAME_PROPERTY " + "JOB_BLOCK_NEED_SIGN_ON_PERFORM_PROPERTY " + "JOB_BLOCK_PERFORMER_PROPERTY " + "JOB_BLOCK_RELATIVE_ABORT_DEADLINE_TYPE_PROPERTY " + "JOB_BLOCK_RELATIVE_DEADLINE_TYPE_PROPERTY " + "JOB_BLOCK_SUBJECT_PROPERTY "; // Language code var language_code_constants = "ENGLISH_LANGUAGE_CODE RUSSIAN_LANGUAGE_CODE "; // Launching external applications var launching_external_applications_constants = "smHidden smMaximized smMinimized smNormal wmNo wmYes "; // Link kind var link_kind_constants = "COMPONENT_TOKEN_LINK_KIND " + "DOCUMENT_LINK_KIND " + "EDOCUMENT_LINK_KIND " + "FOLDER_LINK_KIND " + "JOB_LINK_KIND " + "REFERENCE_LINK_KIND " + "TASK_LINK_KIND "; // Lock type var lock_type_constants = "COMPONENT_TOKEN_LOCK_TYPE EDOCUMENT_VERSION_LOCK_TYPE "; // Monitor block properties var monitor_block_properties_constants = "MONITOR_BLOCK_AFTER_FINISH_EVENT " + "MONITOR_BLOCK_BEFORE_START_EVENT " + "MONITOR_BLOCK_DEADLINE_PROPERTY " + "MONITOR_BLOCK_INTERVAL_PROPERTY " + "MONITOR_BLOCK_INTERVAL_TYPE_PROPERTY " + "MONITOR_BLOCK_IS_RELATIVE_DEADLINE_PROPERTY " + "MONITOR_BLOCK_NAME_PROPERTY " + "MONITOR_BLOCK_RELATIVE_DEADLINE_TYPE_PROPERTY " + "MONITOR_BLOCK_SEARCH_SCRIPT_PROPERTY "; // Notice block properties var notice_block_properties_constants = "NOTICE_BLOCK_AFTER_FINISH_EVENT " + "NOTICE_BLOCK_ATTACHMENT_PROPERTY " + "NOTICE_BLOCK_ATTACHMENTS_RIGHTS_GROUP_PROPERTY " + "NOTICE_BLOCK_ATTACHMENTS_RIGHTS_TYPE_PROPERTY " + "NOTICE_BLOCK_BEFORE_START_EVENT " + "NOTICE_BLOCK_CREATED_NOTICES_PROPERTY " + "NOTICE_BLOCK_DEADLINE_PROPERTY " + "NOTICE_BLOCK_IS_RELATIVE_DEADLINE_PROPERTY " + "NOTICE_BLOCK_NAME_PROPERTY " + "NOTICE_BLOCK_NOTICE_TEXT_PROPERTY " + "NOTICE_BLOCK_PERFORMER_PROPERTY " + "NOTICE_BLOCK_RELATIVE_DEADLINE_TYPE_PROPERTY " + "NOTICE_BLOCK_SUBJECT_PROPERTY "; // Object events var object_events_constants = "dseAfterCancel " + "dseAfterClose " + "dseAfterDelete " + "dseAfterDeleteOutOfTransaction " + "dseAfterInsert " + "dseAfterOpen " + "dseAfterScroll " + "dseAfterUpdate " + "dseAfterUpdateOutOfTransaction " + "dseBeforeCancel " + "dseBeforeClose " + "dseBeforeDelete " + "dseBeforeDetailUpdate " + "dseBeforeInsert " + "dseBeforeOpen " + "dseBeforeUpdate " + "dseOnAnyRequisiteChange " + "dseOnCloseRecord " + "dseOnDeleteError " + "dseOnOpenRecord " + "dseOnPrepareUpdate " + "dseOnUpdateError " + "dseOnUpdateRatifiedRecord " + "dseOnValidDelete " + "dseOnValidUpdate " + "reOnChange " + "reOnChangeValues " + "SELECTION_BEGIN_ROUTE_EVENT " + "SELECTION_END_ROUTE_EVENT "; // Object params var object_params_constants = "CURRENT_PERIOD_IS_REQUIRED " + "PREVIOUS_CARD_TYPE_NAME " + "SHOW_RECORD_PROPERTIES_FORM "; // Other var other_constants = "ACCESS_RIGHTS_SETTING_DIALOG_CODE " + "ADMINISTRATOR_USER_CODE " + "ANALYTIC_REPORT_TYPE " + "asrtHideLocal " + "asrtHideRemote " + "CALCULATED_ROLE_TYPE_CODE " + "COMPONENTS_REFERENCE_DEVELOPER_VIEW_CODE " + "DCTS_TEST_PROTOCOLS_FOLDER_PATH " + "E_EDOC_VERSION_ALREADY_APPROVINGLY_SIGNED " + "E_EDOC_VERSION_ALREADY_APPROVINGLY_SIGNED_BY_USER " + "E_EDOC_VERSION_ALREDY_SIGNED " + "E_EDOC_VERSION_ALREDY_SIGNED_BY_USER " + "EDOC_TYPES_CODE_REQUISITE_FIELD_NAME " + "EDOCUMENTS_ALIAS_NAME " + "FILES_FOLDER_PATH " + "FILTER_OPERANDS_DELIMITER " + "FILTER_OPERATIONS_DELIMITER " + "FORMCARD_NAME " + "FORMLIST_NAME " + "GET_EXTENDED_DOCUMENT_EXTENSION_CREATION_MODE " + "GET_EXTENDED_DOCUMENT_EXTENSION_IMPORT_MODE " + "INTEGRATED_REPORT_TYPE " + "IS_BUILDER_APPLICATION_ROLE " + "IS_BUILDER_APPLICATION_ROLE2 " + "IS_BUILDER_USERS " + "ISBSYSDEV " + "LOG_FOLDER_PATH " + "mbCancel " + "mbNo " + "mbNoToAll " + "mbOK " + "mbYes " + "mbYesToAll " + "MEMORY_DATASET_DESRIPTIONS_FILENAME " + "mrNo " + "mrNoToAll " + "mrYes " + "mrYesToAll " + "MULTIPLE_SELECT_DIALOG_CODE " + "NONOPERATING_RECORD_FLAG_FEMININE " + "NONOPERATING_RECORD_FLAG_MASCULINE " + "OPERATING_RECORD_FLAG_FEMININE " + "OPERATING_RECORD_FLAG_MASCULINE " + "PROFILING_SETTINGS_COMMON_SETTINGS_CODE_VALUE " + "PROGRAM_INITIATED_LOOKUP_ACTION " + "ratDelete " + "ratEdit " + "ratInsert " + "REPORT_TYPE " + "REQUIRED_PICK_VALUES_VARIABLE " + "rmCard " + "rmList " + "SBRTE_PROGID_DEV " + "SBRTE_PROGID_RELEASE " + "STATIC_ROLE_TYPE_CODE " + "SUPPRESS_EMPTY_TEMPLATE_CREATION " + "SYSTEM_USER_CODE " + "UPDATE_DIALOG_DATASET " + "USED_IN_OBJECT_HINT_PARAM " + "USER_INITIATED_LOOKUP_ACTION " + "USER_NAME_FORMAT " + "USER_SELECTION_RESTRICTIONS " + "WORKFLOW_TEST_PROTOCOLS_FOLDER_PATH " + "ELS_SUBTYPE_CONTROL_NAME " + "ELS_FOLDER_KIND_CONTROL_NAME " + "REPEAT_PROCESS_CURRENT_OBJECT_EXCEPTION_NAME "; // Privileges var privileges_constants = "PRIVILEGE_COMPONENT_FULL_ACCESS " + "PRIVILEGE_DEVELOPMENT_EXPORT " + "PRIVILEGE_DEVELOPMENT_IMPORT " + "PRIVILEGE_DOCUMENT_DELETE " + "PRIVILEGE_ESD " + "PRIVILEGE_FOLDER_DELETE " + "PRIVILEGE_MANAGE_ACCESS_RIGHTS " + "PRIVILEGE_MANAGE_REPLICATION " + "PRIVILEGE_MANAGE_SESSION_SERVER " + "PRIVILEGE_OBJECT_FULL_ACCESS " + "PRIVILEGE_OBJECT_VIEW " + "PRIVILEGE_RESERVE_LICENSE " + "PRIVILEGE_SYSTEM_CUSTOMIZE " + "PRIVILEGE_SYSTEM_DEVELOP " + "PRIVILEGE_SYSTEM_INSTALL " + "PRIVILEGE_TASK_DELETE " + "PRIVILEGE_USER_PLUGIN_SETTINGS_CUSTOMIZE " + "PRIVILEGES_PSEUDOREFERENCE_CODE "; // Pseudoreference code var pseudoreference_code_constants = "ACCESS_TYPES_PSEUDOREFERENCE_CODE " + "ALL_AVAILABLE_COMPONENTS_PSEUDOREFERENCE_CODE " + "ALL_AVAILABLE_PRIVILEGES_PSEUDOREFERENCE_CODE " + "ALL_REPLICATE_COMPONENTS_PSEUDOREFERENCE_CODE " + "AVAILABLE_DEVELOPERS_COMPONENTS_PSEUDOREFERENCE_CODE " + "COMPONENTS_PSEUDOREFERENCE_CODE " + "FILTRATER_SETTINGS_CONFLICTS_PSEUDOREFERENCE_CODE " + "GROUPS_PSEUDOREFERENCE_CODE " + "RECEIVE_PROTOCOL_PSEUDOREFERENCE_CODE " + "REFERENCE_REQUISITE_PSEUDOREFERENCE_CODE " + "REFERENCE_REQUISITES_PSEUDOREFERENCE_CODE " + "REFTYPES_PSEUDOREFERENCE_CODE " + "REPLICATION_SEANCES_DIARY_PSEUDOREFERENCE_CODE " + "SEND_PROTOCOL_PSEUDOREFERENCE_CODE " + "SUBSTITUTES_PSEUDOREFERENCE_CODE " + "SYSTEM_SETTINGS_PSEUDOREFERENCE_CODE " + "UNITS_PSEUDOREFERENCE_CODE " + "USERS_PSEUDOREFERENCE_CODE " + "VIEWERS_PSEUDOREFERENCE_CODE "; // Requisite ISBCertificateType values var requisite_ISBCertificateType_values_constants = "CERTIFICATE_TYPE_ENCRYPT " + "CERTIFICATE_TYPE_SIGN " + "CERTIFICATE_TYPE_SIGN_AND_ENCRYPT "; // Requisite ISBEDocStorageType values var requisite_ISBEDocStorageType_values_constants = "STORAGE_TYPE_FILE " + "STORAGE_TYPE_NAS_CIFS " + "STORAGE_TYPE_SAPERION " + "STORAGE_TYPE_SQL_SERVER "; // Requisite CompType2 values var requisite_compType2_values_constants = "COMPTYPE2_REQUISITE_DOCUMENTS_VALUE " + "COMPTYPE2_REQUISITE_TASKS_VALUE " + "COMPTYPE2_REQUISITE_FOLDERS_VALUE " + "COMPTYPE2_REQUISITE_REFERENCES_VALUE "; // Requisite name var requisite_name_constants = "SYSREQ_CODE " + "SYSREQ_COMPTYPE2 " + "SYSREQ_CONST_AVAILABLE_FOR_WEB " + "SYSREQ_CONST_COMMON_CODE " + "SYSREQ_CONST_COMMON_VALUE " + "SYSREQ_CONST_FIRM_CODE " + "SYSREQ_CONST_FIRM_STATUS " + "SYSREQ_CONST_FIRM_VALUE " + "SYSREQ_CONST_SERVER_STATUS " + "SYSREQ_CONTENTS " + "SYSREQ_DATE_OPEN " + "SYSREQ_DATE_CLOSE " + "SYSREQ_DESCRIPTION " + "SYSREQ_DESCRIPTION_LOCALIZE_ID " + "SYSREQ_DOUBLE " + "SYSREQ_EDOC_ACCESS_TYPE " + "SYSREQ_EDOC_AUTHOR " + "SYSREQ_EDOC_CREATED " + "SYSREQ_EDOC_DELEGATE_RIGHTS_REQUISITE_CODE " + "SYSREQ_EDOC_EDITOR " + "SYSREQ_EDOC_ENCODE_TYPE " + "SYSREQ_EDOC_ENCRYPTION_PLUGIN_NAME " + "SYSREQ_EDOC_ENCRYPTION_PLUGIN_VERSION " + "SYSREQ_EDOC_EXPORT_DATE " + "SYSREQ_EDOC_EXPORTER " + "SYSREQ_EDOC_KIND " + "SYSREQ_EDOC_LIFE_STAGE_NAME " + "SYSREQ_EDOC_LOCKED_FOR_SERVER_CODE " + "SYSREQ_EDOC_MODIFIED " + "SYSREQ_EDOC_NAME " + "SYSREQ_EDOC_NOTE " + "SYSREQ_EDOC_QUALIFIED_ID " + "SYSREQ_EDOC_SESSION_KEY " + "SYSREQ_EDOC_SESSION_KEY_ENCRYPTION_PLUGIN_NAME " + "SYSREQ_EDOC_SESSION_KEY_ENCRYPTION_PLUGIN_VERSION " + "SYSREQ_EDOC_SIGNATURE_TYPE " + "SYSREQ_EDOC_SIGNED " + "SYSREQ_EDOC_STORAGE " + "SYSREQ_EDOC_STORAGES_ARCHIVE_STORAGE " + "SYSREQ_EDOC_STORAGES_CHECK_RIGHTS " + "SYSREQ_EDOC_STORAGES_COMPUTER_NAME " + "SYSREQ_EDOC_STORAGES_EDIT_IN_STORAGE " + "SYSREQ_EDOC_STORAGES_EXECUTIVE_STORAGE " + "SYSREQ_EDOC_STORAGES_FUNCTION " + "SYSREQ_EDOC_STORAGES_INITIALIZED " + "SYSREQ_EDOC_STORAGES_LOCAL_PATH " + "SYSREQ_EDOC_STORAGES_SAPERION_DATABASE_NAME " + "SYSREQ_EDOC_STORAGES_SEARCH_BY_TEXT " + "SYSREQ_EDOC_STORAGES_SERVER_NAME " + "SYSREQ_EDOC_STORAGES_SHARED_SOURCE_NAME " + "SYSREQ_EDOC_STORAGES_TYPE " + "SYSREQ_EDOC_TEXT_MODIFIED " + "SYSREQ_EDOC_TYPE_ACT_CODE " + "SYSREQ_EDOC_TYPE_ACT_DESCRIPTION " + "SYSREQ_EDOC_TYPE_ACT_DESCRIPTION_LOCALIZE_ID " + "SYSREQ_EDOC_TYPE_ACT_ON_EXECUTE " + "SYSREQ_EDOC_TYPE_ACT_ON_EXECUTE_EXISTS " + "SYSREQ_EDOC_TYPE_ACT_SECTION " + "SYSREQ_EDOC_TYPE_ADD_PARAMS " + "SYSREQ_EDOC_TYPE_COMMENT " + "SYSREQ_EDOC_TYPE_EVENT_TEXT " + "SYSREQ_EDOC_TYPE_NAME_IN_SINGULAR " + "SYSREQ_EDOC_TYPE_NAME_IN_SINGULAR_LOCALIZE_ID " + "SYSREQ_EDOC_TYPE_NAME_LOCALIZE_ID " + "SYSREQ_EDOC_TYPE_NUMERATION_METHOD " + "SYSREQ_EDOC_TYPE_PSEUDO_REQUISITE_CODE " + "SYSREQ_EDOC_TYPE_REQ_CODE " + "SYSREQ_EDOC_TYPE_REQ_DESCRIPTION " + "SYSREQ_EDOC_TYPE_REQ_DESCRIPTION_LOCALIZE_ID " + "SYSREQ_EDOC_TYPE_REQ_IS_LEADING " + "SYSREQ_EDOC_TYPE_REQ_IS_REQUIRED " + "SYSREQ_EDOC_TYPE_REQ_NUMBER " + "SYSREQ_EDOC_TYPE_REQ_ON_CHANGE " + "SYSREQ_EDOC_TYPE_REQ_ON_CHANGE_EXISTS " + "SYSREQ_EDOC_TYPE_REQ_ON_SELECT " + "SYSREQ_EDOC_TYPE_REQ_ON_SELECT_KIND " + "SYSREQ_EDOC_TYPE_REQ_SECTION " + "SYSREQ_EDOC_TYPE_VIEW_CARD " + "SYSREQ_EDOC_TYPE_VIEW_CODE " + "SYSREQ_EDOC_TYPE_VIEW_COMMENT " + "SYSREQ_EDOC_TYPE_VIEW_IS_MAIN " + "SYSREQ_EDOC_TYPE_VIEW_NAME " + "SYSREQ_EDOC_TYPE_VIEW_NAME_LOCALIZE_ID " + "SYSREQ_EDOC_VERSION_AUTHOR " + "SYSREQ_EDOC_VERSION_CRC " + "SYSREQ_EDOC_VERSION_DATA " + "SYSREQ_EDOC_VERSION_EDITOR " + "SYSREQ_EDOC_VERSION_EXPORT_DATE " + "SYSREQ_EDOC_VERSION_EXPORTER " + "SYSREQ_EDOC_VERSION_HIDDEN " + "SYSREQ_EDOC_VERSION_LIFE_STAGE " + "SYSREQ_EDOC_VERSION_MODIFIED " + "SYSREQ_EDOC_VERSION_NOTE " + "SYSREQ_EDOC_VERSION_SIGNATURE_TYPE " + "SYSREQ_EDOC_VERSION_SIGNED " + "SYSREQ_EDOC_VERSION_SIZE " + "SYSREQ_EDOC_VERSION_SOURCE " + "SYSREQ_EDOC_VERSION_TEXT_MODIFIED " + "SYSREQ_EDOCKIND_DEFAULT_VERSION_STATE_CODE " + "SYSREQ_FOLDER_KIND " + "SYSREQ_FUNC_CATEGORY " + "SYSREQ_FUNC_COMMENT " + "SYSREQ_FUNC_GROUP " + "SYSREQ_FUNC_GROUP_COMMENT " + "SYSREQ_FUNC_GROUP_NUMBER " + "SYSREQ_FUNC_HELP " + "SYSREQ_FUNC_PARAM_DEF_VALUE " + "SYSREQ_FUNC_PARAM_IDENT " + "SYSREQ_FUNC_PARAM_NUMBER " + "SYSREQ_FUNC_PARAM_TYPE " + "SYSREQ_FUNC_TEXT " + "SYSREQ_GROUP_CATEGORY " + "SYSREQ_ID " + "SYSREQ_LAST_UPDATE " + "SYSREQ_LEADER_REFERENCE " + "SYSREQ_LINE_NUMBER " + "SYSREQ_MAIN_RECORD_ID " + "SYSREQ_NAME " + "SYSREQ_NAME_LOCALIZE_ID " + "SYSREQ_NOTE " + "SYSREQ_ORIGINAL_RECORD " + "SYSREQ_OUR_FIRM " + "SYSREQ_PROFILING_SETTINGS_BATCH_LOGING " + "SYSREQ_PROFILING_SETTINGS_BATCH_SIZE " + "SYSREQ_PROFILING_SETTINGS_PROFILING_ENABLED " + "SYSREQ_PROFILING_SETTINGS_SQL_PROFILING_ENABLED " + "SYSREQ_PROFILING_SETTINGS_START_LOGGED " + "SYSREQ_RECORD_STATUS " + "SYSREQ_REF_REQ_FIELD_NAME " + "SYSREQ_REF_REQ_FORMAT " + "SYSREQ_REF_REQ_GENERATED " + "SYSREQ_REF_REQ_LENGTH " + "SYSREQ_REF_REQ_PRECISION " + "SYSREQ_REF_REQ_REFERENCE " + "SYSREQ_REF_REQ_SECTION " + "SYSREQ_REF_REQ_STORED " + "SYSREQ_REF_REQ_TOKENS " + "SYSREQ_REF_REQ_TYPE " + "SYSREQ_REF_REQ_VIEW " + "SYSREQ_REF_TYPE_ACT_CODE " + "SYSREQ_REF_TYPE_ACT_DESCRIPTION " + "SYSREQ_REF_TYPE_ACT_DESCRIPTION_LOCALIZE_ID " + "SYSREQ_REF_TYPE_ACT_ON_EXECUTE " + "SYSREQ_REF_TYPE_ACT_ON_EXECUTE_EXISTS " + "SYSREQ_REF_TYPE_ACT_SECTION " + "SYSREQ_REF_TYPE_ADD_PARAMS " + "SYSREQ_REF_TYPE_COMMENT " + "SYSREQ_REF_TYPE_COMMON_SETTINGS " + "SYSREQ_REF_TYPE_DISPLAY_REQUISITE_NAME " + "SYSREQ_REF_TYPE_EVENT_TEXT " + "SYSREQ_REF_TYPE_MAIN_LEADING_REF " + "SYSREQ_REF_TYPE_NAME_IN_SINGULAR " + "SYSREQ_REF_TYPE_NAME_IN_SINGULAR_LOCALIZE_ID " + "SYSREQ_REF_TYPE_NAME_LOCALIZE_ID " + "SYSREQ_REF_TYPE_NUMERATION_METHOD " + "SYSREQ_REF_TYPE_REQ_CODE " + "SYSREQ_REF_TYPE_REQ_DESCRIPTION " + "SYSREQ_REF_TYPE_REQ_DESCRIPTION_LOCALIZE_ID " + "SYSREQ_REF_TYPE_REQ_IS_CONTROL " + "SYSREQ_REF_TYPE_REQ_IS_FILTER " + "SYSREQ_REF_TYPE_REQ_IS_LEADING " + "SYSREQ_REF_TYPE_REQ_IS_REQUIRED " + "SYSREQ_REF_TYPE_REQ_NUMBER " + "SYSREQ_REF_TYPE_REQ_ON_CHANGE " + "SYSREQ_REF_TYPE_REQ_ON_CHANGE_EXISTS " + "SYSREQ_REF_TYPE_REQ_ON_SELECT " + "SYSREQ_REF_TYPE_REQ_ON_SELECT_KIND " + "SYSREQ_REF_TYPE_REQ_SECTION " + "SYSREQ_REF_TYPE_VIEW_CARD " + "SYSREQ_REF_TYPE_VIEW_CODE " + "SYSREQ_REF_TYPE_VIEW_COMMENT " + "SYSREQ_REF_TYPE_VIEW_IS_MAIN " + "SYSREQ_REF_TYPE_VIEW_NAME " + "SYSREQ_REF_TYPE_VIEW_NAME_LOCALIZE_ID " + "SYSREQ_REFERENCE_TYPE_ID " + "SYSREQ_STATE " + "SYSREQ_STATЕ " + "SYSREQ_SYSTEM_SETTINGS_VALUE " + "SYSREQ_TYPE " + "SYSREQ_UNIT " + "SYSREQ_UNIT_ID " + "SYSREQ_USER_GROUPS_GROUP_FULL_NAME " + "SYSREQ_USER_GROUPS_GROUP_NAME " + "SYSREQ_USER_GROUPS_GROUP_SERVER_NAME " + "SYSREQ_USERS_ACCESS_RIGHTS " + "SYSREQ_USERS_AUTHENTICATION " + "SYSREQ_USERS_CATEGORY " + "SYSREQ_USERS_COMPONENT " + "SYSREQ_USERS_COMPONENT_USER_IS_PUBLIC " + "SYSREQ_USERS_DOMAIN " + "SYSREQ_USERS_FULL_USER_NAME " + "SYSREQ_USERS_GROUP " + "SYSREQ_USERS_IS_MAIN_SERVER " + "SYSREQ_USERS_LOGIN " + "SYSREQ_USERS_REFERENCE_USER_IS_PUBLIC " + "SYSREQ_USERS_STATUS " + "SYSREQ_USERS_USER_CERTIFICATE " + "SYSREQ_USERS_USER_CERTIFICATE_INFO " + "SYSREQ_USERS_USER_CERTIFICATE_PLUGIN_NAME " + "SYSREQ_USERS_USER_CERTIFICATE_PLUGIN_VERSION " + "SYSREQ_USERS_USER_CERTIFICATE_STATE " + "SYSREQ_USERS_USER_CERTIFICATE_SUBJECT_NAME " + "SYSREQ_USERS_USER_CERTIFICATE_THUMBPRINT " + "SYSREQ_USERS_USER_DEFAULT_CERTIFICATE " + "SYSREQ_USERS_USER_DESCRIPTION " + "SYSREQ_USERS_USER_GLOBAL_NAME " + "SYSREQ_USERS_USER_LOGIN " + "SYSREQ_USERS_USER_MAIN_SERVER " + "SYSREQ_USERS_USER_TYPE " + "SYSREQ_WORK_RULES_FOLDER_ID "; // Result var result_constants = "RESULT_VAR_NAME RESULT_VAR_NAME_ENG "; // Rule identification var rule_identification_constants = "AUTO_NUMERATION_RULE_ID " + "CANT_CHANGE_ID_REQUISITE_RULE_ID " + "CANT_CHANGE_OURFIRM_REQUISITE_RULE_ID " + "CHECK_CHANGING_REFERENCE_RECORD_USE_RULE_ID " + "CHECK_CODE_REQUISITE_RULE_ID " + "CHECK_DELETING_REFERENCE_RECORD_USE_RULE_ID " + "CHECK_FILTRATER_CHANGES_RULE_ID " + "CHECK_RECORD_INTERVAL_RULE_ID " + "CHECK_REFERENCE_INTERVAL_RULE_ID " + "CHECK_REQUIRED_DATA_FULLNESS_RULE_ID " + "CHECK_REQUIRED_REQUISITES_FULLNESS_RULE_ID " + "MAKE_RECORD_UNRATIFIED_RULE_ID " + "RESTORE_AUTO_NUMERATION_RULE_ID " + "SET_FIRM_CONTEXT_FROM_RECORD_RULE_ID " + "SET_FIRST_RECORD_IN_LIST_FORM_RULE_ID " + "SET_IDSPS_VALUE_RULE_ID " + "SET_NEXT_CODE_VALUE_RULE_ID " + "SET_OURFIRM_BOUNDS_RULE_ID " + "SET_OURFIRM_REQUISITE_RULE_ID "; // Script block properties var script_block_properties_constants = "SCRIPT_BLOCK_AFTER_FINISH_EVENT " + "SCRIPT_BLOCK_BEFORE_START_EVENT " + "SCRIPT_BLOCK_EXECUTION_RESULTS_PROPERTY " + "SCRIPT_BLOCK_NAME_PROPERTY " + "SCRIPT_BLOCK_SCRIPT_PROPERTY "; // Subtask block properties var subtask_block_properties_constants = "SUBTASK_BLOCK_ABORT_DEADLINE_PROPERTY " + "SUBTASK_BLOCK_AFTER_FINISH_EVENT " + "SUBTASK_BLOCK_ASSIGN_PARAMS_EVENT " + "SUBTASK_BLOCK_ATTACHMENTS_PROPERTY " + "SUBTASK_BLOCK_ATTACHMENTS_RIGHTS_GROUP_PROPERTY " + "SUBTASK_BLOCK_ATTACHMENTS_RIGHTS_TYPE_PROPERTY " + "SUBTASK_BLOCK_BEFORE_START_EVENT " + "SUBTASK_BLOCK_CREATED_TASK_PROPERTY " + "SUBTASK_BLOCK_CREATION_EVENT " + "SUBTASK_BLOCK_DEADLINE_PROPERTY " + "SUBTASK_BLOCK_IMPORTANCE_PROPERTY " + "SUBTASK_BLOCK_INITIATOR_PROPERTY " + "SUBTASK_BLOCK_IS_RELATIVE_ABORT_DEADLINE_PROPERTY " + "SUBTASK_BLOCK_IS_RELATIVE_DEADLINE_PROPERTY " + "SUBTASK_BLOCK_JOBS_TYPE_PROPERTY " + "SUBTASK_BLOCK_NAME_PROPERTY " + "SUBTASK_BLOCK_PARALLEL_ROUTE_PROPERTY " + "SUBTASK_BLOCK_PERFORMERS_PROPERTY " + "SUBTASK_BLOCK_RELATIVE_ABORT_DEADLINE_TYPE_PROPERTY " + "SUBTASK_BLOCK_RELATIVE_DEADLINE_TYPE_PROPERTY " + "SUBTASK_BLOCK_REQUIRE_SIGN_PROPERTY " + "SUBTASK_BLOCK_STANDARD_ROUTE_PROPERTY " + "SUBTASK_BLOCK_START_EVENT " + "SUBTASK_BLOCK_STEP_CONTROL_PROPERTY " + "SUBTASK_BLOCK_SUBJECT_PROPERTY " + "SUBTASK_BLOCK_TASK_CONTROL_PROPERTY " + "SUBTASK_BLOCK_TEXT_PROPERTY " + "SUBTASK_BLOCK_UNLOCK_ATTACHMENTS_ON_STOP_PROPERTY " + "SUBTASK_BLOCK_USE_STANDARD_ROUTE_PROPERTY " + "SUBTASK_BLOCK_WAIT_FOR_TASK_COMPLETE_PROPERTY "; // System component var system_component_constants = "SYSCOMP_CONTROL_JOBS " + "SYSCOMP_FOLDERS " + "SYSCOMP_JOBS " + "SYSCOMP_NOTICES " + "SYSCOMP_TASKS "; // System dialogs var system_dialogs_constants = "SYSDLG_CREATE_EDOCUMENT " + "SYSDLG_CREATE_EDOCUMENT_VERSION " + "SYSDLG_CURRENT_PERIOD " + "SYSDLG_EDIT_FUNCTION_HELP " + "SYSDLG_EDOCUMENT_KINDS_FOR_TEMPLATE " + "SYSDLG_EXPORT_MULTIPLE_EDOCUMENTS " + "SYSDLG_EXPORT_SINGLE_EDOCUMENT " + "SYSDLG_IMPORT_EDOCUMENT " + "SYSDLG_MULTIPLE_SELECT " + "SYSDLG_SETUP_ACCESS_RIGHTS " + "SYSDLG_SETUP_DEFAULT_RIGHTS " + "SYSDLG_SETUP_FILTER_CONDITION " + "SYSDLG_SETUP_SIGN_RIGHTS " + "SYSDLG_SETUP_TASK_OBSERVERS " + "SYSDLG_SETUP_TASK_ROUTE " + "SYSDLG_SETUP_USERS_LIST " + "SYSDLG_SIGN_EDOCUMENT " + "SYSDLG_SIGN_MULTIPLE_EDOCUMENTS "; // System reference names var system_reference_names_constants = "SYSREF_ACCESS_RIGHTS_TYPES " + "SYSREF_ADMINISTRATION_HISTORY " + "SYSREF_ALL_AVAILABLE_COMPONENTS " + "SYSREF_ALL_AVAILABLE_PRIVILEGES " + "SYSREF_ALL_REPLICATING_COMPONENTS " + "SYSREF_AVAILABLE_DEVELOPERS_COMPONENTS " + "SYSREF_CALENDAR_EVENTS " + "SYSREF_COMPONENT_TOKEN_HISTORY " + "SYSREF_COMPONENT_TOKENS " + "SYSREF_COMPONENTS " + "SYSREF_CONSTANTS " + "SYSREF_DATA_RECEIVE_PROTOCOL " + "SYSREF_DATA_SEND_PROTOCOL " + "SYSREF_DIALOGS " + "SYSREF_DIALOGS_REQUISITES " + "SYSREF_EDITORS " + "SYSREF_EDOC_CARDS " + "SYSREF_EDOC_TYPES " + "SYSREF_EDOCUMENT_CARD_REQUISITES " + "SYSREF_EDOCUMENT_CARD_TYPES " + "SYSREF_EDOCUMENT_CARD_TYPES_REFERENCE " + "SYSREF_EDOCUMENT_CARDS " + "SYSREF_EDOCUMENT_HISTORY " + "SYSREF_EDOCUMENT_KINDS " + "SYSREF_EDOCUMENT_REQUISITES " + "SYSREF_EDOCUMENT_SIGNATURES " + "SYSREF_EDOCUMENT_TEMPLATES " + "SYSREF_EDOCUMENT_TEXT_STORAGES " + "SYSREF_EDOCUMENT_VIEWS " + "SYSREF_FILTERER_SETUP_CONFLICTS " + "SYSREF_FILTRATER_SETTING_CONFLICTS " + "SYSREF_FOLDER_HISTORY " + "SYSREF_FOLDERS " + "SYSREF_FUNCTION_GROUPS " + "SYSREF_FUNCTION_PARAMS " + "SYSREF_FUNCTIONS " + "SYSREF_JOB_HISTORY " + "SYSREF_LINKS " + "SYSREF_LOCALIZATION_DICTIONARY " + "SYSREF_LOCALIZATION_LANGUAGES " + "SYSREF_MODULES " + "SYSREF_PRIVILEGES " + "SYSREF_RECORD_HISTORY " + "SYSREF_REFERENCE_REQUISITES " + "SYSREF_REFERENCE_TYPE_VIEWS " + "SYSREF_REFERENCE_TYPES " + "SYSREF_REFERENCES " + "SYSREF_REFERENCES_REQUISITES " + "SYSREF_REMOTE_SERVERS " + "SYSREF_REPLICATION_SESSIONS_LOG " + "SYSREF_REPLICATION_SESSIONS_PROTOCOL " + "SYSREF_REPORTS " + "SYSREF_ROLES " + "SYSREF_ROUTE_BLOCK_GROUPS " + "SYSREF_ROUTE_BLOCKS " + "SYSREF_SCRIPTS " + "SYSREF_SEARCHES " + "SYSREF_SERVER_EVENTS " + "SYSREF_SERVER_EVENTS_HISTORY " + "SYSREF_STANDARD_ROUTE_GROUPS " + "SYSREF_STANDARD_ROUTES " + "SYSREF_STATUSES " + "SYSREF_SYSTEM_SETTINGS " + "SYSREF_TASK_HISTORY " + "SYSREF_TASK_KIND_GROUPS " + "SYSREF_TASK_KINDS " + "SYSREF_TASK_RIGHTS " + "SYSREF_TASK_SIGNATURES " + "SYSREF_TASKS " + "SYSREF_UNITS " + "SYSREF_USER_GROUPS " + "SYSREF_USER_GROUPS_REFERENCE " + "SYSREF_USER_SUBSTITUTION " + "SYSREF_USERS " + "SYSREF_USERS_REFERENCE " + "SYSREF_VIEWERS " + "SYSREF_WORKING_TIME_CALENDARS "; // Table name var table_name_constants = "ACCESS_RIGHTS_TABLE_NAME " + "EDMS_ACCESS_TABLE_NAME " + "EDOC_TYPES_TABLE_NAME "; // Test var test_constants = "TEST_DEV_DB_NAME " + "TEST_DEV_SYSTEM_CODE " + "TEST_EDMS_DB_NAME " + "TEST_EDMS_MAIN_CODE " + "TEST_EDMS_MAIN_DB_NAME " + "TEST_EDMS_SECOND_CODE " + "TEST_EDMS_SECOND_DB_NAME " + "TEST_EDMS_SYSTEM_CODE " + "TEST_ISB5_MAIN_CODE " + "TEST_ISB5_SECOND_CODE " + "TEST_SQL_SERVER_2005_NAME " + "TEST_SQL_SERVER_NAME "; // Using the dialog windows var using_the_dialog_windows_constants = "ATTENTION_CAPTION " + "cbsCommandLinks " + "cbsDefault " + "CONFIRMATION_CAPTION " + "ERROR_CAPTION " + "INFORMATION_CAPTION " + "mrCancel " + "mrOk "; // Using the document var using_the_document_constants = "EDOC_VERSION_ACTIVE_STAGE_CODE " + "EDOC_VERSION_DESIGN_STAGE_CODE " + "EDOC_VERSION_OBSOLETE_STAGE_CODE "; // Using the EA and encryption var using_the_EA_and_encryption_constants = "cpDataEnciphermentEnabled " + "cpDigitalSignatureEnabled " + "cpID " + "cpIssuer " + "cpPluginVersion " + "cpSerial " + "cpSubjectName " + "cpSubjSimpleName " + "cpValidFromDate " + "cpValidToDate "; // Using the ISBL-editor var using_the_ISBL_editor_constants = "ISBL_SYNTAX " + "NO_SYNTAX " + "XML_SYNTAX "; // Wait block properties var wait_block_properties_constants = "WAIT_BLOCK_AFTER_FINISH_EVENT " + "WAIT_BLOCK_BEFORE_START_EVENT " + "WAIT_BLOCK_DEADLINE_PROPERTY " + "WAIT_BLOCK_IS_RELATIVE_DEADLINE_PROPERTY " + "WAIT_BLOCK_NAME_PROPERTY " + "WAIT_BLOCK_RELATIVE_DEADLINE_TYPE_PROPERTY "; // SYSRES Common var sysres_common_constants = "SYSRES_COMMON " + "SYSRES_CONST " + "SYSRES_MBFUNC " + "SYSRES_SBDATA " + "SYSRES_SBGUI " + "SYSRES_SBINTF " + "SYSRES_SBREFDSC " + "SYSRES_SQLERRORS " + "SYSRES_SYSCOMP "; // Константы ==> built_in var CONSTANTS = sysres_constants + base_constants + base_group_name_constants + decision_block_properties_constants + file_extension_constants + job_block_properties_constants + language_code_constants + launching_external_applications_constants + link_kind_constants + lock_type_constants + monitor_block_properties_constants + notice_block_properties_constants + object_events_constants + object_params_constants + other_constants + privileges_constants + pseudoreference_code_constants + requisite_ISBCertificateType_values_constants + requisite_ISBEDocStorageType_values_constants + requisite_compType2_values_constants + requisite_name_constants + result_constants + rule_identification_constants + script_block_properties_constants + subtask_block_properties_constants + system_component_constants + system_dialogs_constants + system_reference_names_constants + table_name_constants + test_constants + using_the_dialog_windows_constants + using_the_document_constants + using_the_EA_and_encryption_constants + using_the_ISBL_editor_constants + wait_block_properties_constants + sysres_common_constants; // enum TAccountType var TAccountType = "atUser atGroup atRole "; // enum TActionEnabledMode var TActionEnabledMode = "aemEnabledAlways " + "aemDisabledAlways " + "aemEnabledOnBrowse " + "aemEnabledOnEdit " + "aemDisabledOnBrowseEmpty "; // enum TAddPosition var TAddPosition = "apBegin apEnd "; // enum TAlignment var TAlignment = "alLeft alRight "; // enum TAreaShowMode var TAreaShowMode = "asmNever " + "asmNoButCustomize " + "asmAsLastTime " + "asmYesButCustomize " + "asmAlways "; // enum TCertificateInvalidationReason var TCertificateInvalidationReason = "cirCommon cirRevoked "; // enum TCertificateType var TCertificateType = "ctSignature ctEncode ctSignatureEncode "; // enum TCheckListBoxItemState var TCheckListBoxItemState = "clbUnchecked clbChecked clbGrayed "; // enum TCloseOnEsc var TCloseOnEsc = "ceISB ceAlways ceNever "; // enum TCompType var TCompType = "ctDocument " + "ctReference " + "ctScript " + "ctUnknown " + "ctReport " + "ctDialog " + "ctFunction " + "ctFolder " + "ctEDocument " + "ctTask " + "ctJob " + "ctNotice " + "ctControlJob "; // enum TConditionFormat var TConditionFormat = "cfInternal cfDisplay "; // enum TConnectionIntent var TConnectionIntent = "ciUnspecified ciWrite ciRead "; // enum TContentKind var TContentKind = "ckFolder " + "ckEDocument " + "ckTask " + "ckJob " + "ckComponentToken " + "ckAny " + "ckReference " + "ckScript " + "ckReport " + "ckDialog "; // enum TControlType var TControlType = "ctISBLEditor " + "ctBevel " + "ctButton " + "ctCheckListBox " + "ctComboBox " + "ctComboEdit " + "ctGrid " + "ctDBCheckBox " + "ctDBComboBox " + "ctDBEdit " + "ctDBEllipsis " + "ctDBMemo " + "ctDBNavigator " + "ctDBRadioGroup " + "ctDBStatusLabel " + "ctEdit " + "ctGroupBox " + "ctInplaceHint " + "ctMemo " + "ctPanel " + "ctListBox " + "ctRadioButton " + "ctRichEdit " + "ctTabSheet " + "ctWebBrowser " + "ctImage " + "ctHyperLink " + "ctLabel " + "ctDBMultiEllipsis " + "ctRibbon " + "ctRichView " + "ctInnerPanel " + "ctPanelGroup " + "ctBitButton "; // enum TCriterionContentType var TCriterionContentType = "cctDate " + "cctInteger " + "cctNumeric " + "cctPick " + "cctReference " + "cctString " + "cctText "; // enum TCultureType var TCultureType = "cltInternal cltPrimary cltGUI "; // enum TDataSetEventType var TDataSetEventType = "dseBeforeOpen " + "dseAfterOpen " + "dseBeforeClose " + "dseAfterClose " + "dseOnValidDelete " + "dseBeforeDelete " + "dseAfterDelete " + "dseAfterDeleteOutOfTransaction " + "dseOnDeleteError " + "dseBeforeInsert " + "dseAfterInsert " + "dseOnValidUpdate " + "dseBeforeUpdate " + "dseOnUpdateRatifiedRecord " + "dseAfterUpdate " + "dseAfterUpdateOutOfTransaction " + "dseOnUpdateError " + "dseAfterScroll " + "dseOnOpenRecord " + "dseOnCloseRecord " + "dseBeforeCancel " + "dseAfterCancel " + "dseOnUpdateDeadlockError " + "dseBeforeDetailUpdate " + "dseOnPrepareUpdate " + "dseOnAnyRequisiteChange "; // enum TDataSetState var TDataSetState = "dssEdit dssInsert dssBrowse dssInActive "; // enum TDateFormatType var TDateFormatType = "dftDate dftShortDate dftDateTime dftTimeStamp "; // enum TDateOffsetType var TDateOffsetType = "dotDays dotHours dotMinutes dotSeconds "; // enum TDateTimeKind var TDateTimeKind = "dtkndLocal dtkndUTC "; // enum TDeaAccessRights var TDeaAccessRights = "arNone arView arEdit arFull "; // enum TDocumentDefaultAction var TDocumentDefaultAction = "ddaView ddaEdit "; // enum TEditMode var TEditMode = "emLock " + "emEdit " + "emSign " + "emExportWithLock " + "emImportWithUnlock " + "emChangeVersionNote " + "emOpenForModify " + "emChangeLifeStage " + "emDelete " + "emCreateVersion " + "emImport " + "emUnlockExportedWithLock " + "emStart " + "emAbort " + "emReInit " + "emMarkAsReaded " + "emMarkAsUnreaded " + "emPerform " + "emAccept " + "emResume " + "emChangeRights " + "emEditRoute " + "emEditObserver " + "emRecoveryFromLocalCopy " + "emChangeWorkAccessType " + "emChangeEncodeTypeToCertificate " + "emChangeEncodeTypeToPassword " + "emChangeEncodeTypeToNone " + "emChangeEncodeTypeToCertificatePassword " + "emChangeStandardRoute " + "emGetText " + "emOpenForView " + "emMoveToStorage " + "emCreateObject " + "emChangeVersionHidden " + "emDeleteVersion " + "emChangeLifeCycleStage " + "emApprovingSign " + "emExport " + "emContinue " + "emLockFromEdit " + "emUnLockForEdit " + "emLockForServer " + "emUnlockFromServer " + "emDelegateAccessRights " + "emReEncode "; // enum TEditorCloseObservType var TEditorCloseObservType = "ecotFile ecotProcess "; // enum TEdmsApplicationAction var TEdmsApplicationAction = "eaGet eaCopy eaCreate eaCreateStandardRoute "; // enum TEDocumentLockType var TEDocumentLockType = "edltAll edltNothing edltQuery "; // enum TEDocumentStepShowMode var TEDocumentStepShowMode = "essmText essmCard "; // enum TEDocumentStepVersionType var TEDocumentStepVersionType = "esvtLast esvtLastActive esvtSpecified "; // enum TEDocumentStorageFunction var TEDocumentStorageFunction = "edsfExecutive edsfArchive "; // enum TEDocumentStorageType var TEDocumentStorageType = "edstSQLServer edstFile "; // enum TEDocumentVersionSourceType var TEDocumentVersionSourceType = "edvstNone edvstEDocumentVersionCopy edvstFile edvstTemplate edvstScannedFile "; // enum TEDocumentVersionState var TEDocumentVersionState = "vsDefault vsDesign vsActive vsObsolete "; // enum TEncodeType var TEncodeType = "etNone etCertificate etPassword etCertificatePassword "; // enum TExceptionCategory var TExceptionCategory = "ecException ecWarning ecInformation "; // enum TExportedSignaturesType var TExportedSignaturesType = "estAll estApprovingOnly "; // enum TExportedVersionType var TExportedVersionType = "evtLast evtLastActive evtQuery "; // enum TFieldDataType var TFieldDataType = "fdtString " + "fdtNumeric " + "fdtInteger " + "fdtDate " + "fdtText " + "fdtUnknown " + "fdtWideString " + "fdtLargeInteger "; // enum TFolderType var TFolderType = "ftInbox " + "ftOutbox " + "ftFavorites " + "ftCommonFolder " + "ftUserFolder " + "ftComponents " + "ftQuickLaunch " + "ftShortcuts " + "ftSearch "; // enum TGridRowHeight var TGridRowHeight = "grhAuto " + "grhX1 " + "grhX2 " + "grhX3 "; // enum THyperlinkType var THyperlinkType = "hltText " + "hltRTF " + "hltHTML "; // enum TImageFileFormat var TImageFileFormat = "iffBMP " + "iffJPEG " + "iffMultiPageTIFF " + "iffSinglePageTIFF " + "iffTIFF " + "iffPNG "; // enum TImageMode var TImageMode = "im8bGrayscale " + "im24bRGB " + "im1bMonochrome "; // enum TImageType var TImageType = "itBMP " + "itJPEG " + "itWMF " + "itPNG "; // enum TInplaceHintKind var TInplaceHintKind = "ikhInformation " + "ikhWarning " + "ikhError " + "ikhNoIcon "; // enum TISBLContext var TISBLContext = "icUnknown " + "icScript " + "icFunction " + "icIntegratedReport " + "icAnalyticReport " + "icDataSetEventHandler " + "icActionHandler " + "icFormEventHandler " + "icLookUpEventHandler " + "icRequisiteChangeEventHandler " + "icBeforeSearchEventHandler " + "icRoleCalculation " + "icSelectRouteEventHandler " + "icBlockPropertyCalculation " + "icBlockQueryParamsEventHandler " + "icChangeSearchResultEventHandler " + "icBlockEventHandler " + "icSubTaskInitEventHandler " + "icEDocDataSetEventHandler " + "icEDocLookUpEventHandler " + "icEDocActionHandler " + "icEDocFormEventHandler " + "icEDocRequisiteChangeEventHandler " + "icStructuredConversionRule " + "icStructuredConversionEventBefore " + "icStructuredConversionEventAfter " + "icWizardEventHandler " + "icWizardFinishEventHandler " + "icWizardStepEventHandler " + "icWizardStepFinishEventHandler " + "icWizardActionEnableEventHandler " + "icWizardActionExecuteEventHandler " + "icCreateJobsHandler " + "icCreateNoticesHandler " + "icBeforeLookUpEventHandler " + "icAfterLookUpEventHandler " + "icTaskAbortEventHandler " + "icWorkflowBlockActionHandler " + "icDialogDataSetEventHandler " + "icDialogActionHandler " + "icDialogLookUpEventHandler " + "icDialogRequisiteChangeEventHandler " + "icDialogFormEventHandler " + "icDialogValidCloseEventHandler " + "icBlockFormEventHandler " + "icTaskFormEventHandler " + "icReferenceMethod " + "icEDocMethod " + "icDialogMethod " + "icProcessMessageHandler "; // enum TItemShow var TItemShow = "isShow " + "isHide " + "isByUserSettings "; // enum TJobKind var TJobKind = "jkJob " + "jkNotice " + "jkControlJob "; // enum TJoinType var TJoinType = "jtInner " + "jtLeft " + "jtRight " + "jtFull " + "jtCross "; // enum TLabelPos var TLabelPos = "lbpAbove " + "lbpBelow " + "lbpLeft " + "lbpRight "; // enum TLicensingType var TLicensingType = "eltPerConnection " + "eltPerUser "; // enum TLifeCycleStageFontColor var TLifeCycleStageFontColor = "sfcUndefined " + "sfcBlack " + "sfcGreen " + "sfcRed " + "sfcBlue " + "sfcOrange " + "sfcLilac "; // enum TLifeCycleStageFontStyle var TLifeCycleStageFontStyle = "sfsItalic " + "sfsStrikeout " + "sfsNormal "; // enum TLockableDevelopmentComponentType var TLockableDevelopmentComponentType = "ldctStandardRoute " + "ldctWizard " + "ldctScript " + "ldctFunction " + "ldctRouteBlock " + "ldctIntegratedReport " + "ldctAnalyticReport " + "ldctReferenceType " + "ldctEDocumentType " + "ldctDialog " + "ldctServerEvents "; // enum TMaxRecordCountRestrictionType var TMaxRecordCountRestrictionType = "mrcrtNone " + "mrcrtUser " + "mrcrtMaximal " + "mrcrtCustom "; // enum TRangeValueType var TRangeValueType = "vtEqual " + "vtGreaterOrEqual " + "vtLessOrEqual " + "vtRange "; // enum TRelativeDate var TRelativeDate = "rdYesterday " + "rdToday " + "rdTomorrow " + "rdThisWeek " + "rdThisMonth " + "rdThisYear " + "rdNextMonth " + "rdNextWeek " + "rdLastWeek " + "rdLastMonth "; // enum TReportDestination var TReportDestination = "rdWindow " + "rdFile " + "rdPrinter "; // enum TReqDataType var TReqDataType = "rdtString " + "rdtNumeric " + "rdtInteger " + "rdtDate " + "rdtReference " + "rdtAccount " + "rdtText " + "rdtPick " + "rdtUnknown " + "rdtLargeInteger " + "rdtDocument "; // enum TRequisiteEventType var TRequisiteEventType = "reOnChange " + "reOnChangeValues "; // enum TSBTimeType var TSBTimeType = "ttGlobal " + "ttLocal " + "ttUser " + "ttSystem "; // enum TSearchShowMode var TSearchShowMode = "ssmBrowse " + "ssmSelect " + "ssmMultiSelect " + "ssmBrowseModal "; // enum TSelectMode var TSelectMode = "smSelect " + "smLike " + "smCard "; // enum TSignatureType var TSignatureType = "stNone " + "stAuthenticating " + "stApproving "; // enum TSignerContentType var TSignerContentType = "sctString " + "sctStream "; // enum TStringsSortType var TStringsSortType = "sstAnsiSort " + "sstNaturalSort "; // enum TStringValueType var TStringValueType = "svtEqual " + "svtContain "; // enum TStructuredObjectAttributeType var TStructuredObjectAttributeType = "soatString " + "soatNumeric " + "soatInteger " + "soatDatetime " + "soatReferenceRecord " + "soatText " + "soatPick " + "soatBoolean " + "soatEDocument " + "soatAccount " + "soatIntegerCollection " + "soatNumericCollection " + "soatStringCollection " + "soatPickCollection " + "soatDatetimeCollection " + "soatBooleanCollection " + "soatReferenceRecordCollection " + "soatEDocumentCollection " + "soatAccountCollection " + "soatContents " + "soatUnknown "; // enum TTaskAbortReason var TTaskAbortReason = "tarAbortByUser " + "tarAbortByWorkflowException "; // enum TTextValueType var TTextValueType = "tvtAllWords " + "tvtExactPhrase " + "tvtAnyWord "; // enum TUserObjectStatus var TUserObjectStatus = "usNone " + "usCompleted " + "usRedSquare " + "usBlueSquare " + "usYellowSquare " + "usGreenSquare " + "usOrangeSquare " + "usPurpleSquare " + "usFollowUp "; // enum TUserType var TUserType = "utUnknown " + "utUser " + "utDeveloper " + "utAdministrator " + "utSystemDeveloper " + "utDisconnected "; // enum TValuesBuildType var TValuesBuildType = "btAnd " + "btDetailAnd " + "btOr " + "btNotOr " + "btOnly "; // enum TViewMode var TViewMode = "vmView " + "vmSelect " + "vmNavigation "; // enum TViewSelectionMode var TViewSelectionMode = "vsmSingle " + "vsmMultiple " + "vsmMultipleCheck " + "vsmNoSelection "; // enum TWizardActionType var TWizardActionType = "wfatPrevious " + "wfatNext " + "wfatCancel " + "wfatFinish "; // enum TWizardFormElementProperty var TWizardFormElementProperty = "wfepUndefined " + "wfepText3 " + "wfepText6 " + "wfepText9 " + "wfepSpinEdit " + "wfepDropDown " + "wfepRadioGroup " + "wfepFlag " + "wfepText12 " + "wfepText15 " + "wfepText18 " + "wfepText21 " + "wfepText24 " + "wfepText27 " + "wfepText30 " + "wfepRadioGroupColumn1 " + "wfepRadioGroupColumn2 " + "wfepRadioGroupColumn3 "; // enum TWizardFormElementType var TWizardFormElementType = "wfetQueryParameter " + "wfetText " + "wfetDelimiter " + "wfetLabel "; // enum TWizardParamType var TWizardParamType = "wptString " + "wptInteger " + "wptNumeric " + "wptBoolean " + "wptDateTime " + "wptPick " + "wptText " + "wptUser " + "wptUserList " + "wptEDocumentInfo " + "wptEDocumentInfoList " + "wptReferenceRecordInfo " + "wptReferenceRecordInfoList " + "wptFolderInfo " + "wptTaskInfo " + "wptContents " + "wptFileName " + "wptDate "; // enum TWizardStepResult var TWizardStepResult = "wsrComplete " + "wsrGoNext " + "wsrGoPrevious " + "wsrCustom " + "wsrCancel " + "wsrGoFinal "; // enum TWizardStepType var TWizardStepType = "wstForm " + "wstEDocument " + "wstTaskCard " + "wstReferenceRecordCard " + "wstFinal "; // enum TWorkAccessType var TWorkAccessType = "waAll " + "waPerformers " + "waManual "; // enum TWorkflowBlockType var TWorkflowBlockType = "wsbStart " + "wsbFinish " + "wsbNotice " + "wsbStep " + "wsbDecision " + "wsbWait " + "wsbMonitor " + "wsbScript " + "wsbConnector " + "wsbSubTask " + "wsbLifeCycleStage " + "wsbPause "; // enum TWorkflowDataType var TWorkflowDataType = "wdtInteger " + "wdtFloat " + "wdtString " + "wdtPick " + "wdtDateTime " + "wdtBoolean " + "wdtTask " + "wdtJob " + "wdtFolder " + "wdtEDocument " + "wdtReferenceRecord " + "wdtUser " + "wdtGroup " + "wdtRole " + "wdtIntegerCollection " + "wdtFloatCollection " + "wdtStringCollection " + "wdtPickCollection " + "wdtDateTimeCollection " + "wdtBooleanCollection " + "wdtTaskCollection " + "wdtJobCollection " + "wdtFolderCollection " + "wdtEDocumentCollection " + "wdtReferenceRecordCollection " + "wdtUserCollection " + "wdtGroupCollection " + "wdtRoleCollection " + "wdtContents " + "wdtUserList " + "wdtSearchDescription " + "wdtDeadLine " + "wdtPickSet " + "wdtAccountCollection "; // enum TWorkImportance var TWorkImportance = "wiLow " + "wiNormal " + "wiHigh "; // enum TWorkRouteType var TWorkRouteType = "wrtSoft " + "wrtHard "; // enum TWorkState var TWorkState = "wsInit " + "wsRunning " + "wsDone " + "wsControlled " + "wsAborted " + "wsContinued "; // enum TWorkTextBuildingMode var TWorkTextBuildingMode = "wtmFull " + "wtmFromCurrent " + "wtmOnlyCurrent "; // Перечисления var ENUMS = TAccountType + TActionEnabledMode + TAddPosition + TAlignment + TAreaShowMode + TCertificateInvalidationReason + TCertificateType + TCheckListBoxItemState + TCloseOnEsc + TCompType + TConditionFormat + TConnectionIntent + TContentKind + TControlType + TCriterionContentType + TCultureType + TDataSetEventType + TDataSetState + TDateFormatType + TDateOffsetType + TDateTimeKind + TDeaAccessRights + TDocumentDefaultAction + TEditMode + TEditorCloseObservType + TEdmsApplicationAction + TEDocumentLockType + TEDocumentStepShowMode + TEDocumentStepVersionType + TEDocumentStorageFunction + TEDocumentStorageType + TEDocumentVersionSourceType + TEDocumentVersionState + TEncodeType + TExceptionCategory + TExportedSignaturesType + TExportedVersionType + TFieldDataType + TFolderType + TGridRowHeight + THyperlinkType + TImageFileFormat + TImageMode + TImageType + TInplaceHintKind + TISBLContext + TItemShow + TJobKind + TJoinType + TLabelPos + TLicensingType + TLifeCycleStageFontColor + TLifeCycleStageFontStyle + TLockableDevelopmentComponentType + TMaxRecordCountRestrictionType + TRangeValueType + TRelativeDate + TReportDestination + TReqDataType + TRequisiteEventType + TSBTimeType + TSearchShowMode + TSelectMode + TSignatureType + TSignerContentType + TStringsSortType + TStringValueType + TStructuredObjectAttributeType + TTaskAbortReason + TTextValueType + TUserObjectStatus + TUserType + TValuesBuildType + TViewMode + TViewSelectionMode + TWizardActionType + TWizardFormElementProperty + TWizardFormElementType + TWizardParamType + TWizardStepResult + TWizardStepType + TWorkAccessType + TWorkflowBlockType + TWorkflowDataType + TWorkImportance + TWorkRouteType + TWorkState + TWorkTextBuildingMode; // Системные функции ==> SYSFUNCTIONS var system_functions = "AddSubString " + "AdjustLineBreaks " + "AmountInWords " + "Analysis " + "ArrayDimCount " + "ArrayHighBound " + "ArrayLowBound " + "ArrayOf " + "ArrayReDim " + "Assert " + "Assigned " + "BeginOfMonth " + "BeginOfPeriod " + "BuildProfilingOperationAnalysis " + "CallProcedure " + "CanReadFile " + "CArrayElement " + "CDataSetRequisite " + "ChangeDate " + "ChangeReferenceDataset " + "Char " + "CharPos " + "CheckParam " + "CheckParamValue " + "CompareStrings " + "ConstantExists " + "ControlState " + "ConvertDateStr " + "Copy " + "CopyFile " + "CreateArray " + "CreateCachedReference " + "CreateConnection " + "CreateDialog " + "CreateDualListDialog " + "CreateEditor " + "CreateException " + "CreateFile " + "CreateFolderDialog " + "CreateInputDialog " + "CreateLinkFile " + "CreateList " + "CreateLock " + "CreateMemoryDataSet " + "CreateObject " + "CreateOpenDialog " + "CreateProgress " + "CreateQuery " + "CreateReference " + "CreateReport " + "CreateSaveDialog " + "CreateScript " + "CreateSQLPivotFunction " + "CreateStringList " + "CreateTreeListSelectDialog " + "CSelectSQL " + "CSQL " + "CSubString " + "CurrentUserID " + "CurrentUserName " + "CurrentVersion " + "DataSetLocateEx " + "DateDiff " + "DateTimeDiff " + "DateToStr " + "DayOfWeek " + "DeleteFile " + "DirectoryExists " + "DisableCheckAccessRights " + "DisableCheckFullShowingRestriction " + "DisableMassTaskSendingRestrictions " + "DropTable " + "DupeString " + "EditText " + "EnableCheckAccessRights " + "EnableCheckFullShowingRestriction " + "EnableMassTaskSendingRestrictions " + "EndOfMonth " + "EndOfPeriod " + "ExceptionExists " + "ExceptionsOff " + "ExceptionsOn " + "Execute " + "ExecuteProcess " + "Exit " + "ExpandEnvironmentVariables " + "ExtractFileDrive " + "ExtractFileExt " + "ExtractFileName " + "ExtractFilePath " + "ExtractParams " + "FileExists " + "FileSize " + "FindFile " + "FindSubString " + "FirmContext " + "ForceDirectories " + "Format " + "FormatDate " + "FormatNumeric " + "FormatSQLDate " + "FormatString " + "FreeException " + "GetComponent " + "GetComponentLaunchParam " + "GetConstant " + "GetLastException " + "GetReferenceRecord " + "GetRefTypeByRefID " + "GetTableID " + "GetTempFolder " + "IfThen " + "In " + "IndexOf " + "InputDialog " + "InputDialogEx " + "InteractiveMode " + "IsFileLocked " + "IsGraphicFile " + "IsNumeric " + "Length " + "LoadString " + "LoadStringFmt " + "LocalTimeToUTC " + "LowerCase " + "Max " + "MessageBox " + "MessageBoxEx " + "MimeDecodeBinary " + "MimeDecodeString " + "MimeEncodeBinary " + "MimeEncodeString " + "Min " + "MoneyInWords " + "MoveFile " + "NewID " + "Now " + "OpenFile " + "Ord " + "Precision " + "Raise " + "ReadCertificateFromFile " + "ReadFile " + "ReferenceCodeByID " + "ReferenceNumber " + "ReferenceRequisiteMode " + "ReferenceRequisiteValue " + "RegionDateSettings " + "RegionNumberSettings " + "RegionTimeSettings " + "RegRead " + "RegWrite " + "RenameFile " + "Replace " + "Round " + "SelectServerCode " + "SelectSQL " + "ServerDateTime " + "SetConstant " + "SetManagedFolderFieldsState " + "ShowConstantsInputDialog " + "ShowMessage " + "Sleep " + "Split " + "SQL " + "SQL2XLSTAB " + "SQLProfilingSendReport " + "StrToDate " + "SubString " + "SubStringCount " + "SystemSetting " + "Time " + "TimeDiff " + "Today " + "Transliterate " + "Trim " + "UpperCase " + "UserStatus " + "UTCToLocalTime " + "ValidateXML " + "VarIsClear " + "VarIsEmpty " + "VarIsNull " + "WorkTimeDiff " + "WriteFile " + "WriteFileEx " + "WriteObjectHistory " + "Анализ " + "БазаДанных " + "БлокЕсть " + "БлокЕстьРасш " + "БлокИнфо " + "БлокСнять " + "БлокСнятьРасш " + "БлокУстановить " + "Ввод " + "ВводМеню " + "ВедС " + "ВедСпр " + "ВерхняяГраницаМассива " + "ВнешПрогр " + "Восст " + "ВременнаяПапка " + "Время " + "ВыборSQL " + "ВыбратьЗапись " + "ВыделитьСтр " + "Вызвать " + "Выполнить " + "ВыпПрогр " + "ГрафическийФайл " + "ГруппаДополнительно " + "ДатаВремяСерв " + "ДеньНедели " + "ДиалогДаНет " + "ДлинаСтр " + "ДобПодстр " + "ЕПусто " + "ЕслиТо " + "ЕЧисло " + "ЗамПодстр " + "ЗаписьСправочника " + "ЗначПоляСпр " + "ИДТипСпр " + "ИзвлечьДиск " + "ИзвлечьИмяФайла " + "ИзвлечьПуть " + "ИзвлечьРасширение " + "ИзмДат " + "ИзменитьРазмерМассива " + "ИзмеренийМассива " + "ИмяОрг " + "ИмяПоляСпр " + "Индекс " + "ИндикаторЗакрыть " + "ИндикаторОткрыть " + "ИндикаторШаг " + "ИнтерактивныйРежим " + "ИтогТблСпр " + "КодВидВедСпр " + "КодВидСпрПоИД " + "КодПоAnalit " + "КодСимвола " + "КодСпр " + "КолПодстр " + "КолПроп " + "КонМес " + "Конст " + "КонстЕсть " + "КонстЗнач " + "КонТран " + "КопироватьФайл " + "КопияСтр " + "КПериод " + "КСтрТблСпр " + "Макс " + "МаксСтрТблСпр " + "Массив " + "Меню " + "МенюРасш " + "Мин " + "НаборДанныхНайтиРасш " + "НаимВидСпр " + "НаимПоAnalit " + "НаимСпр " + "НастроитьПереводыСтрок " + "НачМес " + "НачТран " + "НижняяГраницаМассива " + "НомерСпр " + "НПериод " + "Окно " + "Окр " + "Окружение " + "ОтлИнфДобавить " + "ОтлИнфУдалить " + "Отчет " + "ОтчетАнал " + "ОтчетИнт " + "ПапкаСуществует " + "Пауза " + "ПВыборSQL " + "ПереименоватьФайл " + "Переменные " + "ПереместитьФайл " + "Подстр " + "ПоискПодстр " + "ПоискСтр " + "ПолучитьИДТаблицы " + "ПользовательДополнительно " + "ПользовательИД " + "ПользовательИмя " + "ПользовательСтатус " + "Прервать " + "ПроверитьПараметр " + "ПроверитьПараметрЗнач " + "ПроверитьУсловие " + "РазбСтр " + "РазнВремя " + "РазнДат " + "РазнДатаВремя " + "РазнРабВремя " + "РегУстВрем " + "РегУстДат " + "РегУстЧсл " + "РедТекст " + "РеестрЗапись " + "РеестрСписокИменПарам " + "РеестрЧтение " + "РеквСпр " + "РеквСпрПр " + "Сегодня " + "Сейчас " + "Сервер " + "СерверПроцессИД " + "СертификатФайлСчитать " + "СжПроб " + "Символ " + "СистемаДиректумКод " + "СистемаИнформация " + "СистемаКод " + "Содержит " + "СоединениеЗакрыть " + "СоединениеОткрыть " + "СоздатьДиалог " + "СоздатьДиалогВыбораИзДвухСписков " + "СоздатьДиалогВыбораПапки " + "СоздатьДиалогОткрытияФайла " + "СоздатьДиалогСохраненияФайла " + "СоздатьЗапрос " + "СоздатьИндикатор " + "СоздатьИсключение " + "СоздатьКэшированныйСправочник " + "СоздатьМассив " + "СоздатьНаборДанных " + "СоздатьОбъект " + "СоздатьОтчет " + "СоздатьПапку " + "СоздатьРедактор " + "СоздатьСоединение " + "СоздатьСписок " + "СоздатьСписокСтрок " + "СоздатьСправочник " + "СоздатьСценарий " + "СоздСпр " + "СостСпр " + "Сохр " + "СохрСпр " + "СписокСистем " + "Спр " + "Справочник " + "СпрБлокЕсть " + "СпрБлокСнять " + "СпрБлокСнятьРасш " + "СпрБлокУстановить " + "СпрИзмНабДан " + "СпрКод " + "СпрНомер " + "СпрОбновить " + "СпрОткрыть " + "СпрОтменить " + "СпрПарам " + "СпрПолеЗнач " + "СпрПолеИмя " + "СпрРекв " + "СпрРеквВведЗн " + "СпрРеквНовые " + "СпрРеквПр " + "СпрРеквПредЗн " + "СпрРеквРежим " + "СпрРеквТипТекст " + "СпрСоздать " + "СпрСост " + "СпрСохранить " + "СпрТблИтог " + "СпрТблСтр " + "СпрТблСтрКол " + "СпрТблСтрМакс " + "СпрТблСтрМин " + "СпрТблСтрПред " + "СпрТблСтрСлед " + "СпрТблСтрСозд " + "СпрТблСтрУд " + "СпрТекПредст " + "СпрУдалить " + "СравнитьСтр " + "СтрВерхРегистр " + "СтрНижнРегистр " + "СтрТблСпр " + "СумПроп " + "Сценарий " + "СценарийПарам " + "ТекВерсия " + "ТекОрг " + "Точн " + "Тран " + "Транслитерация " + "УдалитьТаблицу " + "УдалитьФайл " + "УдСпр " + "УдСтрТблСпр " + "Уст " + "УстановкиКонстант " + "ФайлАтрибутСчитать " + "ФайлАтрибутУстановить " + "ФайлВремя " + "ФайлВремяУстановить " + "ФайлВыбрать " + "ФайлЗанят " + "ФайлЗаписать " + "ФайлИскать " + "ФайлКопировать " + "ФайлМожноЧитать " + "ФайлОткрыть " + "ФайлПереименовать " + "ФайлПерекодировать " + "ФайлПереместить " + "ФайлПросмотреть " + "ФайлРазмер " + "ФайлСоздать " + "ФайлСсылкаСоздать " + "ФайлСуществует " + "ФайлСчитать " + "ФайлУдалить " + "ФмтSQLДат " + "ФмтДат " + "ФмтСтр " + "ФмтЧсл " + "Формат " + "ЦМассивЭлемент " + "ЦНаборДанныхРеквизит " + "ЦПодстр "; // Предопределенные переменные ==> built_in var predefined_variables = "AltState " + "Application " + "CallType " + "ComponentTokens " + "CreatedJobs " + "CreatedNotices " + "ControlState " + "DialogResult " + "Dialogs " + "EDocuments " + "EDocumentVersionSource " + "Folders " + "GlobalIDs " + "Job " + "Jobs " + "InputValue " + "LookUpReference " + "LookUpRequisiteNames " + "LookUpSearch " + "Object " + "ParentComponent " + "Processes " + "References " + "Requisite " + "ReportName " + "Reports " + "Result " + "Scripts " + "Searches " + "SelectedAttachments " + "SelectedItems " + "SelectMode " + "Sender " + "ServerEvents " + "ServiceFactory " + "ShiftState " + "SubTask " + "SystemDialogs " + "Tasks " + "Wizard " + "Wizards " + "Work " + "ВызовСпособ " + "ИмяОтчета " + "РеквЗнач "; // Интерфейсы ==> type var interfaces = "IApplication " + "IAccessRights " + "IAccountRepository " + "IAccountSelectionRestrictions " + "IAction " + "IActionList " + "IAdministrationHistoryDescription " + "IAnchors " + "IApplication " + "IArchiveInfo " + "IAttachment " + "IAttachmentList " + "ICheckListBox " + "ICheckPointedList " + "IColumn " + "IComponent " + "IComponentDescription " + "IComponentToken " + "IComponentTokenFactory " + "IComponentTokenInfo " + "ICompRecordInfo " + "IConnection " + "IContents " + "IControl " + "IControlJob " + "IControlJobInfo " + "IControlList " + "ICrypto " + "ICrypto2 " + "ICustomJob " + "ICustomJobInfo " + "ICustomListBox " + "ICustomObjectWizardStep " + "ICustomWork " + "ICustomWorkInfo " + "IDataSet " + "IDataSetAccessInfo " + "IDataSigner " + "IDateCriterion " + "IDateRequisite " + "IDateRequisiteDescription " + "IDateValue " + "IDeaAccessRights " + "IDeaObjectInfo " + "IDevelopmentComponentLock " + "IDialog " + "IDialogFactory " + "IDialogPickRequisiteItems " + "IDialogsFactory " + "IDICSFactory " + "IDocRequisite " + "IDocumentInfo " + "IDualListDialog " + "IECertificate " + "IECertificateInfo " + "IECertificates " + "IEditControl " + "IEditorForm " + "IEdmsExplorer " + "IEdmsObject " + "IEdmsObjectDescription " + "IEdmsObjectFactory " + "IEdmsObjectInfo " + "IEDocument " + "IEDocumentAccessRights " + "IEDocumentDescription " + "IEDocumentEditor " + "IEDocumentFactory " + "IEDocumentInfo " + "IEDocumentStorage " + "IEDocumentVersion " + "IEDocumentVersionListDialog " + "IEDocumentVersionSource " + "IEDocumentWizardStep " + "IEDocVerSignature " + "IEDocVersionState " + "IEnabledMode " + "IEncodeProvider " + "IEncrypter " + "IEvent " + "IEventList " + "IException " + "IExternalEvents " + "IExternalHandler " + "IFactory " + "IField " + "IFileDialog " + "IFolder " + "IFolderDescription " + "IFolderDialog " + "IFolderFactory " + "IFolderInfo " + "IForEach " + "IForm " + "IFormTitle " + "IFormWizardStep " + "IGlobalIDFactory " + "IGlobalIDInfo " + "IGrid " + "IHasher " + "IHistoryDescription " + "IHyperLinkControl " + "IImageButton " + "IImageControl " + "IInnerPanel " + "IInplaceHint " + "IIntegerCriterion " + "IIntegerList " + "IIntegerRequisite " + "IIntegerValue " + "IISBLEditorForm " + "IJob " + "IJobDescription " + "IJobFactory " + "IJobForm " + "IJobInfo " + "ILabelControl " + "ILargeIntegerCriterion " + "ILargeIntegerRequisite " + "ILargeIntegerValue " + "ILicenseInfo " + "ILifeCycleStage " + "IList " + "IListBox " + "ILocalIDInfo " + "ILocalization " + "ILock " + "IMemoryDataSet " + "IMessagingFactory " + "IMetadataRepository " + "INotice " + "INoticeInfo " + "INumericCriterion " + "INumericRequisite " + "INumericValue " + "IObject " + "IObjectDescription " + "IObjectImporter " + "IObjectInfo " + "IObserver " + "IPanelGroup " + "IPickCriterion " + "IPickProperty " + "IPickRequisite " + "IPickRequisiteDescription " + "IPickRequisiteItem " + "IPickRequisiteItems " + "IPickValue " + "IPrivilege " + "IPrivilegeList " + "IProcess " + "IProcessFactory " + "IProcessMessage " + "IProgress " + "IProperty " + "IPropertyChangeEvent " + "IQuery " + "IReference " + "IReferenceCriterion " + "IReferenceEnabledMode " + "IReferenceFactory " + "IReferenceHistoryDescription " + "IReferenceInfo " + "IReferenceRecordCardWizardStep " + "IReferenceRequisiteDescription " + "IReferencesFactory " + "IReferenceValue " + "IRefRequisite " + "IReport " + "IReportFactory " + "IRequisite " + "IRequisiteDescription " + "IRequisiteDescriptionList " + "IRequisiteFactory " + "IRichEdit " + "IRouteStep " + "IRule " + "IRuleList " + "ISchemeBlock " + "IScript " + "IScriptFactory " + "ISearchCriteria " + "ISearchCriterion " + "ISearchDescription " + "ISearchFactory " + "ISearchFolderInfo " + "ISearchForObjectDescription " + "ISearchResultRestrictions " + "ISecuredContext " + "ISelectDialog " + "IServerEvent " + "IServerEventFactory " + "IServiceDialog " + "IServiceFactory " + "ISignature " + "ISignProvider " + "ISignProvider2 " + "ISignProvider3 " + "ISimpleCriterion " + "IStringCriterion " + "IStringList " + "IStringRequisite " + "IStringRequisiteDescription " + "IStringValue " + "ISystemDialogsFactory " + "ISystemInfo " + "ITabSheet " + "ITask " + "ITaskAbortReasonInfo " + "ITaskCardWizardStep " + "ITaskDescription " + "ITaskFactory " + "ITaskInfo " + "ITaskRoute " + "ITextCriterion " + "ITextRequisite " + "ITextValue " + "ITreeListSelectDialog " + "IUser " + "IUserList " + "IValue " + "IView " + "IWebBrowserControl " + "IWizard " + "IWizardAction " + "IWizardFactory " + "IWizardFormElement " + "IWizardParam " + "IWizardPickParam " + "IWizardReferenceParam " + "IWizardStep " + "IWorkAccessRights " + "IWorkDescription " + "IWorkflowAskableParam " + "IWorkflowAskableParams " + "IWorkflowBlock " + "IWorkflowBlockResult " + "IWorkflowEnabledMode " + "IWorkflowParam " + "IWorkflowPickParam " + "IWorkflowReferenceParam " + "IWorkState " + "IWorkTreeCustomNode " + "IWorkTreeJobNode " + "IWorkTreeTaskNode " + "IXMLEditorForm " + "SBCrypto "; // built_in : встроенные или библиотечные объекты (константы, перечисления) var BUILTIN = CONSTANTS + ENUMS; // class: встроенные наборы значений, системные объекты, фабрики var CLASS = predefined_variables; // literal : примитивные типы var LITERAL = "null true false nil "; // number : числа var NUMBERS = { className: "number", begin: hljs.NUMBER_RE, relevance: 0, }; // string : строки var STRINGS = { className: "string", variants: [{ begin: '"', end: '"' }, { begin: "'", end: "'" }], }; // Токены var DOCTAGS = { className: "doctag", begin: "\\b(?:TODO|DONE|BEGIN|END|STUB|CHG|FIXME|NOTE|BUG|XXX)\\b", relevance: 0, }; // Однострочный комментарий var ISBL_LINE_COMMENT_MODE = { className: "comment", begin: "//", end: "$", relevance: 0, contains: [hljs.PHRASAL_WORDS_MODE, DOCTAGS], }; // Многострочный комментарий var ISBL_BLOCK_COMMENT_MODE = { className: "comment", begin: "/\\*", end: "\\*/", relevance: 0, contains: [hljs.PHRASAL_WORDS_MODE, DOCTAGS], }; // comment : комментарии var COMMENTS = { variants: [ISBL_LINE_COMMENT_MODE, ISBL_BLOCK_COMMENT_MODE], }; // keywords : ключевые слова var KEYWORDS = { keyword: KEYWORD, built_in: BUILTIN, class: CLASS, literal: LITERAL, }; // methods : методы var METHODS = { begin: "\\.\\s*" + hljs.UNDERSCORE_IDENT_RE, keywords: KEYWORDS, relevance: 0, }; // type : встроенные типы var TYPES = { className: "type", begin: ":[ \\t]*(" + interfaces.trim().replace(/\s/g, "|") + ")", end: "[ \\t]*=", excludeEnd: true, }; // variables : переменные var VARIABLES = { className: "variable", lexemes: UNDERSCORE_IDENT_RE, keywords: KEYWORDS, begin: UNDERSCORE_IDENT_RE, relevance: 0, contains: [TYPES, METHODS], }; // Имена функций var FUNCTION_TITLE = FUNCTION_NAME_IDENT_RE + "\\("; var TITLE_MODE = { className: "title", lexemes: UNDERSCORE_IDENT_RE, keywords: { built_in: system_functions, }, begin: FUNCTION_TITLE, end: "\\(", returnBegin: true, excludeEnd: true, }; // function : функции var FUNCTIONS = { className: "function", begin: FUNCTION_TITLE, end: "\\)$", returnBegin: true, lexemes: UNDERSCORE_IDENT_RE, keywords: KEYWORDS, illegal: "[\\[\\]\\|\\$\\?%,~#@]", contains: [TITLE_MODE, METHODS, VARIABLES, STRINGS, NUMBERS, COMMENTS], }; return { aliases: ["isbl"], case_insensitive: true, lexemes: UNDERSCORE_IDENT_RE, keywords: KEYWORDS, illegal: "\\$|\\?|%|,|;$|~|#|@| Category: common, enterprise */ function(hljs) { var JAVA_IDENT_RE = '[\u00C0-\u02B8a-zA-Z_$][\u00C0-\u02B8a-zA-Z_$0-9]*'; var GENERIC_IDENT_RE = JAVA_IDENT_RE + '(<' + JAVA_IDENT_RE + '(\\s*,\\s*' + JAVA_IDENT_RE + ')*>)?'; var KEYWORDS = 'false synchronized int abstract float private char boolean var static null if const ' + 'for true while long strictfp finally protected import native final void ' + 'enum else break transient catch instanceof byte super volatile case assert short ' + 'package default double public try this switch continue throws protected public private ' + 'module requires exports do'; // https://docs.oracle.com/javase/7/docs/technotes/guides/language/underscores-literals.html var JAVA_NUMBER_RE = '\\b' + '(' + '0[bB]([01]+[01_]+[01]+|[01]+)' + // 0b... '|' + '0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)' + // 0x... '|' + '(' + '([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?' + '|' + '\\.([\\d]+[\\d_]+[\\d]+|[\\d]+)' + ')' + '([eE][-+]?\\d+)?' + // octal, decimal, float ')' + '[lLfF]?'; var JAVA_NUMBER_MODE = { className: 'number', begin: JAVA_NUMBER_RE, relevance: 0 }; return { aliases: ['jsp'], keywords: KEYWORDS, illegal: /<\/|#/, contains: [ hljs.COMMENT( '/\\*\\*', '\\*/', { relevance : 0, contains : [ { // eat up @'s in emails to prevent them to be recognized as doctags begin: /\w+@/, relevance: 0 }, { className : 'doctag', begin : '@[A-Za-z]+' } ] } ), hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, { className: 'class', beginKeywords: 'class interface', end: /[{;=]/, excludeEnd: true, keywords: 'class interface', illegal: /[:"\[\]]/, contains: [ {beginKeywords: 'extends implements'}, hljs.UNDERSCORE_TITLE_MODE ] }, { // Expression keywords prevent 'keyword Name(...)' from being // recognized as a function definition beginKeywords: 'new throw return else', relevance: 0 }, { className: 'function', begin: '(' + GENERIC_IDENT_RE + '\\s+)+' + hljs.UNDERSCORE_IDENT_RE + '\\s*\\(', returnBegin: true, end: /[{;=]/, excludeEnd: true, keywords: KEYWORDS, contains: [ { begin: hljs.UNDERSCORE_IDENT_RE + '\\s*\\(', returnBegin: true, relevance: 0, contains: [hljs.UNDERSCORE_TITLE_MODE] }, { className: 'params', begin: /\(/, end: /\)/, keywords: KEYWORDS, relevance: 0, contains: [ hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, hljs.C_NUMBER_MODE, hljs.C_BLOCK_COMMENT_MODE ] }, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE ] }, JAVA_NUMBER_MODE, { className: 'meta', begin: '@[A-Za-z]+' } ] }; } },{name:"javascript",create:/* Language: JavaScript Category: common, scripting */ function(hljs) { var IDENT_RE = '[A-Za-z$_][0-9A-Za-z$_]*'; var KEYWORDS = { keyword: 'in of if for while finally var new function do return void else break catch ' + 'instanceof with throw case default try this switch continue typeof delete ' + 'let yield const export super debugger as async await static ' + // ECMAScript 6 modules import 'import from as' , literal: 'true false null undefined NaN Infinity', built_in: 'eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent ' + 'encodeURI encodeURIComponent escape unescape Object Function Boolean Error ' + 'EvalError InternalError RangeError ReferenceError StopIteration SyntaxError ' + 'TypeError URIError Number Math Date String RegExp Array Float32Array ' + 'Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array ' + 'Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require ' + 'module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect ' + 'Promise' }; var NUMBER = { className: 'number', variants: [ { begin: '\\b(0[bB][01]+)' }, { begin: '\\b(0[oO][0-7]+)' }, { begin: hljs.C_NUMBER_RE } ], relevance: 0 }; var SUBST = { className: 'subst', begin: '\\$\\{', end: '\\}', keywords: KEYWORDS, contains: [] // defined later }; var HTML_TEMPLATE = { begin: 'html`', end: '', starts: { end: '`', returnEnd: false, contains: [ hljs.BACKSLASH_ESCAPE, SUBST ], subLanguage: 'xml', } }; var CSS_TEMPLATE = { begin: 'css`', end: '', starts: { end: '`', returnEnd: false, contains: [ hljs.BACKSLASH_ESCAPE, SUBST ], subLanguage: 'css', } }; var TEMPLATE_STRING = { className: 'string', begin: '`', end: '`', contains: [ hljs.BACKSLASH_ESCAPE, SUBST ] }; SUBST.contains = [ hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, HTML_TEMPLATE, CSS_TEMPLATE, TEMPLATE_STRING, NUMBER, hljs.REGEXP_MODE ]; var PARAMS_CONTAINS = SUBST.contains.concat([ hljs.C_BLOCK_COMMENT_MODE, hljs.C_LINE_COMMENT_MODE ]); return { aliases: ['js', 'jsx'], keywords: KEYWORDS, contains: [ { className: 'meta', relevance: 10, begin: /^\s*['"]use (strict|asm)['"]/ }, { className: 'meta', begin: /^#!/, end: /$/ }, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, HTML_TEMPLATE, CSS_TEMPLATE, TEMPLATE_STRING, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, NUMBER, { // object attr container begin: /[{,]\s*/, relevance: 0, contains: [ { begin: IDENT_RE + '\\s*:', returnBegin: true, relevance: 0, contains: [{className: 'attr', begin: IDENT_RE, relevance: 0}] } ] }, { // "value" container begin: '(' + hljs.RE_STARTERS_RE + '|\\b(case|return|throw)\\b)\\s*', keywords: 'return throw case', contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.REGEXP_MODE, { className: 'function', begin: '(\\(.*?\\)|' + IDENT_RE + ')\\s*=>', returnBegin: true, end: '\\s*=>', contains: [ { className: 'params', variants: [ { begin: IDENT_RE }, { begin: /\(\s*\)/, }, { begin: /\(/, end: /\)/, excludeBegin: true, excludeEnd: true, keywords: KEYWORDS, contains: PARAMS_CONTAINS } ] } ] }, { className: '', begin: /\s/, end: /\s*/, skip: true, }, { // E4X / JSX begin: //, subLanguage: 'xml', contains: [ { begin: /<[A-Za-z0-9\\._:-]+\s*\/>/, skip: true }, { begin: /<[A-Za-z0-9\\._:-]+/, end: /(\/[A-Za-z0-9\\._:-]+|[A-Za-z0-9\\._:-]+\/)>/, skip: true, contains: [ { begin: /<[A-Za-z0-9\\._:-]+\s*\/>/, skip: true }, 'self' ] } ] } ], relevance: 0 }, { className: 'function', beginKeywords: 'function', end: /\{/, excludeEnd: true, contains: [ hljs.inherit(hljs.TITLE_MODE, {begin: IDENT_RE}), { className: 'params', begin: /\(/, end: /\)/, excludeBegin: true, excludeEnd: true, contains: PARAMS_CONTAINS } ], illegal: /\[|%/ }, { begin: /\$[(.]/ // relevance booster for a pattern common to JS libs: `$(something)` and `$.something` }, hljs.METHOD_GUARD, { // ES6 class className: 'class', beginKeywords: 'class', end: /[{;=]/, excludeEnd: true, illegal: /[:"\[\]]/, contains: [ {beginKeywords: 'extends'}, hljs.UNDERSCORE_TITLE_MODE ] }, { beginKeywords: 'constructor get set', end: /\{/, excludeEnd: true } ], illegal: /#(?!!)/ }; } },{name:"jboss-cli",create:/* Language: jboss-cli Author: Raphaël Parrëe Description: language definition jboss cli Category: config */ function (hljs) { var PARAM = { begin: /[\w-]+ *=/, returnBegin: true, relevance: 0, contains: [{className: 'attr', begin: /[\w-]+/}] }; var PARAMSBLOCK = { className: 'params', begin: /\(/, end: /\)/, contains: [PARAM], relevance : 0 }; var OPERATION = { className: 'function', begin: /:[\w\-.]+/, relevance: 0 }; var PATH = { className: 'string', begin: /\B(([\/.])[\w\-.\/=]+)+/, }; var COMMAND_PARAMS = { className: 'params', begin: /--[\w\-=\/]+/, }; return { aliases: ['wildfly-cli'], lexemes: '[a-z\-]+', keywords: { keyword: 'alias batch cd clear command connect connection-factory connection-info data-source deploy ' + 'deployment-info deployment-overlay echo echo-dmr help history if jdbc-driver-info jms-queue|20 jms-topic|20 ls ' + 'patch pwd quit read-attribute read-operation reload rollout-plan run-batch set shutdown try unalias ' + 'undeploy unset version xa-data-source', // module literal: 'true false' }, contains: [ hljs.HASH_COMMENT_MODE, hljs.QUOTE_STRING_MODE, COMMAND_PARAMS, OPERATION, PATH, PARAMSBLOCK ] } } },{name:"json",create:/* Language: JSON Author: Ivan Sagalaev Category: common, protocols */ function(hljs) { var LITERALS = {literal: 'true false null'}; var TYPES = [ hljs.QUOTE_STRING_MODE, hljs.C_NUMBER_MODE ]; var VALUE_CONTAINER = { end: ',', endsWithParent: true, excludeEnd: true, contains: TYPES, keywords: LITERALS }; var OBJECT = { begin: '{', end: '}', contains: [ { className: 'attr', begin: /"/, end: /"/, contains: [hljs.BACKSLASH_ESCAPE], illegal: '\\n', }, hljs.inherit(VALUE_CONTAINER, {begin: /:/}) ], illegal: '\\S' }; var ARRAY = { begin: '\\[', end: '\\]', contains: [hljs.inherit(VALUE_CONTAINER)], // inherit is a workaround for a bug that makes shared modes with endsWithParent compile only the ending of one of the parents illegal: '\\S' }; TYPES.splice(TYPES.length, 0, OBJECT, ARRAY); return { contains: TYPES, keywords: LITERALS, illegal: '\\S' }; } },{name:"julia-repl",create:/* Language: Julia REPL Description: Julia REPL sessions Author: Morten Piibeleht Requires: julia.js The Julia REPL code blocks look something like the following: julia> function foo(x) x + 1 end foo (generic function with 1 method) They start on a new line with "julia>". Usually there should also be a space after this, but we also allow the code to start right after the > character. The code may run over multiple lines, but the additional lines must start with six spaces (i.e. be indented to match "julia>"). The rest of the code is assumed to be output from the executed code and will be left un-highlighted. Using simply spaces to identify line continuations may get a false-positive if the output also prints out six spaces, but such cases should be rare. */ function(hljs) { return { contains: [ { className: 'meta', begin: /^julia>/, relevance: 10, starts: { // end the highlighting if we are on a new line and the line does not have at // least six spaces in the beginning end: /^(?![ ]{6})/, subLanguage: 'julia' }, // jldoctest Markdown blocks are used in the Julia manual and package docs indicate // code snippets that should be verified when the documentation is built. They can be // either REPL-like or script-like, but are usually REPL-like and therefore we apply // julia-repl highlighting to them. More information can be found in Documenter's // manual: https://juliadocs.github.io/Documenter.jl/latest/man/doctests.html aliases: ['jldoctest'] } ] } } },{name:"julia",create:/* Language: Julia Author: Kenta Sato Contributors: Alex Arslan */ function(hljs) { // Since there are numerous special names in Julia, it is too much trouble // to maintain them by hand. Hence these names (i.e. keywords, literals and // built-ins) are automatically generated from Julia v0.6 itself through // the following scripts for each. var KEYWORDS = { // # keyword generator, multi-word keywords handled manually below // foreach(println, ["in", "isa", "where"]) // for kw in Base.REPLCompletions.complete_keyword("") // if !(contains(kw, " ") || kw == "struct") // println(kw) // end // end keyword: 'in isa where ' + 'baremodule begin break catch ccall const continue do else elseif end export false finally for function ' + 'global if import importall let local macro module quote return true try using while ' + // legacy, to be deprecated in the next release 'type immutable abstract bitstype typealias ', // # literal generator // println("true") // println("false") // for name in Base.REPLCompletions.completions("", 0)[1] // try // v = eval(Symbol(name)) // if !(v isa Function || v isa Type || v isa TypeVar || v isa Module || v isa Colon) // println(name) // end // end // end literal: 'true false ' + 'ARGS C_NULL DevNull ENDIAN_BOM ENV I Inf Inf16 Inf32 Inf64 InsertionSort JULIA_HOME LOAD_PATH MergeSort ' + 'NaN NaN16 NaN32 NaN64 PROGRAM_FILE QuickSort RoundDown RoundFromZero RoundNearest RoundNearestTiesAway ' + 'RoundNearestTiesUp RoundToZero RoundUp STDERR STDIN STDOUT VERSION catalan e|0 eu|0 eulergamma golden im ' + 'nothing pi γ π φ ', // # built_in generator: // for name in Base.REPLCompletions.completions("", 0)[1] // try // v = eval(Symbol(name)) // if v isa Type || v isa TypeVar // println(name) // end // end // end built_in: 'ANY AbstractArray AbstractChannel AbstractFloat AbstractMatrix AbstractRNG AbstractSerializer AbstractSet ' + 'AbstractSparseArray AbstractSparseMatrix AbstractSparseVector AbstractString AbstractUnitRange AbstractVecOrMat ' + 'AbstractVector Any ArgumentError Array AssertionError Associative Base64DecodePipe Base64EncodePipe Bidiagonal '+ 'BigFloat BigInt BitArray BitMatrix BitVector Bool BoundsError BufferStream CachingPool CapturedException ' + 'CartesianIndex CartesianRange Cchar Cdouble Cfloat Channel Char Cint Cintmax_t Clong Clonglong ClusterManager ' + 'Cmd CodeInfo Colon Complex Complex128 Complex32 Complex64 CompositeException Condition ConjArray ConjMatrix ' + 'ConjVector Cptrdiff_t Cshort Csize_t Cssize_t Cstring Cuchar Cuint Cuintmax_t Culong Culonglong Cushort Cwchar_t ' + 'Cwstring DataType Date DateFormat DateTime DenseArray DenseMatrix DenseVecOrMat DenseVector Diagonal Dict ' + 'DimensionMismatch Dims DirectIndexString Display DivideError DomainError EOFError EachLine Enum Enumerate ' + 'ErrorException Exception ExponentialBackOff Expr Factorization FileMonitor Float16 Float32 Float64 Function ' + 'Future GlobalRef GotoNode HTML Hermitian IO IOBuffer IOContext IOStream IPAddr IPv4 IPv6 IndexCartesian IndexLinear ' + 'IndexStyle InexactError InitError Int Int128 Int16 Int32 Int64 Int8 IntSet Integer InterruptException ' + 'InvalidStateException Irrational KeyError LabelNode LinSpace LineNumberNode LoadError LowerTriangular MIME Matrix ' + 'MersenneTwister Method MethodError MethodTable Module NTuple NewvarNode NullException Nullable Number ObjectIdDict ' + 'OrdinalRange OutOfMemoryError OverflowError Pair ParseError PartialQuickSort PermutedDimsArray Pipe ' + 'PollingFileWatcher ProcessExitedException Ptr QuoteNode RandomDevice Range RangeIndex Rational RawFD ' + 'ReadOnlyMemoryError Real ReentrantLock Ref Regex RegexMatch RemoteChannel RemoteException RevString RoundingMode ' + 'RowVector SSAValue SegmentationFault SerializationState Set SharedArray SharedMatrix SharedVector Signed ' + 'SimpleVector Slot SlotNumber SparseMatrixCSC SparseVector StackFrame StackOverflowError StackTrace StepRange ' + 'StepRangeLen StridedArray StridedMatrix StridedVecOrMat StridedVector String SubArray SubString SymTridiagonal ' + 'Symbol Symmetric SystemError TCPSocket Task Text TextDisplay Timer Tridiagonal Tuple Type TypeError TypeMapEntry ' + 'TypeMapLevel TypeName TypeVar TypedSlot UDPSocket UInt UInt128 UInt16 UInt32 UInt64 UInt8 UndefRefError UndefVarError ' + 'UnicodeError UniformScaling Union UnionAll UnitRange Unsigned UpperTriangular Val Vararg VecElement VecOrMat Vector ' + 'VersionNumber Void WeakKeyDict WeakRef WorkerConfig WorkerPool ' }; // ref: http://julia.readthedocs.org/en/latest/manual/variables/#allowed-variable-names var VARIABLE_NAME_RE = '[A-Za-z_\\u00A1-\\uFFFF][A-Za-z_0-9\\u00A1-\\uFFFF]*'; // placeholder for recursive self-reference var DEFAULT = { lexemes: VARIABLE_NAME_RE, keywords: KEYWORDS, illegal: /<\// }; // ref: http://julia.readthedocs.org/en/latest/manual/integers-and-floating-point-numbers/ var NUMBER = { className: 'number', // supported numeric literals: // * binary literal (e.g. 0x10) // * octal literal (e.g. 0o76543210) // * hexadecimal literal (e.g. 0xfedcba876543210) // * hexadecimal floating point literal (e.g. 0x1p0, 0x1.2p2) // * decimal literal (e.g. 9876543210, 100_000_000) // * floating pointe literal (e.g. 1.2, 1.2f, .2, 1., 1.2e10, 1.2e-10) begin: /(\b0x[\d_]*(\.[\d_]*)?|0x\.\d[\d_]*)p[-+]?\d+|\b0[box][a-fA-F0-9][a-fA-F0-9_]*|(\b\d[\d_]*(\.[\d_]*)?|\.\d[\d_]*)([eEfF][-+]?\d+)?/, relevance: 0 }; var CHAR = { className: 'string', begin: /'(.|\\[xXuU][a-zA-Z0-9]+)'/ }; var INTERPOLATION = { className: 'subst', begin: /\$\(/, end: /\)/, keywords: KEYWORDS }; var INTERPOLATED_VARIABLE = { className: 'variable', begin: '\\$' + VARIABLE_NAME_RE }; // TODO: neatly escape normal code in string literal var STRING = { className: 'string', contains: [hljs.BACKSLASH_ESCAPE, INTERPOLATION, INTERPOLATED_VARIABLE], variants: [ { begin: /\w*"""/, end: /"""\w*/, relevance: 10 }, { begin: /\w*"/, end: /"\w*/ } ] }; var COMMAND = { className: 'string', contains: [hljs.BACKSLASH_ESCAPE, INTERPOLATION, INTERPOLATED_VARIABLE], begin: '`', end: '`' }; var MACROCALL = { className: 'meta', begin: '@' + VARIABLE_NAME_RE }; var COMMENT = { className: 'comment', variants: [ { begin: '#=', end: '=#', relevance: 10 }, { begin: '#', end: '$' } ] }; DEFAULT.contains = [ NUMBER, CHAR, STRING, COMMAND, MACROCALL, COMMENT, hljs.HASH_COMMENT_MODE, { className: 'keyword', begin: '\\b(((abstract|primitive)\\s+)type|(mutable\\s+)?struct)\\b' }, {begin: /<:/} // relevance booster ]; INTERPOLATION.contains = DEFAULT.contains; return DEFAULT; } },{name:"kotlin",create:/* Language: Kotlin Author: Sergey Mashkov */ function(hljs) { var KEYWORDS = { keyword: 'abstract as val var vararg get set class object open private protected public noinline ' + 'crossinline dynamic final enum if else do while for when throw try catch finally ' + 'import package is in fun override companion reified inline lateinit init ' + 'interface annotation data sealed internal infix operator out by constructor super ' + 'tailrec where const inner suspend typealias external expect actual ' + // to be deleted soon 'trait volatile transient native default', built_in: 'Byte Short Char Int Long Boolean Float Double Void Unit Nothing', literal: 'true false null' }; var KEYWORDS_WITH_LABEL = { className: 'keyword', begin: /\b(break|continue|return|this)\b/, starts: { contains: [ { className: 'symbol', begin: /@\w+/ } ] } }; var LABEL = { className: 'symbol', begin: hljs.UNDERSCORE_IDENT_RE + '@' }; // for string templates var SUBST = { className: 'subst', begin: '\\${', end: '}', contains: [hljs.APOS_STRING_MODE, hljs.C_NUMBER_MODE] }; var VARIABLE = { className: 'variable', begin: '\\$' + hljs.UNDERSCORE_IDENT_RE }; var STRING = { className: 'string', variants: [ { begin: '"""', end: '"""', contains: [VARIABLE, SUBST] }, // Can't use built-in modes easily, as we want to use STRING in the meta // context as 'meta-string' and there's no syntax to remove explicitly set // classNames in built-in modes. { begin: '\'', end: '\'', illegal: /\n/, contains: [hljs.BACKSLASH_ESCAPE] }, { begin: '"', end: '"', illegal: /\n/, contains: [hljs.BACKSLASH_ESCAPE, VARIABLE, SUBST] } ] }; var ANNOTATION_USE_SITE = { className: 'meta', begin: '@(?:file|property|field|get|set|receiver|param|setparam|delegate)\\s*:(?:\\s*' + hljs.UNDERSCORE_IDENT_RE + ')?' }; var ANNOTATION = { className: 'meta', begin: '@' + hljs.UNDERSCORE_IDENT_RE, contains: [ { begin: /\(/, end: /\)/, contains: [ hljs.inherit(STRING, {className: 'meta-string'}) ] } ] }; // https://kotlinlang.org/docs/reference/whatsnew11.html#underscores-in-numeric-literals // According to the doc above, the number mode of kotlin is the same as java 8, // so the code below is copied from java.js var KOTLIN_NUMBER_RE = '\\b' + '(' + '0[bB]([01]+[01_]+[01]+|[01]+)' + // 0b... '|' + '0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)' + // 0x... '|' + '(' + '([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?' + '|' + '\\.([\\d]+[\\d_]+[\\d]+|[\\d]+)' + ')' + '([eE][-+]?\\d+)?' + // octal, decimal, float ')' + '[lLfF]?'; var KOTLIN_NUMBER_MODE = { className: 'number', begin: KOTLIN_NUMBER_RE, relevance: 0 }; var KOTLIN_NESTED_COMMENT = hljs.COMMENT( '/\\*', '\\*/', { contains: [ hljs.C_BLOCK_COMMENT_MODE ] } ); var KOTLIN_PAREN_TYPE = { variants: [ { className: 'type', begin: hljs.UNDERSCORE_IDENT_RE }, { begin: /\(/, end: /\)/, contains: [] //defined later } ] }; var KOTLIN_PAREN_TYPE2 = KOTLIN_PAREN_TYPE; KOTLIN_PAREN_TYPE2.variants[1].contains = [ KOTLIN_PAREN_TYPE ]; KOTLIN_PAREN_TYPE.variants[1].contains = [ KOTLIN_PAREN_TYPE2 ]; return { aliases: ['kt'], keywords: KEYWORDS, contains : [ hljs.COMMENT( '/\\*\\*', '\\*/', { relevance : 0, contains : [{ className : 'doctag', begin : '@[A-Za-z]+' }] } ), hljs.C_LINE_COMMENT_MODE, KOTLIN_NESTED_COMMENT, KEYWORDS_WITH_LABEL, LABEL, ANNOTATION_USE_SITE, ANNOTATION, { className: 'function', beginKeywords: 'fun', end: '[(]|$', returnBegin: true, excludeEnd: true, keywords: KEYWORDS, illegal: /fun\s+(<.*>)?[^\s\(]+(\s+[^\s\(]+)\s*=/, relevance: 5, contains: [ { begin: hljs.UNDERSCORE_IDENT_RE + '\\s*\\(', returnBegin: true, relevance: 0, contains: [hljs.UNDERSCORE_TITLE_MODE] }, { className: 'type', begin: //, keywords: 'reified', relevance: 0 }, { className: 'params', begin: /\(/, end: /\)/, endsParent: true, keywords: KEYWORDS, relevance: 0, contains: [ { begin: /:/, end: /[=,\/]/, endsWithParent: true, contains: [ KOTLIN_PAREN_TYPE, hljs.C_LINE_COMMENT_MODE, KOTLIN_NESTED_COMMENT ], relevance: 0 }, hljs.C_LINE_COMMENT_MODE, KOTLIN_NESTED_COMMENT, ANNOTATION_USE_SITE, ANNOTATION, STRING, hljs.C_NUMBER_MODE ] }, KOTLIN_NESTED_COMMENT ] }, { className: 'class', beginKeywords: 'class interface trait', end: /[:\{(]|$/, // remove 'trait' when removed from KEYWORDS excludeEnd: true, illegal: 'extends implements', contains: [ {beginKeywords: 'public protected internal private constructor'}, hljs.UNDERSCORE_TITLE_MODE, { className: 'type', begin: //, excludeBegin: true, excludeEnd: true, relevance: 0 }, { className: 'type', begin: /[,:]\s*/, end: /[<\(,]|$/, excludeBegin: true, returnEnd: true }, ANNOTATION_USE_SITE, ANNOTATION ] }, STRING, { className: 'meta', begin: "^#!/usr/bin/env", end: '$', illegal: '\n' }, KOTLIN_NUMBER_MODE ] }; } },{name:"lasso",create:/* Language: Lasso Author: Eric Knibbe Description: Lasso is a language and server platform for database-driven web applications. This definition handles Lasso 9 syntax and LassoScript for Lasso 8.6 and earlier. */ function(hljs) { var LASSO_IDENT_RE = '[a-zA-Z_][\\w.]*'; var LASSO_ANGLE_RE = '<\\?(lasso(script)?|=)'; var LASSO_CLOSE_RE = '\\]|\\?>'; var LASSO_KEYWORDS = { literal: 'true false none minimal full all void and or not ' + 'bw nbw ew new cn ncn lt lte gt gte eq neq rx nrx ft', built_in: 'array date decimal duration integer map pair string tag xml null ' + 'boolean bytes keyword list locale queue set stack staticarray ' + 'local var variable global data self inherited currentcapture givenblock', keyword: 'cache database_names database_schemanames database_tablenames ' + 'define_tag define_type email_batch encode_set html_comment handle ' + 'handle_error header if inline iterate ljax_target link ' + 'link_currentaction link_currentgroup link_currentrecord link_detail ' + 'link_firstgroup link_firstrecord link_lastgroup link_lastrecord ' + 'link_nextgroup link_nextrecord link_prevgroup link_prevrecord log ' + 'loop namespace_using output_none portal private protect records ' + 'referer referrer repeating resultset rows search_args ' + 'search_arguments select sort_args sort_arguments thread_atomic ' + 'value_list while abort case else fail_if fail_ifnot fail if_empty ' + 'if_false if_null if_true loop_abort loop_continue loop_count params ' + 'params_up return return_value run_children soap_definetag ' + 'soap_lastrequest soap_lastresponse tag_name ascending average by ' + 'define descending do equals frozen group handle_failure import in ' + 'into join let match max min on order parent protected provide public ' + 'require returnhome skip split_thread sum take thread to trait type ' + 'where with yield yieldhome' }; var HTML_COMMENT = hljs.COMMENT( '', { relevance: 0 } ); var LASSO_NOPROCESS = { className: 'meta', begin: '\\[noprocess\\]', starts: { end: '\\[/noprocess\\]', returnEnd: true, contains: [HTML_COMMENT] } }; var LASSO_START = { className: 'meta', begin: '\\[/noprocess|' + LASSO_ANGLE_RE }; var LASSO_DATAMEMBER = { className: 'symbol', begin: '\'' + LASSO_IDENT_RE + '\'' }; var LASSO_CODE = [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.inherit(hljs.C_NUMBER_MODE, {begin: hljs.C_NUMBER_RE + '|(-?infinity|NaN)\\b'}), hljs.inherit(hljs.APOS_STRING_MODE, {illegal: null}), hljs.inherit(hljs.QUOTE_STRING_MODE, {illegal: null}), { className: 'string', begin: '`', end: '`' }, { // variables variants: [ { begin: '[#$]' + LASSO_IDENT_RE }, { begin: '#', end: '\\d+', illegal: '\\W' } ] }, { className: 'type', begin: '::\\s*', end: LASSO_IDENT_RE, illegal: '\\W' }, { className: 'params', variants: [ { begin: '-(?!infinity)' + LASSO_IDENT_RE, relevance: 0 }, { begin: '(\\.\\.\\.)' } ] }, { begin: /(->|\.)\s*/, relevance: 0, contains: [LASSO_DATAMEMBER] }, { className: 'class', beginKeywords: 'define', returnEnd: true, end: '\\(|=>', contains: [ hljs.inherit(hljs.TITLE_MODE, {begin: LASSO_IDENT_RE + '(=(?!>))?|[-+*/%](?!>)'}) ] } ]; return { aliases: ['ls', 'lassoscript'], case_insensitive: true, lexemes: LASSO_IDENT_RE + '|&[lg]t;', keywords: LASSO_KEYWORDS, contains: [ { className: 'meta', begin: LASSO_CLOSE_RE, relevance: 0, starts: { // markup end: '\\[|' + LASSO_ANGLE_RE, returnEnd: true, relevance: 0, contains: [HTML_COMMENT] } }, LASSO_NOPROCESS, LASSO_START, { className: 'meta', begin: '\\[no_square_brackets', starts: { end: '\\[/no_square_brackets\\]', // not implemented in the language lexemes: LASSO_IDENT_RE + '|&[lg]t;', keywords: LASSO_KEYWORDS, contains: [ { className: 'meta', begin: LASSO_CLOSE_RE, relevance: 0, starts: { end: '\\[noprocess\\]|' + LASSO_ANGLE_RE, returnEnd: true, contains: [HTML_COMMENT] } }, LASSO_NOPROCESS, LASSO_START ].concat(LASSO_CODE) } }, { className: 'meta', begin: '\\[', relevance: 0 }, { className: 'meta', begin: '^#!', end:'lasso9$', relevance: 10 } ].concat(LASSO_CODE) }; } },{name:"ldif",create:/* Language: LDIF Contributors: Jacob Childress Category: enterprise, config */ function(hljs) { return { contains: [ { className: 'attribute', begin: '^dn', end: ': ', excludeEnd: true, starts: {end: '$', relevance: 0}, relevance: 10 }, { className: 'attribute', begin: '^\\w', end: ': ', excludeEnd: true, starts: {end: '$', relevance: 0} }, { className: 'literal', begin: '^-', end: '$' }, hljs.HASH_COMMENT_MODE ] }; } },{name:"leaf",create:/* Language: Leaf Author: Hale Chan Description: Based on the Leaf reference from https://vapor.github.io/documentation/guide/leaf.html. */ function (hljs) { return { contains: [ { className: 'function', begin: '#+' + '[A-Za-z_0-9]*' + '\\(', end:' {', returnBegin: true, excludeEnd: true, contains : [ { className: 'keyword', begin: '#+' }, { className: 'title', begin: '[A-Za-z_][A-Za-z_0-9]*' }, { className: 'params', begin: '\\(', end: '\\)', endsParent: true, contains: [ { className: 'string', begin: '"', end: '"' }, { className: 'variable', begin: '[A-Za-z_][A-Za-z_0-9]*' } ] } ] } ] }; } },{name:"less",create:/* Language: Less Author: Max Mikhailov Category: css */ function(hljs) { var IDENT_RE = '[\\w-]+'; // yes, Less identifiers may begin with a digit var INTERP_IDENT_RE = '(' + IDENT_RE + '|@{' + IDENT_RE + '})'; /* Generic Modes */ var RULES = [], VALUE = []; // forward def. for recursive modes var STRING_MODE = function(c) { return { // Less strings are not multiline (also include '~' for more consistent coloring of "escaped" strings) className: 'string', begin: '~?' + c + '.*?' + c };}; var IDENT_MODE = function(name, begin, relevance) { return { className: name, begin: begin, relevance: relevance };}; var PARENS_MODE = { // used only to properly balance nested parens inside mixin call, def. arg list begin: '\\(', end: '\\)', contains: VALUE, relevance: 0 }; // generic Less highlighter (used almost everywhere except selectors): VALUE.push( hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, STRING_MODE("'"), STRING_MODE('"'), hljs.CSS_NUMBER_MODE, // fixme: it does not include dot for numbers like .5em :( { begin: '(url|data-uri)\\(', starts: {className: 'string', end: '[\\)\\n]', excludeEnd: true} }, IDENT_MODE('number', '#[0-9A-Fa-f]+\\b'), PARENS_MODE, IDENT_MODE('variable', '@@?' + IDENT_RE, 10), IDENT_MODE('variable', '@{' + IDENT_RE + '}'), IDENT_MODE('built_in', '~?`[^`]*?`'), // inline javascript (or whatever host language) *multiline* string { // @media features (it’s here to not duplicate things in AT_RULE_MODE with extra PARENS_MODE overriding): className: 'attribute', begin: IDENT_RE + '\\s*:', end: ':', returnBegin: true, excludeEnd: true }, { className: 'meta', begin: '!important' } ); var VALUE_WITH_RULESETS = VALUE.concat({ begin: '{', end: '}', contains: RULES }); var MIXIN_GUARD_MODE = { beginKeywords: 'when', endsWithParent: true, contains: [{beginKeywords: 'and not'}].concat(VALUE) // using this form to override VALUE’s 'function' match }; /* Rule-Level Modes */ var RULE_MODE = { begin: INTERP_IDENT_RE + '\\s*:', returnBegin: true, end: '[;}]', relevance: 0, contains: [ { className: 'attribute', begin: INTERP_IDENT_RE, end: ':', excludeEnd: true, starts: { endsWithParent: true, illegal: '[<=$]', relevance: 0, contains: VALUE } } ] }; var AT_RULE_MODE = { className: 'keyword', begin: '@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b', starts: {end: '[;{}]', returnEnd: true, contains: VALUE, relevance: 0} }; // variable definitions and calls var VAR_RULE_MODE = { className: 'variable', variants: [ // using more strict pattern for higher relevance to increase chances of Less detection. // this is *the only* Less specific statement used in most of the sources, so... // (we’ll still often loose to the css-parser unless there's '//' comment, // simply because 1 variable just can't beat 99 properties :) {begin: '@' + IDENT_RE + '\\s*:', relevance: 15}, {begin: '@' + IDENT_RE} ], starts: {end: '[;}]', returnEnd: true, contains: VALUE_WITH_RULESETS} }; var SELECTOR_MODE = { // first parse unambiguous selectors (i.e. those not starting with tag) // then fall into the scary lookahead-discriminator variant. // this mode also handles mixin definitions and calls variants: [{ begin: '[\\.#:&\\[>]', end: '[;{}]' // mixin calls end with ';' }, { begin: INTERP_IDENT_RE, end: '{' }], returnBegin: true, returnEnd: true, illegal: '[<=\'$"]', relevance: 0, contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, MIXIN_GUARD_MODE, IDENT_MODE('keyword', 'all\\b'), IDENT_MODE('variable', '@{' + IDENT_RE + '}'), // otherwise it’s identified as tag IDENT_MODE('selector-tag', INTERP_IDENT_RE + '%?', 0), // '%' for more consistent coloring of @keyframes "tags" IDENT_MODE('selector-id', '#' + INTERP_IDENT_RE), IDENT_MODE('selector-class', '\\.' + INTERP_IDENT_RE, 0), IDENT_MODE('selector-tag', '&', 0), {className: 'selector-attr', begin: '\\[', end: '\\]'}, {className: 'selector-pseudo', begin: /:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/}, {begin: '\\(', end: '\\)', contains: VALUE_WITH_RULESETS}, // argument list of parametric mixins {begin: '!important'} // eat !important after mixin call or it will be colored as tag ] }; RULES.push( hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, AT_RULE_MODE, VAR_RULE_MODE, RULE_MODE, SELECTOR_MODE ); return { case_insensitive: true, illegal: '[=>\'/<($"]', contains: RULES }; } },{name:"lisp",create:/* Language: Lisp Description: Generic lisp syntax Author: Vasily Polovnyov Category: lisp */ function(hljs) { var LISP_IDENT_RE = '[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#!]*'; var MEC_RE = '\\|[^]*?\\|'; var LISP_SIMPLE_NUMBER_RE = '(\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s|D|E|F|L|S)(\\+|\\-)?\\d+)?'; var SHEBANG = { className: 'meta', begin: '^#!', end: '$' }; var LITERAL = { className: 'literal', begin: '\\b(t{1}|nil)\\b' }; var NUMBER = { className: 'number', variants: [ {begin: LISP_SIMPLE_NUMBER_RE, relevance: 0}, {begin: '#(b|B)[0-1]+(/[0-1]+)?'}, {begin: '#(o|O)[0-7]+(/[0-7]+)?'}, {begin: '#(x|X)[0-9a-fA-F]+(/[0-9a-fA-F]+)?'}, {begin: '#(c|C)\\(' + LISP_SIMPLE_NUMBER_RE + ' +' + LISP_SIMPLE_NUMBER_RE, end: '\\)'} ] }; var STRING = hljs.inherit(hljs.QUOTE_STRING_MODE, {illegal: null}); var COMMENT = hljs.COMMENT( ';', '$', { relevance: 0 } ); var VARIABLE = { begin: '\\*', end: '\\*' }; var KEYWORD = { className: 'symbol', begin: '[:&]' + LISP_IDENT_RE }; var IDENT = { begin: LISP_IDENT_RE, relevance: 0 }; var MEC = { begin: MEC_RE }; var QUOTED_LIST = { begin: '\\(', end: '\\)', contains: ['self', LITERAL, STRING, NUMBER, IDENT] }; var QUOTED = { contains: [NUMBER, STRING, VARIABLE, KEYWORD, QUOTED_LIST, IDENT], variants: [ { begin: '[\'`]\\(', end: '\\)' }, { begin: '\\(quote ', end: '\\)', keywords: {name: 'quote'} }, { begin: '\'' + MEC_RE } ] }; var QUOTED_ATOM = { variants: [ {begin: '\'' + LISP_IDENT_RE}, {begin: '#\'' + LISP_IDENT_RE + '(::' + LISP_IDENT_RE + ')*'} ] }; var LIST = { begin: '\\(\\s*', end: '\\)' }; var BODY = { endsWithParent: true, relevance: 0 }; LIST.contains = [ { className: 'name', variants: [ {begin: LISP_IDENT_RE}, {begin: MEC_RE} ] }, BODY ]; BODY.contains = [QUOTED, QUOTED_ATOM, LIST, LITERAL, NUMBER, STRING, COMMENT, VARIABLE, KEYWORD, MEC, IDENT]; return { illegal: /\S/, contains: [ NUMBER, SHEBANG, LITERAL, STRING, COMMENT, QUOTED, QUOTED_ATOM, LIST, IDENT ] }; } },{name:"livecodeserver",create:/* Language: LiveCode Author: Ralf Bitter Description: Language definition for LiveCode server accounting for revIgniter (a web application framework) characteristics. Version: 1.1 Date: 2019-04-17 Category: enterprise */ function(hljs) { var VARIABLE = { className: 'variable', variants: [ {begin: '\\b([gtps][A-Z]{1}[a-zA-Z0-9]*)(\\[.+\\])?(?:\\s*?)'}, {begin: '\\$_[A-Z]+'} ], relevance: 0 }; var COMMENT_MODES = [ hljs.C_BLOCK_COMMENT_MODE, hljs.HASH_COMMENT_MODE, hljs.COMMENT('--', '$'), hljs.COMMENT('[^:]//', '$') ]; var TITLE1 = hljs.inherit(hljs.TITLE_MODE, { variants: [ {begin: '\\b_*rig[A-Z]+[A-Za-z0-9_\\-]*'}, {begin: '\\b_[a-z0-9\\-]+'} ] }); var TITLE2 = hljs.inherit(hljs.TITLE_MODE, {begin: '\\b([A-Za-z0-9_\\-]+)\\b'}); return { case_insensitive: false, keywords: { keyword: '$_COOKIE $_FILES $_GET $_GET_BINARY $_GET_RAW $_POST $_POST_BINARY $_POST_RAW $_SESSION $_SERVER ' + 'codepoint codepoints segment segments codeunit codeunits sentence sentences trueWord trueWords paragraph ' + 'after byte bytes english the until http forever descending using line real8 with seventh ' + 'for stdout finally element word words fourth before black ninth sixth characters chars stderr ' + 'uInt1 uInt1s uInt2 uInt2s stdin string lines relative rel any fifth items from middle mid ' + 'at else of catch then third it file milliseconds seconds second secs sec int1 int1s int4 ' + 'int4s internet int2 int2s normal text item last long detailed effective uInt4 uInt4s repeat ' + 'end repeat URL in try into switch to words https token binfile each tenth as ticks tick ' + 'system real4 by dateItems without char character ascending eighth whole dateTime numeric short ' + 'first ftp integer abbreviated abbr abbrev private case while if ' + 'div mod wrap and or bitAnd bitNot bitOr bitXor among not in a an within ' + 'contains ends with begins the keys of keys', literal: 'SIX TEN FORMFEED NINE ZERO NONE SPACE FOUR FALSE COLON CRLF PI COMMA ENDOFFILE EOF EIGHT FIVE ' + 'QUOTE EMPTY ONE TRUE RETURN CR LINEFEED RIGHT BACKSLASH NULL SEVEN TAB THREE TWO ' + 'six ten formfeed nine zero none space four false colon crlf pi comma endoffile eof eight five ' + 'quote empty one true return cr linefeed right backslash null seven tab three two ' + 'RIVERSION RISTATE FILE_READ_MODE FILE_WRITE_MODE FILE_WRITE_MODE DIR_WRITE_MODE FILE_READ_UMASK ' + 'FILE_WRITE_UMASK DIR_READ_UMASK DIR_WRITE_UMASK', built_in: 'put abs acos aliasReference annuity arrayDecode arrayEncode asin atan atan2 average avg avgDev base64Decode ' + 'base64Encode baseConvert binaryDecode binaryEncode byteOffset byteToNum cachedURL cachedURLs charToNum ' + 'cipherNames codepointOffset codepointProperty codepointToNum codeunitOffset commandNames compound compress ' + 'constantNames cos date dateFormat decompress difference directories ' + 'diskSpace DNSServers exp exp1 exp2 exp10 extents files flushEvents folders format functionNames geometricMean global ' + 'globals hasMemory harmonicMean hostAddress hostAddressToName hostName hostNameToAddress isNumber ISOToMac itemOffset ' + 'keys len length libURLErrorData libUrlFormData libURLftpCommand libURLLastHTTPHeaders libURLLastRHHeaders ' + 'libUrlMultipartFormAddPart libUrlMultipartFormData libURLVersion lineOffset ln ln1 localNames log log2 log10 ' + 'longFilePath lower macToISO matchChunk matchText matrixMultiply max md5Digest median merge messageAuthenticationCode messageDigest millisec ' + 'millisecs millisecond milliseconds min monthNames nativeCharToNum normalizeText num number numToByte numToChar ' + 'numToCodepoint numToNativeChar offset open openfiles openProcesses openProcessIDs openSockets ' + 'paragraphOffset paramCount param params peerAddress pendingMessages platform popStdDev populationStandardDeviation ' + 'populationVariance popVariance processID random randomBytes replaceText result revCreateXMLTree revCreateXMLTreeFromFile ' + 'revCurrentRecord revCurrentRecordIsFirst revCurrentRecordIsLast revDatabaseColumnCount revDatabaseColumnIsNull ' + 'revDatabaseColumnLengths revDatabaseColumnNames revDatabaseColumnNamed revDatabaseColumnNumbered ' + 'revDatabaseColumnTypes revDatabaseConnectResult revDatabaseCursors revDatabaseID revDatabaseTableNames ' + 'revDatabaseType revDataFromQuery revdb_closeCursor revdb_columnbynumber revdb_columncount revdb_columnisnull ' + 'revdb_columnlengths revdb_columnnames revdb_columntypes revdb_commit revdb_connect revdb_connections ' + 'revdb_connectionerr revdb_currentrecord revdb_cursorconnection revdb_cursorerr revdb_cursors revdb_dbtype ' + 'revdb_disconnect revdb_execute revdb_iseof revdb_isbof revdb_movefirst revdb_movelast revdb_movenext ' + 'revdb_moveprev revdb_query revdb_querylist revdb_recordcount revdb_rollback revdb_tablenames ' + 'revGetDatabaseDriverPath revNumberOfRecords revOpenDatabase revOpenDatabases revQueryDatabase ' + 'revQueryDatabaseBlob revQueryResult revQueryIsAtStart revQueryIsAtEnd revUnixFromMacPath revXMLAttribute ' + 'revXMLAttributes revXMLAttributeValues revXMLChildContents revXMLChildNames revXMLCreateTreeFromFileWithNamespaces ' + 'revXMLCreateTreeWithNamespaces revXMLDataFromXPathQuery revXMLEvaluateXPath revXMLFirstChild revXMLMatchingNode ' + 'revXMLNextSibling revXMLNodeContents revXMLNumberOfChildren revXMLParent revXMLPreviousSibling ' + 'revXMLRootNode revXMLRPC_CreateRequest revXMLRPC_Documents revXMLRPC_Error ' + 'revXMLRPC_GetHost revXMLRPC_GetMethod revXMLRPC_GetParam revXMLText revXMLRPC_Execute ' + 'revXMLRPC_GetParamCount revXMLRPC_GetParamNode revXMLRPC_GetParamType revXMLRPC_GetPath revXMLRPC_GetPort ' + 'revXMLRPC_GetProtocol revXMLRPC_GetRequest revXMLRPC_GetResponse revXMLRPC_GetSocket revXMLTree ' + 'revXMLTrees revXMLValidateDTD revZipDescribeItem revZipEnumerateItems revZipOpenArchives round sampVariance ' + 'sec secs seconds sentenceOffset sha1Digest shell shortFilePath sin specialFolderPath sqrt standardDeviation statRound ' + 'stdDev sum sysError systemVersion tan tempName textDecode textEncode tick ticks time to tokenOffset toLower toUpper ' + 'transpose truewordOffset trunc uniDecode uniEncode upper URLDecode URLEncode URLStatus uuid value variableNames ' + 'variance version waitDepth weekdayNames wordOffset xsltApplyStylesheet xsltApplyStylesheetFromFile xsltLoadStylesheet ' + 'xsltLoadStylesheetFromFile add breakpoint cancel clear local variable file word line folder directory URL close socket process ' + 'combine constant convert create new alias folder directory decrypt delete variable word line folder ' + 'directory URL dispatch divide do encrypt filter get include intersect kill libURLDownloadToFile ' + 'libURLFollowHttpRedirects libURLftpUpload libURLftpUploadFile libURLresetAll libUrlSetAuthCallback libURLSetDriver ' + 'libURLSetCustomHTTPHeaders libUrlSetExpect100 libURLSetFTPListCommand libURLSetFTPMode libURLSetFTPStopTime ' + 'libURLSetStatusCallback load extension loadedExtensions multiply socket prepare process post seek rel relative read from process rename ' + 'replace require resetAll resolve revAddXMLNode revAppendXML revCloseCursor revCloseDatabase revCommitDatabase ' + 'revCopyFile revCopyFolder revCopyXMLNode revDeleteFolder revDeleteXMLNode revDeleteAllXMLTrees ' + 'revDeleteXMLTree revExecuteSQL revGoURL revInsertXMLNode revMoveFolder revMoveToFirstRecord revMoveToLastRecord ' + 'revMoveToNextRecord revMoveToPreviousRecord revMoveToRecord revMoveXMLNode revPutIntoXMLNode revRollBackDatabase ' + 'revSetDatabaseDriverPath revSetXMLAttribute revXMLRPC_AddParam revXMLRPC_DeleteAllDocuments revXMLAddDTD ' + 'revXMLRPC_Free revXMLRPC_FreeAll revXMLRPC_DeleteDocument revXMLRPC_DeleteParam revXMLRPC_SetHost ' + 'revXMLRPC_SetMethod revXMLRPC_SetPort revXMLRPC_SetProtocol revXMLRPC_SetSocket revZipAddItemWithData ' + 'revZipAddItemWithFile revZipAddUncompressedItemWithData revZipAddUncompressedItemWithFile revZipCancel ' + 'revZipCloseArchive revZipDeleteItem revZipExtractItemToFile revZipExtractItemToVariable revZipSetProgressCallback ' + 'revZipRenameItem revZipReplaceItemWithData revZipReplaceItemWithFile revZipOpenArchive send set sort split start stop ' + 'subtract symmetric union unload vectorDotProduct wait write' }, contains: [ VARIABLE, { className: 'keyword', begin: '\\bend\\sif\\b' }, { className: 'function', beginKeywords: 'function', end: '$', contains: [ VARIABLE, TITLE2, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, hljs.BINARY_NUMBER_MODE, hljs.C_NUMBER_MODE, TITLE1 ] }, { className: 'function', begin: '\\bend\\s+', end: '$', keywords: 'end', contains: [ TITLE2, TITLE1 ], relevance: 0 }, { beginKeywords: 'command on', end: '$', contains: [ VARIABLE, TITLE2, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, hljs.BINARY_NUMBER_MODE, hljs.C_NUMBER_MODE, TITLE1 ] }, { className: 'meta', variants: [ { begin: '<\\?(rev|lc|livecode)', relevance: 10 }, { begin: '<\\?' }, { begin: '\\?>' } ] }, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, hljs.BINARY_NUMBER_MODE, hljs.C_NUMBER_MODE, TITLE1 ].concat(COMMENT_MODES), illegal: ';$|^\\[|^=|&|{' }; } },{name:"livescript",create:/* Language: LiveScript Author: Taneli Vatanen Contributors: Jen Evers-Corvina Origin: coffeescript.js Description: LiveScript is a programming language that transcompiles to JavaScript. For info about language see http://livescript.net/ Category: scripting */ function(hljs) { var KEYWORDS = { keyword: // JS keywords 'in if for while finally new do return else break catch instanceof throw try this ' + 'switch continue typeof delete debugger case default function var with ' + // LiveScript keywords 'then unless until loop of by when and or is isnt not it that otherwise from to til fallthrough super ' + 'case default function var void const let enum export import native ' + '__hasProp __extends __slice __bind __indexOf', literal: // JS literals 'true false null undefined ' + // LiveScript literals 'yes no on off it that void', built_in: 'npm require console print module global window document' }; var JS_IDENT_RE = '[A-Za-z$_](?:\-[0-9A-Za-z$_]|[0-9A-Za-z$_])*'; var TITLE = hljs.inherit(hljs.TITLE_MODE, {begin: JS_IDENT_RE}); var SUBST = { className: 'subst', begin: /#\{/, end: /}/, keywords: KEYWORDS }; var SUBST_SIMPLE = { className: 'subst', begin: /#[A-Za-z$_]/, end: /(?:\-[0-9A-Za-z$_]|[0-9A-Za-z$_])*/, keywords: KEYWORDS }; var EXPRESSIONS = [ hljs.BINARY_NUMBER_MODE, { className: 'number', begin: '(\\b0[xX][a-fA-F0-9_]+)|(\\b\\d(\\d|_\\d)*(\\.(\\d(\\d|_\\d)*)?)?(_*[eE]([-+]\\d(_\\d|\\d)*)?)?[_a-z]*)', relevance: 0, starts: {end: '(\\s*/)?', relevance: 0} // a number tries to eat the following slash to prevent treating it as a regexp }, { className: 'string', variants: [ { begin: /'''/, end: /'''/, contains: [hljs.BACKSLASH_ESCAPE] }, { begin: /'/, end: /'/, contains: [hljs.BACKSLASH_ESCAPE] }, { begin: /"""/, end: /"""/, contains: [hljs.BACKSLASH_ESCAPE, SUBST, SUBST_SIMPLE] }, { begin: /"/, end: /"/, contains: [hljs.BACKSLASH_ESCAPE, SUBST, SUBST_SIMPLE] }, { begin: /\\/, end: /(\s|$)/, excludeEnd: true } ] }, { className: 'regexp', variants: [ { begin: '//', end: '//[gim]*', contains: [SUBST, hljs.HASH_COMMENT_MODE] }, { // regex can't start with space to parse x / 2 / 3 as two divisions // regex can't start with *, and it supports an "illegal" in the main mode begin: /\/(?![ *])(\\\/|.)*?\/[gim]*(?=\W|$)/ } ] }, { begin: '@' + JS_IDENT_RE }, { begin: '``', end: '``', excludeBegin: true, excludeEnd: true, subLanguage: 'javascript' } ]; SUBST.contains = EXPRESSIONS; var PARAMS = { className: 'params', begin: '\\(', returnBegin: true, /* We need another contained nameless mode to not have every nested pair of parens to be called "params" */ contains: [ { begin: /\(/, end: /\)/, keywords: KEYWORDS, contains: ['self'].concat(EXPRESSIONS) } ] }; return { aliases: ['ls'], keywords: KEYWORDS, illegal: /\/\*/, contains: EXPRESSIONS.concat([ hljs.COMMENT('\\/\\*', '\\*\\/'), hljs.HASH_COMMENT_MODE, { className: 'function', contains: [TITLE, PARAMS], returnBegin: true, variants: [ { begin: '(' + JS_IDENT_RE + '\\s*(?:=|:=)\\s*)?(\\(.*\\))?\\s*\\B\\->\\*?', end: '\\->\\*?' }, { begin: '(' + JS_IDENT_RE + '\\s*(?:=|:=)\\s*)?!?(\\(.*\\))?\\s*\\B[-~]{1,2}>\\*?', end: '[-~]{1,2}>\\*?' }, { begin: '(' + JS_IDENT_RE + '\\s*(?:=|:=)\\s*)?(\\(.*\\))?\\s*\\B!?[-~]{1,2}>\\*?', end: '!?[-~]{1,2}>\\*?' } ] }, { className: 'class', beginKeywords: 'class', end: '$', illegal: /[:="\[\]]/, contains: [ { beginKeywords: 'extends', endsWithParent: true, illegal: /[:="\[\]]/, contains: [TITLE] }, TITLE ] }, { begin: JS_IDENT_RE + ':', end: ':', returnBegin: true, returnEnd: true, relevance: 0 } ]) }; } },{name:"llvm",create:/* Language: LLVM IR Author: Michael Rodler Description: language used as intermediate representation in the LLVM compiler framework Category: assembler */ function(hljs) { var identifier = '([-a-zA-Z$._][\\w\\-$.]*)'; return { //lexemes: '[.%]?' + hljs.IDENT_RE, keywords: 'begin end true false declare define global ' + 'constant private linker_private internal ' + 'available_externally linkonce linkonce_odr weak ' + 'weak_odr appending dllimport dllexport common ' + 'default hidden protected extern_weak external ' + 'thread_local zeroinitializer undef null to tail ' + 'target triple datalayout volatile nuw nsw nnan ' + 'ninf nsz arcp fast exact inbounds align ' + 'addrspace section alias module asm sideeffect ' + 'gc dbg linker_private_weak attributes blockaddress ' + 'initialexec localdynamic localexec prefix unnamed_addr ' + 'ccc fastcc coldcc x86_stdcallcc x86_fastcallcc ' + 'arm_apcscc arm_aapcscc arm_aapcs_vfpcc ptx_device ' + 'ptx_kernel intel_ocl_bicc msp430_intrcc spir_func ' + 'spir_kernel x86_64_sysvcc x86_64_win64cc x86_thiscallcc ' + 'cc c signext zeroext inreg sret nounwind ' + 'noreturn noalias nocapture byval nest readnone ' + 'readonly inlinehint noinline alwaysinline optsize ssp ' + 'sspreq noredzone noimplicitfloat naked builtin cold ' + 'nobuiltin noduplicate nonlazybind optnone returns_twice ' + 'sanitize_address sanitize_memory sanitize_thread sspstrong ' + 'uwtable returned type opaque eq ne slt sgt ' + 'sle sge ult ugt ule uge oeq one olt ogt ' + 'ole oge ord uno ueq une x acq_rel acquire ' + 'alignstack atomic catch cleanup filter inteldialect ' + 'max min monotonic nand personality release seq_cst ' + 'singlethread umax umin unordered xchg add fadd ' + 'sub fsub mul fmul udiv sdiv fdiv urem srem ' + 'frem shl lshr ashr and or xor icmp fcmp ' + 'phi call trunc zext sext fptrunc fpext uitofp ' + 'sitofp fptoui fptosi inttoptr ptrtoint bitcast ' + 'addrspacecast select va_arg ret br switch invoke ' + 'unwind unreachable indirectbr landingpad resume ' + 'malloc alloca free load store getelementptr ' + 'extractelement insertelement shufflevector getresult ' + 'extractvalue insertvalue atomicrmw cmpxchg fence ' + 'argmemonly double', contains: [ { className: 'keyword', begin: 'i\\d+' }, hljs.COMMENT( ';', '\\n', {relevance: 0} ), // Double quote string hljs.QUOTE_STRING_MODE, { className: 'string', variants: [ // Double-quoted string { begin: '"', end: '[^\\\\]"' }, ], relevance: 0 }, { className: 'title', variants: [ { begin: '@' + identifier }, { begin: '@\\d+' }, { begin: '!' + identifier }, { begin: '!\\d+' + identifier } ] }, { className: 'symbol', variants: [ { begin: '%' + identifier }, { begin: '%\\d+' }, { begin: '#\\d+' }, ] }, { className: 'number', variants: [ { begin: '0[xX][a-fA-F0-9]+' }, { begin: '-?\\d+(?:[.]\\d+)?(?:[eE][-+]?\\d+(?:[.]\\d+)?)?' } ], relevance: 0 }, ] }; } },{name:"lsl",create:/* Language: Linden Scripting Language Description: The Linden Scripting Language is used in Second Life by Linden Labs. Author: Builder's Brewery Category: scripting */ function(hljs) { var LSL_STRING_ESCAPE_CHARS = { className: 'subst', begin: /\\[tn"\\]/ }; var LSL_STRINGS = { className: 'string', begin: '"', end: '"', contains: [ LSL_STRING_ESCAPE_CHARS ] }; var LSL_NUMBERS = { className: 'number', begin: hljs.C_NUMBER_RE }; var LSL_CONSTANTS = { className: 'literal', variants: [ { begin: '\\b(?:PI|TWO_PI|PI_BY_TWO|DEG_TO_RAD|RAD_TO_DEG|SQRT2)\\b' }, { begin: '\\b(?:XP_ERROR_(?:EXPERIENCES_DISABLED|EXPERIENCE_(?:DISABLED|SUSPENDED)|INVALID_(?:EXPERIENCE|PARAMETERS)|KEY_NOT_FOUND|MATURITY_EXCEEDED|NONE|NOT_(?:FOUND|PERMITTED(?:_LAND)?)|NO_EXPERIENCE|QUOTA_EXCEEDED|RETRY_UPDATE|STORAGE_EXCEPTION|STORE_DISABLED|THROTTLED|UNKNOWN_ERROR)|JSON_APPEND|STATUS_(?:PHYSICS|ROTATE_[XYZ]|PHANTOM|SANDBOX|BLOCK_GRAB(?:_OBJECT)?|(?:DIE|RETURN)_AT_EDGE|CAST_SHADOWS|OK|MALFORMED_PARAMS|TYPE_MISMATCH|BOUNDS_ERROR|NOT_(?:FOUND|SUPPORTED)|INTERNAL_ERROR|WHITELIST_FAILED)|AGENT(?:_(?:BY_(?:LEGACY_|USER)NAME|FLYING|ATTACHMENTS|SCRIPTED|MOUSELOOK|SITTING|ON_OBJECT|AWAY|WALKING|IN_AIR|TYPING|CROUCHING|BUSY|ALWAYS_RUN|AUTOPILOT|LIST_(?:PARCEL(?:_OWNER)?|REGION)))?|CAMERA_(?:PITCH|DISTANCE|BEHINDNESS_(?:ANGLE|LAG)|(?:FOCUS|POSITION)(?:_(?:THRESHOLD|LOCKED|LAG))?|FOCUS_OFFSET|ACTIVE)|ANIM_ON|LOOP|REVERSE|PING_PONG|SMOOTH|ROTATE|SCALE|ALL_SIDES|LINK_(?:ROOT|SET|ALL_(?:OTHERS|CHILDREN)|THIS)|ACTIVE|PASS(?:IVE|_(?:ALWAYS|IF_NOT_HANDLED|NEVER))|SCRIPTED|CONTROL_(?:FWD|BACK|(?:ROT_)?(?:LEFT|RIGHT)|UP|DOWN|(?:ML_)?LBUTTON)|PERMISSION_(?:RETURN_OBJECTS|DEBIT|OVERRIDE_ANIMATIONS|SILENT_ESTATE_MANAGEMENT|TAKE_CONTROLS|TRIGGER_ANIMATION|ATTACH|CHANGE_LINKS|(?:CONTROL|TRACK)_CAMERA|TELEPORT)|INVENTORY_(?:TEXTURE|SOUND|OBJECT|SCRIPT|LANDMARK|CLOTHING|NOTECARD|BODYPART|ANIMATION|GESTURE|ALL|NONE)|CHANGED_(?:INVENTORY|COLOR|SHAPE|SCALE|TEXTURE|LINK|ALLOWED_DROP|OWNER|REGION(?:_START)?|TELEPORT|MEDIA)|OBJECT_(?:CLICK_ACTION|HOVER_HEIGHT|LAST_OWNER_ID|(?:PHYSICS|SERVER|STREAMING)_COST|UNKNOWN_DETAIL|CHARACTER_TIME|PHANTOM|PHYSICS|TEMP_ON_REZ|NAME|DESC|POS|PRIM_(?:COUNT|EQUIVALENCE)|RETURN_(?:PARCEL(?:_OWNER)?|REGION)|REZZER_KEY|ROO?T|VELOCITY|OMEGA|OWNER|GROUP|CREATOR|ATTACHED_POINT|RENDER_WEIGHT|(?:BODY_SHAPE|PATHFINDING)_TYPE|(?:RUNNING|TOTAL)_SCRIPT_COUNT|TOTAL_INVENTORY_COUNT|SCRIPT_(?:MEMORY|TIME))|TYPE_(?:INTEGER|FLOAT|STRING|KEY|VECTOR|ROTATION|INVALID)|(?:DEBUG|PUBLIC)_CHANNEL|ATTACH_(?:AVATAR_CENTER|CHEST|HEAD|BACK|PELVIS|MOUTH|CHIN|NECK|NOSE|BELLY|[LR](?:SHOULDER|HAND|FOOT|EAR|EYE|[UL](?:ARM|LEG)|HIP)|(?:LEFT|RIGHT)_PEC|HUD_(?:CENTER_[12]|TOP_(?:RIGHT|CENTER|LEFT)|BOTTOM(?:_(?:RIGHT|LEFT))?)|[LR]HAND_RING1|TAIL_(?:BASE|TIP)|[LR]WING|FACE_(?:JAW|[LR]EAR|[LR]EYE|TOUNGE)|GROIN|HIND_[LR]FOOT)|LAND_(?:LEVEL|RAISE|LOWER|SMOOTH|NOISE|REVERT)|DATA_(?:ONLINE|NAME|BORN|SIM_(?:POS|STATUS|RATING)|PAYINFO)|PAYMENT_INFO_(?:ON_FILE|USED)|REMOTE_DATA_(?:CHANNEL|REQUEST|REPLY)|PSYS_(?:PART_(?:BF_(?:ZERO|ONE(?:_MINUS_(?:DEST_COLOR|SOURCE_(ALPHA|COLOR)))?|DEST_COLOR|SOURCE_(ALPHA|COLOR))|BLEND_FUNC_(DEST|SOURCE)|FLAGS|(?:START|END)_(?:COLOR|ALPHA|SCALE|GLOW)|MAX_AGE|(?:RIBBON|WIND|INTERP_(?:COLOR|SCALE)|BOUNCE|FOLLOW_(?:SRC|VELOCITY)|TARGET_(?:POS|LINEAR)|EMISSIVE)_MASK)|SRC_(?:MAX_AGE|PATTERN|ANGLE_(?:BEGIN|END)|BURST_(?:RATE|PART_COUNT|RADIUS|SPEED_(?:MIN|MAX))|ACCEL|TEXTURE|TARGET_KEY|OMEGA|PATTERN_(?:DROP|EXPLODE|ANGLE(?:_CONE(?:_EMPTY)?)?)))|VEHICLE_(?:REFERENCE_FRAME|TYPE_(?:NONE|SLED|CAR|BOAT|AIRPLANE|BALLOON)|(?:LINEAR|ANGULAR)_(?:FRICTION_TIMESCALE|MOTOR_DIRECTION)|LINEAR_MOTOR_OFFSET|HOVER_(?:HEIGHT|EFFICIENCY|TIMESCALE)|BUOYANCY|(?:LINEAR|ANGULAR)_(?:DEFLECTION_(?:EFFICIENCY|TIMESCALE)|MOTOR_(?:DECAY_)?TIMESCALE)|VERTICAL_ATTRACTION_(?:EFFICIENCY|TIMESCALE)|BANKING_(?:EFFICIENCY|MIX|TIMESCALE)|FLAG_(?:NO_DEFLECTION_UP|LIMIT_(?:ROLL_ONLY|MOTOR_UP)|HOVER_(?:(?:WATER|TERRAIN|UP)_ONLY|GLOBAL_HEIGHT)|MOUSELOOK_(?:STEER|BANK)|CAMERA_DECOUPLED))|PRIM_(?:ALPHA_MODE(?:_(?:BLEND|EMISSIVE|MASK|NONE))?|NORMAL|SPECULAR|TYPE(?:_(?:BOX|CYLINDER|PRISM|SPHERE|TORUS|TUBE|RING|SCULPT))?|HOLE_(?:DEFAULT|CIRCLE|SQUARE|TRIANGLE)|MATERIAL(?:_(?:STONE|METAL|GLASS|WOOD|FLESH|PLASTIC|RUBBER))?|SHINY_(?:NONE|LOW|MEDIUM|HIGH)|BUMP_(?:NONE|BRIGHT|DARK|WOOD|BARK|BRICKS|CHECKER|CONCRETE|TILE|STONE|DISKS|GRAVEL|BLOBS|SIDING|LARGETILE|STUCCO|SUCTION|WEAVE)|TEXGEN_(?:DEFAULT|PLANAR)|SCULPT_(?:TYPE_(?:SPHERE|TORUS|PLANE|CYLINDER|MASK)|FLAG_(?:MIRROR|INVERT))|PHYSICS(?:_(?:SHAPE_(?:CONVEX|NONE|PRIM|TYPE)))?|(?:POS|ROT)_LOCAL|SLICE|TEXT|FLEXIBLE|POINT_LIGHT|TEMP_ON_REZ|PHANTOM|POSITION|SIZE|ROTATION|TEXTURE|NAME|OMEGA|DESC|LINK_TARGET|COLOR|BUMP_SHINY|FULLBRIGHT|TEXGEN|GLOW|MEDIA_(?:ALT_IMAGE_ENABLE|CONTROLS|(?:CURRENT|HOME)_URL|AUTO_(?:LOOP|PLAY|SCALE|ZOOM)|FIRST_CLICK_INTERACT|(?:WIDTH|HEIGHT)_PIXELS|WHITELIST(?:_ENABLE)?|PERMS_(?:INTERACT|CONTROL)|PARAM_MAX|CONTROLS_(?:STANDARD|MINI)|PERM_(?:NONE|OWNER|GROUP|ANYONE)|MAX_(?:URL_LENGTH|WHITELIST_(?:SIZE|COUNT)|(?:WIDTH|HEIGHT)_PIXELS)))|MASK_(?:BASE|OWNER|GROUP|EVERYONE|NEXT)|PERM_(?:TRANSFER|MODIFY|COPY|MOVE|ALL)|PARCEL_(?:MEDIA_COMMAND_(?:STOP|PAUSE|PLAY|LOOP|TEXTURE|URL|TIME|AGENT|UNLOAD|AUTO_ALIGN|TYPE|SIZE|DESC|LOOP_SET)|FLAG_(?:ALLOW_(?:FLY|(?:GROUP_)?SCRIPTS|LANDMARK|TERRAFORM|DAMAGE|CREATE_(?:GROUP_)?OBJECTS)|USE_(?:ACCESS_(?:GROUP|LIST)|BAN_LIST|LAND_PASS_LIST)|LOCAL_SOUND_ONLY|RESTRICT_PUSHOBJECT|ALLOW_(?:GROUP|ALL)_OBJECT_ENTRY)|COUNT_(?:TOTAL|OWNER|GROUP|OTHER|SELECTED|TEMP)|DETAILS_(?:NAME|DESC|OWNER|GROUP|AREA|ID|SEE_AVATARS))|LIST_STAT_(?:MAX|MIN|MEAN|MEDIAN|STD_DEV|SUM(?:_SQUARES)?|NUM_COUNT|GEOMETRIC_MEAN|RANGE)|PAY_(?:HIDE|DEFAULT)|REGION_FLAG_(?:ALLOW_DAMAGE|FIXED_SUN|BLOCK_TERRAFORM|SANDBOX|DISABLE_(?:COLLISIONS|PHYSICS)|BLOCK_FLY|ALLOW_DIRECT_TELEPORT|RESTRICT_PUSHOBJECT)|HTTP_(?:METHOD|MIMETYPE|BODY_(?:MAXLENGTH|TRUNCATED)|CUSTOM_HEADER|PRAGMA_NO_CACHE|VERBOSE_THROTTLE|VERIFY_CERT)|STRING_(?:TRIM(?:_(?:HEAD|TAIL))?)|CLICK_ACTION_(?:NONE|TOUCH|SIT|BUY|PAY|OPEN(?:_MEDIA)?|PLAY|ZOOM)|TOUCH_INVALID_FACE|PROFILE_(?:NONE|SCRIPT_MEMORY)|RC_(?:DATA_FLAGS|DETECT_PHANTOM|GET_(?:LINK_NUM|NORMAL|ROOT_KEY)|MAX_HITS|REJECT_(?:TYPES|AGENTS|(?:NON)?PHYSICAL|LAND))|RCERR_(?:CAST_TIME_EXCEEDED|SIM_PERF_LOW|UNKNOWN)|ESTATE_ACCESS_(?:ALLOWED_(?:AGENT|GROUP)_(?:ADD|REMOVE)|BANNED_AGENT_(?:ADD|REMOVE))|DENSITY|FRICTION|RESTITUTION|GRAVITY_MULTIPLIER|KFM_(?:COMMAND|CMD_(?:PLAY|STOP|PAUSE)|MODE|FORWARD|LOOP|PING_PONG|REVERSE|DATA|ROTATION|TRANSLATION)|ERR_(?:GENERIC|PARCEL_PERMISSIONS|MALFORMED_PARAMS|RUNTIME_PERMISSIONS|THROTTLED)|CHARACTER_(?:CMD_(?:(?:SMOOTH_)?STOP|JUMP)|DESIRED_(?:TURN_)?SPEED|RADIUS|STAY_WITHIN_PARCEL|LENGTH|ORIENTATION|ACCOUNT_FOR_SKIPPED_FRAMES|AVOIDANCE_MODE|TYPE(?:_(?:[ABCD]|NONE))?|MAX_(?:DECEL|TURN_RADIUS|(?:ACCEL|SPEED)))|PURSUIT_(?:OFFSET|FUZZ_FACTOR|GOAL_TOLERANCE|INTERCEPT)|REQUIRE_LINE_OF_SIGHT|FORCE_DIRECT_PATH|VERTICAL|HORIZONTAL|AVOID_(?:CHARACTERS|DYNAMIC_OBSTACLES|NONE)|PU_(?:EVADE_(?:HIDDEN|SPOTTED)|FAILURE_(?:DYNAMIC_PATHFINDING_DISABLED|INVALID_(?:GOAL|START)|NO_(?:NAVMESH|VALID_DESTINATION)|OTHER|TARGET_GONE|(?:PARCEL_)?UNREACHABLE)|(?:GOAL|SLOWDOWN_DISTANCE)_REACHED)|TRAVERSAL_TYPE(?:_(?:FAST|NONE|SLOW))?|CONTENT_TYPE_(?:ATOM|FORM|HTML|JSON|LLSD|RSS|TEXT|XHTML|XML)|GCNP_(?:RADIUS|STATIC)|(?:PATROL|WANDER)_PAUSE_AT_WAYPOINTS|OPT_(?:AVATAR|CHARACTER|EXCLUSION_VOLUME|LEGACY_LINKSET|MATERIAL_VOLUME|OTHER|STATIC_OBSTACLE|WALKABLE)|SIM_STAT_PCT_CHARS_STEPPED)\\b' }, { begin: '\\b(?:FALSE|TRUE)\\b' }, { begin: '\\b(?:ZERO_ROTATION)\\b' }, { begin: '\\b(?:EOF|JSON_(?:ARRAY|DELETE|FALSE|INVALID|NULL|NUMBER|OBJECT|STRING|TRUE)|NULL_KEY|TEXTURE_(?:BLANK|DEFAULT|MEDIA|PLYWOOD|TRANSPARENT)|URL_REQUEST_(?:GRANTED|DENIED))\\b' }, { begin: '\\b(?:ZERO_VECTOR|TOUCH_INVALID_(?:TEXCOORD|VECTOR))\\b' } ] }; var LSL_FUNCTIONS = { className: 'built_in', begin: '\\b(?:ll(?:AgentInExperience|(?:Create|DataSize|Delete|KeyCount|Keys|Read|Update)KeyValue|GetExperience(?:Details|ErrorMessage)|ReturnObjectsBy(?:ID|Owner)|Json(?:2List|[GS]etValue|ValueType)|Sin|Cos|Tan|Atan2|Sqrt|Pow|Abs|Fabs|Frand|Floor|Ceil|Round|Vec(?:Mag|Norm|Dist)|Rot(?:Between|2(?:Euler|Fwd|Left|Up))|(?:Euler|Axes)2Rot|Whisper|(?:Region|Owner)?Say|Shout|Listen(?:Control|Remove)?|Sensor(?:Repeat|Remove)?|Detected(?:Name|Key|Owner|Type|Pos|Vel|Grab|Rot|Group|LinkNumber)|Die|Ground|Wind|(?:[GS]et)(?:AnimationOverride|MemoryLimit|PrimMediaParams|ParcelMusicURL|Object(?:Desc|Name)|PhysicsMaterial|Status|Scale|Color|Alpha|Texture|Pos|Rot|Force|Torque)|ResetAnimationOverride|(?:Scale|Offset|Rotate)Texture|(?:Rot)?Target(?:Remove)?|(?:Stop)?MoveToTarget|Apply(?:Rotational)?Impulse|Set(?:KeyframedMotion|ContentType|RegionPos|(?:Angular)?Velocity|Buoyancy|HoverHeight|ForceAndTorque|TimerEvent|ScriptState|Damage|TextureAnim|Sound(?:Queueing|Radius)|Vehicle(?:Type|(?:Float|Vector|Rotation)Param)|(?:Touch|Sit)?Text|Camera(?:Eye|At)Offset|PrimitiveParams|ClickAction|Link(?:Alpha|Color|PrimitiveParams(?:Fast)?|Texture(?:Anim)?|Camera|Media)|RemoteScriptAccessPin|PayPrice|LocalRot)|ScaleByFactor|Get(?:(?:Max|Min)ScaleFactor|ClosestNavPoint|StaticPath|SimStats|Env|PrimitiveParams|Link(?:PrimitiveParams|Number(?:OfSides)?|Key|Name|Media)|HTTPHeader|FreeURLs|Object(?:Details|PermMask|PrimCount)|Parcel(?:MaxPrims|Details|Prim(?:Count|Owners))|Attached(?:List)?|(?:SPMax|Free|Used)Memory|Region(?:Name|TimeDilation|FPS|Corner|AgentCount)|Root(?:Position|Rotation)|UnixTime|(?:Parcel|Region)Flags|(?:Wall|GMT)clock|SimulatorHostname|BoundingBox|GeometricCenter|Creator|NumberOf(?:Prims|NotecardLines|Sides)|Animation(?:List)?|(?:Camera|Local)(?:Pos|Rot)|Vel|Accel|Omega|Time(?:stamp|OfDay)|(?:Object|CenterOf)?Mass|MassMKS|Energy|Owner|(?:Owner)?Key|SunDirection|Texture(?:Offset|Scale|Rot)|Inventory(?:Number|Name|Key|Type|Creator|PermMask)|Permissions(?:Key)?|StartParameter|List(?:Length|EntryType)|Date|Agent(?:Size|Info|Language|List)|LandOwnerAt|NotecardLine|Script(?:Name|State))|(?:Get|Reset|GetAndReset)Time|PlaySound(?:Slave)?|LoopSound(?:Master|Slave)?|(?:Trigger|Stop|Preload)Sound|(?:(?:Get|Delete)Sub|Insert)String|To(?:Upper|Lower)|Give(?:InventoryList|Money)|RezObject|(?:Stop)?LookAt|Sleep|CollisionFilter|(?:Take|Release)Controls|DetachFromAvatar|AttachToAvatar(?:Temp)?|InstantMessage|(?:GetNext)?Email|StopHover|MinEventDelay|RotLookAt|String(?:Length|Trim)|(?:Start|Stop)Animation|TargetOmega|Request(?:Experience)?Permissions|(?:Create|Break)Link|BreakAllLinks|(?:Give|Remove)Inventory|Water|PassTouches|Request(?:Agent|Inventory)Data|TeleportAgent(?:Home|GlobalCoords)?|ModifyLand|CollisionSound|ResetScript|MessageLinked|PushObject|PassCollisions|AxisAngle2Rot|Rot2(?:Axis|Angle)|A(?:cos|sin)|AngleBetween|AllowInventoryDrop|SubStringIndex|List2(?:CSV|Integer|Json|Float|String|Key|Vector|Rot|List(?:Strided)?)|DeleteSubList|List(?:Statistics|Sort|Randomize|(?:Insert|Find|Replace)List)|EdgeOfWorld|AdjustSoundVolume|Key2Name|TriggerSoundLimited|EjectFromLand|(?:CSV|ParseString)2List|OverMyLand|SameGroup|UnSit|Ground(?:Slope|Normal|Contour)|GroundRepel|(?:Set|Remove)VehicleFlags|(?:AvatarOn)?(?:Link)?SitTarget|Script(?:Danger|Profiler)|Dialog|VolumeDetect|ResetOtherScript|RemoteLoadScriptPin|(?:Open|Close)RemoteDataChannel|SendRemoteData|RemoteDataReply|(?:Integer|String)ToBase64|XorBase64|Log(?:10)?|Base64To(?:String|Integer)|ParseStringKeepNulls|RezAtRoot|RequestSimulatorData|ForceMouselook|(?:Load|Release|(?:E|Une)scape)URL|ParcelMedia(?:CommandList|Query)|ModPow|MapDestination|(?:RemoveFrom|AddTo|Reset)Land(?:Pass|Ban)List|(?:Set|Clear)CameraParams|HTTP(?:Request|Response)|TextBox|DetectedTouch(?:UV|Face|Pos|(?:N|Bin)ormal|ST)|(?:MD5|SHA1|DumpList2)String|Request(?:Secure)?URL|Clear(?:Prim|Link)Media|(?:Link)?ParticleSystem|(?:Get|Request)(?:Username|DisplayName)|RegionSayTo|CastRay|GenerateKey|TransferLindenDollars|ManageEstateAccess|(?:Create|Delete)Character|ExecCharacterCmd|Evade|FleeFrom|NavigateTo|PatrolPoints|Pursue|UpdateCharacter|WanderWithin))\\b' }; return { illegal: ':', contains: [ LSL_STRINGS, { className: 'comment', variants: [ hljs.COMMENT('//', '$'), hljs.COMMENT('/\\*', '\\*/') ] }, LSL_NUMBERS, { className: 'section', variants: [ { begin: '\\b(?:state|default)\\b' }, { begin: '\\b(?:state_(?:entry|exit)|touch(?:_(?:start|end))?|(?:land_)?collision(?:_(?:start|end))?|timer|listen|(?:no_)?sensor|control|(?:not_)?at_(?:rot_)?target|money|email|experience_permissions(?:_denied)?|run_time_permissions|changed|attach|dataserver|moving_(?:start|end)|link_message|(?:on|object)_rez|remote_data|http_re(?:sponse|quest)|path_update|transaction_result)\\b' } ] }, LSL_FUNCTIONS, LSL_CONSTANTS, { className: 'type', begin: '\\b(?:integer|float|string|key|vector|quaternion|rotation|list)\\b' } ] }; } },{name:"lua",create:/* Language: Lua Author: Andrew Fedorov Category: scripting */ function(hljs) { var OPENING_LONG_BRACKET = '\\[=*\\['; var CLOSING_LONG_BRACKET = '\\]=*\\]'; var LONG_BRACKETS = { begin: OPENING_LONG_BRACKET, end: CLOSING_LONG_BRACKET, contains: ['self'] }; var COMMENTS = [ hljs.COMMENT('--(?!' + OPENING_LONG_BRACKET + ')', '$'), hljs.COMMENT( '--' + OPENING_LONG_BRACKET, CLOSING_LONG_BRACKET, { contains: [LONG_BRACKETS], relevance: 10 } ) ]; return { lexemes: hljs.UNDERSCORE_IDENT_RE, keywords: { literal: "true false nil", keyword: "and break do else elseif end for goto if in local not or repeat return then until while", built_in: //Metatags and globals: '_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len ' + '__gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert ' + //Standard methods and properties: 'collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring' + 'module next pairs pcall print rawequal rawget rawset require select setfenv' + 'setmetatable tonumber tostring type unpack xpcall arg self' + //Library methods and properties (one line per library): 'coroutine resume yield status wrap create running debug getupvalue ' + 'debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv ' + 'io lines write close flush open output type read stderr stdin input stdout popen tmpfile ' + 'math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan ' + 'os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall ' + 'string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower ' + 'table setn insert getn foreachi maxn foreach concat sort remove' }, contains: COMMENTS.concat([ { className: 'function', beginKeywords: 'function', end: '\\)', contains: [ hljs.inherit(hljs.TITLE_MODE, {begin: '([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*'}), { className: 'params', begin: '\\(', endsWithParent: true, contains: COMMENTS } ].concat(COMMENTS) }, hljs.C_NUMBER_MODE, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, { className: 'string', begin: OPENING_LONG_BRACKET, end: CLOSING_LONG_BRACKET, contains: [LONG_BRACKETS], relevance: 5 } ]) }; } },{name:"makefile",create:/* Language: Makefile Author: Ivan Sagalaev Contributors: Joël Porquet Category: common */ function(hljs) { /* Variables: simple (eg $(var)) and special (eg $@) */ var VARIABLE = { className: 'variable', variants: [ { begin: '\\$\\(' + hljs.UNDERSCORE_IDENT_RE + '\\)', contains: [hljs.BACKSLASH_ESCAPE], }, { begin: /\$[@% Website: http://seejohncode.com/ Category: common, markup */ function(hljs) { return { aliases: ['md', 'mkdown', 'mkd'], contains: [ // highlight headers { className: 'section', variants: [ { begin: '^#{1,6}', end: '$' }, { begin: '^.+?\\n[=-]{2,}$' } ] }, // inline html { begin: '<', end: '>', subLanguage: 'xml', relevance: 0 }, // lists (indicators only) { className: 'bullet', begin: '^\\s*([*+-]|(\\d+\\.))\\s+' }, // strong segments { className: 'strong', begin: '[*_]{2}.+?[*_]{2}' }, // emphasis segments { className: 'emphasis', variants: [ { begin: '\\*.+?\\*' }, { begin: '_.+?_' , relevance: 0 } ] }, // blockquotes { className: 'quote', begin: '^>\\s+', end: '$' }, // code snippets { className: 'code', variants: [ { begin: '^```\w*\s*$', end: '^```\s*$' }, { begin: '`.+?`' }, { begin: '^( {4}|\t)', end: '$', relevance: 0 } ] }, // horizontal rules { begin: '^[-\\*]{3,}', end: '$' }, // using links - title and link { begin: '\\[.+?\\][\\(\\[].*?[\\)\\]]', returnBegin: true, contains: [ { className: 'string', begin: '\\[', end: '\\]', excludeBegin: true, returnEnd: true, relevance: 0 }, { className: 'link', begin: '\\]\\(', end: '\\)', excludeBegin: true, excludeEnd: true }, { className: 'symbol', begin: '\\]\\[', end: '\\]', excludeBegin: true, excludeEnd: true } ], relevance: 10 }, { begin: /^\[[^\n]+\]:/, returnBegin: true, contains: [ { className: 'symbol', begin: /\[/, end: /\]/, excludeBegin: true, excludeEnd: true }, { className: 'link', begin: /:\s*/, end: /$/, excludeBegin: true } ] } ] }; } },{name:"mathematica",create:/* Language: Mathematica Authors: Daniel Kvasnicka , Jan Poeschko Category: scientific */ function(hljs) { return { aliases: ['mma', 'wl'], lexemes: '(\\$|\\b)' + hljs.IDENT_RE + '\\b', // // The list of "keywords" (System` symbols) was determined by evaluating the following Wolfram Language code in Mathematica 12.0: // // StringRiffle[ // "'" <> StringRiffle[#, " "] <> "'" & /@ // Values[GroupBy[ // Select[Names["System`*"], // StringStartsQ[#, CharacterRange["A", "Z"] | "$"] &], // First[Characters[#]] &]], " +\n"] // keywords: 'AASTriangle AbelianGroup Abort AbortKernels AbortProtect AbortScheduledTask Above Abs AbsArg AbsArgPlot Absolute AbsoluteCorrelation AbsoluteCorrelationFunction AbsoluteCurrentValue AbsoluteDashing AbsoluteFileName AbsoluteOptions AbsolutePointSize AbsoluteThickness AbsoluteTime AbsoluteTiming AcceptanceThreshold AccountingForm Accumulate Accuracy AccuracyGoal ActionDelay ActionMenu ActionMenuBox ActionMenuBoxOptions Activate Active ActiveClassification ActiveClassificationObject ActiveItem ActivePrediction ActivePredictionObject ActiveStyle AcyclicGraphQ AddOnHelpPath AddSides AddTo AddToSearchIndex AddUsers AdjacencyGraph AdjacencyList AdjacencyMatrix AdjustmentBox AdjustmentBoxOptions AdjustTimeSeriesForecast AdministrativeDivisionData AffineHalfSpace AffineSpace AffineStateSpaceModel AffineTransform After AggregatedEntityClass AggregationLayer AircraftData AirportData AirPressureData AirTemperatureData AiryAi AiryAiPrime AiryAiZero AiryBi AiryBiPrime AiryBiZero AlgebraicIntegerQ AlgebraicNumber AlgebraicNumberDenominator AlgebraicNumberNorm AlgebraicNumberPolynomial AlgebraicNumberTrace AlgebraicRules AlgebraicRulesData Algebraics AlgebraicUnitQ Alignment AlignmentMarker AlignmentPoint All AllowAdultContent AllowedCloudExtraParameters AllowedCloudParameterExtensions AllowedDimensions AllowedFrequencyRange AllowedHeads AllowGroupClose AllowIncomplete AllowInlineCells AllowKernelInitialization AllowLooseGrammar AllowReverseGroupClose AllowScriptLevelChange AllTrue Alphabet AlphabeticOrder AlphabeticSort AlphaChannel AlternateImage AlternatingFactorial AlternatingGroup AlternativeHypothesis Alternatives AltitudeMethod AmbientLight AmbiguityFunction AmbiguityList Analytic AnatomyData AnatomyForm AnatomyPlot3D AnatomySkinStyle AnatomyStyling AnchoredSearch And AndersonDarlingTest AngerJ AngleBisector AngleBracket AnglePath AnglePath3D AngleVector AngularGauge Animate AnimationCycleOffset AnimationCycleRepetitions AnimationDirection AnimationDisplayTime AnimationRate AnimationRepetitions AnimationRunning AnimationRunTime AnimationTimeIndex Animator AnimatorBox AnimatorBoxOptions AnimatorElements Annotate Annotation AnnotationDelete AnnotationNames AnnotationRules AnnotationValue Annuity AnnuityDue Annulus AnomalyDetection AnomalyDetectorFunction Anonymous Antialiasing AntihermitianMatrixQ Antisymmetric AntisymmetricMatrixQ Antonyms AnyOrder AnySubset AnyTrue Apart ApartSquareFree APIFunction Appearance AppearanceElements AppearanceRules AppellF1 Append AppendCheck AppendLayer AppendTo ApplicationIdentificationKey Apply ApplySides ArcCos ArcCosh ArcCot ArcCoth ArcCsc ArcCsch ArcCurvature ARCHProcess ArcLength ArcSec ArcSech ArcSin ArcSinDistribution ArcSinh ArcTan ArcTanh Area Arg ArgMax ArgMin ArgumentCountQ ARIMAProcess ArithmeticGeometricMean ARMAProcess Around AroundReplace ARProcess Array ArrayComponents ArrayDepth ArrayFilter ArrayFlatten ArrayMesh ArrayPad ArrayPlot ArrayQ ArrayResample ArrayReshape ArrayRules Arrays Arrow Arrow3DBox ArrowBox Arrowheads ASATriangle Ask AskAppend AskConfirm AskDisplay AskedQ AskedValue AskFunction AskState AskTemplateDisplay AspectRatio AspectRatioFixed Assert AssociateTo Association AssociationFormat AssociationMap AssociationQ AssociationThread AssumeDeterministic Assuming Assumptions AstronomicalData AsymptoticDSolveValue AsymptoticEqual AsymptoticEquivalent AsymptoticGreater AsymptoticGreaterEqual AsymptoticIntegrate AsymptoticLess AsymptoticLessEqual AsymptoticOutputTracker AsymptoticRSolveValue AsymptoticSolve AsymptoticSum Asynchronous AsynchronousTaskObject AsynchronousTasks Atom AtomCoordinates AtomCount AtomDiagramCoordinates AtomList AtomQ AttentionLayer Attributes Audio AudioAmplify AudioAnnotate AudioAnnotationLookup AudioBlockMap AudioCapture AudioChannelAssignment AudioChannelCombine AudioChannelMix AudioChannels AudioChannelSeparate AudioData AudioDelay AudioDelete AudioDevice AudioDistance AudioFade AudioFrequencyShift AudioGenerator AudioIdentify AudioInputDevice AudioInsert AudioIntervals AudioJoin AudioLabel AudioLength AudioLocalMeasurements AudioLooping AudioLoudness AudioMeasurements AudioNormalize AudioOutputDevice AudioOverlay AudioPad AudioPan AudioPartition AudioPause AudioPitchShift AudioPlay AudioPlot AudioQ AudioRecord AudioReplace AudioResample AudioReverb AudioSampleRate AudioSpectralMap AudioSpectralTransformation AudioSplit AudioStop AudioStream AudioStreams AudioTimeStretch AudioTrim AudioType AugmentedPolyhedron AugmentedSymmetricPolynomial Authenticate Authentication AuthenticationDialog AutoAction Autocomplete AutocompletionFunction AutoCopy AutocorrelationTest AutoDelete AutoEvaluateEvents AutoGeneratedPackage AutoIndent AutoIndentSpacings AutoItalicWords AutoloadPath AutoMatch Automatic AutomaticImageSize AutoMultiplicationSymbol AutoNumberFormatting AutoOpenNotebooks AutoOpenPalettes AutoQuoteCharacters AutoRefreshed AutoRemove AutorunSequencing AutoScaling AutoScroll AutoSpacing AutoStyleOptions AutoStyleWords AutoSubmitting Axes AxesEdge AxesLabel AxesOrigin AxesStyle AxiomaticTheory Axis' + 'BabyMonsterGroupB Back Background BackgroundAppearance BackgroundTasksSettings Backslash Backsubstitution Backward Ball Band BandpassFilter BandstopFilter BarabasiAlbertGraphDistribution BarChart BarChart3D BarcodeImage BarcodeRecognize BaringhausHenzeTest BarLegend BarlowProschanImportance BarnesG BarOrigin BarSpacing BartlettHannWindow BartlettWindow BaseDecode BaseEncode BaseForm Baseline BaselinePosition BaseStyle BasicRecurrentLayer BatchNormalizationLayer BatchSize BatesDistribution BattleLemarieWavelet BayesianMaximization BayesianMaximizationObject BayesianMinimization BayesianMinimizationObject Because BeckmannDistribution Beep Before Begin BeginDialogPacket BeginFrontEndInteractionPacket BeginPackage BellB BellY Below BenfordDistribution BeniniDistribution BenktanderGibratDistribution BenktanderWeibullDistribution BernoulliB BernoulliDistribution BernoulliGraphDistribution BernoulliProcess BernsteinBasis BesselFilterModel BesselI BesselJ BesselJZero BesselK BesselY BesselYZero Beta BetaBinomialDistribution BetaDistribution BetaNegativeBinomialDistribution BetaPrimeDistribution BetaRegularized Between BetweennessCentrality BeveledPolyhedron BezierCurve BezierCurve3DBox BezierCurve3DBoxOptions BezierCurveBox BezierCurveBoxOptions BezierFunction BilateralFilter Binarize BinaryDeserialize BinaryDistance BinaryFormat BinaryImageQ BinaryRead BinaryReadList BinarySerialize BinaryWrite BinCounts BinLists Binomial BinomialDistribution BinomialProcess BinormalDistribution BiorthogonalSplineWavelet BipartiteGraphQ BiquadraticFilterModel BirnbaumImportance BirnbaumSaundersDistribution BitAnd BitClear BitGet BitLength BitNot BitOr BitSet BitShiftLeft BitShiftRight BitXor BiweightLocation BiweightMidvariance Black BlackmanHarrisWindow BlackmanNuttallWindow BlackmanWindow Blank BlankForm BlankNullSequence BlankSequence Blend Block BlockchainAddressData BlockchainBase BlockchainBlockData BlockchainContractValue BlockchainData BlockchainGet BlockchainKeyEncode BlockchainPut BlockchainTokenData BlockchainTransaction BlockchainTransactionData BlockchainTransactionSign BlockchainTransactionSubmit BlockMap BlockRandom BlomqvistBeta BlomqvistBetaTest Blue Blur BodePlot BohmanWindow Bold Bond BondCount BondList BondQ Bookmarks Boole BooleanConsecutiveFunction BooleanConvert BooleanCountingFunction BooleanFunction BooleanGraph BooleanMaxterms BooleanMinimize BooleanMinterms BooleanQ BooleanRegion Booleans BooleanStrings BooleanTable BooleanVariables BorderDimensions BorelTannerDistribution Bottom BottomHatTransform BoundaryDiscretizeGraphics BoundaryDiscretizeRegion BoundaryMesh BoundaryMeshRegion BoundaryMeshRegionQ BoundaryStyle BoundedRegionQ BoundingRegion Bounds Box BoxBaselineShift BoxData BoxDimensions Boxed Boxes BoxForm BoxFormFormatTypes BoxFrame BoxID BoxMargins BoxMatrix BoxObject BoxRatios BoxRotation BoxRotationPoint BoxStyle BoxWhiskerChart Bra BracketingBar BraKet BrayCurtisDistance BreadthFirstScan Break BridgeData BrightnessEqualize BroadcastStationData Brown BrownForsytheTest BrownianBridgeProcess BrowserCategory BSplineBasis BSplineCurve BSplineCurve3DBox BSplineCurve3DBoxOptions BSplineCurveBox BSplineCurveBoxOptions BSplineFunction BSplineSurface BSplineSurface3DBox BSplineSurface3DBoxOptions BubbleChart BubbleChart3D BubbleScale BubbleSizes BuildingData BulletGauge BusinessDayQ ButterflyGraph ButterworthFilterModel Button ButtonBar ButtonBox ButtonBoxOptions ButtonCell ButtonContents ButtonData ButtonEvaluator ButtonExpandable ButtonFrame ButtonFunction ButtonMargins ButtonMinHeight ButtonNote ButtonNotebook ButtonSource ButtonStyle ButtonStyleMenuListing Byte ByteArray ByteArrayFormat ByteArrayQ ByteArrayToString ByteCount ByteOrdering' + 'C CachedValue CacheGraphics CachePersistence CalendarConvert CalendarData CalendarType Callout CalloutMarker CalloutStyle CallPacket CanberraDistance Cancel CancelButton CandlestickChart CanonicalGraph CanonicalizePolygon CanonicalizePolyhedron CanonicalName CanonicalWarpingCorrespondence CanonicalWarpingDistance CantorMesh CantorStaircase Cap CapForm CapitalDifferentialD Capitalize CapsuleShape CaptureRunning CardinalBSplineBasis CarlemanLinearize CarmichaelLambda CaseOrdering Cases CaseSensitive Cashflow Casoratian Catalan CatalanNumber Catch Catenate CatenateLayer CauchyDistribution CauchyWindow CayleyGraph CDF CDFDeploy CDFInformation CDFWavelet Ceiling CelestialSystem Cell CellAutoOverwrite CellBaseline CellBoundingBox CellBracketOptions CellChangeTimes CellContents CellContext CellDingbat CellDynamicExpression CellEditDuplicate CellElementsBoundingBox CellElementSpacings CellEpilog CellEvaluationDuplicate CellEvaluationFunction CellEvaluationLanguage CellEventActions CellFrame CellFrameColor CellFrameLabelMargins CellFrameLabels CellFrameMargins CellGroup CellGroupData CellGrouping CellGroupingRules CellHorizontalScrolling CellID CellLabel CellLabelAutoDelete CellLabelMargins CellLabelPositioning CellLabelStyle CellLabelTemplate CellMargins CellObject CellOpen CellPrint CellProlog Cells CellSize CellStyle CellTags CellularAutomaton CensoredDistribution Censoring Center CenterArray CenterDot CentralFeature CentralMoment CentralMomentGeneratingFunction Cepstrogram CepstrogramArray CepstrumArray CForm ChampernowneNumber ChangeOptions ChannelBase ChannelBrokerAction ChannelDatabin ChannelHistoryLength ChannelListen ChannelListener ChannelListeners ChannelListenerWait ChannelObject ChannelPreSendFunction ChannelReceiverFunction ChannelSend ChannelSubscribers ChanVeseBinarize Character CharacterCounts CharacterEncoding CharacterEncodingsPath CharacteristicFunction CharacteristicPolynomial CharacterName CharacterRange Characters ChartBaseStyle ChartElementData ChartElementDataFunction ChartElementFunction ChartElements ChartLabels ChartLayout ChartLegends ChartStyle Chebyshev1FilterModel Chebyshev2FilterModel ChebyshevDistance ChebyshevT ChebyshevU Check CheckAbort CheckAll Checkbox CheckboxBar CheckboxBox CheckboxBoxOptions ChemicalData ChessboardDistance ChiDistribution ChineseRemainder ChiSquareDistribution ChoiceButtons ChoiceDialog CholeskyDecomposition Chop ChromaticityPlot ChromaticityPlot3D ChromaticPolynomial Circle CircleBox CircleDot CircleMinus CirclePlus CirclePoints CircleThrough CircleTimes CirculantGraph CircularOrthogonalMatrixDistribution CircularQuaternionMatrixDistribution CircularRealMatrixDistribution CircularSymplecticMatrixDistribution CircularUnitaryMatrixDistribution Circumsphere CityData ClassifierFunction ClassifierInformation ClassifierMeasurements ClassifierMeasurementsObject Classify ClassPriors Clear ClearAll ClearAttributes ClearCookies ClearPermissions ClearSystemCache ClebschGordan ClickPane Clip ClipboardNotebook ClipFill ClippingStyle ClipPlanes ClipPlanesStyle ClipRange Clock ClockGauge ClockwiseContourIntegral Close Closed CloseKernels ClosenessCentrality Closing ClosingAutoSave ClosingEvent CloudAccountData CloudBase CloudConnect CloudDeploy CloudDirectory CloudDisconnect CloudEvaluate CloudExport CloudExpression CloudExpressions CloudFunction CloudGet CloudImport CloudLoggingData CloudObject CloudObjectInformation CloudObjectInformationData CloudObjectNameFormat CloudObjects CloudObjectURLType CloudPublish CloudPut CloudRenderingMethod CloudSave CloudShare CloudSubmit CloudSymbol CloudUnshare ClusterClassify ClusterDissimilarityFunction ClusteringComponents ClusteringTree CMYKColor Coarse CodeAssistOptions Coefficient CoefficientArrays CoefficientDomain CoefficientList CoefficientRules CoifletWavelet Collect Colon ColonForm ColorBalance ColorCombine ColorConvert ColorCoverage ColorData ColorDataFunction ColorDetect ColorDistance ColorFunction ColorFunctionScaling Colorize ColorNegate ColorOutput ColorProfileData ColorQ ColorQuantize ColorReplace ColorRules ColorSelectorSettings ColorSeparate ColorSetter ColorSetterBox ColorSetterBoxOptions ColorSlider ColorsNear ColorSpace ColorToneMapping Column ColumnAlignments ColumnBackgrounds ColumnForm ColumnLines ColumnsEqual ColumnSpacings ColumnWidths CombinedEntityClass CombinerFunction CometData CommonDefaultFormatTypes Commonest CommonestFilter CommonName CommonUnits CommunityBoundaryStyle CommunityGraphPlot CommunityLabels CommunityRegionStyle CompanyData CompatibleUnitQ CompilationOptions CompilationTarget Compile Compiled CompiledCodeFunction CompiledFunction CompilerOptions Complement CompleteGraph CompleteGraphQ CompleteKaryTree CompletionsListPacket Complex Complexes ComplexExpand ComplexInfinity ComplexityFunction ComplexListPlot ComplexPlot ComplexPlot3D ComponentMeasurements ComponentwiseContextMenu Compose ComposeList ComposeSeries CompositeQ Composition CompoundElement CompoundExpression CompoundPoissonDistribution CompoundPoissonProcess CompoundRenewalProcess Compress CompressedData ComputeUncertainty Condition ConditionalExpression Conditioned Cone ConeBox ConfidenceLevel ConfidenceRange ConfidenceTransform ConfigurationPath ConformAudio ConformImages Congruent ConicHullRegion ConicHullRegion3DBox ConicHullRegionBox ConicOptimization Conjugate ConjugateTranspose Conjunction Connect ConnectedComponents ConnectedGraphComponents ConnectedGraphQ ConnectedMeshComponents ConnectedMoleculeComponents ConnectedMoleculeQ ConnectionSettings ConnectLibraryCallbackFunction ConnectSystemModelComponents ConnesWindow ConoverTest ConsoleMessage ConsoleMessagePacket ConsolePrint Constant ConstantArray ConstantArrayLayer ConstantImage ConstantPlusLayer ConstantRegionQ Constants ConstantTimesLayer ConstellationData ConstrainedMax ConstrainedMin Construct Containing ContainsAll ContainsAny ContainsExactly ContainsNone ContainsOnly ContentFieldOptions ContentLocationFunction ContentObject ContentPadding ContentsBoundingBox ContentSelectable ContentSize Context ContextMenu Contexts ContextToFileName Continuation Continue ContinuedFraction ContinuedFractionK ContinuousAction ContinuousMarkovProcess ContinuousTask ContinuousTimeModelQ ContinuousWaveletData ContinuousWaveletTransform ContourDetect ContourGraphics ContourIntegral ContourLabels ContourLines ContourPlot ContourPlot3D Contours ContourShading ContourSmoothing ContourStyle ContraharmonicMean ContrastiveLossLayer Control ControlActive ControlAlignment ControlGroupContentsBox ControllabilityGramian ControllabilityMatrix ControllableDecomposition ControllableModelQ ControllerDuration ControllerInformation ControllerInformationData ControllerLinking ControllerManipulate ControllerMethod ControllerPath ControllerState ControlPlacement ControlsRendering ControlType Convergents ConversionOptions ConversionRules ConvertToBitmapPacket ConvertToPostScript ConvertToPostScriptPacket ConvexHullMesh ConvexPolygonQ ConvexPolyhedronQ ConvolutionLayer Convolve ConwayGroupCo1 ConwayGroupCo2 ConwayGroupCo3 CookieFunction Cookies CoordinateBoundingBox CoordinateBoundingBoxArray CoordinateBounds CoordinateBoundsArray CoordinateChartData CoordinatesToolOptions CoordinateTransform CoordinateTransformData CoprimeQ Coproduct CopulaDistribution Copyable CopyDatabin CopyDirectory CopyFile CopyTag CopyToClipboard CornerFilter CornerNeighbors Correlation CorrelationDistance CorrelationFunction CorrelationTest Cos Cosh CoshIntegral CosineDistance CosineWindow CosIntegral Cot Coth Count CountDistinct CountDistinctBy CounterAssignments CounterBox CounterBoxOptions CounterClockwiseContourIntegral CounterEvaluator CounterFunction CounterIncrements CounterStyle CounterStyleMenuListing CountRoots CountryData Counts CountsBy Covariance CovarianceEstimatorFunction CovarianceFunction CoxianDistribution CoxIngersollRossProcess CoxModel CoxModelFit CramerVonMisesTest CreateArchive CreateCellID CreateChannel CreateCloudExpression CreateDatabin CreateDataSystemModel CreateDialog CreateDirectory CreateDocument CreateFile CreateIntermediateDirectories CreateManagedLibraryExpression CreateNotebook CreatePalette CreatePalettePacket CreatePermissionsGroup CreateScheduledTask CreateSearchIndex CreateSystemModel CreateTemporary CreateUUID CreateWindow CriterionFunction CriticalityFailureImportance CriticalitySuccessImportance CriticalSection Cross CrossEntropyLossLayer CrossingCount CrossingDetect CrossingPolygon CrossMatrix Csc Csch CTCLossLayer Cube CubeRoot Cubics Cuboid CuboidBox Cumulant CumulantGeneratingFunction Cup CupCap Curl CurlyDoubleQuote CurlyQuote CurrencyConvert CurrentDate CurrentImage CurrentlySpeakingPacket CurrentNotebookImage CurrentScreenImage CurrentValue Curry CurvatureFlowFilter CurveClosed Cyan CycleGraph CycleIndexPolynomial Cycles CyclicGroup Cyclotomic Cylinder CylinderBox CylindricalDecomposition' + 'D DagumDistribution DamData DamerauLevenshteinDistance DampingFactor Darker Dashed Dashing DatabaseConnect DatabaseDisconnect DatabaseReference Databin DatabinAdd DatabinRemove Databins DatabinUpload DataCompression DataDistribution DataRange DataReversed Dataset Date DateBounds Dated DateDelimiters DateDifference DatedUnit DateFormat DateFunction DateHistogram DateList DateListLogPlot DateListPlot DateListStepPlot DateObject DateObjectQ DateOverlapsQ DatePattern DatePlus DateRange DateReduction DateString DateTicksFormat DateValue DateWithinQ DaubechiesWavelet DavisDistribution DawsonF DayCount DayCountConvention DayHemisphere DaylightQ DayMatchQ DayName DayNightTerminator DayPlus DayRange DayRound DeBruijnGraph DeBruijnSequence Debug DebugTag Decapitalize Decimal DecimalForm DeclareKnownSymbols DeclarePackage Decompose DeconvolutionLayer Decrement Decrypt DecryptFile DedekindEta DeepSpaceProbeData Default DefaultAxesStyle DefaultBaseStyle DefaultBoxStyle DefaultButton DefaultColor DefaultControlPlacement DefaultDuplicateCellStyle DefaultDuration DefaultElement DefaultFaceGridsStyle DefaultFieldHintStyle DefaultFont DefaultFontProperties DefaultFormatType DefaultFormatTypeForStyle DefaultFrameStyle DefaultFrameTicksStyle DefaultGridLinesStyle DefaultInlineFormatType DefaultInputFormatType DefaultLabelStyle DefaultMenuStyle DefaultNaturalLanguage DefaultNewCellStyle DefaultNewInlineCellStyle DefaultNotebook DefaultOptions DefaultOutputFormatType DefaultPrintPrecision DefaultStyle DefaultStyleDefinitions DefaultTextFormatType DefaultTextInlineFormatType DefaultTicksStyle DefaultTooltipStyle DefaultValue DefaultValues Defer DefineExternal DefineInputStreamMethod DefineOutputStreamMethod DefineResourceFunction Definition Degree DegreeCentrality DegreeGraphDistribution DegreeLexicographic DegreeReverseLexicographic DEigensystem DEigenvalues Deinitialization Del DelaunayMesh Delayed Deletable Delete DeleteAnomalies DeleteBorderComponents DeleteCases DeleteChannel DeleteCloudExpression DeleteContents DeleteDirectory DeleteDuplicates DeleteDuplicatesBy DeleteFile DeleteMissing DeleteObject DeletePermissionsKey DeleteSearchIndex DeleteSmallComponents DeleteStopwords DeleteWithContents DeletionWarning DelimitedArray DelimitedSequence Delimiter DelimiterFlashTime DelimiterMatching Delimiters DeliveryFunction Dendrogram Denominator DensityGraphics DensityHistogram DensityPlot DensityPlot3D DependentVariables Deploy Deployed Depth DepthFirstScan Derivative DerivativeFilter DerivedKey DescriptorStateSpace DesignMatrix DestroyAfterEvaluation Det DeviceClose DeviceConfigure DeviceExecute DeviceExecuteAsynchronous DeviceObject DeviceOpen DeviceOpenQ DeviceRead DeviceReadBuffer DeviceReadLatest DeviceReadList DeviceReadTimeSeries Devices DeviceStreams DeviceWrite DeviceWriteBuffer DGaussianWavelet DiacriticalPositioning Diagonal DiagonalizableMatrixQ DiagonalMatrix DiagonalMatrixQ Dialog DialogIndent DialogInput DialogLevel DialogNotebook DialogProlog DialogReturn DialogSymbols Diamond DiamondMatrix DiceDissimilarity DictionaryLookup DictionaryWordQ DifferenceDelta DifferenceOrder DifferenceQuotient DifferenceRoot DifferenceRootReduce Differences DifferentialD DifferentialRoot DifferentialRootReduce DifferentiatorFilter DigitalSignature DigitBlock DigitBlockMinimum DigitCharacter DigitCount DigitQ DihedralAngle DihedralGroup Dilation DimensionalCombinations DimensionalMeshComponents DimensionReduce DimensionReducerFunction DimensionReduction Dimensions DiracComb DiracDelta DirectedEdge DirectedEdges DirectedGraph DirectedGraphQ DirectedInfinity Direction Directive Directory DirectoryName DirectoryQ DirectoryStack DirichletBeta DirichletCharacter DirichletCondition DirichletConvolve DirichletDistribution DirichletEta DirichletL DirichletLambda DirichletTransform DirichletWindow DisableConsolePrintPacket DisableFormatting DiscreteChirpZTransform DiscreteConvolve DiscreteDelta DiscreteHadamardTransform DiscreteIndicator DiscreteLimit DiscreteLQEstimatorGains DiscreteLQRegulatorGains DiscreteLyapunovSolve DiscreteMarkovProcess DiscreteMaxLimit DiscreteMinLimit DiscretePlot DiscretePlot3D DiscreteRatio DiscreteRiccatiSolve DiscreteShift DiscreteTimeModelQ DiscreteUniformDistribution DiscreteVariables DiscreteWaveletData DiscreteWaveletPacketTransform DiscreteWaveletTransform DiscretizeGraphics DiscretizeRegion Discriminant DisjointQ Disjunction Disk DiskBox DiskMatrix DiskSegment Dispatch DispatchQ DispersionEstimatorFunction Display DisplayAllSteps DisplayEndPacket DisplayFlushImagePacket DisplayForm DisplayFunction DisplayPacket DisplayRules DisplaySetSizePacket DisplayString DisplayTemporary DisplayWith DisplayWithRef DisplayWithVariable DistanceFunction DistanceMatrix DistanceTransform Distribute Distributed DistributedContexts DistributeDefinitions DistributionChart DistributionDomain DistributionFitTest DistributionParameterAssumptions DistributionParameterQ Dithering Div Divergence Divide DivideBy Dividers DivideSides Divisible Divisors DivisorSigma DivisorSum DMSList DMSString Do DockedCells DocumentGenerator DocumentGeneratorInformation DocumentGeneratorInformationData DocumentGenerators DocumentNotebook DocumentWeightingRules Dodecahedron DomainRegistrationInformation DominantColors DOSTextFormat Dot DotDashed DotEqual DotLayer DotPlusLayer Dotted DoubleBracketingBar DoubleContourIntegral DoubleDownArrow DoubleLeftArrow DoubleLeftRightArrow DoubleLeftTee DoubleLongLeftArrow DoubleLongLeftRightArrow DoubleLongRightArrow DoubleRightArrow DoubleRightTee DoubleUpArrow DoubleUpDownArrow DoubleVerticalBar DoublyInfinite Down DownArrow DownArrowBar DownArrowUpArrow DownLeftRightVector DownLeftTeeVector DownLeftVector DownLeftVectorBar DownRightTeeVector DownRightVector DownRightVectorBar Downsample DownTee DownTeeArrow DownValues DragAndDrop DrawEdges DrawFrontFaces DrawHighlighted Drop DropoutLayer DSolve DSolveValue Dt DualLinearProgramming DualPolyhedron DualSystemsModel DumpGet DumpSave DuplicateFreeQ Duration Dynamic DynamicBox DynamicBoxOptions DynamicEvaluationTimeout DynamicGeoGraphics DynamicImage DynamicLocation DynamicModule DynamicModuleBox DynamicModuleBoxOptions DynamicModuleParent DynamicModuleValues DynamicName DynamicNamespace DynamicReference DynamicSetting DynamicUpdating DynamicWrapper DynamicWrapperBox DynamicWrapperBoxOptions' + 'E EarthImpactData EarthquakeData EccentricityCentrality Echo EchoFunction EclipseType EdgeAdd EdgeBetweennessCentrality EdgeCapacity EdgeCapForm EdgeColor EdgeConnectivity EdgeContract EdgeCost EdgeCount EdgeCoverQ EdgeCycleMatrix EdgeDashing EdgeDelete EdgeDetect EdgeForm EdgeIndex EdgeJoinForm EdgeLabeling EdgeLabels EdgeLabelStyle EdgeList EdgeOpacity EdgeQ EdgeRenderingFunction EdgeRules EdgeShapeFunction EdgeStyle EdgeThickness EdgeWeight EdgeWeightedGraphQ Editable EditButtonSettings EditCellTagsSettings EditDistance EffectiveInterest Eigensystem Eigenvalues EigenvectorCentrality Eigenvectors Element ElementData ElementwiseLayer ElidedForms Eliminate EliminationOrder Ellipsoid EllipticE EllipticExp EllipticExpPrime EllipticF EllipticFilterModel EllipticK EllipticLog EllipticNomeQ EllipticPi EllipticReducedHalfPeriods EllipticTheta EllipticThetaPrime EmbedCode EmbeddedHTML EmbeddedService EmbeddingLayer EmbeddingObject EmitSound EmphasizeSyntaxErrors EmpiricalDistribution Empty EmptyGraphQ EmptyRegion EnableConsolePrintPacket Enabled Encode Encrypt EncryptedObject EncryptFile End EndAdd EndDialogPacket EndFrontEndInteractionPacket EndOfBuffer EndOfFile EndOfLine EndOfString EndPackage EngineEnvironment EngineeringForm Enter EnterExpressionPacket EnterTextPacket Entity EntityClass EntityClassList EntityCopies EntityFunction EntityGroup EntityInstance EntityList EntityPrefetch EntityProperties EntityProperty EntityPropertyClass EntityRegister EntityStore EntityStores EntityTypeName EntityUnregister EntityValue Entropy EntropyFilter Environment Epilog EpilogFunction Equal EqualColumns EqualRows EqualTilde EqualTo EquatedTo Equilibrium EquirippleFilterKernel Equivalent Erf Erfc Erfi ErlangB ErlangC ErlangDistribution Erosion ErrorBox ErrorBoxOptions ErrorNorm ErrorPacket ErrorsDialogSettings EscapeRadius EstimatedBackground EstimatedDistribution EstimatedProcess EstimatorGains EstimatorRegulator EuclideanDistance EulerAngles EulerCharacteristic EulerE EulerGamma EulerianGraphQ EulerMatrix EulerPhi Evaluatable Evaluate Evaluated EvaluatePacket EvaluateScheduledTask EvaluationBox EvaluationCell EvaluationCompletionAction EvaluationData EvaluationElements EvaluationEnvironment EvaluationMode EvaluationMonitor EvaluationNotebook EvaluationObject EvaluationOrder Evaluator EvaluatorNames EvenQ EventData EventEvaluator EventHandler EventHandlerTag EventLabels EventSeries ExactBlackmanWindow ExactNumberQ ExactRootIsolation ExampleData Except ExcludedForms ExcludedLines ExcludedPhysicalQuantities ExcludePods Exclusions ExclusionsStyle Exists Exit ExitDialog ExoplanetData Exp Expand ExpandAll ExpandDenominator ExpandFileName ExpandNumerator Expectation ExpectationE ExpectedValue ExpGammaDistribution ExpIntegralE ExpIntegralEi ExpirationDate Exponent ExponentFunction ExponentialDistribution ExponentialFamily ExponentialGeneratingFunction ExponentialMovingAverage ExponentialPowerDistribution ExponentPosition ExponentStep Export ExportAutoReplacements ExportByteArray ExportForm ExportPacket ExportString Expression ExpressionCell ExpressionPacket ExpressionUUID ExpToTrig ExtendedEntityClass ExtendedGCD Extension ExtentElementFunction ExtentMarkers ExtentSize ExternalBundle ExternalCall ExternalDataCharacterEncoding ExternalEvaluate ExternalFunction ExternalFunctionName ExternalObject ExternalOptions ExternalSessionObject ExternalSessions ExternalTypeSignature ExternalValue Extract ExtractArchive ExtractLayer ExtremeValueDistribution' + 'FaceForm FaceGrids FaceGridsStyle FacialFeatures Factor FactorComplete Factorial Factorial2 FactorialMoment FactorialMomentGeneratingFunction FactorialPower FactorInteger FactorList FactorSquareFree FactorSquareFreeList FactorTerms FactorTermsList Fail Failure FailureAction FailureDistribution FailureQ False FareySequence FARIMAProcess FeatureDistance FeatureExtract FeatureExtraction FeatureExtractor FeatureExtractorFunction FeatureNames FeatureNearest FeatureSpacePlot FeatureSpacePlot3D FeatureTypes FEDisableConsolePrintPacket FeedbackLinearize FeedbackSector FeedbackSectorStyle FeedbackType FEEnableConsolePrintPacket FetalGrowthData Fibonacci Fibonorial FieldCompletionFunction FieldHint FieldHintStyle FieldMasked FieldSize File FileBaseName FileByteCount FileConvert FileDate FileExistsQ FileExtension FileFormat FileHandler FileHash FileInformation FileName FileNameDepth FileNameDialogSettings FileNameDrop FileNameForms FileNameJoin FileNames FileNameSetter FileNameSplit FileNameTake FilePrint FileSize FileSystemMap FileSystemScan FileTemplate FileTemplateApply FileType FilledCurve FilledCurveBox FilledCurveBoxOptions Filling FillingStyle FillingTransform FilteredEntityClass FilterRules FinancialBond FinancialData FinancialDerivative FinancialIndicator Find FindAnomalies FindArgMax FindArgMin FindChannels FindClique FindClusters FindCookies FindCurvePath FindCycle FindDevices FindDistribution FindDistributionParameters FindDivisions FindEdgeCover FindEdgeCut FindEdgeIndependentPaths FindEquationalProof FindEulerianCycle FindExternalEvaluators FindFaces FindFile FindFit FindFormula FindFundamentalCycles FindGeneratingFunction FindGeoLocation FindGeometricConjectures FindGeometricTransform FindGraphCommunities FindGraphIsomorphism FindGraphPartition FindHamiltonianCycle FindHamiltonianPath FindHiddenMarkovStates FindIndependentEdgeSet FindIndependentVertexSet FindInstance FindIntegerNullVector FindKClan FindKClique FindKClub FindKPlex FindLibrary FindLinearRecurrence FindList FindMatchingColor FindMaximum FindMaximumFlow FindMaxValue FindMeshDefects FindMinimum FindMinimumCostFlow FindMinimumCut FindMinValue FindMoleculeSubstructure FindPath FindPeaks FindPermutation FindPostmanTour FindProcessParameters FindRepeat FindRoot FindSequenceFunction FindSettings FindShortestPath FindShortestTour FindSpanningTree FindSystemModelEquilibrium FindTextualAnswer FindThreshold FindTransientRepeat FindVertexCover FindVertexCut FindVertexIndependentPaths Fine FinishDynamic FiniteAbelianGroupCount FiniteGroupCount FiniteGroupData First FirstCase FirstPassageTimeDistribution FirstPosition FischerGroupFi22 FischerGroupFi23 FischerGroupFi24Prime FisherHypergeometricDistribution FisherRatioTest FisherZDistribution Fit FitAll FitRegularization FittedModel FixedOrder FixedPoint FixedPointList FlashSelection Flat Flatten FlattenAt FlattenLayer FlatTopWindow FlipView Floor FlowPolynomial FlushPrintOutputPacket Fold FoldList FoldPair FoldPairList FollowRedirects Font FontColor FontFamily FontForm FontName FontOpacity FontPostScriptName FontProperties FontReencoding FontSize FontSlant FontSubstitutions FontTracking FontVariations FontWeight For ForAll Format FormatRules FormatType FormatTypeAutoConvert FormatValues FormBox FormBoxOptions FormControl FormFunction FormLayoutFunction FormObject FormPage FormTheme FormulaData FormulaLookup FortranForm Forward ForwardBackward Fourier FourierCoefficient FourierCosCoefficient FourierCosSeries FourierCosTransform FourierDCT FourierDCTFilter FourierDCTMatrix FourierDST FourierDSTMatrix FourierMatrix FourierParameters FourierSequenceTransform FourierSeries FourierSinCoefficient FourierSinSeries FourierSinTransform FourierTransform FourierTrigSeries FractionalBrownianMotionProcess FractionalGaussianNoiseProcess FractionalPart FractionBox FractionBoxOptions FractionLine Frame FrameBox FrameBoxOptions Framed FrameInset FrameLabel Frameless FrameMargins FrameRate FrameStyle FrameTicks FrameTicksStyle FRatioDistribution FrechetDistribution FreeQ FrenetSerretSystem FrequencySamplingFilterKernel FresnelC FresnelF FresnelG FresnelS Friday FrobeniusNumber FrobeniusSolve FromAbsoluteTime FromCharacterCode FromCoefficientRules FromContinuedFraction FromDate FromDigits FromDMS FromEntity FromJulianDate FromLetterNumber FromPolarCoordinates FromRomanNumeral FromSphericalCoordinates FromUnixTime Front FrontEndDynamicExpression FrontEndEventActions FrontEndExecute FrontEndObject FrontEndResource FrontEndResourceString FrontEndStackSize FrontEndToken FrontEndTokenExecute FrontEndValueCache FrontEndVersion FrontFaceColor FrontFaceOpacity Full FullAxes FullDefinition FullForm FullGraphics FullInformationOutputRegulator FullOptions FullRegion FullSimplify Function FunctionCompile FunctionCompileExport FunctionCompileExportByteArray FunctionCompileExportLibrary FunctionCompileExportString FunctionDomain FunctionExpand FunctionInterpolation FunctionPeriod FunctionRange FunctionSpace FussellVeselyImportance' + 'GaborFilter GaborMatrix GaborWavelet GainMargins GainPhaseMargins GalaxyData GalleryView Gamma GammaDistribution GammaRegularized GapPenalty GARCHProcess GatedRecurrentLayer Gather GatherBy GaugeFaceElementFunction GaugeFaceStyle GaugeFrameElementFunction GaugeFrameSize GaugeFrameStyle GaugeLabels GaugeMarkers GaugeStyle GaussianFilter GaussianIntegers GaussianMatrix GaussianOrthogonalMatrixDistribution GaussianSymplecticMatrixDistribution GaussianUnitaryMatrixDistribution GaussianWindow GCD GegenbauerC General GeneralizedLinearModelFit GenerateAsymmetricKeyPair GenerateConditions GeneratedCell GeneratedDocumentBinding GenerateDerivedKey GenerateDigitalSignature GenerateDocument GeneratedParameters GeneratedQuantityMagnitudes GenerateHTTPResponse GenerateSecuredAuthenticationKey GenerateSymmetricKey GeneratingFunction GeneratorDescription GeneratorHistoryLength GeneratorOutputType Generic GenericCylindricalDecomposition GenomeData GenomeLookup GeoAntipode GeoArea GeoArraySize GeoBackground GeoBoundingBox GeoBounds GeoBoundsRegion GeoBubbleChart GeoCenter GeoCircle GeodesicClosing GeodesicDilation GeodesicErosion GeodesicOpening GeoDestination GeodesyData GeoDirection GeoDisk GeoDisplacement GeoDistance GeoDistanceList GeoElevationData GeoEntities GeoGraphics GeogravityModelData GeoGridDirectionDifference GeoGridLines GeoGridLinesStyle GeoGridPosition GeoGridRange GeoGridRangePadding GeoGridUnitArea GeoGridUnitDistance GeoGridVector GeoGroup GeoHemisphere GeoHemisphereBoundary GeoHistogram GeoIdentify GeoImage GeoLabels GeoLength GeoListPlot GeoLocation GeologicalPeriodData GeomagneticModelData GeoMarker GeometricAssertion GeometricBrownianMotionProcess GeometricDistribution GeometricMean GeometricMeanFilter GeometricScene GeometricTransformation GeometricTransformation3DBox GeometricTransformation3DBoxOptions GeometricTransformationBox GeometricTransformationBoxOptions GeoModel GeoNearest GeoPath GeoPosition GeoPositionENU GeoPositionXYZ GeoProjection GeoProjectionData GeoRange GeoRangePadding GeoRegionValuePlot GeoResolution GeoScaleBar GeoServer GeoSmoothHistogram GeoStreamPlot GeoStyling GeoStylingImageFunction GeoVariant GeoVector GeoVectorENU GeoVectorPlot GeoVectorXYZ GeoVisibleRegion GeoVisibleRegionBoundary GeoWithinQ GeoZoomLevel GestureHandler GestureHandlerTag Get GetBoundingBoxSizePacket GetContext GetEnvironment GetFileName GetFrontEndOptionsDataPacket GetLinebreakInformationPacket GetMenusPacket GetPageBreakInformationPacket Glaisher GlobalClusteringCoefficient GlobalPreferences GlobalSession Glow GoldenAngle GoldenRatio GompertzMakehamDistribution GoodmanKruskalGamma GoodmanKruskalGammaTest Goto Grad Gradient GradientFilter GradientOrientationFilter GrammarApply GrammarRules GrammarToken Graph Graph3D GraphAssortativity GraphAutomorphismGroup GraphCenter GraphComplement GraphData GraphDensity GraphDiameter GraphDifference GraphDisjointUnion GraphDistance GraphDistanceMatrix GraphElementData GraphEmbedding GraphHighlight GraphHighlightStyle GraphHub Graphics Graphics3D Graphics3DBox Graphics3DBoxOptions GraphicsArray GraphicsBaseline GraphicsBox GraphicsBoxOptions GraphicsColor GraphicsColumn GraphicsComplex GraphicsComplex3DBox GraphicsComplex3DBoxOptions GraphicsComplexBox GraphicsComplexBoxOptions GraphicsContents GraphicsData GraphicsGrid GraphicsGridBox GraphicsGroup GraphicsGroup3DBox GraphicsGroup3DBoxOptions GraphicsGroupBox GraphicsGroupBoxOptions GraphicsGrouping GraphicsHighlightColor GraphicsRow GraphicsSpacing GraphicsStyle GraphIntersection GraphLayout GraphLinkEfficiency GraphPeriphery GraphPlot GraphPlot3D GraphPower GraphPropertyDistribution GraphQ GraphRadius GraphReciprocity GraphRoot GraphStyle GraphUnion Gray GrayLevel Greater GreaterEqual GreaterEqualLess GreaterEqualThan GreaterFullEqual GreaterGreater GreaterLess GreaterSlantEqual GreaterThan GreaterTilde Green GreenFunction Grid GridBaseline GridBox GridBoxAlignment GridBoxBackground GridBoxDividers GridBoxFrame GridBoxItemSize GridBoxItemStyle GridBoxOptions GridBoxSpacings GridCreationSettings GridDefaultElement GridElementStyleOptions GridFrame GridFrameMargins GridGraph GridLines GridLinesStyle GroebnerBasis GroupActionBase GroupBy GroupCentralizer GroupElementFromWord GroupElementPosition GroupElementQ GroupElements GroupElementToWord GroupGenerators Groupings GroupMultiplicationTable GroupOrbits GroupOrder GroupPageBreakWithin GroupSetwiseStabilizer GroupStabilizer GroupStabilizerChain GroupTogetherGrouping GroupTogetherNestedGrouping GrowCutComponents Gudermannian GuidedFilter GumbelDistribution' + 'HaarWavelet HadamardMatrix HalfLine HalfNormalDistribution HalfPlane HalfSpace HamiltonianGraphQ HammingDistance HammingWindow HandlerFunctions HandlerFunctionsKeys HankelH1 HankelH2 HankelMatrix HankelTransform HannPoissonWindow HannWindow HaradaNortonGroupHN HararyGraph HarmonicMean HarmonicMeanFilter HarmonicNumber Hash Haversine HazardFunction Head HeadCompose HeaderLines Heads HeavisideLambda HeavisidePi HeavisideTheta HeldGroupHe HeldPart HelpBrowserLookup HelpBrowserNotebook HelpBrowserSettings Here HermiteDecomposition HermiteH HermitianMatrixQ HessenbergDecomposition Hessian HexadecimalCharacter Hexahedron HexahedronBox HexahedronBoxOptions HiddenMarkovProcess HiddenSurface Highlighted HighlightGraph HighlightImage HighlightMesh HighpassFilter HigmanSimsGroupHS HilbertCurve HilbertFilter HilbertMatrix Histogram Histogram3D HistogramDistribution HistogramList HistogramTransform HistogramTransformInterpolation HistoricalPeriodData HitMissTransform HITSCentrality HjorthDistribution HodgeDual HoeffdingD HoeffdingDTest Hold HoldAll HoldAllComplete HoldComplete HoldFirst HoldForm HoldPattern HoldRest HolidayCalendar HomeDirectory HomePage Horizontal HorizontalForm HorizontalGauge HorizontalScrollPosition HornerForm HostLookup HotellingTSquareDistribution HoytDistribution HTMLSave HTTPErrorResponse HTTPRedirect HTTPRequest HTTPRequestData HTTPResponse Hue HumanGrowthData HumpDownHump HumpEqual HurwitzLerchPhi HurwitzZeta HyperbolicDistribution HypercubeGraph HyperexponentialDistribution Hyperfactorial Hypergeometric0F1 Hypergeometric0F1Regularized Hypergeometric1F1 Hypergeometric1F1Regularized Hypergeometric2F1 Hypergeometric2F1Regularized HypergeometricDistribution HypergeometricPFQ HypergeometricPFQRegularized HypergeometricU Hyperlink HyperlinkCreationSettings Hyperplane Hyphenation HyphenationOptions HypoexponentialDistribution HypothesisTestData' + 'I IconData Iconize IconizedObject IconRules Icosahedron Identity IdentityMatrix If IgnoreCase IgnoreDiacritics IgnorePunctuation IgnoreSpellCheck IgnoringInactive Im Image Image3D Image3DProjection Image3DSlices ImageAccumulate ImageAdd ImageAdjust ImageAlign ImageApply ImageApplyIndexed ImageAspectRatio ImageAssemble ImageAugmentationLayer ImageBoundingBoxes ImageCache ImageCacheValid ImageCapture ImageCaptureFunction ImageCases ImageChannels ImageClip ImageCollage ImageColorSpace ImageCompose ImageContainsQ ImageContents ImageConvolve ImageCooccurrence ImageCorners ImageCorrelate ImageCorrespondingPoints ImageCrop ImageData ImageDeconvolve ImageDemosaic ImageDifference ImageDimensions ImageDisplacements ImageDistance ImageEffect ImageExposureCombine ImageFeatureTrack ImageFileApply ImageFileFilter ImageFileScan ImageFilter ImageFocusCombine ImageForestingComponents ImageFormattingWidth ImageForwardTransformation ImageGraphics ImageHistogram ImageIdentify ImageInstanceQ ImageKeypoints ImageLevels ImageLines ImageMargins ImageMarker ImageMarkers ImageMeasurements ImageMesh ImageMultiply ImageOffset ImagePad ImagePadding ImagePartition ImagePeriodogram ImagePerspectiveTransformation ImagePosition ImagePreviewFunction ImagePyramid ImagePyramidApply ImageQ ImageRangeCache ImageRecolor ImageReflect ImageRegion ImageResize ImageResolution ImageRestyle ImageRotate ImageRotated ImageSaliencyFilter ImageScaled ImageScan ImageSize ImageSizeAction ImageSizeCache ImageSizeMultipliers ImageSizeRaw ImageSubtract ImageTake ImageTransformation ImageTrim ImageType ImageValue ImageValuePositions ImagingDevice ImplicitRegion Implies Import ImportAutoReplacements ImportByteArray ImportOptions ImportString ImprovementImportance In Inactivate Inactive IncidenceGraph IncidenceList IncidenceMatrix IncludeAromaticBonds IncludeConstantBasis IncludeDefinitions IncludeDirectories IncludeFileExtension IncludeGeneratorTasks IncludeHydrogens IncludeInflections IncludeMetaInformation IncludePods IncludeQuantities IncludeRelatedTables IncludeSingularTerm IncludeWindowTimes Increment IndefiniteMatrixQ Indent IndentingNewlineSpacings IndentMaxFraction IndependenceTest IndependentEdgeSetQ IndependentPhysicalQuantity IndependentUnit IndependentUnitDimension IndependentVertexSetQ Indeterminate IndeterminateThreshold IndexCreationOptions Indexed IndexGraph IndexTag Inequality InexactNumberQ InexactNumbers InfiniteLine InfinitePlane Infinity Infix InflationAdjust InflationMethod Information InformationData InformationDataGrid Inherited InheritScope InhomogeneousPoissonProcess InitialEvaluationHistory Initialization InitializationCell InitializationCellEvaluation InitializationCellWarning InitializationObjects InitializationValue Initialize InitialSeeding InlineCounterAssignments InlineCounterIncrements InlineRules Inner InnerPolygon InnerPolyhedron Inpaint Input InputAliases InputAssumptions InputAutoReplacements InputField InputFieldBox InputFieldBoxOptions InputForm InputGrouping InputNamePacket InputNotebook InputPacket InputSettings InputStream InputString InputStringPacket InputToBoxFormPacket Insert InsertionFunction InsertionPointObject InsertLinebreaks InsertResults Inset Inset3DBox Inset3DBoxOptions InsetBox InsetBoxOptions Insphere Install InstallService InstanceNormalizationLayer InString Integer IntegerDigits IntegerExponent IntegerLength IntegerName IntegerPart IntegerPartitions IntegerQ IntegerReverse Integers IntegerString Integral Integrate Interactive InteractiveTradingChart Interlaced Interleaving InternallyBalancedDecomposition InterpolatingFunction InterpolatingPolynomial Interpolation InterpolationOrder InterpolationPoints InterpolationPrecision Interpretation InterpretationBox InterpretationBoxOptions InterpretationFunction Interpreter InterpretTemplate InterquartileRange Interrupt InterruptSettings IntersectingQ Intersection Interval IntervalIntersection IntervalMarkers IntervalMarkersStyle IntervalMemberQ IntervalSlider IntervalUnion Into Inverse InverseBetaRegularized InverseCDF InverseChiSquareDistribution InverseContinuousWaveletTransform InverseDistanceTransform InverseEllipticNomeQ InverseErf InverseErfc InverseFourier InverseFourierCosTransform InverseFourierSequenceTransform InverseFourierSinTransform InverseFourierTransform InverseFunction InverseFunctions InverseGammaDistribution InverseGammaRegularized InverseGaussianDistribution InverseGudermannian InverseHankelTransform InverseHaversine InverseImagePyramid InverseJacobiCD InverseJacobiCN InverseJacobiCS InverseJacobiDC InverseJacobiDN InverseJacobiDS InverseJacobiNC InverseJacobiND InverseJacobiNS InverseJacobiSC InverseJacobiSD InverseJacobiSN InverseLaplaceTransform InverseMellinTransform InversePermutation InverseRadon InverseRadonTransform InverseSeries InverseShortTimeFourier InverseSpectrogram InverseSurvivalFunction InverseTransformedRegion InverseWaveletTransform InverseWeierstrassP InverseWishartMatrixDistribution InverseZTransform Invisible InvisibleApplication InvisibleTimes IPAddress IrreduciblePolynomialQ IslandData IsolatingInterval IsomorphicGraphQ IsotopeData Italic Item ItemAspectRatio ItemBox ItemBoxOptions ItemSize ItemStyle ItoProcess' + 'JaccardDissimilarity JacobiAmplitude Jacobian JacobiCD JacobiCN JacobiCS JacobiDC JacobiDN JacobiDS JacobiNC JacobiND JacobiNS JacobiP JacobiSC JacobiSD JacobiSN JacobiSymbol JacobiZeta JankoGroupJ1 JankoGroupJ2 JankoGroupJ3 JankoGroupJ4 JarqueBeraALMTest JohnsonDistribution Join JoinAcross Joined JoinedCurve JoinedCurveBox JoinedCurveBoxOptions JoinForm JordanDecomposition JordanModelDecomposition JulianDate JuliaSetBoettcher JuliaSetIterationCount JuliaSetPlot JuliaSetPoints' + 'K KagiChart KaiserBesselWindow KaiserWindow KalmanEstimator KalmanFilter KarhunenLoeveDecomposition KaryTree KatzCentrality KCoreComponents KDistribution KEdgeConnectedComponents KEdgeConnectedGraphQ KelvinBei KelvinBer KelvinKei KelvinKer KendallTau KendallTauTest KernelExecute KernelFunction KernelMixtureDistribution Kernels Ket Key KeyCollisionFunction KeyComplement KeyDrop KeyDropFrom KeyExistsQ KeyFreeQ KeyIntersection KeyMap KeyMemberQ KeypointStrength Keys KeySelect KeySort KeySortBy KeyTake KeyUnion KeyValueMap KeyValuePattern Khinchin KillProcess KirchhoffGraph KirchhoffMatrix KleinInvariantJ KnapsackSolve KnightTourGraph KnotData KnownUnitQ KochCurve KolmogorovSmirnovTest KroneckerDelta KroneckerModelDecomposition KroneckerProduct KroneckerSymbol KuiperTest KumaraswamyDistribution Kurtosis KuwaharaFilter KVertexConnectedComponents KVertexConnectedGraphQ' + 'LABColor Label Labeled LabeledSlider LabelingFunction LabelingSize LabelStyle LabelVisibility LaguerreL LakeData LambdaComponents LambertW LaminaData LanczosWindow LandauDistribution Language LanguageCategory LanguageData LanguageIdentify LanguageOptions LaplaceDistribution LaplaceTransform Laplacian LaplacianFilter LaplacianGaussianFilter Large Larger Last Latitude LatitudeLongitude LatticeData LatticeReduce Launch LaunchKernels LayeredGraphPlot LayerSizeFunction LayoutInformation LCHColor LCM LeaderSize LeafCount LeapYearQ LearnDistribution LearnedDistribution LearningRate LearningRateMultipliers LeastSquares LeastSquaresFilterKernel Left LeftArrow LeftArrowBar LeftArrowRightArrow LeftDownTeeVector LeftDownVector LeftDownVectorBar LeftRightArrow LeftRightVector LeftTee LeftTeeArrow LeftTeeVector LeftTriangle LeftTriangleBar LeftTriangleEqual LeftUpDownVector LeftUpTeeVector LeftUpVector LeftUpVectorBar LeftVector LeftVectorBar LegendAppearance Legended LegendFunction LegendLabel LegendLayout LegendMargins LegendMarkers LegendMarkerSize LegendreP LegendreQ LegendreType Length LengthWhile LerchPhi Less LessEqual LessEqualGreater LessEqualThan LessFullEqual LessGreater LessLess LessSlantEqual LessThan LessTilde LetterCharacter LetterCounts LetterNumber LetterQ Level LeveneTest LeviCivitaTensor LevyDistribution Lexicographic LibraryDataType LibraryFunction LibraryFunctionError LibraryFunctionInformation LibraryFunctionLoad LibraryFunctionUnload LibraryLoad LibraryUnload LicenseID LiftingFilterData LiftingWaveletTransform LightBlue LightBrown LightCyan Lighter LightGray LightGreen Lighting LightingAngle LightMagenta LightOrange LightPink LightPurple LightRed LightSources LightYellow Likelihood Limit LimitsPositioning LimitsPositioningTokens LindleyDistribution Line Line3DBox Line3DBoxOptions LinearFilter LinearFractionalOptimization LinearFractionalTransform LinearGradientImage LinearizingTransformationData LinearLayer LinearModelFit LinearOffsetFunction LinearOptimization LinearProgramming LinearRecurrence LinearSolve LinearSolveFunction LineBox LineBoxOptions LineBreak LinebreakAdjustments LineBreakChart LinebreakSemicolonWeighting LineBreakWithin LineColor LineGraph LineIndent LineIndentMaxFraction LineIntegralConvolutionPlot LineIntegralConvolutionScale LineLegend LineOpacity LineSpacing LineWrapParts LinkActivate LinkClose LinkConnect LinkConnectedQ LinkCreate LinkError LinkFlush LinkFunction LinkHost LinkInterrupt LinkLaunch LinkMode LinkObject LinkOpen LinkOptions LinkPatterns LinkProtocol LinkRankCentrality LinkRead LinkReadHeld LinkReadyQ Links LinkService LinkWrite LinkWriteHeld LiouvilleLambda List Listable ListAnimate ListContourPlot ListContourPlot3D ListConvolve ListCorrelate ListCurvePathPlot ListDeconvolve ListDensityPlot ListDensityPlot3D Listen ListFormat ListFourierSequenceTransform ListInterpolation ListLineIntegralConvolutionPlot ListLinePlot ListLogLinearPlot ListLogLogPlot ListLogPlot ListPicker ListPickerBox ListPickerBoxBackground ListPickerBoxOptions ListPlay ListPlot ListPlot3D ListPointPlot3D ListPolarPlot ListQ ListSliceContourPlot3D ListSliceDensityPlot3D ListSliceVectorPlot3D ListStepPlot ListStreamDensityPlot ListStreamPlot ListSurfacePlot3D ListVectorDensityPlot ListVectorPlot ListVectorPlot3D ListZTransform Literal LiteralSearch LocalAdaptiveBinarize LocalCache LocalClusteringCoefficient LocalizeDefinitions LocalizeVariables LocalObject LocalObjects LocalResponseNormalizationLayer LocalSubmit LocalSymbol LocalTime LocalTimeZone LocationEquivalenceTest LocationTest Locator LocatorAutoCreate LocatorBox LocatorBoxOptions LocatorCentering LocatorPane LocatorPaneBox LocatorPaneBoxOptions LocatorRegion Locked Log Log10 Log2 LogBarnesG LogGamma LogGammaDistribution LogicalExpand LogIntegral LogisticDistribution LogisticSigmoid LogitModelFit LogLikelihood LogLinearPlot LogLogisticDistribution LogLogPlot LogMultinormalDistribution LogNormalDistribution LogPlot LogRankTest LogSeriesDistribution LongEqual Longest LongestCommonSequence LongestCommonSequencePositions LongestCommonSubsequence LongestCommonSubsequencePositions LongestMatch LongestOrderedSequence LongForm Longitude LongLeftArrow LongLeftRightArrow LongRightArrow LongShortTermMemoryLayer Lookup Loopback LoopFreeGraphQ LossFunction LowerCaseQ LowerLeftArrow LowerRightArrow LowerTriangularize LowerTriangularMatrixQ LowpassFilter LQEstimatorGains LQGRegulator LQOutputRegulatorGains LQRegulatorGains LUBackSubstitution LucasL LuccioSamiComponents LUDecomposition LunarEclipse LUVColor LyapunovSolve LyonsGroupLy' + 'MachineID MachineName MachineNumberQ MachinePrecision MacintoshSystemPageSetup Magenta Magnification Magnify MailAddressValidation MailExecute MailFolder MailItem MailReceiverFunction MailResponseFunction MailSearch MailServerConnect MailServerConnection MailSettings MainSolve MaintainDynamicCaches Majority MakeBoxes MakeExpression MakeRules ManagedLibraryExpressionID ManagedLibraryExpressionQ MandelbrotSetBoettcher MandelbrotSetDistance MandelbrotSetIterationCount MandelbrotSetMemberQ MandelbrotSetPlot MangoldtLambda ManhattanDistance Manipulate Manipulator MannedSpaceMissionData MannWhitneyTest MantissaExponent Manual Map MapAll MapAt MapIndexed MAProcess MapThread MarchenkoPasturDistribution MarcumQ MardiaCombinedTest MardiaKurtosisTest MardiaSkewnessTest MarginalDistribution MarkovProcessProperties Masking MatchingDissimilarity MatchLocalNameQ MatchLocalNames MatchQ Material MathematicalFunctionData MathematicaNotation MathieuC MathieuCharacteristicA MathieuCharacteristicB MathieuCharacteristicExponent MathieuCPrime MathieuGroupM11 MathieuGroupM12 MathieuGroupM22 MathieuGroupM23 MathieuGroupM24 MathieuS MathieuSPrime MathMLForm MathMLText Matrices MatrixExp MatrixForm MatrixFunction MatrixLog MatrixNormalDistribution MatrixPlot MatrixPower MatrixPropertyDistribution MatrixQ MatrixRank MatrixTDistribution Max MaxBend MaxCellMeasure MaxColorDistance MaxDetect MaxDuration MaxExtraBandwidths MaxExtraConditions MaxFeatureDisplacement MaxFeatures MaxFilter MaximalBy Maximize MaxItems MaxIterations MaxLimit MaxMemoryUsed MaxMixtureKernels MaxOverlapFraction MaxPlotPoints MaxPoints MaxRecursion MaxStableDistribution MaxStepFraction MaxSteps MaxStepSize MaxTrainingRounds MaxValue MaxwellDistribution MaxWordGap McLaughlinGroupMcL Mean MeanAbsoluteLossLayer MeanAround MeanClusteringCoefficient MeanDegreeConnectivity MeanDeviation MeanFilter MeanGraphDistance MeanNeighborDegree MeanShift MeanShiftFilter MeanSquaredLossLayer Median MedianDeviation MedianFilter MedicalTestData Medium MeijerG MeijerGReduce MeixnerDistribution MellinConvolve MellinTransform MemberQ MemoryAvailable MemoryConstrained MemoryConstraint MemoryInUse MengerMesh Menu MenuAppearance MenuCommandKey MenuEvaluator MenuItem MenuList MenuPacket MenuSortingValue MenuStyle MenuView Merge MergeDifferences MergingFunction MersennePrimeExponent MersennePrimeExponentQ Mesh MeshCellCentroid MeshCellCount MeshCellHighlight MeshCellIndex MeshCellLabel MeshCellMarker MeshCellMeasure MeshCellQuality MeshCells MeshCellShapeFunction MeshCellStyle MeshCoordinates MeshFunctions MeshPrimitives MeshQualityGoal MeshRange MeshRefinementFunction MeshRegion MeshRegionQ MeshShading MeshStyle Message MessageDialog MessageList MessageName MessageObject MessageOptions MessagePacket Messages MessagesNotebook MetaCharacters MetaInformation MeteorShowerData Method MethodOptions MexicanHatWavelet MeyerWavelet Midpoint Min MinColorDistance MinDetect MineralData MinFilter MinimalBy MinimalPolynomial MinimalStateSpaceModel Minimize MinimumTimeIncrement MinIntervalSize MinkowskiQuestionMark MinLimit MinMax MinorPlanetData Minors MinRecursion MinSize MinStableDistribution Minus MinusPlus MinValue Missing MissingBehavior MissingDataMethod MissingDataRules MissingQ MissingString MissingStyle MissingValuePattern MittagLefflerE MixedFractionParts MixedGraphQ MixedMagnitude MixedRadix MixedRadixQuantity MixedUnit MixtureDistribution Mod Modal Mode Modular ModularInverse ModularLambda Module Modulus MoebiusMu Molecule MoleculeContainsQ MoleculeEquivalentQ MoleculeGraph MoleculeModify MoleculePattern MoleculePlot MoleculePlot3D MoleculeProperty MoleculeQ MoleculeValue Moment Momentary MomentConvert MomentEvaluate MomentGeneratingFunction MomentOfInertia Monday Monitor MonomialList MonomialOrder MonsterGroupM MoonPhase MoonPosition MorletWavelet MorphologicalBinarize MorphologicalBranchPoints MorphologicalComponents MorphologicalEulerNumber MorphologicalGraph MorphologicalPerimeter MorphologicalTransform MortalityData Most MountainData MouseAnnotation MouseAppearance MouseAppearanceTag MouseButtons Mouseover MousePointerNote MousePosition MovieData MovingAverage MovingMap MovingMedian MoyalDistribution Multicolumn MultiedgeStyle MultigraphQ MultilaunchWarning MultiLetterItalics MultiLetterStyle MultilineFunction Multinomial MultinomialDistribution MultinormalDistribution MultiplicativeOrder Multiplicity MultiplySides Multiselection MultivariateHypergeometricDistribution MultivariatePoissonDistribution MultivariateTDistribution' + 'N NakagamiDistribution NameQ Names NamespaceBox NamespaceBoxOptions Nand NArgMax NArgMin NBernoulliB NBodySimulation NBodySimulationData NCache NDEigensystem NDEigenvalues NDSolve NDSolveValue Nearest NearestFunction NearestNeighborGraph NearestTo NebulaData NeedCurrentFrontEndPackagePacket NeedCurrentFrontEndSymbolsPacket NeedlemanWunschSimilarity Needs Negative NegativeBinomialDistribution NegativeDefiniteMatrixQ NegativeIntegers NegativeMultinomialDistribution NegativeRationals NegativeReals NegativeSemidefiniteMatrixQ NeighborhoodData NeighborhoodGraph Nest NestedGreaterGreater NestedLessLess NestedScriptRules NestGraph NestList NestWhile NestWhileList NetAppend NetBidirectionalOperator NetChain NetDecoder NetDelete NetDrop NetEncoder NetEvaluationMode NetExtract NetFlatten NetFoldOperator NetGraph NetInformation NetInitialize NetInsert NetInsertSharedArrays NetJoin NetMapOperator NetMapThreadOperator NetMeasurements NetModel NetNestOperator NetPairEmbeddingOperator NetPort NetPortGradient NetPrepend NetRename NetReplace NetReplacePart NetSharedArray NetStateObject NetTake NetTrain NetTrainResultsObject NetworkPacketCapture NetworkPacketRecording NetworkPacketRecordingDuring NetworkPacketTrace NeumannValue NevilleThetaC NevilleThetaD NevilleThetaN NevilleThetaS NewPrimitiveStyle NExpectation Next NextCell NextDate NextPrime NextScheduledTaskTime NHoldAll NHoldFirst NHoldRest NicholsGridLines NicholsPlot NightHemisphere NIntegrate NMaximize NMaxValue NMinimize NMinValue NominalVariables NonAssociative NoncentralBetaDistribution NoncentralChiSquareDistribution NoncentralFRatioDistribution NoncentralStudentTDistribution NonCommutativeMultiply NonConstants NondimensionalizationTransform None NoneTrue NonlinearModelFit NonlinearStateSpaceModel NonlocalMeansFilter NonNegative NonNegativeIntegers NonNegativeRationals NonNegativeReals NonPositive NonPositiveIntegers NonPositiveRationals NonPositiveReals Nor NorlundB Norm Normal NormalDistribution NormalGrouping NormalizationLayer Normalize Normalized NormalizedSquaredEuclideanDistance NormalMatrixQ NormalsFunction NormFunction Not NotCongruent NotCupCap NotDoubleVerticalBar Notebook NotebookApply NotebookAutoSave NotebookClose NotebookConvertSettings NotebookCreate NotebookCreateReturnObject NotebookDefault NotebookDelete NotebookDirectory NotebookDynamicExpression NotebookEvaluate NotebookEventActions NotebookFileName NotebookFind NotebookFindReturnObject NotebookGet NotebookGetLayoutInformationPacket NotebookGetMisspellingsPacket NotebookImport NotebookInformation NotebookInterfaceObject NotebookLocate NotebookObject NotebookOpen NotebookOpenReturnObject NotebookPath NotebookPrint NotebookPut NotebookPutReturnObject NotebookRead NotebookResetGeneratedCells Notebooks NotebookSave NotebookSaveAs NotebookSelection NotebookSetupLayoutInformationPacket NotebooksMenu NotebookTemplate NotebookWrite NotElement NotEqualTilde NotExists NotGreater NotGreaterEqual NotGreaterFullEqual NotGreaterGreater NotGreaterLess NotGreaterSlantEqual NotGreaterTilde Nothing NotHumpDownHump NotHumpEqual NotificationFunction NotLeftTriangle NotLeftTriangleBar NotLeftTriangleEqual NotLess NotLessEqual NotLessFullEqual NotLessGreater NotLessLess NotLessSlantEqual NotLessTilde NotNestedGreaterGreater NotNestedLessLess NotPrecedes NotPrecedesEqual NotPrecedesSlantEqual NotPrecedesTilde NotReverseElement NotRightTriangle NotRightTriangleBar NotRightTriangleEqual NotSquareSubset NotSquareSubsetEqual NotSquareSuperset NotSquareSupersetEqual NotSubset NotSubsetEqual NotSucceeds NotSucceedsEqual NotSucceedsSlantEqual NotSucceedsTilde NotSuperset NotSupersetEqual NotTilde NotTildeEqual NotTildeFullEqual NotTildeTilde NotVerticalBar Now NoWhitespace NProbability NProduct NProductFactors NRoots NSolve NSum NSumTerms NuclearExplosionData NuclearReactorData Null NullRecords NullSpace NullWords Number NumberCompose NumberDecompose NumberExpand NumberFieldClassNumber NumberFieldDiscriminant NumberFieldFundamentalUnits NumberFieldIntegralBasis NumberFieldNormRepresentatives NumberFieldRegulator NumberFieldRootsOfUnity NumberFieldSignature NumberForm NumberFormat NumberLinePlot NumberMarks NumberMultiplier NumberPadding NumberPoint NumberQ NumberSeparator NumberSigns NumberString Numerator NumeratorDenominator NumericalOrder NumericalSort NumericArray NumericArrayQ NumericArrayType NumericFunction NumericQ NuttallWindow NValues NyquistGridLines NyquistPlot' + 'O ObservabilityGramian ObservabilityMatrix ObservableDecomposition ObservableModelQ OceanData Octahedron OddQ Off Offset OLEData On ONanGroupON Once OneIdentity Opacity OpacityFunction OpacityFunctionScaling Open OpenAppend Opener OpenerBox OpenerBoxOptions OpenerView OpenFunctionInspectorPacket Opening OpenRead OpenSpecialOptions OpenTemporary OpenWrite Operate OperatingSystem OptimumFlowData Optional OptionalElement OptionInspectorSettings OptionQ Options OptionsPacket OptionsPattern OptionValue OptionValueBox OptionValueBoxOptions Or Orange Order OrderDistribution OrderedQ Ordering OrderingBy OrderingLayer Orderless OrderlessPatternSequence OrnsteinUhlenbeckProcess Orthogonalize OrthogonalMatrixQ Out Outer OuterPolygon OuterPolyhedron OutputAutoOverwrite OutputControllabilityMatrix OutputControllableModelQ OutputForm OutputFormData OutputGrouping OutputMathEditExpression OutputNamePacket OutputResponse OutputSizeLimit OutputStream Over OverBar OverDot Overflow OverHat Overlaps Overlay OverlayBox OverlayBoxOptions Overscript OverscriptBox OverscriptBoxOptions OverTilde OverVector OverwriteTarget OwenT OwnValues' + 'Package PackingMethod PaddedForm Padding PaddingLayer PaddingSize PadeApproximant PadLeft PadRight PageBreakAbove PageBreakBelow PageBreakWithin PageFooterLines PageFooters PageHeaderLines PageHeaders PageHeight PageRankCentrality PageTheme PageWidth Pagination PairedBarChart PairedHistogram PairedSmoothHistogram PairedTTest PairedZTest PaletteNotebook PalettePath PalindromeQ Pane PaneBox PaneBoxOptions Panel PanelBox PanelBoxOptions Paneled PaneSelector PaneSelectorBox PaneSelectorBoxOptions PaperWidth ParabolicCylinderD ParagraphIndent ParagraphSpacing ParallelArray ParallelCombine ParallelDo Parallelepiped ParallelEvaluate Parallelization Parallelize ParallelMap ParallelNeeds Parallelogram ParallelProduct ParallelSubmit ParallelSum ParallelTable ParallelTry Parameter ParameterEstimator ParameterMixtureDistribution ParameterVariables ParametricFunction ParametricNDSolve ParametricNDSolveValue ParametricPlot ParametricPlot3D ParametricRegion ParentBox ParentCell ParentConnect ParentDirectory ParentForm Parenthesize ParentList ParentNotebook ParetoDistribution ParetoPickandsDistribution ParkData Part PartBehavior PartialCorrelationFunction PartialD ParticleAcceleratorData ParticleData Partition PartitionGranularity PartitionsP PartitionsQ PartLayer PartOfSpeech PartProtection ParzenWindow PascalDistribution PassEventsDown PassEventsUp Paste PasteAutoQuoteCharacters PasteBoxFormInlineCells PasteButton Path PathGraph PathGraphQ Pattern PatternSequence PatternTest PauliMatrix PaulWavelet Pause PausedTime PDF PeakDetect PeanoCurve PearsonChiSquareTest PearsonCorrelationTest PearsonDistribution PercentForm PerfectNumber PerfectNumberQ PerformanceGoal Perimeter PeriodicBoundaryCondition PeriodicInterpolation Periodogram PeriodogramArray Permanent Permissions PermissionsGroup PermissionsGroupMemberQ PermissionsGroups PermissionsKey PermissionsKeys PermutationCycles PermutationCyclesQ PermutationGroup PermutationLength PermutationList PermutationListQ PermutationMax PermutationMin PermutationOrder PermutationPower PermutationProduct PermutationReplace Permutations PermutationSupport Permute PeronaMalikFilter Perpendicular PerpendicularBisector PersistenceLocation PersistenceTime PersistentObject PersistentObjects PersistentValue PersonData PERTDistribution PetersenGraph PhaseMargins PhaseRange PhysicalSystemData Pi Pick PIDData PIDDerivativeFilter PIDFeedforward PIDTune Piecewise PiecewiseExpand PieChart PieChart3D PillaiTrace PillaiTraceTest PingTime Pink PitchRecognize Pivoting PixelConstrained PixelValue PixelValuePositions Placed Placeholder PlaceholderReplace Plain PlanarAngle PlanarGraph PlanarGraphQ PlanckRadiationLaw PlaneCurveData PlanetaryMoonData PlanetData PlantData Play PlayRange Plot Plot3D Plot3Matrix PlotDivision PlotJoined PlotLabel PlotLabels PlotLayout PlotLegends PlotMarkers PlotPoints PlotRange PlotRangeClipping PlotRangeClipPlanesStyle PlotRangePadding PlotRegion PlotStyle PlotTheme Pluralize Plus PlusMinus Pochhammer PodStates PodWidth Point Point3DBox Point3DBoxOptions PointBox PointBoxOptions PointFigureChart PointLegend PointSize PoissonConsulDistribution PoissonDistribution PoissonProcess PoissonWindow PolarAxes PolarAxesOrigin PolarGridLines PolarPlot PolarTicks PoleZeroMarkers PolyaAeppliDistribution PolyGamma Polygon Polygon3DBox Polygon3DBoxOptions PolygonalNumber PolygonAngle PolygonBox PolygonBoxOptions PolygonCoordinates PolygonDecomposition PolygonHoleScale PolygonIntersections PolygonScale Polyhedron PolyhedronAngle PolyhedronCoordinates PolyhedronData PolyhedronDecomposition PolyhedronGenus PolyLog PolynomialExtendedGCD PolynomialForm PolynomialGCD PolynomialLCM PolynomialMod PolynomialQ PolynomialQuotient PolynomialQuotientRemainder PolynomialReduce PolynomialRemainder Polynomials PoolingLayer PopupMenu PopupMenuBox PopupMenuBoxOptions PopupView PopupWindow Position PositionIndex Positive PositiveDefiniteMatrixQ PositiveIntegers PositiveRationals PositiveReals PositiveSemidefiniteMatrixQ PossibleZeroQ Postfix PostScript Power PowerDistribution PowerExpand PowerMod PowerModList PowerRange PowerSpectralDensity PowersRepresentations PowerSymmetricPolynomial Precedence PrecedenceForm Precedes PrecedesEqual PrecedesSlantEqual PrecedesTilde Precision PrecisionGoal PreDecrement Predict PredictionRoot PredictorFunction PredictorInformation PredictorMeasurements PredictorMeasurementsObject PreemptProtect PreferencesPath Prefix PreIncrement Prepend PrependLayer PrependTo PreprocessingRules PreserveColor PreserveImageOptions Previous PreviousCell PreviousDate PriceGraphDistribution PrimaryPlaceholder Prime PrimeNu PrimeOmega PrimePi PrimePowerQ PrimeQ Primes PrimeZetaP PrimitivePolynomialQ PrimitiveRoot PrimitiveRootList PrincipalComponents PrincipalValue Print PrintableASCIIQ PrintAction PrintForm PrintingCopies PrintingOptions PrintingPageRange PrintingStartingPageNumber PrintingStyleEnvironment Printout3D Printout3DPreviewer PrintPrecision PrintTemporary Prism PrismBox PrismBoxOptions PrivateCellOptions PrivateEvaluationOptions PrivateFontOptions PrivateFrontEndOptions PrivateKey PrivateNotebookOptions PrivatePaths Probability ProbabilityDistribution ProbabilityPlot ProbabilityPr ProbabilityScalePlot ProbitModelFit ProcessConnection ProcessDirectory ProcessEnvironment Processes ProcessEstimator ProcessInformation ProcessObject ProcessParameterAssumptions ProcessParameterQ ProcessStateDomain ProcessStatus ProcessTimeDomain Product ProductDistribution ProductLog ProgressIndicator ProgressIndicatorBox ProgressIndicatorBoxOptions Projection Prolog PromptForm ProofObject Properties Property PropertyList PropertyValue Proportion Proportional Protect Protected ProteinData Pruning PseudoInverse PsychrometricPropertyData PublicKey PublisherID PulsarData PunctuationCharacter Purple Put PutAppend Pyramid PyramidBox PyramidBoxOptions' + 'QBinomial QFactorial QGamma QHypergeometricPFQ QnDispersion QPochhammer QPolyGamma QRDecomposition QuadraticIrrationalQ QuadraticOptimization Quantile QuantilePlot Quantity QuantityArray QuantityDistribution QuantityForm QuantityMagnitude QuantityQ QuantityUnit QuantityVariable QuantityVariableCanonicalUnit QuantityVariableDimensions QuantityVariableIdentifier QuantityVariablePhysicalQuantity Quartics QuartileDeviation Quartiles QuartileSkewness Query QueueingNetworkProcess QueueingProcess QueueProperties Quiet Quit Quotient QuotientRemainder' + 'RadialGradientImage RadialityCentrality RadicalBox RadicalBoxOptions RadioButton RadioButtonBar RadioButtonBox RadioButtonBoxOptions Radon RadonTransform RamanujanTau RamanujanTauL RamanujanTauTheta RamanujanTauZ Ramp Random RandomChoice RandomColor RandomComplex RandomEntity RandomFunction RandomGeoPosition RandomGraph RandomImage RandomInstance RandomInteger RandomPermutation RandomPoint RandomPolygon RandomPolyhedron RandomPrime RandomReal RandomSample RandomSeed RandomSeeding RandomVariate RandomWalkProcess RandomWord Range RangeFilter RangeSpecification RankedMax RankedMin RarerProbability Raster Raster3D Raster3DBox Raster3DBoxOptions RasterArray RasterBox RasterBoxOptions Rasterize RasterSize Rational RationalFunctions Rationalize Rationals Ratios RawArray RawBoxes RawData RawMedium RayleighDistribution Re Read ReadByteArray ReadLine ReadList ReadProtected ReadString Real RealAbs RealBlockDiagonalForm RealDigits RealExponent Reals RealSign Reap RecognitionPrior RecognitionThreshold Record RecordLists RecordSeparators Rectangle RectangleBox RectangleBoxOptions RectangleChart RectangleChart3D RectangularRepeatingElement RecurrenceFilter RecurrenceTable RecurringDigitsForm Red Reduce RefBox ReferenceLineStyle ReferenceMarkers ReferenceMarkerStyle Refine ReflectionMatrix ReflectionTransform Refresh RefreshRate Region RegionBinarize RegionBoundary RegionBounds RegionCentroid RegionDifference RegionDimension RegionDisjoint RegionDistance RegionDistanceFunction RegionEmbeddingDimension RegionEqual RegionFunction RegionImage RegionIntersection RegionMeasure RegionMember RegionMemberFunction RegionMoment RegionNearest RegionNearestFunction RegionPlot RegionPlot3D RegionProduct RegionQ RegionResize RegionSize RegionSymmetricDifference RegionUnion RegionWithin RegisterExternalEvaluator RegularExpression Regularization RegularlySampledQ RegularPolygon ReIm ReImLabels ReImPlot ReImStyle Reinstall RelationalDatabase RelationGraph Release ReleaseHold ReliabilityDistribution ReliefImage ReliefPlot RemoteAuthorizationCaching RemoteConnect RemoteConnectionObject RemoteFile RemoteRun RemoteRunProcess Remove RemoveAlphaChannel RemoveAsynchronousTask RemoveAudioStream RemoveBackground RemoveChannelListener RemoveChannelSubscribers Removed RemoveDiacritics RemoveInputStreamMethod RemoveOutputStreamMethod RemoveProperty RemoveScheduledTask RemoveUsers RenameDirectory RenameFile RenderAll RenderingOptions RenewalProcess RenkoChart RepairMesh Repeated RepeatedNull RepeatedString RepeatedTiming RepeatingElement Replace ReplaceAll ReplaceHeldPart ReplaceImageValue ReplaceList ReplacePart ReplacePixelValue ReplaceRepeated ReplicateLayer RequiredPhysicalQuantities Resampling ResamplingAlgorithmData ResamplingMethod Rescale RescalingTransform ResetDirectory ResetMenusPacket ResetScheduledTask ReshapeLayer Residue ResizeLayer Resolve ResourceAcquire ResourceData ResourceFunction ResourceObject ResourceRegister ResourceRemove ResourceSearch ResourceSubmissionObject ResourceSubmit ResourceSystemBase ResourceUpdate ResponseForm Rest RestartInterval Restricted Resultant ResumePacket Return ReturnEntersInput ReturnExpressionPacket ReturnInputFormPacket ReturnPacket ReturnReceiptFunction ReturnTextPacket Reverse ReverseBiorthogonalSplineWavelet ReverseElement ReverseEquilibrium ReverseGraph ReverseSort ReverseSortBy ReverseUpEquilibrium RevolutionAxis RevolutionPlot3D RGBColor RiccatiSolve RiceDistribution RidgeFilter RiemannR RiemannSiegelTheta RiemannSiegelZ RiemannXi Riffle Right RightArrow RightArrowBar RightArrowLeftArrow RightComposition RightCosetRepresentative RightDownTeeVector RightDownVector RightDownVectorBar RightTee RightTeeArrow RightTeeVector RightTriangle RightTriangleBar RightTriangleEqual RightUpDownVector RightUpTeeVector RightUpVector RightUpVectorBar RightVector RightVectorBar RiskAchievementImportance RiskReductionImportance RogersTanimotoDissimilarity RollPitchYawAngles RollPitchYawMatrix RomanNumeral Root RootApproximant RootIntervals RootLocusPlot RootMeanSquare RootOfUnityQ RootReduce Roots RootSum Rotate RotateLabel RotateLeft RotateRight RotationAction RotationBox RotationBoxOptions RotationMatrix RotationTransform Round RoundImplies RoundingRadius Row RowAlignments RowBackgrounds RowBox RowHeights RowLines RowMinHeight RowReduce RowsEqual RowSpacings RSolve RSolveValue RudinShapiro RudvalisGroupRu Rule RuleCondition RuleDelayed RuleForm RulePlot RulerUnits Run RunProcess RunScheduledTask RunThrough RuntimeAttributes RuntimeOptions RussellRaoDissimilarity' + 'SameQ SameTest SampledEntityClass SampleDepth SampledSoundFunction SampledSoundList SampleRate SamplingPeriod SARIMAProcess SARMAProcess SASTriangle SatelliteData SatisfiabilityCount SatisfiabilityInstances SatisfiableQ Saturday Save Saveable SaveAutoDelete SaveConnection SaveDefinitions SavitzkyGolayMatrix SawtoothWave Scale Scaled ScaleDivisions ScaledMousePosition ScaleOrigin ScalePadding ScaleRanges ScaleRangeStyle ScalingFunctions ScalingMatrix ScalingTransform Scan ScheduledTask ScheduledTaskActiveQ ScheduledTaskInformation ScheduledTaskInformationData ScheduledTaskObject ScheduledTasks SchurDecomposition ScientificForm ScientificNotationThreshold ScorerGi ScorerGiPrime ScorerHi ScorerHiPrime ScreenRectangle ScreenStyleEnvironment ScriptBaselineShifts ScriptForm ScriptLevel ScriptMinSize ScriptRules ScriptSizeMultipliers Scrollbars ScrollingOptions ScrollPosition SearchAdjustment SearchIndexObject SearchIndices SearchQueryString SearchResultObject Sec Sech SechDistribution SecondOrderConeOptimization SectionGrouping SectorChart SectorChart3D SectorOrigin SectorSpacing SecuredAuthenticationKey SecuredAuthenticationKeys SeedRandom Select Selectable SelectComponents SelectedCells SelectedNotebook SelectFirst Selection SelectionAnimate SelectionCell SelectionCellCreateCell SelectionCellDefaultStyle SelectionCellParentStyle SelectionCreateCell SelectionDebuggerTag SelectionDuplicateCell SelectionEvaluate SelectionEvaluateCreateCell SelectionMove SelectionPlaceholder SelectionSetStyle SelectWithContents SelfLoops SelfLoopStyle SemanticImport SemanticImportString SemanticInterpretation SemialgebraicComponentInstances SemidefiniteOptimization SendMail SendMessage Sequence SequenceAlignment SequenceAttentionLayer SequenceCases SequenceCount SequenceFold SequenceFoldList SequenceForm SequenceHold SequenceLastLayer SequenceMostLayer SequencePosition SequencePredict SequencePredictorFunction SequenceReplace SequenceRestLayer SequenceReverseLayer SequenceSplit Series SeriesCoefficient SeriesData ServiceConnect ServiceDisconnect ServiceExecute ServiceObject ServiceRequest ServiceResponse ServiceSubmit SessionSubmit SessionTime Set SetAccuracy SetAlphaChannel SetAttributes Setbacks SetBoxFormNamesPacket SetCloudDirectory SetCookies SetDelayed SetDirectory SetEnvironment SetEvaluationNotebook SetFileDate SetFileLoadingContext SetNotebookStatusLine SetOptions SetOptionsPacket SetPermissions SetPrecision SetProperty SetSecuredAuthenticationKey SetSelectedNotebook SetSharedFunction SetSharedVariable SetSpeechParametersPacket SetStreamPosition SetSystemModel SetSystemOptions Setter SetterBar SetterBox SetterBoxOptions Setting SetUsers SetValue Shading Shallow ShannonWavelet ShapiroWilkTest Share SharingList Sharpen ShearingMatrix ShearingTransform ShellRegion ShenCastanMatrix ShiftedGompertzDistribution ShiftRegisterSequence Short ShortDownArrow Shortest ShortestMatch ShortestPathFunction ShortLeftArrow ShortRightArrow ShortTimeFourier ShortTimeFourierData ShortUpArrow Show ShowAutoConvert ShowAutoSpellCheck ShowAutoStyles ShowCellBracket ShowCellLabel ShowCellTags ShowClosedCellArea ShowCodeAssist ShowContents ShowControls ShowCursorTracker ShowGroupOpenCloseIcon ShowGroupOpener ShowInvisibleCharacters ShowPageBreaks ShowPredictiveInterface ShowSelection ShowShortBoxForm ShowSpecialCharacters ShowStringCharacters ShowSyntaxStyles ShrinkingDelay ShrinkWrapBoundingBox SiderealTime SiegelTheta SiegelTukeyTest SierpinskiCurve SierpinskiMesh Sign Signature SignedRankTest SignedRegionDistance SignificanceLevel SignPadding SignTest SimilarityRules SimpleGraph SimpleGraphQ SimplePolygonQ SimplePolyhedronQ Simplex Simplify Sin Sinc SinghMaddalaDistribution SingleEvaluation SingleLetterItalics SingleLetterStyle SingularValueDecomposition SingularValueList SingularValuePlot SingularValues Sinh SinhIntegral SinIntegral SixJSymbol Skeleton SkeletonTransform SkellamDistribution Skewness SkewNormalDistribution SkinStyle Skip SliceContourPlot3D SliceDensityPlot3D SliceDistribution SliceVectorPlot3D Slider Slider2D Slider2DBox Slider2DBoxOptions SliderBox SliderBoxOptions SlideView Slot SlotSequence Small SmallCircle Smaller SmithDecomposition SmithDelayCompensator SmithWatermanSimilarity SmoothDensityHistogram SmoothHistogram SmoothHistogram3D SmoothKernelDistribution SnDispersion Snippet SnubPolyhedron SocialMediaData Socket SocketConnect SocketListen SocketListener SocketObject SocketOpen SocketReadMessage SocketReadyQ Sockets SocketWaitAll SocketWaitNext SoftmaxLayer SokalSneathDissimilarity SolarEclipse SolarSystemFeatureData SolidAngle SolidData SolidRegionQ Solve SolveAlways SolveDelayed Sort SortBy SortedBy SortedEntityClass Sound SoundAndGraphics SoundNote SoundVolume SourceLink Sow Space SpaceCurveData SpaceForm Spacer Spacings Span SpanAdjustments SpanCharacterRounding SpanFromAbove SpanFromBoth SpanFromLeft SpanLineThickness SpanMaxSize SpanMinSize SpanningCharacters SpanSymmetric SparseArray SpatialGraphDistribution SpatialMedian SpatialTransformationLayer Speak SpeakTextPacket SpearmanRankTest SpearmanRho SpeciesData SpecificityGoal SpectralLineData Spectrogram SpectrogramArray Specularity SpeechRecognize SpeechSynthesize SpellingCorrection SpellingCorrectionList SpellingDictionaries SpellingDictionariesPath SpellingOptions SpellingSuggestionsPacket Sphere SphereBox SpherePoints SphericalBesselJ SphericalBesselY SphericalHankelH1 SphericalHankelH2 SphericalHarmonicY SphericalPlot3D SphericalRegion SphericalShell SpheroidalEigenvalue SpheroidalJoiningFactor SpheroidalPS SpheroidalPSPrime SpheroidalQS SpheroidalQSPrime SpheroidalRadialFactor SpheroidalS1 SpheroidalS1Prime SpheroidalS2 SpheroidalS2Prime Splice SplicedDistribution SplineClosed SplineDegree SplineKnots SplineWeights Split SplitBy SpokenString Sqrt SqrtBox SqrtBoxOptions Square SquaredEuclideanDistance SquareFreeQ SquareIntersection SquareMatrixQ SquareRepeatingElement SquaresR SquareSubset SquareSubsetEqual SquareSuperset SquareSupersetEqual SquareUnion SquareWave SSSTriangle StabilityMargins StabilityMarginsStyle StableDistribution Stack StackBegin StackComplete StackedDateListPlot StackedListPlot StackInhibit StadiumShape StandardAtmosphereData StandardDeviation StandardDeviationFilter StandardForm Standardize Standardized StandardOceanData StandbyDistribution Star StarClusterData StarData StarGraph StartAsynchronousTask StartExternalSession StartingStepSize StartOfLine StartOfString StartProcess StartScheduledTask StartupSound StartWebSession StateDimensions StateFeedbackGains StateOutputEstimator StateResponse StateSpaceModel StateSpaceRealization StateSpaceTransform StateTransformationLinearize StationaryDistribution StationaryWaveletPacketTransform StationaryWaveletTransform StatusArea StatusCentrality StepMonitor StereochemistryElements StieltjesGamma StirlingS1 StirlingS2 StopAsynchronousTask StoppingPowerData StopScheduledTask StrataVariables StratonovichProcess StreamColorFunction StreamColorFunctionScaling StreamDensityPlot StreamMarkers StreamPlot StreamPoints StreamPosition Streams StreamScale StreamStyle String StringBreak StringByteCount StringCases StringContainsQ StringCount StringDelete StringDrop StringEndsQ StringExpression StringExtract StringForm StringFormat StringFreeQ StringInsert StringJoin StringLength StringMatchQ StringPadLeft StringPadRight StringPart StringPartition StringPosition StringQ StringRepeat StringReplace StringReplaceList StringReplacePart StringReverse StringRiffle StringRotateLeft StringRotateRight StringSkeleton StringSplit StringStartsQ StringTake StringTemplate StringToByteArray StringToStream StringTrim StripBoxes StripOnInput StripWrapperBoxes StrokeForm StructuralImportance StructuredArray StructuredSelection StruveH StruveL Stub StudentTDistribution Style StyleBox StyleBoxAutoDelete StyleData StyleDefinitions StyleForm StyleHints StyleKeyMapping StyleMenuListing StyleNameDialogSettings StyleNames StylePrint StyleSheetPath Subdivide Subfactorial Subgraph SubMinus SubPlus SubresultantPolynomialRemainders SubresultantPolynomials Subresultants Subscript SubscriptBox SubscriptBoxOptions Subscripted Subsequences Subset SubsetEqual SubsetMap SubsetQ Subsets SubStar SubstitutionSystem Subsuperscript SubsuperscriptBox SubsuperscriptBoxOptions Subtract SubtractFrom SubtractSides SubValues Succeeds SucceedsEqual SucceedsSlantEqual SucceedsTilde Success SuchThat Sum SumConvergence SummationLayer Sunday SunPosition Sunrise Sunset SuperDagger SuperMinus SupernovaData SuperPlus Superscript SuperscriptBox SuperscriptBoxOptions Superset SupersetEqual SuperStar Surd SurdForm SurfaceArea SurfaceColor SurfaceData SurfaceGraphics SurvivalDistribution SurvivalFunction SurvivalModel SurvivalModelFit SuspendPacket SuzukiDistribution SuzukiGroupSuz SwatchLegend Switch Symbol SymbolName SymletWavelet Symmetric SymmetricGroup SymmetricKey SymmetricMatrixQ SymmetricPolynomial SymmetricReduction Symmetrize SymmetrizedArray SymmetrizedArrayRules SymmetrizedDependentComponents SymmetrizedIndependentComponents SymmetrizedReplacePart SynchronousInitialization SynchronousUpdating Synonyms Syntax SyntaxForm SyntaxInformation SyntaxLength SyntaxPacket SyntaxQ SynthesizeMissingValues SystemDialogInput SystemException SystemGet SystemHelpPath SystemInformation SystemInformationData SystemInstall SystemModel SystemModeler SystemModelExamples SystemModelLinearize SystemModelParametricSimulate SystemModelPlot SystemModelProgressReporting SystemModelReliability SystemModels SystemModelSimulate SystemModelSimulateSensitivity SystemModelSimulationData SystemOpen SystemOptions SystemProcessData SystemProcesses SystemsConnectionsModel SystemsModelDelay SystemsModelDelayApproximate SystemsModelDelete SystemsModelDimensions SystemsModelExtract SystemsModelFeedbackConnect SystemsModelLabels SystemsModelLinearity SystemsModelMerge SystemsModelOrder SystemsModelParallelConnect SystemsModelSeriesConnect SystemsModelStateFeedbackConnect SystemsModelVectorRelativeOrders SystemStub SystemTest' + 'Tab TabFilling Table TableAlignments TableDepth TableDirections TableForm TableHeadings TableSpacing TableView TableViewBox TableViewBoxBackground TableViewBoxOptions TabSpacings TabView TabViewBox TabViewBoxOptions TagBox TagBoxNote TagBoxOptions TaggingRules TagSet TagSetDelayed TagStyle TagUnset Take TakeDrop TakeLargest TakeLargestBy TakeList TakeSmallest TakeSmallestBy TakeWhile Tally Tan Tanh TargetDevice TargetFunctions TargetSystem TargetUnits TaskAbort TaskExecute TaskObject TaskRemove TaskResume Tasks TaskSuspend TaskWait TautologyQ TelegraphProcess TemplateApply TemplateArgBox TemplateBox TemplateBoxOptions TemplateEvaluate TemplateExpression TemplateIf TemplateObject TemplateSequence TemplateSlot TemplateSlotSequence TemplateUnevaluated TemplateVerbatim TemplateWith TemporalData TemporalRegularity Temporary TemporaryVariable TensorContract TensorDimensions TensorExpand TensorProduct TensorQ TensorRank TensorReduce TensorSymmetry TensorTranspose TensorWedge TestID TestReport TestReportObject TestResultObject Tetrahedron TetrahedronBox TetrahedronBoxOptions TeXForm TeXSave Text Text3DBox Text3DBoxOptions TextAlignment TextBand TextBoundingBox TextBox TextCases TextCell TextClipboardType TextContents TextData TextElement TextForm TextGrid TextJustification TextLine TextPacket TextParagraph TextPosition TextRecognize TextSearch TextSearchReport TextSentences TextString TextStructure TextStyle TextTranslation Texture TextureCoordinateFunction TextureCoordinateScaling TextWords Therefore ThermodynamicData ThermometerGauge Thick Thickness Thin Thinning ThisLink ThompsonGroupTh Thread ThreadingLayer ThreeJSymbol Threshold Through Throw ThueMorse Thumbnail Thursday Ticks TicksStyle TideData Tilde TildeEqual TildeFullEqual TildeTilde TimeConstrained TimeConstraint TimeDirection TimeFormat TimeGoal TimelinePlot TimeObject TimeObjectQ Times TimesBy TimeSeries TimeSeriesAggregate TimeSeriesForecast TimeSeriesInsert TimeSeriesInvertibility TimeSeriesMap TimeSeriesMapThread TimeSeriesModel TimeSeriesModelFit TimeSeriesResample TimeSeriesRescale TimeSeriesShift TimeSeriesThread TimeSeriesWindow TimeUsed TimeValue TimeWarpingCorrespondence TimeWarpingDistance TimeZone TimeZoneConvert TimeZoneOffset Timing Tiny TitleGrouping TitsGroupT ToBoxes ToCharacterCode ToColor ToContinuousTimeModel ToDate Today ToDiscreteTimeModel ToEntity ToeplitzMatrix ToExpression ToFileName Together Toggle ToggleFalse Toggler TogglerBar TogglerBox TogglerBoxOptions ToHeldExpression ToInvertibleTimeSeries TokenWords Tolerance ToLowerCase Tomorrow ToNumberField TooBig Tooltip TooltipBox TooltipBoxOptions TooltipDelay TooltipStyle Top TopHatTransform ToPolarCoordinates TopologicalSort ToRadicals ToRules ToSphericalCoordinates ToString Total TotalHeight TotalLayer TotalVariationFilter TotalWidth TouchPosition TouchscreenAutoZoom TouchscreenControlPlacement ToUpperCase Tr Trace TraceAbove TraceAction TraceBackward TraceDepth TraceDialog TraceForward TraceInternal TraceLevel TraceOff TraceOn TraceOriginal TracePrint TraceScan TrackedSymbols TrackingFunction TracyWidomDistribution TradingChart TraditionalForm TraditionalFunctionNotation TraditionalNotation TraditionalOrder TrainingProgressCheckpointing TrainingProgressFunction TrainingProgressMeasurements TrainingProgressReporting TrainingStoppingCriterion TransferFunctionCancel TransferFunctionExpand TransferFunctionFactor TransferFunctionModel TransferFunctionPoles TransferFunctionTransform TransferFunctionZeros TransformationClass TransformationFunction TransformationFunctions TransformationMatrix TransformedDistribution TransformedField TransformedProcess TransformedRegion TransitionDirection TransitionDuration TransitionEffect TransitiveClosureGraph TransitiveReductionGraph Translate TranslationOptions TranslationTransform Transliterate Transparent TransparentColor Transpose TransposeLayer TrapSelection TravelDirections TravelDirectionsData TravelDistance TravelDistanceList TravelMethod TravelTime TreeForm TreeGraph TreeGraphQ TreePlot TrendStyle Triangle TriangleCenter TriangleConstruct TriangleMeasurement TriangleWave TriangularDistribution TriangulateMesh Trig TrigExpand TrigFactor TrigFactorList Trigger TrigReduce TrigToExp TrimmedMean TrimmedVariance TropicalStormData True TrueQ TruncatedDistribution TruncatedPolyhedron TsallisQExponentialDistribution TsallisQGaussianDistribution TTest Tube TubeBezierCurveBox TubeBezierCurveBoxOptions TubeBox TubeBoxOptions TubeBSplineCurveBox TubeBSplineCurveBoxOptions Tuesday TukeyLambdaDistribution TukeyWindow TunnelData Tuples TuranGraph TuringMachine TuttePolynomial TwoWayRule Typed TypeSpecifier' + 'UnateQ Uncompress UnconstrainedParameters Undefined UnderBar Underflow Underlined Underoverscript UnderoverscriptBox UnderoverscriptBoxOptions Underscript UnderscriptBox UnderscriptBoxOptions UnderseaFeatureData UndirectedEdge UndirectedGraph UndirectedGraphQ UndoOptions UndoTrackedVariables Unequal UnequalTo Unevaluated UniformDistribution UniformGraphDistribution UniformPolyhedron UniformSumDistribution Uninstall Union UnionPlus Unique UnitaryMatrixQ UnitBox UnitConvert UnitDimensions Unitize UnitRootTest UnitSimplify UnitStep UnitSystem UnitTriangle UnitVector UnitVectorLayer UnityDimensions UniverseModelData UniversityData UnixTime Unprotect UnregisterExternalEvaluator UnsameQ UnsavedVariables Unset UnsetShared UntrackedVariables Up UpArrow UpArrowBar UpArrowDownArrow Update UpdateDynamicObjects UpdateDynamicObjectsSynchronous UpdateInterval UpdateSearchIndex UpDownArrow UpEquilibrium UpperCaseQ UpperLeftArrow UpperRightArrow UpperTriangularize UpperTriangularMatrixQ Upsample UpSet UpSetDelayed UpTee UpTeeArrow UpTo UpValues URL URLBuild URLDecode URLDispatcher URLDownload URLDownloadSubmit URLEncode URLExecute URLExpand URLFetch URLFetchAsynchronous URLParse URLQueryDecode URLQueryEncode URLRead URLResponseTime URLSave URLSaveAsynchronous URLShorten URLSubmit UseGraphicsRange UserDefinedWavelet Using UsingFrontEnd UtilityFunction' + 'V2Get ValenceErrorHandling ValidationLength ValidationSet Value ValueBox ValueBoxOptions ValueDimensions ValueForm ValuePreprocessingFunction ValueQ Values ValuesData Variables Variance VarianceEquivalenceTest VarianceEstimatorFunction VarianceGammaDistribution VarianceTest VectorAngle VectorAround VectorColorFunction VectorColorFunctionScaling VectorDensityPlot VectorGlyphData VectorGreater VectorGreaterEqual VectorLess VectorLessEqual VectorMarkers VectorPlot VectorPlot3D VectorPoints VectorQ Vectors VectorScale VectorStyle Vee Verbatim Verbose VerboseConvertToPostScriptPacket VerificationTest VerifyConvergence VerifyDerivedKey VerifyDigitalSignature VerifyInterpretation VerifySecurityCertificates VerifySolutions VerifyTestAssumptions Version VersionNumber VertexAdd VertexCapacity VertexColors VertexComponent VertexConnectivity VertexContract VertexCoordinateRules VertexCoordinates VertexCorrelationSimilarity VertexCosineSimilarity VertexCount VertexCoverQ VertexDataCoordinates VertexDegree VertexDelete VertexDiceSimilarity VertexEccentricity VertexInComponent VertexInDegree VertexIndex VertexJaccardSimilarity VertexLabeling VertexLabels VertexLabelStyle VertexList VertexNormals VertexOutComponent VertexOutDegree VertexQ VertexRenderingFunction VertexReplace VertexShape VertexShapeFunction VertexSize VertexStyle VertexTextureCoordinates VertexWeight VertexWeightedGraphQ Vertical VerticalBar VerticalForm VerticalGauge VerticalSeparator VerticalSlider VerticalTilde ViewAngle ViewCenter ViewMatrix ViewPoint ViewPointSelectorSettings ViewPort ViewProjection ViewRange ViewVector ViewVertical VirtualGroupData Visible VisibleCell VoiceStyleData VoigtDistribution VolcanoData Volume VonMisesDistribution VoronoiMesh' + 'WaitAll WaitAsynchronousTask WaitNext WaitUntil WakebyDistribution WalleniusHypergeometricDistribution WaringYuleDistribution WarpingCorrespondence WarpingDistance WatershedComponents WatsonUSquareTest WattsStrogatzGraphDistribution WaveletBestBasis WaveletFilterCoefficients WaveletImagePlot WaveletListPlot WaveletMapIndexed WaveletMatrixPlot WaveletPhi WaveletPsi WaveletScale WaveletScalogram WaveletThreshold WeaklyConnectedComponents WeaklyConnectedGraphComponents WeaklyConnectedGraphQ WeakStationarity WeatherData WeatherForecastData WebAudioSearch WebElementObject WeberE WebExecute WebImage WebImageSearch WebSearch WebSessionObject WebSessions WebWindowObject Wedge Wednesday WeibullDistribution WeierstrassE1 WeierstrassE2 WeierstrassE3 WeierstrassEta1 WeierstrassEta2 WeierstrassEta3 WeierstrassHalfPeriods WeierstrassHalfPeriodW1 WeierstrassHalfPeriodW2 WeierstrassHalfPeriodW3 WeierstrassInvariantG2 WeierstrassInvariantG3 WeierstrassInvariants WeierstrassP WeierstrassPPrime WeierstrassSigma WeierstrassZeta WeightedAdjacencyGraph WeightedAdjacencyMatrix WeightedData WeightedGraphQ Weights WelchWindow WheelGraph WhenEvent Which While White WhiteNoiseProcess WhitePoint Whitespace WhitespaceCharacter WhittakerM WhittakerW WienerFilter WienerProcess WignerD WignerSemicircleDistribution WikipediaData WikipediaSearch WilksW WilksWTest WindDirectionData WindingCount WindingPolygon WindowClickSelect WindowElements WindowFloating WindowFrame WindowFrameElements WindowMargins WindowMovable WindowOpacity WindowPersistentStyles WindowSelected WindowSize WindowStatusArea WindowTitle WindowToolbars WindowWidth WindSpeedData WindVectorData WinsorizedMean WinsorizedVariance WishartMatrixDistribution With WolframAlpha WolframAlphaDate WolframAlphaQuantity WolframAlphaResult WolframLanguageData Word WordBoundary WordCharacter WordCloud WordCount WordCounts WordData WordDefinition WordFrequency WordFrequencyData WordList WordOrientation WordSearch WordSelectionFunction WordSeparators WordSpacings WordStem WordTranslation WorkingPrecision WrapAround Write WriteLine WriteString Wronskian' + 'XMLElement XMLObject XMLTemplate Xnor Xor XYZColor' + 'Yellow Yesterday YuleDissimilarity' + 'ZernikeR ZeroSymmetric ZeroTest ZeroWidthTimes Zeta ZetaZero ZIPCodeData ZipfDistribution ZoomCenter ZoomFactor ZTest ZTransform' + '$Aborted $ActivationGroupID $ActivationKey $ActivationUserRegistered $AddOnsDirectory $AllowExternalChannelFunctions $AssertFunction $Assumptions $AsynchronousTask $AudioInputDevices $AudioOutputDevices $BaseDirectory $BatchInput $BatchOutput $BlockchainBase $BoxForms $ByteOrdering $CacheBaseDirectory $Canceled $ChannelBase $CharacterEncoding $CharacterEncodings $CloudBase $CloudConnected $CloudCreditsAvailable $CloudEvaluation $CloudExpressionBase $CloudObjectNameFormat $CloudObjectURLType $CloudRootDirectory $CloudSymbolBase $CloudUserID $CloudUserUUID $CloudVersion $CloudVersionNumber $CloudWolframEngineVersionNumber $CommandLine $CompilationTarget $ConditionHold $ConfiguredKernels $Context $ContextPath $ControlActiveSetting $Cookies $CookieStore $CreationDate $CurrentLink $CurrentTask $CurrentWebSession $DateStringFormat $DefaultAudioInputDevice $DefaultAudioOutputDevice $DefaultFont $DefaultFrontEnd $DefaultImagingDevice $DefaultLocalBase $DefaultMailbox $DefaultNetworkInterface $DefaultPath $Display $DisplayFunction $DistributedContexts $DynamicEvaluation $Echo $EmbedCodeEnvironments $EmbeddableServices $EntityStores $Epilog $EvaluationCloudBase $EvaluationCloudObject $EvaluationEnvironment $ExportFormats $Failed $FinancialDataSource $FontFamilies $FormatType $FrontEnd $FrontEndSession $GeoEntityTypes $GeoLocation $GeoLocationCity $GeoLocationCountry $GeoLocationPrecision $GeoLocationSource $HistoryLength $HomeDirectory $HTMLExportRules $HTTPCookies $HTTPRequest $IgnoreEOF $ImageFormattingWidth $ImagingDevice $ImagingDevices $ImportFormats $IncomingMailSettings $InitialDirectory $Initialization $InitializationContexts $Input $InputFileName $InputStreamMethods $Inspector $InstallationDate $InstallationDirectory $InterfaceEnvironment $InterpreterTypes $IterationLimit $KernelCount $KernelID $Language $LaunchDirectory $LibraryPath $LicenseExpirationDate $LicenseID $LicenseProcesses $LicenseServer $LicenseSubprocesses $LicenseType $Line $Linked $LinkSupported $LoadedFiles $LocalBase $LocalSymbolBase $MachineAddresses $MachineDomain $MachineDomains $MachineEpsilon $MachineID $MachineName $MachinePrecision $MachineType $MaxExtraPrecision $MaxLicenseProcesses $MaxLicenseSubprocesses $MaxMachineNumber $MaxNumber $MaxPiecewiseCases $MaxPrecision $MaxRootDegree $MessageGroups $MessageList $MessagePrePrint $Messages $MinMachineNumber $MinNumber $MinorReleaseNumber $MinPrecision $MobilePhone $ModuleNumber $NetworkConnected $NetworkInterfaces $NetworkLicense $NewMessage $NewSymbol $Notebooks $NoValue $NumberMarks $Off $OperatingSystem $Output $OutputForms $OutputSizeLimit $OutputStreamMethods $Packages $ParentLink $ParentProcessID $PasswordFile $PatchLevelID $Path $PathnameSeparator $PerformanceGoal $Permissions $PermissionsGroupBase $PersistenceBase $PersistencePath $PipeSupported $PlotTheme $Post $Pre $PreferencesDirectory $PreInitialization $PrePrint $PreRead $PrintForms $PrintLiteral $Printout3DPreviewer $ProcessID $ProcessorCount $ProcessorType $ProductInformation $ProgramName $PublisherID $RandomState $RecursionLimit $RegisteredDeviceClasses $RegisteredUserName $ReleaseNumber $RequesterAddress $RequesterWolframID $RequesterWolframUUID $ResourceSystemBase $RootDirectory $ScheduledTask $ScriptCommandLine $ScriptInputString $SecuredAuthenticationKeyTokens $ServiceCreditsAvailable $Services $SessionID $SetParentLink $SharedFunctions $SharedVariables $SoundDisplay $SoundDisplayFunction $SourceLink $SSHAuthentication $SummaryBoxDataSizeLimit $SuppressInputFormHeads $SynchronousEvaluation $SyntaxHandler $System $SystemCharacterEncoding $SystemID $SystemMemory $SystemShell $SystemTimeZone $SystemWordLength $TemplatePath $TemporaryDirectory $TemporaryPrefix $TestFileName $TextStyle $TimedOut $TimeUnit $TimeZone $TimeZoneEntity $TopDirectory $TraceOff $TraceOn $TracePattern $TracePostAction $TracePreAction $UnitSystem $Urgent $UserAddOnsDirectory $UserAgentLanguages $UserAgentMachine $UserAgentName $UserAgentOperatingSystem $UserAgentString $UserAgentVersion $UserBaseDirectory $UserDocumentsDirectory $Username $UserName $UserURLBase $Version $VersionNumber $VoiceStyles $WolframID $WolframUUID', contains: [ hljs.COMMENT('\\(\\*', '\\*\\)', {contains: ['self']}), hljs.QUOTE_STRING_MODE, hljs.C_NUMBER_MODE ] }; } },{name:"matlab",create:/* Language: Matlab Author: Denis Bardadym Contributors: Eugene Nizhibitsky , Egor Rogov Category: scientific */ /* Formal syntax is not published, helpful link: https://github.com/kornilova-l/matlab-IntelliJ-plugin/blob/master/src/main/grammar/Matlab.bnf */ function(hljs) { var TRANSPOSE_RE = '(\'|\\.\')+'; var TRANSPOSE = { relevance: 0, contains: [ { begin: TRANSPOSE_RE } ] }; return { keywords: { keyword: 'break case catch classdef continue else elseif end enumerated events for function ' + 'global if methods otherwise parfor persistent properties return spmd switch try while', built_in: 'sin sind sinh asin asind asinh cos cosd cosh acos acosd acosh tan tand tanh atan ' + 'atand atan2 atanh sec secd sech asec asecd asech csc cscd csch acsc acscd acsch cot ' + 'cotd coth acot acotd acoth hypot exp expm1 log log1p log10 log2 pow2 realpow reallog ' + 'realsqrt sqrt nthroot nextpow2 abs angle complex conj imag real unwrap isreal ' + 'cplxpair fix floor ceil round mod rem sign airy besselj bessely besselh besseli ' + 'besselk beta betainc betaln ellipj ellipke erf erfc erfcx erfinv expint gamma ' + 'gammainc gammaln psi legendre cross dot factor isprime primes gcd lcm rat rats perms ' + 'nchoosek factorial cart2sph cart2pol pol2cart sph2cart hsv2rgb rgb2hsv zeros ones ' + 'eye repmat rand randn linspace logspace freqspace meshgrid accumarray size length ' + 'ndims numel disp isempty isequal isequalwithequalnans cat reshape diag blkdiag tril ' + 'triu fliplr flipud flipdim rot90 find sub2ind ind2sub bsxfun ndgrid permute ipermute ' + 'shiftdim circshift squeeze isscalar isvector ans eps realmax realmin pi i inf nan ' + 'isnan isinf isfinite j why compan gallery hadamard hankel hilb invhilb magic pascal ' + 'rosser toeplitz vander wilkinson max min nanmax nanmin mean nanmean type table ' + 'readtable writetable sortrows sort figure plot plot3 scatter scatter3 cellfun ' + 'legend intersect ismember procrustes hold num2cell ' }, illegal: '(//|"|#|/\\*|\\s+/\\w+)', contains: [ { className: 'function', beginKeywords: 'function', end: '$', contains: [ hljs.UNDERSCORE_TITLE_MODE, { className: 'params', variants: [ {begin: '\\(', end: '\\)'}, {begin: '\\[', end: '\\]'} ] } ] }, { className: 'built_in', begin: /true|false/, relevance: 0, starts: TRANSPOSE }, { begin: '[a-zA-Z][a-zA-Z_0-9]*' + TRANSPOSE_RE, relevance: 0 }, { className: 'number', begin: hljs.C_NUMBER_RE, relevance: 0, starts: TRANSPOSE }, { className: 'string', begin: '\'', end: '\'', contains: [ hljs.BACKSLASH_ESCAPE, {begin: '\'\''}] }, { begin: /\]|}|\)/, relevance: 0, starts: TRANSPOSE }, { className: 'string', begin: '"', end: '"', contains: [ hljs.BACKSLASH_ESCAPE, {begin: '""'} ], starts: TRANSPOSE }, hljs.COMMENT('^\\s*\\%\\{\\s*$', '^\\s*\\%\\}\\s*$'), hljs.COMMENT('\\%', '$') ] }; } },{name:"maxima",create:/* Language: Maxima Author: Robert Dodier Category: scientific */ function(hljs) { var KEYWORDS = 'if then else elseif for thru do while unless step in and or not'; var LITERALS = 'true false unknown inf minf ind und %e %i %pi %phi %gamma'; var BUILTIN_FUNCTIONS = ' abasep abs absint absolute_real_time acos acosh acot acoth acsc acsch activate' + ' addcol add_edge add_edges addmatrices addrow add_vertex add_vertices adjacency_matrix' + ' adjoin adjoint af agd airy airy_ai airy_bi airy_dai airy_dbi algsys alg_type' + ' alias allroots alphacharp alphanumericp amortization %and annuity_fv' + ' annuity_pv antid antidiff AntiDifference append appendfile apply apply1 apply2' + ' applyb1 apropos args arit_amortization arithmetic arithsum array arrayapply' + ' arrayinfo arraymake arraysetapply ascii asec asech asin asinh askinteger' + ' asksign assoc assoc_legendre_p assoc_legendre_q assume assume_external_byte_order' + ' asympa at atan atan2 atanh atensimp atom atvalue augcoefmatrix augmented_lagrangian_method' + ' av average_degree backtrace bars barsplot barsplot_description base64 base64_decode' + ' bashindices batch batchload bc2 bdvac belln benefit_cost bern bernpoly bernstein_approx' + ' bernstein_expand bernstein_poly bessel bessel_i bessel_j bessel_k bessel_simplify' + ' bessel_y beta beta_incomplete beta_incomplete_generalized beta_incomplete_regularized' + ' bezout bfallroots bffac bf_find_root bf_fmin_cobyla bfhzeta bfloat bfloatp' + ' bfpsi bfpsi0 bfzeta biconnected_components bimetric binomial bipartition' + ' block blockmatrixp bode_gain bode_phase bothcoef box boxplot boxplot_description' + ' break bug_report build_info|10 buildq build_sample burn cabs canform canten' + ' cardinality carg cartan cartesian_product catch cauchy_matrix cbffac cdf_bernoulli' + ' cdf_beta cdf_binomial cdf_cauchy cdf_chi2 cdf_continuous_uniform cdf_discrete_uniform' + ' cdf_exp cdf_f cdf_gamma cdf_general_finite_discrete cdf_geometric cdf_gumbel' + ' cdf_hypergeometric cdf_laplace cdf_logistic cdf_lognormal cdf_negative_binomial' + ' cdf_noncentral_chi2 cdf_noncentral_student_t cdf_normal cdf_pareto cdf_poisson' + ' cdf_rank_sum cdf_rayleigh cdf_signed_rank cdf_student_t cdf_weibull cdisplay' + ' ceiling central_moment cequal cequalignore cf cfdisrep cfexpand cgeodesic' + ' cgreaterp cgreaterpignore changename changevar chaosgame charat charfun charfun2' + ' charlist charp charpoly chdir chebyshev_t chebyshev_u checkdiv check_overlaps' + ' chinese cholesky christof chromatic_index chromatic_number cint circulant_graph' + ' clear_edge_weight clear_rules clear_vertex_label clebsch_gordan clebsch_graph' + ' clessp clesspignore close closefile cmetric coeff coefmatrix cograd col collapse' + ' collectterms columnop columnspace columnswap columnvector combination combine' + ' comp2pui compare compfile compile compile_file complement_graph complete_bipartite_graph' + ' complete_graph complex_number_p components compose_functions concan concat' + ' conjugate conmetderiv connected_components connect_vertices cons constant' + ' constantp constituent constvalue cont2part content continuous_freq contortion' + ' contour_plot contract contract_edge contragrad contrib_ode convert coord' + ' copy copy_file copy_graph copylist copymatrix cor cos cosh cot coth cov cov1' + ' covdiff covect covers crc24sum create_graph create_list csc csch csetup cspline' + ' ctaylor ct_coordsys ctransform ctranspose cube_graph cuboctahedron_graph' + ' cunlisp cv cycle_digraph cycle_graph cylindrical days360 dblint deactivate' + ' declare declare_constvalue declare_dimensions declare_fundamental_dimensions' + ' declare_fundamental_units declare_qty declare_translated declare_unit_conversion' + ' declare_units declare_weights decsym defcon define define_alt_display define_variable' + ' defint defmatch defrule defstruct deftaylor degree_sequence del delete deleten' + ' delta demo demoivre denom depends derivdegree derivlist describe desolve' + ' determinant dfloat dgauss_a dgauss_b dgeev dgemm dgeqrf dgesv dgesvd diag' + ' diagmatrix diag_matrix diagmatrixp diameter diff digitcharp dimacs_export' + ' dimacs_import dimension dimensionless dimensions dimensions_as_list direct' + ' directory discrete_freq disjoin disjointp disolate disp dispcon dispform' + ' dispfun dispJordan display disprule dispterms distrib divide divisors divsum' + ' dkummer_m dkummer_u dlange dodecahedron_graph dotproduct dotsimp dpart' + ' draw draw2d draw3d drawdf draw_file draw_graph dscalar echelon edge_coloring' + ' edge_connectivity edges eigens_by_jacobi eigenvalues eigenvectors eighth' + ' einstein eivals eivects elapsed_real_time elapsed_run_time ele2comp ele2polynome' + ' ele2pui elem elementp elevation_grid elim elim_allbut eliminate eliminate_using' + ' ellipse elliptic_e elliptic_ec elliptic_eu elliptic_f elliptic_kc elliptic_pi' + ' ematrix empty_graph emptyp endcons entermatrix entertensor entier equal equalp' + ' equiv_classes erf erfc erf_generalized erfi errcatch error errormsg errors' + ' euler ev eval_string evenp every evolution evolution2d evundiff example exp' + ' expand expandwrt expandwrt_factored expint expintegral_chi expintegral_ci' + ' expintegral_e expintegral_e1 expintegral_ei expintegral_e_simplify expintegral_li' + ' expintegral_shi expintegral_si explicit explose exponentialize express expt' + ' exsec extdiff extract_linear_equations extremal_subset ezgcd %f f90 facsum' + ' factcomb factor factorfacsum factorial factorout factorsum facts fast_central_elements' + ' fast_linsolve fasttimes featurep fernfale fft fib fibtophi fifth filename_merge' + ' file_search file_type fillarray findde find_root find_root_abs find_root_error' + ' find_root_rel first fix flatten flength float floatnump floor flower_snark' + ' flush flush1deriv flushd flushnd flush_output fmin_cobyla forget fortran' + ' fourcos fourexpand fourier fourier_elim fourint fourintcos fourintsin foursimp' + ' foursin fourth fposition frame_bracket freeof freshline fresnel_c fresnel_s' + ' from_adjacency_matrix frucht_graph full_listify fullmap fullmapl fullratsimp' + ' fullratsubst fullsetify funcsolve fundamental_dimensions fundamental_units' + ' fundef funmake funp fv g0 g1 gamma gamma_greek gamma_incomplete gamma_incomplete_generalized' + ' gamma_incomplete_regularized gauss gauss_a gauss_b gaussprob gcd gcdex gcdivide' + ' gcfac gcfactor gd generalized_lambert_w genfact gen_laguerre genmatrix gensym' + ' geo_amortization geo_annuity_fv geo_annuity_pv geomap geometric geometric_mean' + ' geosum get getcurrentdirectory get_edge_weight getenv get_lu_factors get_output_stream_string' + ' get_pixel get_plot_option get_tex_environment get_tex_environment_default' + ' get_vertex_label gfactor gfactorsum ggf girth global_variances gn gnuplot_close' + ' gnuplot_replot gnuplot_reset gnuplot_restart gnuplot_start go Gosper GosperSum' + ' gr2d gr3d gradef gramschmidt graph6_decode graph6_encode graph6_export graph6_import' + ' graph_center graph_charpoly graph_eigenvalues graph_flow graph_order graph_periphery' + ' graph_product graph_size graph_union great_rhombicosidodecahedron_graph great_rhombicuboctahedron_graph' + ' grid_graph grind grobner_basis grotzch_graph hamilton_cycle hamilton_path' + ' hankel hankel_1 hankel_2 harmonic harmonic_mean hav heawood_graph hermite' + ' hessian hgfred hilbertmap hilbert_matrix hipow histogram histogram_description' + ' hodge horner hypergeometric i0 i1 %ibes ic1 ic2 ic_convert ichr1 ichr2 icosahedron_graph' + ' icosidodecahedron_graph icurvature ident identfor identity idiff idim idummy' + ' ieqn %if ifactors iframes ifs igcdex igeodesic_coords ilt image imagpart' + ' imetric implicit implicit_derivative implicit_plot indexed_tensor indices' + ' induced_subgraph inferencep inference_result infix info_display init_atensor' + ' init_ctensor in_neighbors innerproduct inpart inprod inrt integerp integer_partitions' + ' integrate intersect intersection intervalp intopois intosum invariant1 invariant2' + ' inverse_fft inverse_jacobi_cd inverse_jacobi_cn inverse_jacobi_cs inverse_jacobi_dc' + ' inverse_jacobi_dn inverse_jacobi_ds inverse_jacobi_nc inverse_jacobi_nd inverse_jacobi_ns' + ' inverse_jacobi_sc inverse_jacobi_sd inverse_jacobi_sn invert invert_by_adjoint' + ' invert_by_lu inv_mod irr is is_biconnected is_bipartite is_connected is_digraph' + ' is_edge_in_graph is_graph is_graph_or_digraph ishow is_isomorphic isolate' + ' isomorphism is_planar isqrt isreal_p is_sconnected is_tree is_vertex_in_graph' + ' items_inference %j j0 j1 jacobi jacobian jacobi_cd jacobi_cn jacobi_cs jacobi_dc' + ' jacobi_dn jacobi_ds jacobi_nc jacobi_nd jacobi_ns jacobi_p jacobi_sc jacobi_sd' + ' jacobi_sn JF jn join jordan julia julia_set julia_sin %k kdels kdelta kill' + ' killcontext kostka kron_delta kronecker_product kummer_m kummer_u kurtosis' + ' kurtosis_bernoulli kurtosis_beta kurtosis_binomial kurtosis_chi2 kurtosis_continuous_uniform' + ' kurtosis_discrete_uniform kurtosis_exp kurtosis_f kurtosis_gamma kurtosis_general_finite_discrete' + ' kurtosis_geometric kurtosis_gumbel kurtosis_hypergeometric kurtosis_laplace' + ' kurtosis_logistic kurtosis_lognormal kurtosis_negative_binomial kurtosis_noncentral_chi2' + ' kurtosis_noncentral_student_t kurtosis_normal kurtosis_pareto kurtosis_poisson' + ' kurtosis_rayleigh kurtosis_student_t kurtosis_weibull label labels lagrange' + ' laguerre lambda lambert_w laplace laplacian_matrix last lbfgs lc2kdt lcharp' + ' lc_l lcm lc_u ldefint ldisp ldisplay legendre_p legendre_q leinstein length' + ' let letrules letsimp levi_civita lfreeof lgtreillis lhs li liediff limit' + ' Lindstedt linear linearinterpol linear_program linear_regression line_graph' + ' linsolve listarray list_correlations listify list_matrix_entries list_nc_monomials' + ' listoftens listofvars listp lmax lmin load loadfile local locate_matrix_entry' + ' log logcontract log_gamma lopow lorentz_gauge lowercasep lpart lratsubst' + ' lreduce lriemann lsquares_estimates lsquares_estimates_approximate lsquares_estimates_exact' + ' lsquares_mse lsquares_residual_mse lsquares_residuals lsum ltreillis lu_backsub' + ' lucas lu_factor %m macroexpand macroexpand1 make_array makebox makefact makegamma' + ' make_graph make_level_picture makelist makeOrders make_poly_continent make_poly_country' + ' make_polygon make_random_state make_rgb_picture makeset make_string_input_stream' + ' make_string_output_stream make_transform mandelbrot mandelbrot_set map mapatom' + ' maplist matchdeclare matchfix mat_cond mat_fullunblocker mat_function mathml_display' + ' mat_norm matrix matrixmap matrixp matrix_size mattrace mat_trace mat_unblocker' + ' max max_clique max_degree max_flow maximize_lp max_independent_set max_matching' + ' maybe md5sum mean mean_bernoulli mean_beta mean_binomial mean_chi2 mean_continuous_uniform' + ' mean_deviation mean_discrete_uniform mean_exp mean_f mean_gamma mean_general_finite_discrete' + ' mean_geometric mean_gumbel mean_hypergeometric mean_laplace mean_logistic' + ' mean_lognormal mean_negative_binomial mean_noncentral_chi2 mean_noncentral_student_t' + ' mean_normal mean_pareto mean_poisson mean_rayleigh mean_student_t mean_weibull' + ' median median_deviation member mesh metricexpandall mgf1_sha1 min min_degree' + ' min_edge_cut minfactorial minimalPoly minimize_lp minimum_spanning_tree minor' + ' minpack_lsquares minpack_solve min_vertex_cover min_vertex_cut mkdir mnewton' + ' mod mode_declare mode_identity ModeMatrix moebius mon2schur mono monomial_dimensions' + ' multibernstein_poly multi_display_for_texinfo multi_elem multinomial multinomial_coeff' + ' multi_orbit multiplot_mode multi_pui multsym multthru mycielski_graph nary' + ' natural_unit nc_degree ncexpt ncharpoly negative_picture neighbors new newcontext' + ' newdet new_graph newline newton new_variable next_prime nicedummies niceindices' + ' ninth nofix nonarray noncentral_moment nonmetricity nonnegintegerp nonscalarp' + ' nonzeroandfreeof notequal nounify nptetrad npv nroots nterms ntermst' + ' nthroot nullity nullspace num numbered_boundaries numberp number_to_octets' + ' num_distinct_partitions numerval numfactor num_partitions nusum nzeta nzetai' + ' nzetar octets_to_number octets_to_oid odd_girth oddp ode2 ode_check odelin' + ' oid_to_octets op opena opena_binary openr openr_binary openw openw_binary' + ' operatorp opsubst optimize %or orbit orbits ordergreat ordergreatp orderless' + ' orderlessp orthogonal_complement orthopoly_recur orthopoly_weight outermap' + ' out_neighbors outofpois pade parabolic_cylinder_d parametric parametric_surface' + ' parg parGosper parse_string parse_timedate part part2cont partfrac partition' + ' partition_set partpol path_digraph path_graph pathname_directory pathname_name' + ' pathname_type pdf_bernoulli pdf_beta pdf_binomial pdf_cauchy pdf_chi2 pdf_continuous_uniform' + ' pdf_discrete_uniform pdf_exp pdf_f pdf_gamma pdf_general_finite_discrete' + ' pdf_geometric pdf_gumbel pdf_hypergeometric pdf_laplace pdf_logistic pdf_lognormal' + ' pdf_negative_binomial pdf_noncentral_chi2 pdf_noncentral_student_t pdf_normal' + ' pdf_pareto pdf_poisson pdf_rank_sum pdf_rayleigh pdf_signed_rank pdf_student_t' + ' pdf_weibull pearson_skewness permanent permut permutation permutations petersen_graph' + ' petrov pickapart picture_equalp picturep piechart piechart_description planar_embedding' + ' playback plog plot2d plot3d plotdf ploteq plsquares pochhammer points poisdiff' + ' poisexpt poisint poismap poisplus poissimp poissubst poistimes poistrim polar' + ' polarform polartorect polar_to_xy poly_add poly_buchberger poly_buchberger_criterion' + ' poly_colon_ideal poly_content polydecomp poly_depends_p poly_elimination_ideal' + ' poly_exact_divide poly_expand poly_expt poly_gcd polygon poly_grobner poly_grobner_equal' + ' poly_grobner_member poly_grobner_subsetp poly_ideal_intersection poly_ideal_polysaturation' + ' poly_ideal_polysaturation1 poly_ideal_saturation poly_ideal_saturation1 poly_lcm' + ' poly_minimization polymod poly_multiply polynome2ele polynomialp poly_normal_form' + ' poly_normalize poly_normalize_list poly_polysaturation_extension poly_primitive_part' + ' poly_pseudo_divide poly_reduced_grobner poly_reduction poly_saturation_extension' + ' poly_s_polynomial poly_subtract polytocompanion pop postfix potential power_mod' + ' powerseries powerset prefix prev_prime primep primes principal_components' + ' print printf printfile print_graph printpois printprops prodrac product properties' + ' propvars psi psubst ptriangularize pui pui2comp pui2ele pui2polynome pui_direct' + ' puireduc push put pv qput qrange qty quad_control quad_qag quad_qagi quad_qagp' + ' quad_qags quad_qawc quad_qawf quad_qawo quad_qaws quadrilateral quantile' + ' quantile_bernoulli quantile_beta quantile_binomial quantile_cauchy quantile_chi2' + ' quantile_continuous_uniform quantile_discrete_uniform quantile_exp quantile_f' + ' quantile_gamma quantile_general_finite_discrete quantile_geometric quantile_gumbel' + ' quantile_hypergeometric quantile_laplace quantile_logistic quantile_lognormal' + ' quantile_negative_binomial quantile_noncentral_chi2 quantile_noncentral_student_t' + ' quantile_normal quantile_pareto quantile_poisson quantile_rayleigh quantile_student_t' + ' quantile_weibull quartile_skewness quit qunit quotient racah_v racah_w radcan' + ' radius random random_bernoulli random_beta random_binomial random_bipartite_graph' + ' random_cauchy random_chi2 random_continuous_uniform random_digraph random_discrete_uniform' + ' random_exp random_f random_gamma random_general_finite_discrete random_geometric' + ' random_graph random_graph1 random_gumbel random_hypergeometric random_laplace' + ' random_logistic random_lognormal random_negative_binomial random_network' + ' random_noncentral_chi2 random_noncentral_student_t random_normal random_pareto' + ' random_permutation random_poisson random_rayleigh random_regular_graph random_student_t' + ' random_tournament random_tree random_weibull range rank rat ratcoef ratdenom' + ' ratdiff ratdisrep ratexpand ratinterpol rational rationalize ratnumer ratnump' + ' ratp ratsimp ratsubst ratvars ratweight read read_array read_binary_array' + ' read_binary_list read_binary_matrix readbyte readchar read_hashed_array readline' + ' read_list read_matrix read_nested_list readonly read_xpm real_imagpart_to_conjugate' + ' realpart realroots rearray rectangle rectform rectform_log_if_constant recttopolar' + ' rediff reduce_consts reduce_order region region_boundaries region_boundaries_plus' + ' rem remainder remarray rembox remcomps remcon remcoord remfun remfunction' + ' remlet remove remove_constvalue remove_dimensions remove_edge remove_fundamental_dimensions' + ' remove_fundamental_units remove_plot_option remove_vertex rempart remrule' + ' remsym remvalue rename rename_file reset reset_displays residue resolvante' + ' resolvante_alternee1 resolvante_bipartite resolvante_diedrale resolvante_klein' + ' resolvante_klein3 resolvante_produit_sym resolvante_unitaire resolvante_vierer' + ' rest resultant return reveal reverse revert revert2 rgb2level rhs ricci riemann' + ' rinvariant risch rk rmdir rncombine romberg room rootscontract round row' + ' rowop rowswap rreduce run_testsuite %s save saving scalarp scaled_bessel_i' + ' scaled_bessel_i0 scaled_bessel_i1 scalefactors scanmap scatterplot scatterplot_description' + ' scene schur2comp sconcat scopy scsimp scurvature sdowncase sec sech second' + ' sequal sequalignore set_alt_display setdifference set_draw_defaults set_edge_weight' + ' setelmx setequalp setify setp set_partitions set_plot_option set_prompt set_random_state' + ' set_tex_environment set_tex_environment_default setunits setup_autoload set_up_dot_simplifications' + ' set_vertex_label seventh sexplode sf sha1sum sha256sum shortest_path shortest_weighted_path' + ' show showcomps showratvars sierpinskiale sierpinskimap sign signum similaritytransform' + ' simp_inequality simplify_sum simplode simpmetderiv simtran sin sinh sinsert' + ' sinvertcase sixth skewness skewness_bernoulli skewness_beta skewness_binomial' + ' skewness_chi2 skewness_continuous_uniform skewness_discrete_uniform skewness_exp' + ' skewness_f skewness_gamma skewness_general_finite_discrete skewness_geometric' + ' skewness_gumbel skewness_hypergeometric skewness_laplace skewness_logistic' + ' skewness_lognormal skewness_negative_binomial skewness_noncentral_chi2 skewness_noncentral_student_t' + ' skewness_normal skewness_pareto skewness_poisson skewness_rayleigh skewness_student_t' + ' skewness_weibull slength smake small_rhombicosidodecahedron_graph small_rhombicuboctahedron_graph' + ' smax smin smismatch snowmap snub_cube_graph snub_dodecahedron_graph solve' + ' solve_rec solve_rec_rat some somrac sort sparse6_decode sparse6_encode sparse6_export' + ' sparse6_import specint spherical spherical_bessel_j spherical_bessel_y spherical_hankel1' + ' spherical_hankel2 spherical_harmonic spherical_to_xyz splice split sposition' + ' sprint sqfr sqrt sqrtdenest sremove sremovefirst sreverse ssearch ssort sstatus' + ' ssubst ssubstfirst staircase standardize standardize_inverse_trig starplot' + ' starplot_description status std std1 std_bernoulli std_beta std_binomial' + ' std_chi2 std_continuous_uniform std_discrete_uniform std_exp std_f std_gamma' + ' std_general_finite_discrete std_geometric std_gumbel std_hypergeometric std_laplace' + ' std_logistic std_lognormal std_negative_binomial std_noncentral_chi2 std_noncentral_student_t' + ' std_normal std_pareto std_poisson std_rayleigh std_student_t std_weibull' + ' stemplot stirling stirling1 stirling2 strim striml strimr string stringout' + ' stringp strong_components struve_h struve_l sublis sublist sublist_indices' + ' submatrix subsample subset subsetp subst substinpart subst_parallel substpart' + ' substring subvar subvarp sum sumcontract summand_to_rec supcase supcontext' + ' symbolp symmdifference symmetricp system take_channel take_inference tan' + ' tanh taylor taylorinfo taylorp taylor_simplifier taytorat tcl_output tcontract' + ' tellrat tellsimp tellsimpafter tentex tenth test_mean test_means_difference' + ' test_normality test_proportion test_proportions_difference test_rank_sum' + ' test_sign test_signed_rank test_variance test_variance_ratio tex tex1 tex_display' + ' texput %th third throw time timedate timer timer_info tldefint tlimit todd_coxeter' + ' toeplitz tokens to_lisp topological_sort to_poly to_poly_solve totaldisrep' + ' totalfourier totient tpartpol trace tracematrix trace_options transform_sample' + ' translate translate_file transpose treefale tree_reduce treillis treinat' + ' triangle triangularize trigexpand trigrat trigreduce trigsimp trunc truncate' + ' truncated_cube_graph truncated_dodecahedron_graph truncated_icosahedron_graph' + ' truncated_tetrahedron_graph tr_warnings_get tube tutte_graph ueivects uforget' + ' ultraspherical underlying_graph undiff union unique uniteigenvectors unitp' + ' units unit_step unitvector unorder unsum untellrat untimer' + ' untrace uppercasep uricci uriemann uvect vandermonde_matrix var var1 var_bernoulli' + ' var_beta var_binomial var_chi2 var_continuous_uniform var_discrete_uniform' + ' var_exp var_f var_gamma var_general_finite_discrete var_geometric var_gumbel' + ' var_hypergeometric var_laplace var_logistic var_lognormal var_negative_binomial' + ' var_noncentral_chi2 var_noncentral_student_t var_normal var_pareto var_poisson' + ' var_rayleigh var_student_t var_weibull vector vectorpotential vectorsimp' + ' verbify vers vertex_coloring vertex_connectivity vertex_degree vertex_distance' + ' vertex_eccentricity vertex_in_degree vertex_out_degree vertices vertices_to_cycle' + ' vertices_to_path %w weyl wheel_graph wiener_index wigner_3j wigner_6j' + ' wigner_9j with_stdout write_binary_data writebyte write_data writefile wronskian' + ' xreduce xthru %y Zeilberger zeroequiv zerofor zeromatrix zeromatrixp zeta' + ' zgeev zheev zlange zn_add_table zn_carmichael_lambda zn_characteristic_factors' + ' zn_determinant zn_factor_generators zn_invert_by_lu zn_log zn_mult_table' + ' absboxchar activecontexts adapt_depth additive adim aform algebraic' + ' algepsilon algexact aliases allbut all_dotsimp_denoms allocation allsym alphabetic' + ' animation antisymmetric arrays askexp assume_pos assume_pos_pred assumescalar' + ' asymbol atomgrad atrig1 axes axis_3d axis_bottom axis_left axis_right axis_top' + ' azimuth background background_color backsubst berlefact bernstein_explicit' + ' besselexpand beta_args_sum_to_integer beta_expand bftorat bftrunc bindtest' + ' border boundaries_array box boxchar breakup %c capping cauchysum cbrange' + ' cbtics center cflength cframe_flag cnonmet_flag color color_bar color_bar_tics' + ' colorbox columns commutative complex cone context contexts contour contour_levels' + ' cosnpiflag ctaypov ctaypt ctayswitch ctayvar ct_coords ctorsion_flag ctrgsimp' + ' cube current_let_rule_package cylinder data_file_name debugmode decreasing' + ' default_let_rule_package delay dependencies derivabbrev derivsubst detout' + ' diagmetric diff dim dimensions dispflag display2d|10 display_format_internal' + ' distribute_over doallmxops domain domxexpt domxmxops domxnctimes dontfactor' + ' doscmxops doscmxplus dot0nscsimp dot0simp dot1simp dotassoc dotconstrules' + ' dotdistrib dotexptsimp dotident dotscrules draw_graph_program draw_realpart' + ' edge_color edge_coloring edge_partition edge_type edge_width %edispflag' + ' elevation %emode endphi endtheta engineering_format_floats enhanced3d %enumer' + ' epsilon_lp erfflag erf_representation errormsg error_size error_syms error_type' + ' %e_to_numlog eval even evenfun evflag evfun ev_point expandwrt_denom expintexpand' + ' expintrep expon expop exptdispflag exptisolate exptsubst facexpand facsum_combine' + ' factlim factorflag factorial_expand factors_only fb feature features' + ' file_name file_output_append file_search_demo file_search_lisp file_search_maxima|10' + ' file_search_tests file_search_usage file_type_lisp file_type_maxima|10 fill_color' + ' fill_density filled_func fixed_vertices flipflag float2bf font font_size' + ' fortindent fortspaces fpprec fpprintprec functions gamma_expand gammalim' + ' gdet genindex gensumnum GGFCFMAX GGFINFINITY globalsolve gnuplot_command' + ' gnuplot_curve_styles gnuplot_curve_titles gnuplot_default_term_command gnuplot_dumb_term_command' + ' gnuplot_file_args gnuplot_file_name gnuplot_out_file gnuplot_pdf_term_command' + ' gnuplot_pm3d gnuplot_png_term_command gnuplot_postamble gnuplot_preamble' + ' gnuplot_ps_term_command gnuplot_svg_term_command gnuplot_term gnuplot_view_args' + ' Gosper_in_Zeilberger gradefs grid grid2d grind halfangles head_angle head_both' + ' head_length head_type height hypergeometric_representation %iargs ibase' + ' icc1 icc2 icounter idummyx ieqnprint ifb ifc1 ifc2 ifg ifgi ifr iframe_bracket_form' + ' ifri igeowedge_flag ikt1 ikt2 imaginary inchar increasing infeval' + ' infinity inflag infolists inm inmc1 inmc2 intanalysis integer integervalued' + ' integrate_use_rootsof integration_constant integration_constant_counter interpolate_color' + ' intfaclim ip_grid ip_grid_in irrational isolate_wrt_times iterations itr' + ' julia_parameter %k1 %k2 keepfloat key key_pos kinvariant kt label label_alignment' + ' label_orientation labels lassociative lbfgs_ncorrections lbfgs_nfeval_max' + ' leftjust legend letrat let_rule_packages lfg lg lhospitallim limsubst linear' + ' linear_solver linechar linel|10 linenum line_type linewidth line_width linsolve_params' + ' linsolvewarn lispdisp listarith listconstvars listdummyvars lmxchar load_pathname' + ' loadprint logabs logarc logcb logconcoeffp logexpand lognegint logsimp logx' + ' logx_secondary logy logy_secondary logz lriem m1pbranch macroexpansion macros' + ' mainvar manual_demo maperror mapprint matrix_element_add matrix_element_mult' + ' matrix_element_transpose maxapplydepth maxapplyheight maxima_tempdir|10 maxima_userdir|10' + ' maxnegex MAX_ORD maxposex maxpsifracdenom maxpsifracnum maxpsinegint maxpsiposint' + ' maxtayorder mesh_lines_color method mod_big_prime mode_check_errorp' + ' mode_checkp mode_check_warnp mod_test mod_threshold modular_linear_solver' + ' modulus multiplicative multiplicities myoptions nary negdistrib negsumdispflag' + ' newline newtonepsilon newtonmaxiter nextlayerfactor niceindicespref nm nmc' + ' noeval nolabels nonegative_lp noninteger nonscalar noun noundisp nouns np' + ' npi nticks ntrig numer numer_pbranch obase odd oddfun opacity opproperties' + ' opsubst optimprefix optionset orientation origin orthopoly_returns_intervals' + ' outative outchar packagefile palette partswitch pdf_file pfeformat phiresolution' + ' %piargs piece pivot_count_sx pivot_max_sx plot_format plot_options plot_realpart' + ' png_file pochhammer_max_index points pointsize point_size points_joined point_type' + ' poislim poisson poly_coefficient_ring poly_elimination_order polyfactor poly_grobner_algorithm' + ' poly_grobner_debug poly_monomial_order poly_primary_elimination_order poly_return_term_list' + ' poly_secondary_elimination_order poly_top_reduction_only posfun position' + ' powerdisp pred prederror primep_number_of_tests product_use_gamma program' + ' programmode promote_float_to_bigfloat prompt proportional_axes props psexpand' + ' ps_file radexpand radius radsubstflag rassociative ratalgdenom ratchristof' + ' ratdenomdivide rateinstein ratepsilon ratfac rational ratmx ratprint ratriemann' + ' ratsimpexpons ratvarswitch ratweights ratweyl ratwtlvl real realonly redraw' + ' refcheck resolution restart resultant ric riem rmxchar %rnum_list rombergabs' + ' rombergit rombergmin rombergtol rootsconmode rootsepsilon run_viewer same_xy' + ' same_xyz savedef savefactors scalar scalarmatrixp scale scale_lp setcheck' + ' setcheckbreak setval show_edge_color show_edges show_edge_type show_edge_width' + ' show_id show_label showtime show_vertex_color show_vertex_size show_vertex_type' + ' show_vertices show_weight simp simplified_output simplify_products simpproduct' + ' simpsum sinnpiflag solvedecomposes solveexplicit solvefactors solvenullwarn' + ' solveradcan solvetrigwarn space sparse sphere spring_embedding_depth sqrtdispflag' + ' stardisp startphi starttheta stats_numer stringdisp structures style sublis_apply_lambda' + ' subnumsimp sumexpand sumsplitfact surface surface_hide svg_file symmetric' + ' tab taylordepth taylor_logexpand taylor_order_coefficients taylor_truncate_polynomials' + ' tensorkill terminal testsuite_files thetaresolution timer_devalue title tlimswitch' + ' tr track transcompile transform transform_xy translate_fast_arrays transparent' + ' transrun tr_array_as_ref tr_bound_function_applyp tr_file_tty_messagesp tr_float_can_branch_complex' + ' tr_function_call_default trigexpandplus trigexpandtimes triginverses trigsign' + ' trivial_solutions tr_numer tr_optimize_max_loop tr_semicompile tr_state_vars' + ' tr_warn_bad_function_calls tr_warn_fexpr tr_warn_meval tr_warn_mode' + ' tr_warn_undeclared tr_warn_undefined_variable tstep ttyoff tube_extremes' + ' ufg ug %unitexpand unit_vectors uric uriem use_fast_arrays user_preamble' + ' usersetunits values vect_cross verbose vertex_color vertex_coloring vertex_partition' + ' vertex_size vertex_type view warnings weyl width windowname windowtitle wired_surface' + ' wireframe xaxis xaxis_color xaxis_secondary xaxis_type xaxis_width xlabel' + ' xlabel_secondary xlength xrange xrange_secondary xtics xtics_axis xtics_rotate' + ' xtics_rotate_secondary xtics_secondary xtics_secondary_axis xu_grid x_voxel' + ' xy_file xyplane xy_scale yaxis yaxis_color yaxis_secondary yaxis_type yaxis_width' + ' ylabel ylabel_secondary ylength yrange yrange_secondary ytics ytics_axis' + ' ytics_rotate ytics_rotate_secondary ytics_secondary ytics_secondary_axis' + ' yv_grid y_voxel yx_ratio zaxis zaxis_color zaxis_type zaxis_width zeroa zerob' + ' zerobern zeta%pi zlabel zlabel_rotate zlength zmin zn_primroot_limit zn_primroot_pretest'; var SYMBOLS = '_ __ %|0 %%|0'; return { lexemes: '[A-Za-z_%][0-9A-Za-z_%]*', keywords: { keyword: KEYWORDS, literal: LITERALS, built_in: BUILTIN_FUNCTIONS, symbol: SYMBOLS, }, contains: [ { className: 'comment', begin: '/\\*', end: '\\*/', contains: ['self'] }, hljs.QUOTE_STRING_MODE, { className: 'number', relevance: 0, variants: [ { // float number w/ exponent // hmm, I wonder if we ought to include other exponent markers? begin: '\\b(\\d+|\\d+\\.|\\.\\d+|\\d+\\.\\d+)[Ee][-+]?\\d+\\b', }, { // bigfloat number begin: '\\b(\\d+|\\d+\\.|\\.\\d+|\\d+\\.\\d+)[Bb][-+]?\\d+\\b', relevance: 10 }, { // float number w/out exponent // Doesn't seem to recognize floats which start with '.' begin: '\\b(\\.\\d+|\\d+\\.\\d+)\\b', }, { // integer in base up to 36 // Doesn't seem to recognize integers which end with '.' begin: '\\b(\\d+|0[0-9A-Za-z]+)\\.?\\b', } ] } ], illegal: /@/ } } },{name:"mel",create:/* Language: MEL Description: Maya Embedded Language Author: Shuen-Huei Guan Category: graphics */ function(hljs) { return { keywords: 'int float string vector matrix if else switch case default while do for in break ' + 'continue global proc return about abs addAttr addAttributeEditorNodeHelp addDynamic ' + 'addNewShelfTab addPP addPanelCategory addPrefixToName advanceToNextDrivenKey ' + 'affectedNet affects aimConstraint air alias aliasAttr align alignCtx alignCurve ' + 'alignSurface allViewFit ambientLight angle angleBetween animCone animCurveEditor ' + 'animDisplay animView annotate appendStringArray applicationName applyAttrPreset ' + 'applyTake arcLenDimContext arcLengthDimension arclen arrayMapper art3dPaintCtx ' + 'artAttrCtx artAttrPaintVertexCtx artAttrSkinPaintCtx artAttrTool artBuildPaintMenu ' + 'artFluidAttrCtx artPuttyCtx artSelectCtx artSetPaintCtx artUserPaintCtx assignCommand ' + 'assignInputDevice assignViewportFactories attachCurve attachDeviceAttr attachSurface ' + 'attrColorSliderGrp attrCompatibility attrControlGrp attrEnumOptionMenu ' + 'attrEnumOptionMenuGrp attrFieldGrp attrFieldSliderGrp attrNavigationControlGrp ' + 'attrPresetEditWin attributeExists attributeInfo attributeMenu attributeQuery ' + 'autoKeyframe autoPlace bakeClip bakeFluidShading bakePartialHistory bakeResults ' + 'bakeSimulation basename basenameEx batchRender bessel bevel bevelPlus binMembership ' + 'bindSkin blend2 blendShape blendShapeEditor blendShapePanel blendTwoAttr blindDataType ' + 'boneLattice boundary boxDollyCtx boxZoomCtx bufferCurve buildBookmarkMenu ' + 'buildKeyframeMenu button buttonManip CBG cacheFile cacheFileCombine cacheFileMerge ' + 'cacheFileTrack camera cameraView canCreateManip canvas capitalizeString catch ' + 'catchQuiet ceil changeSubdivComponentDisplayLevel changeSubdivRegion channelBox ' + 'character characterMap characterOutlineEditor characterize chdir checkBox checkBoxGrp ' + 'checkDefaultRenderGlobals choice circle circularFillet clamp clear clearCache clip ' + 'clipEditor clipEditorCurrentTimeCtx clipSchedule clipSchedulerOutliner clipTrimBefore ' + 'closeCurve closeSurface cluster cmdFileOutput cmdScrollFieldExecuter ' + 'cmdScrollFieldReporter cmdShell coarsenSubdivSelectionList collision color ' + 'colorAtPoint colorEditor colorIndex colorIndexSliderGrp colorSliderButtonGrp ' + 'colorSliderGrp columnLayout commandEcho commandLine commandPort compactHairSystem ' + 'componentEditor compositingInterop computePolysetVolume condition cone confirmDialog ' + 'connectAttr connectControl connectDynamic connectJoint connectionInfo constrain ' + 'constrainValue constructionHistory container containsMultibyte contextInfo control ' + 'convertFromOldLayers convertIffToPsd convertLightmap convertSolidTx convertTessellation ' + 'convertUnit copyArray copyFlexor copyKey copySkinWeights cos cpButton cpCache ' + 'cpClothSet cpCollision cpConstraint cpConvClothToMesh cpForces cpGetSolverAttr cpPanel ' + 'cpProperty cpRigidCollisionFilter cpSeam cpSetEdit cpSetSolverAttr cpSolver ' + 'cpSolverTypes cpTool cpUpdateClothUVs createDisplayLayer createDrawCtx createEditor ' + 'createLayeredPsdFile createMotionField createNewShelf createNode createRenderLayer ' + 'createSubdivRegion cross crossProduct ctxAbort ctxCompletion ctxEditMode ctxTraverse ' + 'currentCtx currentTime currentTimeCtx currentUnit curve curveAddPtCtx ' + 'curveCVCtx curveEPCtx curveEditorCtx curveIntersect curveMoveEPCtx curveOnSurface ' + 'curveSketchCtx cutKey cycleCheck cylinder dagPose date defaultLightListCheckBox ' + 'defaultNavigation defineDataServer defineVirtualDevice deformer deg_to_rad delete ' + 'deleteAttr deleteShadingGroupsAndMaterials deleteShelfTab deleteUI deleteUnusedBrushes ' + 'delrandstr detachCurve detachDeviceAttr detachSurface deviceEditor devicePanel dgInfo ' + 'dgdirty dgeval dgtimer dimWhen directKeyCtx directionalLight dirmap dirname disable ' + 'disconnectAttr disconnectJoint diskCache displacementToPoly displayAffected ' + 'displayColor displayCull displayLevelOfDetail displayPref displayRGBColor ' + 'displaySmoothness displayStats displayString displaySurface distanceDimContext ' + 'distanceDimension doBlur dolly dollyCtx dopeSheetEditor dot dotProduct ' + 'doubleProfileBirailSurface drag dragAttrContext draggerContext dropoffLocator ' + 'duplicate duplicateCurve duplicateSurface dynCache dynControl dynExport dynExpression ' + 'dynGlobals dynPaintEditor dynParticleCtx dynPref dynRelEdPanel dynRelEditor ' + 'dynamicLoad editAttrLimits editDisplayLayerGlobals editDisplayLayerMembers ' + 'editRenderLayerAdjustment editRenderLayerGlobals editRenderLayerMembers editor ' + 'editorTemplate effector emit emitter enableDevice encodeString endString endsWith env ' + 'equivalent equivalentTol erf error eval evalDeferred evalEcho event ' + 'exactWorldBoundingBox exclusiveLightCheckBox exec executeForEachObject exists exp ' + 'expression expressionEditorListen extendCurve extendSurface extrude fcheck fclose feof ' + 'fflush fgetline fgetword file fileBrowserDialog fileDialog fileExtension fileInfo ' + 'filetest filletCurve filter filterCurve filterExpand filterStudioImport ' + 'findAllIntersections findAnimCurves findKeyframe findMenuItem findRelatedSkinCluster ' + 'finder firstParentOf fitBspline flexor floatEq floatField floatFieldGrp floatScrollBar ' + 'floatSlider floatSlider2 floatSliderButtonGrp floatSliderGrp floor flow fluidCacheInfo ' + 'fluidEmitter fluidVoxelInfo flushUndo fmod fontDialog fopen formLayout format fprint ' + 'frameLayout fread freeFormFillet frewind fromNativePath fwrite gamma gauss ' + 'geometryConstraint getApplicationVersionAsFloat getAttr getClassification ' + 'getDefaultBrush getFileList getFluidAttr getInputDeviceRange getMayaPanelTypes ' + 'getModifiers getPanel getParticleAttr getPluginResource getenv getpid glRender ' + 'glRenderEditor globalStitch gmatch goal gotoBindPose grabColor gradientControl ' + 'gradientControlNoAttr graphDollyCtx graphSelectContext graphTrackCtx gravity grid ' + 'gridLayout group groupObjectsByName HfAddAttractorToAS HfAssignAS HfBuildEqualMap ' + 'HfBuildFurFiles HfBuildFurImages HfCancelAFR HfConnectASToHF HfCreateAttractor ' + 'HfDeleteAS HfEditAS HfPerformCreateAS HfRemoveAttractorFromAS HfSelectAttached ' + 'HfSelectAttractors HfUnAssignAS hardenPointCurve hardware hardwareRenderPanel ' + 'headsUpDisplay headsUpMessage help helpLine hermite hide hilite hitTest hotBox hotkey ' + 'hotkeyCheck hsv_to_rgb hudButton hudSlider hudSliderButton hwReflectionMap hwRender ' + 'hwRenderLoad hyperGraph hyperPanel hyperShade hypot iconTextButton iconTextCheckBox ' + 'iconTextRadioButton iconTextRadioCollection iconTextScrollList iconTextStaticLabel ' + 'ikHandle ikHandleCtx ikHandleDisplayScale ikSolver ikSplineHandleCtx ikSystem ' + 'ikSystemInfo ikfkDisplayMethod illustratorCurves image imfPlugins inheritTransform ' + 'insertJoint insertJointCtx insertKeyCtx insertKnotCurve insertKnotSurface instance ' + 'instanceable instancer intField intFieldGrp intScrollBar intSlider intSliderGrp ' + 'interToUI internalVar intersect iprEngine isAnimCurve isConnected isDirty isParentOf ' + 'isSameObject isTrue isValidObjectName isValidString isValidUiName isolateSelect ' + 'itemFilter itemFilterAttr itemFilterRender itemFilterType joint jointCluster jointCtx ' + 'jointDisplayScale jointLattice keyTangent keyframe keyframeOutliner ' + 'keyframeRegionCurrentTimeCtx keyframeRegionDirectKeyCtx keyframeRegionDollyCtx ' + 'keyframeRegionInsertKeyCtx keyframeRegionMoveKeyCtx keyframeRegionScaleKeyCtx ' + 'keyframeRegionSelectKeyCtx keyframeRegionSetKeyCtx keyframeRegionTrackCtx ' + 'keyframeStats lassoContext lattice latticeDeformKeyCtx launch launchImageEditor ' + 'layerButton layeredShaderPort layeredTexturePort layout layoutDialog lightList ' + 'lightListEditor lightListPanel lightlink lineIntersection linearPrecision linstep ' + 'listAnimatable listAttr listCameras listConnections listDeviceAttachments listHistory ' + 'listInputDeviceAxes listInputDeviceButtons listInputDevices listMenuAnnotation ' + 'listNodeTypes listPanelCategories listRelatives listSets listTransforms ' + 'listUnselected listerEditor loadFluid loadNewShelf loadPlugin ' + 'loadPluginLanguageResources loadPrefObjects localizedPanelLabel lockNode loft log ' + 'longNameOf lookThru ls lsThroughFilter lsType lsUI Mayatomr mag makeIdentity makeLive ' + 'makePaintable makeRoll makeSingleSurface makeTubeOn makebot manipMoveContext ' + 'manipMoveLimitsCtx manipOptions manipRotateContext manipRotateLimitsCtx ' + 'manipScaleContext manipScaleLimitsCtx marker match max memory menu menuBarLayout ' + 'menuEditor menuItem menuItemToShelf menuSet menuSetPref messageLine min minimizeApp ' + 'mirrorJoint modelCurrentTimeCtx modelEditor modelPanel mouse movIn movOut move ' + 'moveIKtoFK moveKeyCtx moveVertexAlongDirection multiProfileBirailSurface mute ' + 'nParticle nameCommand nameField namespace namespaceInfo newPanelItems newton nodeCast ' + 'nodeIconButton nodeOutliner nodePreset nodeType noise nonLinear normalConstraint ' + 'normalize nurbsBoolean nurbsCopyUVSet nurbsCube nurbsEditUV nurbsPlane nurbsSelect ' + 'nurbsSquare nurbsToPoly nurbsToPolygonsPref nurbsToSubdiv nurbsToSubdivPref ' + 'nurbsUVSet nurbsViewDirectionVector objExists objectCenter objectLayer objectType ' + 'objectTypeUI obsoleteProc oceanNurbsPreviewPlane offsetCurve offsetCurveOnSurface ' + 'offsetSurface openGLExtension openMayaPref optionMenu optionMenuGrp optionVar orbit ' + 'orbitCtx orientConstraint outlinerEditor outlinerPanel overrideModifier ' + 'paintEffectsDisplay pairBlend palettePort paneLayout panel panelConfiguration ' + 'panelHistory paramDimContext paramDimension paramLocator parent parentConstraint ' + 'particle particleExists particleInstancer particleRenderInfo partition pasteKey ' + 'pathAnimation pause pclose percent performanceOptions pfxstrokes pickWalk picture ' + 'pixelMove planarSrf plane play playbackOptions playblast plugAttr plugNode pluginInfo ' + 'pluginResourceUtil pointConstraint pointCurveConstraint pointLight pointMatrixMult ' + 'pointOnCurve pointOnSurface pointPosition poleVectorConstraint polyAppend ' + 'polyAppendFacetCtx polyAppendVertex polyAutoProjection polyAverageNormal ' + 'polyAverageVertex polyBevel polyBlendColor polyBlindData polyBoolOp polyBridgeEdge ' + 'polyCacheMonitor polyCheck polyChipOff polyClipboard polyCloseBorder polyCollapseEdge ' + 'polyCollapseFacet polyColorBlindData polyColorDel polyColorPerVertex polyColorSet ' + 'polyCompare polyCone polyCopyUV polyCrease polyCreaseCtx polyCreateFacet ' + 'polyCreateFacetCtx polyCube polyCut polyCutCtx polyCylinder polyCylindricalProjection ' + 'polyDelEdge polyDelFacet polyDelVertex polyDuplicateAndConnect polyDuplicateEdge ' + 'polyEditUV polyEditUVShell polyEvaluate polyExtrudeEdge polyExtrudeFacet ' + 'polyExtrudeVertex polyFlipEdge polyFlipUV polyForceUV polyGeoSampler polyHelix ' + 'polyInfo polyInstallAction polyLayoutUV polyListComponentConversion polyMapCut ' + 'polyMapDel polyMapSew polyMapSewMove polyMergeEdge polyMergeEdgeCtx polyMergeFacet ' + 'polyMergeFacetCtx polyMergeUV polyMergeVertex polyMirrorFace polyMoveEdge ' + 'polyMoveFacet polyMoveFacetUV polyMoveUV polyMoveVertex polyNormal polyNormalPerVertex ' + 'polyNormalizeUV polyOptUvs polyOptions polyOutput polyPipe polyPlanarProjection ' + 'polyPlane polyPlatonicSolid polyPoke polyPrimitive polyPrism polyProjection ' + 'polyPyramid polyQuad polyQueryBlindData polyReduce polySelect polySelectConstraint ' + 'polySelectConstraintMonitor polySelectCtx polySelectEditCtx polySeparate ' + 'polySetToFaceNormal polySewEdge polyShortestPathCtx polySmooth polySoftEdge ' + 'polySphere polySphericalProjection polySplit polySplitCtx polySplitEdge polySplitRing ' + 'polySplitVertex polyStraightenUVBorder polySubdivideEdge polySubdivideFacet ' + 'polyToSubdiv polyTorus polyTransfer polyTriangulate polyUVSet polyUnite polyWedgeFace ' + 'popen popupMenu pose pow preloadRefEd print progressBar progressWindow projFileViewer ' + 'projectCurve projectTangent projectionContext projectionManip promptDialog propModCtx ' + 'propMove psdChannelOutliner psdEditTextureFile psdExport psdTextureFile putenv pwd ' + 'python querySubdiv quit rad_to_deg radial radioButton radioButtonGrp radioCollection ' + 'radioMenuItemCollection rampColorPort rand randomizeFollicles randstate rangeControl ' + 'readTake rebuildCurve rebuildSurface recordAttr recordDevice redo reference ' + 'referenceEdit referenceQuery refineSubdivSelectionList refresh refreshAE ' + 'registerPluginResource rehash reloadImage removeJoint removeMultiInstance ' + 'removePanelCategory rename renameAttr renameSelectionList renameUI render ' + 'renderGlobalsNode renderInfo renderLayerButton renderLayerParent ' + 'renderLayerPostProcess renderLayerUnparent renderManip renderPartition ' + 'renderQualityNode renderSettings renderThumbnailUpdate renderWindowEditor ' + 'renderWindowSelectContext renderer reorder reorderDeformers requires reroot ' + 'resampleFluid resetAE resetPfxToPolyCamera resetTool resolutionNode retarget ' + 'reverseCurve reverseSurface revolve rgb_to_hsv rigidBody rigidSolver roll rollCtx ' + 'rootOf rot rotate rotationInterpolation roundConstantRadius rowColumnLayout rowLayout ' + 'runTimeCommand runup sampleImage saveAllShelves saveAttrPreset saveFluid saveImage ' + 'saveInitialState saveMenu savePrefObjects savePrefs saveShelf saveToolSettings scale ' + 'scaleBrushBrightness scaleComponents scaleConstraint scaleKey scaleKeyCtx sceneEditor ' + 'sceneUIReplacement scmh scriptCtx scriptEditorInfo scriptJob scriptNode scriptTable ' + 'scriptToShelf scriptedPanel scriptedPanelType scrollField scrollLayout sculpt ' + 'searchPathArray seed selLoadSettings select selectContext selectCurveCV selectKey ' + 'selectKeyCtx selectKeyframeRegionCtx selectMode selectPref selectPriority selectType ' + 'selectedNodes selectionConnection separator setAttr setAttrEnumResource ' + 'setAttrMapping setAttrNiceNameResource setConstraintRestPosition ' + 'setDefaultShadingGroup setDrivenKeyframe setDynamic setEditCtx setEditor setFluidAttr ' + 'setFocus setInfinity setInputDeviceMapping setKeyCtx setKeyPath setKeyframe ' + 'setKeyframeBlendshapeTargetWts setMenuMode setNodeNiceNameResource setNodeTypeFlag ' + 'setParent setParticleAttr setPfxToPolyCamera setPluginResource setProject ' + 'setStampDensity setStartupMessage setState setToolTo setUITemplate setXformManip sets ' + 'shadingConnection shadingGeometryRelCtx shadingLightRelCtx shadingNetworkCompare ' + 'shadingNode shapeCompare shelfButton shelfLayout shelfTabLayout shellField ' + 'shortNameOf showHelp showHidden showManipCtx showSelectionInTitle ' + 'showShadingGroupAttrEditor showWindow sign simplify sin singleProfileBirailSurface ' + 'size sizeBytes skinCluster skinPercent smoothCurve smoothTangentSurface smoothstep ' + 'snap2to2 snapKey snapMode snapTogetherCtx snapshot soft softMod softModCtx sort sound ' + 'soundControl source spaceLocator sphere sphrand spotLight spotLightPreviewPort ' + 'spreadSheetEditor spring sqrt squareSurface srtContext stackTrace startString ' + 'startsWith stitchAndExplodeShell stitchSurface stitchSurfacePoints strcmp ' + 'stringArrayCatenate stringArrayContains stringArrayCount stringArrayInsertAtIndex ' + 'stringArrayIntersector stringArrayRemove stringArrayRemoveAtIndex ' + 'stringArrayRemoveDuplicates stringArrayRemoveExact stringArrayToString ' + 'stringToStringArray strip stripPrefixFromName stroke subdAutoProjection ' + 'subdCleanTopology subdCollapse subdDuplicateAndConnect subdEditUV ' + 'subdListComponentConversion subdMapCut subdMapSewMove subdMatchTopology subdMirror ' + 'subdToBlind subdToPoly subdTransferUVsToCache subdiv subdivCrease ' + 'subdivDisplaySmoothness substitute substituteAllString substituteGeometry substring ' + 'surface surfaceSampler surfaceShaderList swatchDisplayPort switchTable symbolButton ' + 'symbolCheckBox sysFile system tabLayout tan tangentConstraint texLatticeDeformContext ' + 'texManipContext texMoveContext texMoveUVShellContext texRotateContext texScaleContext ' + 'texSelectContext texSelectShortestPathCtx texSmudgeUVContext texWinToolCtx text ' + 'textCurves textField textFieldButtonGrp textFieldGrp textManip textScrollList ' + 'textToShelf textureDisplacePlane textureHairColor texturePlacementContext ' + 'textureWindow threadCount threePointArcCtx timeControl timePort timerX toNativePath ' + 'toggle toggleAxis toggleWindowVisibility tokenize tokenizeList tolerance tolower ' + 'toolButton toolCollection toolDropped toolHasOptions toolPropertyWindow torus toupper ' + 'trace track trackCtx transferAttributes transformCompare transformLimits translator ' + 'trim trunc truncateFluidCache truncateHairCache tumble tumbleCtx turbulence ' + 'twoPointArcCtx uiRes uiTemplate unassignInputDevice undo undoInfo ungroup uniform unit ' + 'unloadPlugin untangleUV untitledFileName untrim upAxis updateAE userCtx uvLink ' + 'uvSnapshot validateShelfName vectorize view2dToolCtx viewCamera viewClipPlane ' + 'viewFit viewHeadOn viewLookAt viewManip viewPlace viewSet visor volumeAxis vortex ' + 'waitCursor warning webBrowser webBrowserPrefs whatIs window windowPref wire ' + 'wireContext workspace wrinkle wrinkleContext writeTake xbmLangPathList xform', illegal: ' Description: Mercury is a logic/functional programming language which combines the clarity and expressiveness of declarative programming with advanced static analysis and error detection features. */ function(hljs) { var KEYWORDS = { keyword: 'module use_module import_module include_module end_module initialise ' + 'mutable initialize finalize finalise interface implementation pred ' + 'mode func type inst solver any_pred any_func is semidet det nondet ' + 'multi erroneous failure cc_nondet cc_multi typeclass instance where ' + 'pragma promise external trace atomic or_else require_complete_switch ' + 'require_det require_semidet require_multi require_nondet ' + 'require_cc_multi require_cc_nondet require_erroneous require_failure', meta: // pragma 'inline no_inline type_spec source_file fact_table obsolete memo ' + 'loop_check minimal_model terminates does_not_terminate ' + 'check_termination promise_equivalent_clauses ' + // preprocessor 'foreign_proc foreign_decl foreign_code foreign_type ' + 'foreign_import_module foreign_export_enum foreign_export ' + 'foreign_enum may_call_mercury will_not_call_mercury thread_safe ' + 'not_thread_safe maybe_thread_safe promise_pure promise_semipure ' + 'tabled_for_io local untrailed trailed attach_to_io_state ' + 'can_pass_as_mercury_type stable will_not_throw_exception ' + 'may_modify_trail will_not_modify_trail may_duplicate ' + 'may_not_duplicate affects_liveness does_not_affect_liveness ' + 'doesnt_affect_liveness no_sharing unknown_sharing sharing', built_in: 'some all not if then else true fail false try catch catch_any ' + 'semidet_true semidet_false semidet_fail impure_true impure semipure' }; var COMMENT = hljs.COMMENT('%', '$'); var NUMCODE = { className: 'number', begin: "0'.\\|0[box][0-9a-fA-F]*" }; var ATOM = hljs.inherit(hljs.APOS_STRING_MODE, {relevance: 0}); var STRING = hljs.inherit(hljs.QUOTE_STRING_MODE, {relevance: 0}); var STRING_FMT = { className: 'subst', begin: '\\\\[abfnrtv]\\|\\\\x[0-9a-fA-F]*\\\\\\|%[-+# *.0-9]*[dioxXucsfeEgGp]', relevance: 0 }; STRING.contains.push(STRING_FMT); var IMPLICATION = { className: 'built_in', variants: [ {begin: '<=>'}, {begin: '<=', relevance: 0}, {begin: '=>', relevance: 0}, {begin: '/\\\\'}, {begin: '\\\\/'} ] }; var HEAD_BODY_CONJUNCTION = { className: 'built_in', variants: [ {begin: ':-\\|-->'}, {begin: '=', relevance: 0} ] }; return { aliases: ['m', 'moo'], keywords: KEYWORDS, contains: [ IMPLICATION, HEAD_BODY_CONJUNCTION, COMMENT, hljs.C_BLOCK_COMMENT_MODE, NUMCODE, hljs.NUMBER_MODE, ATOM, STRING, {begin: /:-/} // relevance booster ] }; } },{name:"mipsasm",create:/* Language: MIPS Assembly Author: Nebuleon Fumika Description: MIPS Assembly (up to MIPS32R2) Category: assembler */ function(hljs) { //local labels: %?[FB]?[AT]?\d{1,2}\w+ return { case_insensitive: true, aliases: ['mips'], lexemes: '\\.?' + hljs.IDENT_RE, keywords: { meta: //GNU preprocs '.2byte .4byte .align .ascii .asciz .balign .byte .code .data .else .end .endif .endm .endr .equ .err .exitm .extern .global .hword .if .ifdef .ifndef .include .irp .long .macro .rept .req .section .set .skip .space .text .word .ltorg ', built_in: '$0 $1 $2 $3 $4 $5 $6 $7 $8 $9 $10 $11 $12 $13 $14 $15 ' + // integer registers '$16 $17 $18 $19 $20 $21 $22 $23 $24 $25 $26 $27 $28 $29 $30 $31 ' + // integer registers 'zero at v0 v1 a0 a1 a2 a3 a4 a5 a6 a7 ' + // integer register aliases 't0 t1 t2 t3 t4 t5 t6 t7 t8 t9 s0 s1 s2 s3 s4 s5 s6 s7 s8 ' + // integer register aliases 'k0 k1 gp sp fp ra ' + // integer register aliases '$f0 $f1 $f2 $f2 $f4 $f5 $f6 $f7 $f8 $f9 $f10 $f11 $f12 $f13 $f14 $f15 ' + // floating-point registers '$f16 $f17 $f18 $f19 $f20 $f21 $f22 $f23 $f24 $f25 $f26 $f27 $f28 $f29 $f30 $f31 ' + // floating-point registers 'Context Random EntryLo0 EntryLo1 Context PageMask Wired EntryHi ' + // Coprocessor 0 registers 'HWREna BadVAddr Count Compare SR IntCtl SRSCtl SRSMap Cause EPC PRId ' + // Coprocessor 0 registers 'EBase Config Config1 Config2 Config3 LLAddr Debug DEPC DESAVE CacheErr ' + // Coprocessor 0 registers 'ECC ErrorEPC TagLo DataLo TagHi DataHi WatchLo WatchHi PerfCtl PerfCnt ' // Coprocessor 0 registers }, contains: [ { className: 'keyword', begin: '\\b('+ //mnemonics // 32-bit integer instructions 'addi?u?|andi?|b(al)?|beql?|bgez(al)?l?|bgtzl?|blezl?|bltz(al)?l?|' + 'bnel?|cl[oz]|divu?|ext|ins|j(al)?|jalr(\.hb)?|jr(\.hb)?|lbu?|lhu?|' + 'll|lui|lw[lr]?|maddu?|mfhi|mflo|movn|movz|move|msubu?|mthi|mtlo|mul|' + 'multu?|nop|nor|ori?|rotrv?|sb|sc|se[bh]|sh|sllv?|slti?u?|srav?|' + 'srlv?|subu?|sw[lr]?|xori?|wsbh|' + // floating-point instructions 'abs\.[sd]|add\.[sd]|alnv.ps|bc1[ft]l?|' + 'c\.(s?f|un|u?eq|[ou]lt|[ou]le|ngle?|seq|l[et]|ng[et])\.[sd]|' + '(ceil|floor|round|trunc)\.[lw]\.[sd]|cfc1|cvt\.d\.[lsw]|' + 'cvt\.l\.[dsw]|cvt\.ps\.s|cvt\.s\.[dlw]|cvt\.s\.p[lu]|cvt\.w\.[dls]|' + 'div\.[ds]|ldx?c1|luxc1|lwx?c1|madd\.[sd]|mfc1|mov[fntz]?\.[ds]|' + 'msub\.[sd]|mth?c1|mul\.[ds]|neg\.[ds]|nmadd\.[ds]|nmsub\.[ds]|' + 'p[lu][lu]\.ps|recip\.fmt|r?sqrt\.[ds]|sdx?c1|sub\.[ds]|suxc1|' + 'swx?c1|' + // system control instructions 'break|cache|d?eret|[de]i|ehb|mfc0|mtc0|pause|prefx?|rdhwr|' + 'rdpgpr|sdbbp|ssnop|synci?|syscall|teqi?|tgei?u?|tlb(p|r|w[ir])|' + 'tlti?u?|tnei?|wait|wrpgpr'+ ')', end: '\\s' }, hljs.COMMENT('[;#]', '$'), hljs.C_BLOCK_COMMENT_MODE, hljs.QUOTE_STRING_MODE, { className: 'string', begin: '\'', end: '[^\\\\]\'', relevance: 0 }, { className: 'title', begin: '\\|', end: '\\|', illegal: '\\n', relevance: 0 }, { className: 'number', variants: [ {begin: '0x[0-9a-f]+'}, //hex {begin: '\\b-?\\d+'} //bare number ], relevance: 0 }, { className: 'symbol', variants: [ {begin: '^\\s*[a-z_\\.\\$][a-z0-9_\\.\\$]+:'}, //GNU MIPS syntax {begin: '^\\s*[0-9]+:'}, // numbered local labels {begin: '[0-9]+[bf]' } // number local label reference (backwards, forwards) ], relevance: 0 } ], illegal: '\/' }; } },{name:"mizar",create:/* Language: Mizar Author: Kelley van Evert Category: scientific */ function(hljs) { return { keywords: 'environ vocabularies notations constructors definitions ' + 'registrations theorems schemes requirements begin end definition ' + 'registration cluster existence pred func defpred deffunc theorem ' + 'proof let take assume then thus hence ex for st holds consider ' + 'reconsider such that and in provided of as from be being by means ' + 'equals implies iff redefine define now not or attr is mode ' + 'suppose per cases set thesis contradiction scheme reserve struct ' + 'correctness compatibility coherence symmetry assymetry ' + 'reflexivity irreflexivity connectedness uniqueness commutativity ' + 'idempotence involutiveness projectivity', contains: [ hljs.COMMENT('::', '$') ] }; } },{name:"mojolicious",create:/* Language: Mojolicious Requires: xml.js, perl.js Author: Dotan Dimet Description: Mojolicious .ep (Embedded Perl) templates Category: template */ function(hljs) { return { subLanguage: 'xml', contains: [ { className: 'meta', begin: '^__(END|DATA)__$' }, // mojolicious line { begin: "^\\s*%{1,2}={0,2}", end: '$', subLanguage: 'perl' }, // mojolicious block { begin: "<%{1,2}={0,2}", end: "={0,1}%>", subLanguage: 'perl', excludeBegin: true, excludeEnd: true } ] }; } },{name:"monkey",create:/* Language: Monkey Author: Arthur Bikmullin */ function(hljs) { var NUMBER = { className: 'number', relevance: 0, variants: [ { begin: '[$][a-fA-F0-9]+' }, hljs.NUMBER_MODE ] }; return { case_insensitive: true, keywords: { keyword: 'public private property continue exit extern new try catch ' + 'eachin not abstract final select case default const local global field ' + 'end if then else elseif endif while wend repeat until forever for ' + 'to step next return module inline throw import', built_in: 'DebugLog DebugStop Error Print ACos ACosr ASin ASinr ATan ATan2 ATan2r ATanr Abs Abs Ceil ' + 'Clamp Clamp Cos Cosr Exp Floor Log Max Max Min Min Pow Sgn Sgn Sin Sinr Sqrt Tan Tanr Seed PI HALFPI TWOPI', literal: 'true false null and or shl shr mod' }, illegal: /\/\*/, contains: [ hljs.COMMENT('#rem', '#end'), hljs.COMMENT( "'", '$', { relevance: 0 } ), { className: 'function', beginKeywords: 'function method', end: '[(=:]|$', illegal: /\n/, contains: [ hljs.UNDERSCORE_TITLE_MODE ] }, { className: 'class', beginKeywords: 'class interface', end: '$', contains: [ { beginKeywords: 'extends implements' }, hljs.UNDERSCORE_TITLE_MODE ] }, { className: 'built_in', begin: '\\b(self|super)\\b' }, { className: 'meta', begin: '\\s*#', end: '$', keywords: {'meta-keyword': 'if else elseif endif end then'} }, { className: 'meta', begin: '^\\s*strict\\b' }, { beginKeywords: 'alias', end: '=', contains: [hljs.UNDERSCORE_TITLE_MODE] }, hljs.QUOTE_STRING_MODE, NUMBER ] } } },{name:"moonscript",create:/* Language: MoonScript Author: Billy Quith Description: MoonScript is a programming language that transcompiles to Lua. For info about language see http://moonscript.org/ Origin: coffeescript.js Category: scripting */ function(hljs) { var KEYWORDS = { keyword: // Moonscript keywords 'if then not for in while do return else elseif break continue switch and or ' + 'unless when class extends super local import export from using', literal: 'true false nil', built_in: '_G _VERSION assert collectgarbage dofile error getfenv getmetatable ipairs load ' + 'loadfile loadstring module next pairs pcall print rawequal rawget rawset require ' + 'select setfenv setmetatable tonumber tostring type unpack xpcall coroutine debug ' + 'io math os package string table' }; var JS_IDENT_RE = '[A-Za-z$_][0-9A-Za-z$_]*'; var SUBST = { className: 'subst', begin: /#\{/, end: /}/, keywords: KEYWORDS }; var EXPRESSIONS = [ hljs.inherit(hljs.C_NUMBER_MODE, {starts: {end: '(\\s*/)?', relevance: 0}}), // a number tries to eat the following slash to prevent treating it as a regexp { className: 'string', variants: [ { begin: /'/, end: /'/, contains: [hljs.BACKSLASH_ESCAPE] }, { begin: /"/, end: /"/, contains: [hljs.BACKSLASH_ESCAPE, SUBST] } ] }, { className: 'built_in', begin: '@__' + hljs.IDENT_RE }, { begin: '@' + hljs.IDENT_RE // relevance booster on par with CoffeeScript }, { begin: hljs.IDENT_RE + '\\\\' + hljs.IDENT_RE // inst\method } ]; SUBST.contains = EXPRESSIONS; var TITLE = hljs.inherit(hljs.TITLE_MODE, {begin: JS_IDENT_RE}); var PARAMS_RE = '(\\(.*\\))?\\s*\\B[-=]>'; var PARAMS = { className: 'params', begin: '\\([^\\(]', returnBegin: true, /* We need another contained nameless mode to not have every nested pair of parens to be called "params" */ contains: [{ begin: /\(/, end: /\)/, keywords: KEYWORDS, contains: ['self'].concat(EXPRESSIONS) }] }; return { aliases: ['moon'], keywords: KEYWORDS, illegal: /\/\*/, contains: EXPRESSIONS.concat([ hljs.COMMENT('--', '$'), { className: 'function', // function: -> => begin: '^\\s*' + JS_IDENT_RE + '\\s*=\\s*' + PARAMS_RE, end: '[-=]>', returnBegin: true, contains: [TITLE, PARAMS] }, { begin: /[\(,:=]\s*/, // anonymous function start relevance: 0, contains: [ { className: 'function', begin: PARAMS_RE, end: '[-=]>', returnBegin: true, contains: [PARAMS] } ] }, { className: 'class', beginKeywords: 'class', end: '$', illegal: /[:="\[\]]/, contains: [ { beginKeywords: 'extends', endsWithParent: true, illegal: /[:="\[\]]/, contains: [TITLE] }, TITLE ] }, { className: 'name', // table begin: JS_IDENT_RE + ':', end: ':', returnBegin: true, returnEnd: true, relevance: 0 } ]) }; } },{name:"n1ql",create:/* Language: N1QL Author: Andres Täht Contributors: Rene Saarsoo Description: Couchbase query language */ function(hljs) { return { case_insensitive: true, contains: [ { beginKeywords: 'build create index delete drop explain infer|10 insert merge prepare select update upsert|10', end: /;/, endsWithParent: true, keywords: { // Taken from http://developer.couchbase.com/documentation/server/current/n1ql/n1ql-language-reference/reservedwords.html keyword: 'all alter analyze and any array as asc begin between binary boolean break bucket build by call ' + 'case cast cluster collate collection commit connect continue correlate cover create database ' + 'dataset datastore declare decrement delete derived desc describe distinct do drop each element ' + 'else end every except exclude execute exists explain fetch first flatten for force from ' + 'function grant group gsi having if ignore ilike in include increment index infer inline inner ' + 'insert intersect into is join key keys keyspace known last left let letting like limit lsm map ' + 'mapping matched materialized merge minus namespace nest not number object offset on ' + 'option or order outer over parse partition password path pool prepare primary private privilege ' + 'procedure public raw realm reduce rename return returning revoke right role rollback satisfies ' + 'schema select self semi set show some start statistics string system then to transaction trigger ' + 'truncate under union unique unknown unnest unset update upsert use user using validate value ' + 'valued values via view when where while with within work xor', // Taken from http://developer.couchbase.com/documentation/server/4.5/n1ql/n1ql-language-reference/literals.html literal: 'true false null missing|5', // Taken from http://developer.couchbase.com/documentation/server/4.5/n1ql/n1ql-language-reference/functions.html built_in: 'array_agg array_append array_concat array_contains array_count array_distinct array_ifnull array_length ' + 'array_max array_min array_position array_prepend array_put array_range array_remove array_repeat array_replace ' + 'array_reverse array_sort array_sum avg count max min sum greatest least ifmissing ifmissingornull ifnull ' + 'missingif nullif ifinf ifnan ifnanorinf naninf neginfif posinfif clock_millis clock_str date_add_millis ' + 'date_add_str date_diff_millis date_diff_str date_part_millis date_part_str date_trunc_millis date_trunc_str ' + 'duration_to_str millis str_to_millis millis_to_str millis_to_utc millis_to_zone_name now_millis now_str ' + 'str_to_duration str_to_utc str_to_zone_name decode_json encode_json encoded_size poly_length base64 base64_encode ' + 'base64_decode meta uuid abs acos asin atan atan2 ceil cos degrees e exp ln log floor pi power radians random ' + 'round sign sin sqrt tan trunc object_length object_names object_pairs object_inner_pairs object_values ' + 'object_inner_values object_add object_put object_remove object_unwrap regexp_contains regexp_like regexp_position ' + 'regexp_replace contains initcap length lower ltrim position repeat replace rtrim split substr title trim upper ' + 'isarray isatom isboolean isnumber isobject isstring type toarray toatom toboolean tonumber toobject tostring' }, contains: [ { className: 'string', begin: '\'', end: '\'', contains: [hljs.BACKSLASH_ESCAPE], relevance: 0 }, { className: 'string', begin: '"', end: '"', contains: [hljs.BACKSLASH_ESCAPE], relevance: 0 }, { className: 'symbol', begin: '`', end: '`', contains: [hljs.BACKSLASH_ESCAPE], relevance: 2 }, hljs.C_NUMBER_MODE, hljs.C_BLOCK_COMMENT_MODE ] }, hljs.C_BLOCK_COMMENT_MODE ] }; } },{name:"nginx",create:/* Language: Nginx Author: Peter Leonov Contributors: Ivan Sagalaev Category: common, config */ function(hljs) { var VAR = { className: 'variable', variants: [ {begin: /\$\d+/}, {begin: /\$\{/, end: /}/}, {begin: '[\\$\\@]' + hljs.UNDERSCORE_IDENT_RE} ] }; var DEFAULT = { endsWithParent: true, lexemes: '[a-z/_]+', keywords: { literal: 'on off yes no true false none blocked debug info notice warn error crit ' + 'select break last permanent redirect kqueue rtsig epoll poll /dev/poll' }, relevance: 0, illegal: '=>', contains: [ hljs.HASH_COMMENT_MODE, { className: 'string', contains: [hljs.BACKSLASH_ESCAPE, VAR], variants: [ {begin: /"/, end: /"/}, {begin: /'/, end: /'/} ] }, // this swallows entire URLs to avoid detecting numbers within { begin: '([a-z]+):/', end: '\\s', endsWithParent: true, excludeEnd: true, contains: [VAR] }, { className: 'regexp', contains: [hljs.BACKSLASH_ESCAPE, VAR], variants: [ {begin: "\\s\\^", end: "\\s|{|;", returnEnd: true}, // regexp locations (~, ~*) {begin: "~\\*?\\s+", end: "\\s|{|;", returnEnd: true}, // *.example.com {begin: "\\*(\\.[a-z\\-]+)+"}, // sub.example.* {begin: "([a-z\\-]+\\.)+\\*"} ] }, // IP { className: 'number', begin: '\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b' }, // units { className: 'number', begin: '\\b\\d+[kKmMgGdshdwy]*\\b', relevance: 0 }, VAR ] }; return { aliases: ['nginxconf'], contains: [ hljs.HASH_COMMENT_MODE, { begin: hljs.UNDERSCORE_IDENT_RE + '\\s+{', returnBegin: true, end: '{', contains: [ { className: 'section', begin: hljs.UNDERSCORE_IDENT_RE } ], relevance: 0 }, { begin: hljs.UNDERSCORE_IDENT_RE + '\\s', end: ';|{', returnBegin: true, contains: [ { className: 'attribute', begin: hljs.UNDERSCORE_IDENT_RE, starts: DEFAULT } ], relevance: 0 } ], illegal: '[^\\s\\}]' }; } },{name:"nimrod",create:/* Language: Nimrod */ function(hljs) { return { aliases: ['nim'], keywords: { keyword: 'addr and as asm bind block break case cast const continue converter ' + 'discard distinct div do elif else end enum except export finally ' + 'for from generic if import in include interface is isnot iterator ' + 'let macro method mixin mod nil not notin object of or out proc ptr ' + 'raise ref return shl shr static template try tuple type using var ' + 'when while with without xor yield', literal: 'shared guarded stdin stdout stderr result true false', built_in: 'int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 float ' + 'float32 float64 bool char string cstring pointer expr stmt void ' + 'auto any range array openarray varargs seq set clong culong cchar ' + 'cschar cshort cint csize clonglong cfloat cdouble clongdouble ' + 'cuchar cushort cuint culonglong cstringarray semistatic' }, contains: [ { className: 'meta', // Actually pragma begin: /{\./, end: /\.}/, relevance: 10 }, { className: 'string', begin: /[a-zA-Z]\w*"/, end: /"/, contains: [{begin: /""/}] }, { className: 'string', begin: /([a-zA-Z]\w*)?"""/, end: /"""/ }, hljs.QUOTE_STRING_MODE, { className: 'type', begin: /\b[A-Z]\w+\b/, relevance: 0 }, { className: 'number', relevance: 0, variants: [ {begin: /\b(0[xX][0-9a-fA-F][_0-9a-fA-F]*)('?[iIuU](8|16|32|64))?/}, {begin: /\b(0o[0-7][_0-7]*)('?[iIuUfF](8|16|32|64))?/}, {begin: /\b(0(b|B)[01][_01]*)('?[iIuUfF](8|16|32|64))?/}, {begin: /\b(\d[_\d]*)('?[iIuUfF](8|16|32|64))?/} ] }, hljs.HASH_COMMENT_MODE ] } } },{name:"nix",create:/* Language: Nix Author: Domen Kožar Description: Nix functional language. See http://nixos.org/nix */ function(hljs) { var NIX_KEYWORDS = { keyword: 'rec with let in inherit assert if else then', literal: 'true false or and null', built_in: 'import abort baseNameOf dirOf isNull builtins map removeAttrs throw ' + 'toString derivation' }; var ANTIQUOTE = { className: 'subst', begin: /\$\{/, end: /}/, keywords: NIX_KEYWORDS }; var ATTRS = { begin: /[a-zA-Z0-9-_]+(\s*=)/, returnBegin: true, relevance: 0, contains: [ { className: 'attr', begin: /\S+/ } ] }; var STRING = { className: 'string', contains: [ANTIQUOTE], variants: [ {begin: "''", end: "''"}, {begin: '"', end: '"'} ] }; var EXPRESSIONS = [ hljs.NUMBER_MODE, hljs.HASH_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, STRING, ATTRS ]; ANTIQUOTE.contains = EXPRESSIONS; return { aliases: ["nixos"], keywords: NIX_KEYWORDS, contains: EXPRESSIONS }; } },{name:"nsis",create:/* Language: NSIS Description: Nullsoft Scriptable Install System Author: Jan T. Sott Website: http://github.com/idleberg */ function(hljs) { var CONSTANTS = { className: 'variable', begin: /\$(ADMINTOOLS|APPDATA|CDBURN_AREA|CMDLINE|COMMONFILES32|COMMONFILES64|COMMONFILES|COOKIES|DESKTOP|DOCUMENTS|EXEDIR|EXEFILE|EXEPATH|FAVORITES|FONTS|HISTORY|HWNDPARENT|INSTDIR|INTERNET_CACHE|LANGUAGE|LOCALAPPDATA|MUSIC|NETHOOD|OUTDIR|PICTURES|PLUGINSDIR|PRINTHOOD|PROFILE|PROGRAMFILES32|PROGRAMFILES64|PROGRAMFILES|QUICKLAUNCH|RECENT|RESOURCES_LOCALIZED|RESOURCES|SENDTO|SMPROGRAMS|SMSTARTUP|STARTMENU|SYSDIR|TEMP|TEMPLATES|VIDEOS|WINDIR)/ }; var DEFINES = { // ${defines} className: 'variable', begin: /\$+{[\w\.:-]+}/ }; var VARIABLES = { // $variables className: 'variable', begin: /\$+\w+/, illegal: /\(\){}/ }; var LANGUAGES = { // $(language_strings) className: 'variable', begin: /\$+\([\w\^\.:-]+\)/ }; var PARAMETERS = { // command parameters className: 'params', begin: '(ARCHIVE|FILE_ATTRIBUTE_ARCHIVE|FILE_ATTRIBUTE_NORMAL|FILE_ATTRIBUTE_OFFLINE|FILE_ATTRIBUTE_READONLY|FILE_ATTRIBUTE_SYSTEM|FILE_ATTRIBUTE_TEMPORARY|HKCR|HKCU|HKDD|HKEY_CLASSES_ROOT|HKEY_CURRENT_CONFIG|HKEY_CURRENT_USER|HKEY_DYN_DATA|HKEY_LOCAL_MACHINE|HKEY_PERFORMANCE_DATA|HKEY_USERS|HKLM|HKPD|HKU|IDABORT|IDCANCEL|IDIGNORE|IDNO|IDOK|IDRETRY|IDYES|MB_ABORTRETRYIGNORE|MB_DEFBUTTON1|MB_DEFBUTTON2|MB_DEFBUTTON3|MB_DEFBUTTON4|MB_ICONEXCLAMATION|MB_ICONINFORMATION|MB_ICONQUESTION|MB_ICONSTOP|MB_OK|MB_OKCANCEL|MB_RETRYCANCEL|MB_RIGHT|MB_RTLREADING|MB_SETFOREGROUND|MB_TOPMOST|MB_USERICON|MB_YESNO|NORMAL|OFFLINE|READONLY|SHCTX|SHELL_CONTEXT|SYSTEM|TEMPORARY)' }; var COMPILER = { // !compiler_flags className: 'keyword', begin: /\!(addincludedir|addplugindir|appendfile|cd|define|delfile|echo|else|endif|error|execute|finalize|getdllversion|gettlbversion|if|ifdef|ifmacrodef|ifmacrondef|ifndef|include|insertmacro|macro|macroend|makensis|packhdr|searchparse|searchreplace|system|tempfile|undef|verbose|warning)/ }; var METACHARS = { // $\n, $\r, $\t, $$ className: 'meta', begin: /\$(\\[nrt]|\$)/ }; var PLUGINS = { // plug::ins className: 'class', begin: /\w+\:\:\w+/ }; var STRING = { className: 'string', variants: [ { begin: '"', end: '"' }, { begin: '\'', end: '\'' }, { begin: '`', end: '`' } ], illegal: /\n/, contains: [ METACHARS, CONSTANTS, DEFINES, VARIABLES, LANGUAGES ] }; return { case_insensitive: false, keywords: { keyword: 'Abort AddBrandingImage AddSize AllowRootDirInstall AllowSkipFiles AutoCloseWindow BGFont BGGradient BrandingText BringToFront Call CallInstDLL Caption ChangeUI CheckBitmap ClearErrors CompletedText ComponentText CopyFiles CRCCheck CreateDirectory CreateFont CreateShortCut Delete DeleteINISec DeleteINIStr DeleteRegKey DeleteRegValue DetailPrint DetailsButtonText DirText DirVar DirVerify EnableWindow EnumRegKey EnumRegValue Exch Exec ExecShell ExecShellWait ExecWait ExpandEnvStrings File FileBufSize FileClose FileErrorText FileOpen FileRead FileReadByte FileReadUTF16LE FileReadWord FileSeek FileWrite FileWriteByte FileWriteUTF16LE FileWriteWord FindClose FindFirst FindNext FindWindow FlushINI FunctionEnd GetCurInstType GetCurrentAddress GetDlgItem GetDLLVersion GetDLLVersionLocal GetErrorLevel GetFileTime GetFileTimeLocal GetFullPathName GetFunctionAddress GetInstDirError GetLabelAddress GetTempFileName Goto HideWindow Icon IfAbort IfErrors IfFileExists IfRebootFlag IfSilent InitPluginsDir InstallButtonText InstallColors InstallDir InstallDirRegKey InstProgressFlags InstType InstTypeGetText InstTypeSetText Int64Cmp Int64CmpU Int64Fmt IntCmp IntCmpU IntFmt IntOp IntPtrCmp IntPtrCmpU IntPtrOp IsWindow LangString LicenseBkColor LicenseData LicenseForceSelection LicenseLangString LicenseText LoadLanguageFile LockWindow LogSet LogText ManifestDPIAware ManifestSupportedOS MessageBox MiscButtonText Name Nop OutFile Page PageCallbacks PageExEnd Pop Push Quit ReadEnvStr ReadINIStr ReadRegDWORD ReadRegStr Reboot RegDLL Rename RequestExecutionLevel ReserveFile Return RMDir SearchPath SectionEnd SectionGetFlags SectionGetInstTypes SectionGetSize SectionGetText SectionGroupEnd SectionIn SectionSetFlags SectionSetInstTypes SectionSetSize SectionSetText SendMessage SetAutoClose SetBrandingImage SetCompress SetCompressor SetCompressorDictSize SetCtlColors SetCurInstType SetDatablockOptimize SetDateSave SetDetailsPrint SetDetailsView SetErrorLevel SetErrors SetFileAttributes SetFont SetOutPath SetOverwrite SetRebootFlag SetRegView SetShellVarContext SetSilent ShowInstDetails ShowUninstDetails ShowWindow SilentInstall SilentUnInstall Sleep SpaceTexts StrCmp StrCmpS StrCpy StrLen SubCaption Unicode UninstallButtonText UninstallCaption UninstallIcon UninstallSubCaption UninstallText UninstPage UnRegDLL Var VIAddVersionKey VIFileVersion VIProductVersion WindowIcon WriteINIStr WriteRegBin WriteRegDWORD WriteRegExpandStr WriteRegMultiStr WriteRegNone WriteRegStr WriteUninstaller XPStyle', literal: 'admin all auto both bottom bzip2 colored components current custom directory false force hide highest ifdiff ifnewer instfiles lastused leave left license listonly lzma nevershow none normal notset off on open print right show silent silentlog smooth textonly top true try un.components un.custom un.directory un.instfiles un.license uninstConfirm user Win10 Win7 Win8 WinVista zlib' }, contains: [ hljs.HASH_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.COMMENT( ';', '$', { relevance: 0 } ), { className: 'function', beginKeywords: 'Function PageEx Section SectionGroup', end: '$' }, STRING, COMPILER, DEFINES, VARIABLES, LANGUAGES, PARAMETERS, PLUGINS, hljs.NUMBER_MODE ] }; } },{name:"objectivec",create:/* Language: Objective-C Author: Valerii Hiora Contributors: Angel G. Olloqui , Matt Diephouse , Andrew Farmer , Minh Nguyễn Category: common */ function(hljs) { var API_CLASS = { className: 'built_in', begin: '\\b(AV|CA|CF|CG|CI|CL|CM|CN|CT|MK|MP|MTK|MTL|NS|SCN|SK|UI|WK|XC)\\w+', }; var OBJC_KEYWORDS = { keyword: 'int float while char export sizeof typedef const struct for union ' + 'unsigned long volatile static bool mutable if do return goto void ' + 'enum else break extern asm case short default double register explicit ' + 'signed typename this switch continue wchar_t inline readonly assign ' + 'readwrite self @synchronized id typeof ' + 'nonatomic super unichar IBOutlet IBAction strong weak copy ' + 'in out inout bycopy byref oneway __strong __weak __block __autoreleasing ' + '@private @protected @public @try @property @end @throw @catch @finally ' + '@autoreleasepool @synthesize @dynamic @selector @optional @required ' + '@encode @package @import @defs @compatibility_alias ' + '__bridge __bridge_transfer __bridge_retained __bridge_retain ' + '__covariant __contravariant __kindof ' + '_Nonnull _Nullable _Null_unspecified ' + '__FUNCTION__ __PRETTY_FUNCTION__ __attribute__ ' + 'getter setter retain unsafe_unretained ' + 'nonnull nullable null_unspecified null_resettable class instancetype ' + 'NS_DESIGNATED_INITIALIZER NS_UNAVAILABLE NS_REQUIRES_SUPER ' + 'NS_RETURNS_INNER_POINTER NS_INLINE NS_AVAILABLE NS_DEPRECATED ' + 'NS_ENUM NS_OPTIONS NS_SWIFT_UNAVAILABLE ' + 'NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END ' + 'NS_REFINED_FOR_SWIFT NS_SWIFT_NAME NS_SWIFT_NOTHROW ' + 'NS_DURING NS_HANDLER NS_ENDHANDLER NS_VALUERETURN NS_VOIDRETURN', literal: 'false true FALSE TRUE nil YES NO NULL', built_in: 'BOOL dispatch_once_t dispatch_queue_t dispatch_sync dispatch_async dispatch_once' }; var LEXEMES = /[a-zA-Z@][a-zA-Z0-9_]*/; var CLASS_KEYWORDS = '@interface @class @protocol @implementation'; return { aliases: ['mm', 'objc', 'obj-c'], keywords: OBJC_KEYWORDS, lexemes: LEXEMES, illegal: '' } ] } ] }, { className: 'class', begin: '(' + CLASS_KEYWORDS.split(' ').join('|') + ')\\b', end: '({|$)', excludeEnd: true, keywords: CLASS_KEYWORDS, lexemes: LEXEMES, contains: [ hljs.UNDERSCORE_TITLE_MODE ] }, { begin: '\\.'+hljs.UNDERSCORE_IDENT_RE, relevance: 0 } ] }; } },{name:"ocaml",create:/* Language: OCaml Author: Mehdi Dogguy Contributors: Nicolas Braud-Santoni , Mickael Delahaye Description: OCaml language definition. Category: functional */ function(hljs) { /* missing support for heredoc-like string (OCaml 4.0.2+) */ return { aliases: ['ml'], keywords: { keyword: 'and as assert asr begin class constraint do done downto else end ' + 'exception external for fun function functor if in include ' + 'inherit! inherit initializer land lazy let lor lsl lsr lxor match method!|10 method ' + 'mod module mutable new object of open! open or private rec sig struct ' + 'then to try type val! val virtual when while with ' + /* camlp4 */ 'parser value', built_in: /* built-in types */ 'array bool bytes char exn|5 float int int32 int64 list lazy_t|5 nativeint|5 string unit ' + /* (some) types in Pervasives */ 'in_channel out_channel ref', literal: 'true false' }, illegal: /\/\/|>>/, lexemes: '[a-z_]\\w*!?', contains: [ { className: 'literal', begin: '\\[(\\|\\|)?\\]|\\(\\)', relevance: 0 }, hljs.COMMENT( '\\(\\*', '\\*\\)', { contains: ['self'] } ), { /* type variable */ className: 'symbol', begin: '\'[A-Za-z_](?!\')[\\w\']*' /* the grammar is ambiguous on how 'a'b should be interpreted but not the compiler */ }, { /* polymorphic variant */ className: 'type', begin: '`[A-Z][\\w\']*' }, { /* module or constructor */ className: 'type', begin: '\\b[A-Z][\\w\']*', relevance: 0 }, { /* don't color identifiers, but safely catch all identifiers with '*/ begin: '[a-z_]\\w*\'[\\w\']*', relevance: 0 }, hljs.inherit(hljs.APOS_STRING_MODE, {className: 'string', relevance: 0}), hljs.inherit(hljs.QUOTE_STRING_MODE, {illegal: null}), { className: 'number', begin: '\\b(0[xX][a-fA-F0-9_]+[Lln]?|' + '0[oO][0-7_]+[Lln]?|' + '0[bB][01_]+[Lln]?|' + '[0-9][0-9_]*([Lln]|(\\.[0-9_]*)?([eE][-+]?[0-9_]+)?)?)', relevance: 0 }, { begin: /[-=]>/ // relevance booster } ] } } },{name:"openscad",create:/* Language: OpenSCAD Author: Dan Panzarella Description: OpenSCAD is a language for the 3D CAD modeling software of the same name. Category: scientific */ function(hljs) { var SPECIAL_VARS = { className: 'keyword', begin: '\\$(f[asn]|t|vp[rtd]|children)' }, LITERALS = { className: 'literal', begin: 'false|true|PI|undef' }, NUMBERS = { className: 'number', begin: '\\b\\d+(\\.\\d+)?(e-?\\d+)?', //adds 1e5, 1e-10 relevance: 0 }, STRING = hljs.inherit(hljs.QUOTE_STRING_MODE,{illegal: null}), PREPRO = { className: 'meta', keywords: {'meta-keyword': 'include use'}, begin: 'include|use <', end: '>' }, PARAMS = { className: 'params', begin: '\\(', end: '\\)', contains: ['self', NUMBERS, STRING, SPECIAL_VARS, LITERALS] }, MODIFIERS = { begin: '[*!#%]', relevance: 0 }, FUNCTIONS = { className: 'function', beginKeywords: 'module function', end: '\\=|\\{', contains: [PARAMS, hljs.UNDERSCORE_TITLE_MODE] }; return { aliases: ['scad'], keywords: { keyword: 'function module include use for intersection_for if else \\%', literal: 'false true PI undef', built_in: 'circle square polygon text sphere cube cylinder polyhedron translate rotate scale resize mirror multmatrix color offset hull minkowski union difference intersection abs sign sin cos tan acos asin atan atan2 floor round ceil ln log pow sqrt exp rands min max concat lookup str chr search version version_num norm cross parent_module echo import import_dxf dxf_linear_extrude linear_extrude rotate_extrude surface projection render children dxf_cross dxf_dim let assign' }, contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, NUMBERS, PREPRO, STRING, SPECIAL_VARS, MODIFIERS, FUNCTIONS ] } } },{name:"oxygene",create:/* Language: Oxygene Author: Carlo Kok Description: Language definition for RemObjects Oxygene (http://www.remobjects.com) */ function(hljs) { var OXYGENE_KEYWORDS = 'abstract add and array as asc aspect assembly async begin break block by case class concat const copy constructor continue '+ 'create default delegate desc distinct div do downto dynamic each else empty end ensure enum equals event except exit extension external false '+ 'final finalize finalizer finally flags for forward from function future global group has if implementation implements implies in index inherited '+ 'inline interface into invariants is iterator join locked locking loop matching method mod module namespace nested new nil not notify nullable of '+ 'old on operator or order out override parallel params partial pinned private procedure property protected public queryable raise read readonly '+ 'record reintroduce remove repeat require result reverse sealed select self sequence set shl shr skip static step soft take then to true try tuple '+ 'type union unit unsafe until uses using var virtual raises volatile where while with write xor yield await mapped deprecated stdcall cdecl pascal '+ 'register safecall overload library platform reference packed strict published autoreleasepool selector strong weak unretained'; var CURLY_COMMENT = hljs.COMMENT( '{', '}', { relevance: 0 } ); var PAREN_COMMENT = hljs.COMMENT( '\\(\\*', '\\*\\)', { relevance: 10 } ); var STRING = { className: 'string', begin: '\'', end: '\'', contains: [{begin: '\'\''}] }; var CHAR_STRING = { className: 'string', begin: '(#\\d+)+' }; var FUNCTION = { className: 'function', beginKeywords: 'function constructor destructor procedure method', end: '[:;]', keywords: 'function constructor|10 destructor|10 procedure|10 method|10', contains: [ hljs.TITLE_MODE, { className: 'params', begin: '\\(', end: '\\)', keywords: OXYGENE_KEYWORDS, contains: [STRING, CHAR_STRING] }, CURLY_COMMENT, PAREN_COMMENT ] }; return { case_insensitive: true, lexemes: /\.?\w+/, keywords: OXYGENE_KEYWORDS, illegal: '("|\\$[G-Zg-z]|\\/\\*||->)', contains: [ CURLY_COMMENT, PAREN_COMMENT, hljs.C_LINE_COMMENT_MODE, STRING, CHAR_STRING, hljs.NUMBER_MODE, FUNCTION, { className: 'class', begin: '=\\bclass\\b', end: 'end;', keywords: OXYGENE_KEYWORDS, contains: [ STRING, CHAR_STRING, CURLY_COMMENT, PAREN_COMMENT, hljs.C_LINE_COMMENT_MODE, FUNCTION ] } ] }; } },{name:"parser3",create:/* Language: Parser3 Requires: xml.js Author: Oleg Volchkov Category: template */ function(hljs) { var CURLY_SUBCOMMENT = hljs.COMMENT( '{', '}', { contains: ['self'] } ); return { subLanguage: 'xml', relevance: 0, contains: [ hljs.COMMENT('^#', '$'), hljs.COMMENT( '\\^rem{', '}', { relevance: 10, contains: [ CURLY_SUBCOMMENT ] } ), { className: 'meta', begin: '^@(?:BASE|USE|CLASS|OPTIONS)$', relevance: 10 }, { className: 'title', begin: '@[\\w\\-]+\\[[\\w^;\\-]*\\](?:\\[[\\w^;\\-]*\\])?(?:.*)$' }, { className: 'variable', begin: '\\$\\{?[\\w\\-\\.\\:]+\\}?' }, { className: 'keyword', begin: '\\^[\\w\\-\\.\\:]+' }, { className: 'number', begin: '\\^#[0-9a-fA-F]+' }, hljs.C_NUMBER_MODE ] }; } },{name:"perl",create:/* Language: Perl Author: Peter Leonov Category: common */ function(hljs) { var PERL_KEYWORDS = 'getpwent getservent quotemeta msgrcv scalar kill dbmclose undef lc ' + 'ma syswrite tr send umask sysopen shmwrite vec qx utime local oct semctl localtime ' + 'readpipe do return format read sprintf dbmopen pop getpgrp not getpwnam rewinddir qq' + 'fileno qw endprotoent wait sethostent bless s|0 opendir continue each sleep endgrent ' + 'shutdown dump chomp connect getsockname die socketpair close flock exists index shmget' + 'sub for endpwent redo lstat msgctl setpgrp abs exit select print ref gethostbyaddr ' + 'unshift fcntl syscall goto getnetbyaddr join gmtime symlink semget splice x|0 ' + 'getpeername recv log setsockopt cos last reverse gethostbyname getgrnam study formline ' + 'endhostent times chop length gethostent getnetent pack getprotoent getservbyname rand ' + 'mkdir pos chmod y|0 substr endnetent printf next open msgsnd readdir use unlink ' + 'getsockopt getpriority rindex wantarray hex system getservbyport endservent int chr ' + 'untie rmdir prototype tell listen fork shmread ucfirst setprotoent else sysseek link ' + 'getgrgid shmctl waitpid unpack getnetbyname reset chdir grep split require caller ' + 'lcfirst until warn while values shift telldir getpwuid my getprotobynumber delete and ' + 'sort uc defined srand accept package seekdir getprotobyname semop our rename seek if q|0 ' + 'chroot sysread setpwent no crypt getc chown sqrt write setnetent setpriority foreach ' + 'tie sin msgget map stat getlogin unless elsif truncate exec keys glob tied closedir' + 'ioctl socket readlink eval xor readline binmode setservent eof ord bind alarm pipe ' + 'atan2 getgrent exp time push setgrent gt lt or ne m|0 break given say state when'; var SUBST = { className: 'subst', begin: '[$@]\\{', end: '\\}', keywords: PERL_KEYWORDS }; var METHOD = { begin: '->{', end: '}' // contains defined later }; var VAR = { variants: [ {begin: /\$\d/}, {begin: /[\$%@](\^\w\b|#\w+(::\w+)*|{\w+}|\w+(::\w*)*)/}, {begin: /[\$%@][^\s\w{]/, relevance: 0} ] }; var STRING_CONTAINS = [hljs.BACKSLASH_ESCAPE, SUBST, VAR]; var PERL_DEFAULT_CONTAINS = [ VAR, hljs.HASH_COMMENT_MODE, hljs.COMMENT( '^\\=\\w', '\\=cut', { endsWithParent: true } ), METHOD, { className: 'string', contains: STRING_CONTAINS, variants: [ { begin: 'q[qwxr]?\\s*\\(', end: '\\)', relevance: 5 }, { begin: 'q[qwxr]?\\s*\\[', end: '\\]', relevance: 5 }, { begin: 'q[qwxr]?\\s*\\{', end: '\\}', relevance: 5 }, { begin: 'q[qwxr]?\\s*\\|', end: '\\|', relevance: 5 }, { begin: 'q[qwxr]?\\s*\\<', end: '\\>', relevance: 5 }, { begin: 'qw\\s+q', end: 'q', relevance: 5 }, { begin: '\'', end: '\'', contains: [hljs.BACKSLASH_ESCAPE] }, { begin: '"', end: '"' }, { begin: '`', end: '`', contains: [hljs.BACKSLASH_ESCAPE] }, { begin: '{\\w+}', contains: [], relevance: 0 }, { begin: '\-?\\w+\\s*\\=\\>', contains: [], relevance: 0 } ] }, { className: 'number', begin: '(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b', relevance: 0 }, { // regexp container begin: '(\\/\\/|' + hljs.RE_STARTERS_RE + '|\\b(split|return|print|reverse|grep)\\b)\\s*', keywords: 'split return print reverse grep', relevance: 0, contains: [ hljs.HASH_COMMENT_MODE, { className: 'regexp', begin: '(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*', relevance: 10 }, { className: 'regexp', begin: '(m|qr)?/', end: '/[a-z]*', contains: [hljs.BACKSLASH_ESCAPE], relevance: 0 // allows empty "//" which is a common comment delimiter in other languages } ] }, { className: 'function', beginKeywords: 'sub', end: '(\\s*\\(.*?\\))?[;{]', excludeEnd: true, relevance: 5, contains: [hljs.TITLE_MODE] }, { begin: '-\\w\\b', relevance: 0 }, { begin: "^__DATA__$", end: "^__END__$", subLanguage: 'mojolicious', contains: [ { begin: "^@@.*", end: "$", className: "comment" } ] } ]; SUBST.contains = PERL_DEFAULT_CONTAINS; METHOD.contains = PERL_DEFAULT_CONTAINS; return { aliases: ['pl', 'pm'], lexemes: /[\w\.]+/, keywords: PERL_KEYWORDS, contains: PERL_DEFAULT_CONTAINS }; } },{name:"pf",create:/* Language: pf Category: config Author: Peter Piwowarski Description: The pf.conf(5) format as of OpenBSD 5.6 */ function(hljs) { var MACRO = { className: 'variable', begin: /\$[\w\d#@][\w\d_]*/ }; var TABLE = { className: 'variable', begin: /<(?!\/)/, end: />/ }; var QUOTE_STRING = { className: 'string', begin: /"/, end: /"/ }; return { aliases: ['pf.conf'], lexemes: /[a-z0-9_<>-]+/, keywords: { built_in: /* block match pass are "actions" in pf.conf(5), the rest are * lexically similar top-level commands. */ 'block match pass load anchor|5 antispoof|10 set table', keyword: 'in out log quick on rdomain inet inet6 proto from port os to route' + 'allow-opts divert-packet divert-reply divert-to flags group icmp-type' + 'icmp6-type label once probability recieved-on rtable prio queue' + 'tos tag tagged user keep fragment for os drop' + 'af-to|10 binat-to|10 nat-to|10 rdr-to|10 bitmask least-stats random round-robin' + 'source-hash static-port' + 'dup-to reply-to route-to' + 'parent bandwidth default min max qlimit' + 'block-policy debug fingerprints hostid limit loginterface optimization' + 'reassemble ruleset-optimization basic none profile skip state-defaults' + 'state-policy timeout' + 'const counters persist' + 'no modulate synproxy state|5 floating if-bound no-sync pflow|10 sloppy' + 'source-track global rule max-src-nodes max-src-states max-src-conn' + 'max-src-conn-rate overload flush' + 'scrub|5 max-mss min-ttl no-df|10 random-id', literal: 'all any no-route self urpf-failed egress|5 unknown' }, contains: [ hljs.HASH_COMMENT_MODE, hljs.NUMBER_MODE, hljs.QUOTE_STRING_MODE, MACRO, TABLE ] }; } },{name:"pgsql",create:/* Language: PostgreSQL SQL dialect and PL/pgSQL Author: Egor Rogov (e.rogov@postgrespro.ru) Description: This language incorporates both PostgreSQL SQL dialect and PL/pgSQL language. It is based on PostgreSQL version 11. Some notes: - Text in double-dollar-strings is _always_ interpreted as some programming code. Text in ordinary quotes is _never_ interpreted that way and highlighted just as a string. - There are quite a bit "special cases". That's because many keywords are not strictly they are keywords in some contexts and ordinary identifiers in others. Only some of such cases are handled; you still can get some of your identifiers highlighted wrong way. - Function names deliberately are not highlighted. There is no way to tell function call from other constructs, hence we can't highlight _all_ function names. And some names highlighted while others not looks ugly. */ function(hljs) { var COMMENT_MODE = hljs.COMMENT('--', '$'); var UNQUOTED_IDENT = '[a-zA-Z_][a-zA-Z_0-9$]*'; var DOLLAR_STRING = '\\$([a-zA-Z_]?|[a-zA-Z_][a-zA-Z_0-9]*)\\$'; var LABEL = '<<\\s*' + UNQUOTED_IDENT + '\\s*>>'; var SQL_KW = // https://www.postgresql.org/docs/11/static/sql-keywords-appendix.html // https://www.postgresql.org/docs/11/static/sql-commands.html // SQL commands (starting words) 'ABORT ALTER ANALYZE BEGIN CALL CHECKPOINT|10 CLOSE CLUSTER COMMENT COMMIT COPY CREATE DEALLOCATE DECLARE ' + 'DELETE DISCARD DO DROP END EXECUTE EXPLAIN FETCH GRANT IMPORT INSERT LISTEN LOAD LOCK MOVE NOTIFY ' + 'PREPARE REASSIGN|10 REFRESH REINDEX RELEASE RESET REVOKE ROLLBACK SAVEPOINT SECURITY SELECT SET SHOW ' + 'START TRUNCATE UNLISTEN|10 UPDATE VACUUM|10 VALUES ' + // SQL commands (others) 'AGGREGATE COLLATION CONVERSION|10 DATABASE DEFAULT PRIVILEGES DOMAIN TRIGGER EXTENSION FOREIGN ' + 'WRAPPER|10 TABLE FUNCTION GROUP LANGUAGE LARGE OBJECT MATERIALIZED VIEW OPERATOR CLASS ' + 'FAMILY POLICY PUBLICATION|10 ROLE RULE SCHEMA SEQUENCE SERVER STATISTICS SUBSCRIPTION SYSTEM ' + 'TABLESPACE CONFIGURATION DICTIONARY PARSER TEMPLATE TYPE USER MAPPING PREPARED ACCESS ' + 'METHOD CAST AS TRANSFORM TRANSACTION OWNED TO INTO SESSION AUTHORIZATION ' + 'INDEX PROCEDURE ASSERTION ' + // additional reserved key words 'ALL ANALYSE AND ANY ARRAY ASC ASYMMETRIC|10 BOTH CASE CHECK ' + 'COLLATE COLUMN CONCURRENTLY|10 CONSTRAINT CROSS ' + 'DEFERRABLE RANGE ' + 'DESC DISTINCT ELSE EXCEPT FOR FREEZE|10 FROM FULL HAVING ' + 'ILIKE IN INITIALLY INNER INTERSECT IS ISNULL JOIN LATERAL LEADING LIKE LIMIT ' + 'NATURAL NOT NOTNULL NULL OFFSET ON ONLY OR ORDER OUTER OVERLAPS PLACING PRIMARY ' + 'REFERENCES RETURNING SIMILAR SOME SYMMETRIC TABLESAMPLE THEN ' + 'TRAILING UNION UNIQUE USING VARIADIC|10 VERBOSE WHEN WHERE WINDOW WITH ' + // some of non-reserved (which are used in clauses or as PL/pgSQL keyword) 'BY RETURNS INOUT OUT SETOF|10 IF STRICT CURRENT CONTINUE OWNER LOCATION OVER PARTITION WITHIN ' + 'BETWEEN ESCAPE EXTERNAL INVOKER DEFINER WORK RENAME VERSION CONNECTION CONNECT ' + 'TABLES TEMP TEMPORARY FUNCTIONS SEQUENCES TYPES SCHEMAS OPTION CASCADE RESTRICT ADD ADMIN ' + 'EXISTS VALID VALIDATE ENABLE DISABLE REPLICA|10 ALWAYS PASSING COLUMNS PATH ' + 'REF VALUE OVERRIDING IMMUTABLE STABLE VOLATILE BEFORE AFTER EACH ROW PROCEDURAL ' + 'ROUTINE NO HANDLER VALIDATOR OPTIONS STORAGE OIDS|10 WITHOUT INHERIT DEPENDS CALLED ' + 'INPUT LEAKPROOF|10 COST ROWS NOWAIT SEARCH UNTIL ENCRYPTED|10 PASSWORD CONFLICT|10 ' + 'INSTEAD INHERITS CHARACTERISTICS WRITE CURSOR ALSO STATEMENT SHARE EXCLUSIVE INLINE ' + 'ISOLATION REPEATABLE READ COMMITTED SERIALIZABLE UNCOMMITTED LOCAL GLOBAL SQL PROCEDURES ' + 'RECURSIVE SNAPSHOT ROLLUP CUBE TRUSTED|10 INCLUDE FOLLOWING PRECEDING UNBOUNDED RANGE GROUPS ' + 'UNENCRYPTED|10 SYSID FORMAT DELIMITER HEADER QUOTE ENCODING FILTER OFF ' + // some parameters of VACUUM/ANALYZE/EXPLAIN 'FORCE_QUOTE FORCE_NOT_NULL FORCE_NULL COSTS BUFFERS TIMING SUMMARY DISABLE_PAGE_SKIPPING ' + // 'RESTART CYCLE GENERATED IDENTITY DEFERRED IMMEDIATE LEVEL LOGGED UNLOGGED ' + 'OF NOTHING NONE EXCLUDE ATTRIBUTE ' + // from GRANT (not keywords actually) 'USAGE ROUTINES ' + // actually literals, but look better this way (due to IS TRUE, IS FALSE, ISNULL etc) 'TRUE FALSE NAN INFINITY '; var ROLE_ATTRS = // only those not in keywrods already 'SUPERUSER NOSUPERUSER CREATEDB NOCREATEDB CREATEROLE NOCREATEROLE INHERIT NOINHERIT ' + 'LOGIN NOLOGIN REPLICATION NOREPLICATION BYPASSRLS NOBYPASSRLS '; var PLPGSQL_KW = 'ALIAS BEGIN CONSTANT DECLARE END EXCEPTION RETURN PERFORM|10 RAISE GET DIAGNOSTICS ' + 'STACKED|10 FOREACH LOOP ELSIF EXIT WHILE REVERSE SLICE DEBUG LOG INFO NOTICE WARNING ASSERT ' + 'OPEN '; var TYPES = // https://www.postgresql.org/docs/11/static/datatype.html 'BIGINT INT8 BIGSERIAL SERIAL8 BIT VARYING VARBIT BOOLEAN BOOL BOX BYTEA CHARACTER CHAR VARCHAR ' + 'CIDR CIRCLE DATE DOUBLE PRECISION FLOAT8 FLOAT INET INTEGER INT INT4 INTERVAL JSON JSONB LINE LSEG|10 ' + 'MACADDR MACADDR8 MONEY NUMERIC DEC DECIMAL PATH POINT POLYGON REAL FLOAT4 SMALLINT INT2 ' + 'SMALLSERIAL|10 SERIAL2|10 SERIAL|10 SERIAL4|10 TEXT TIME ZONE TIMETZ|10 TIMESTAMP TIMESTAMPTZ|10 TSQUERY|10 TSVECTOR|10 ' + 'TXID_SNAPSHOT|10 UUID XML NATIONAL NCHAR ' + 'INT4RANGE|10 INT8RANGE|10 NUMRANGE|10 TSRANGE|10 TSTZRANGE|10 DATERANGE|10 ' + // pseudotypes 'ANYELEMENT ANYARRAY ANYNONARRAY ANYENUM ANYRANGE CSTRING INTERNAL ' + 'RECORD PG_DDL_COMMAND VOID UNKNOWN OPAQUE REFCURSOR ' + // spec. type 'NAME ' + // OID-types 'OID REGPROC|10 REGPROCEDURE|10 REGOPER|10 REGOPERATOR|10 REGCLASS|10 REGTYPE|10 REGROLE|10 ' + 'REGNAMESPACE|10 REGCONFIG|10 REGDICTIONARY|10 ';// + // some types from standard extensions 'HSTORE|10 LO LTREE|10 '; var TYPES_RE = TYPES.trim() .split(' ') .map( function(val) { return val.split('|')[0]; } ) .join('|'); var SQL_BI = 'CURRENT_TIME CURRENT_TIMESTAMP CURRENT_USER CURRENT_CATALOG|10 CURRENT_DATE LOCALTIME LOCALTIMESTAMP ' + 'CURRENT_ROLE|10 CURRENT_SCHEMA|10 SESSION_USER PUBLIC '; var PLPGSQL_BI = 'FOUND NEW OLD TG_NAME|10 TG_WHEN|10 TG_LEVEL|10 TG_OP|10 TG_RELID|10 TG_RELNAME|10 ' + 'TG_TABLE_NAME|10 TG_TABLE_SCHEMA|10 TG_NARGS|10 TG_ARGV|10 TG_EVENT|10 TG_TAG|10 ' + // get diagnostics 'ROW_COUNT RESULT_OID|10 PG_CONTEXT|10 RETURNED_SQLSTATE COLUMN_NAME CONSTRAINT_NAME ' + 'PG_DATATYPE_NAME|10 MESSAGE_TEXT TABLE_NAME SCHEMA_NAME PG_EXCEPTION_DETAIL|10 ' + 'PG_EXCEPTION_HINT|10 PG_EXCEPTION_CONTEXT|10 '; var PLPGSQL_EXCEPTIONS = // exceptions https://www.postgresql.org/docs/current/static/errcodes-appendix.html 'SQLSTATE SQLERRM|10 ' + 'SUCCESSFUL_COMPLETION WARNING DYNAMIC_RESULT_SETS_RETURNED IMPLICIT_ZERO_BIT_PADDING ' + 'NULL_VALUE_ELIMINATED_IN_SET_FUNCTION PRIVILEGE_NOT_GRANTED PRIVILEGE_NOT_REVOKED ' + 'STRING_DATA_RIGHT_TRUNCATION DEPRECATED_FEATURE NO_DATA NO_ADDITIONAL_DYNAMIC_RESULT_SETS_RETURNED ' + 'SQL_STATEMENT_NOT_YET_COMPLETE CONNECTION_EXCEPTION CONNECTION_DOES_NOT_EXIST CONNECTION_FAILURE ' + 'SQLCLIENT_UNABLE_TO_ESTABLISH_SQLCONNECTION SQLSERVER_REJECTED_ESTABLISHMENT_OF_SQLCONNECTION ' + 'TRANSACTION_RESOLUTION_UNKNOWN PROTOCOL_VIOLATION TRIGGERED_ACTION_EXCEPTION FEATURE_NOT_SUPPORTED ' + 'INVALID_TRANSACTION_INITIATION LOCATOR_EXCEPTION INVALID_LOCATOR_SPECIFICATION INVALID_GRANTOR ' + 'INVALID_GRANT_OPERATION INVALID_ROLE_SPECIFICATION DIAGNOSTICS_EXCEPTION ' + 'STACKED_DIAGNOSTICS_ACCESSED_WITHOUT_ACTIVE_HANDLER CASE_NOT_FOUND CARDINALITY_VIOLATION ' + 'DATA_EXCEPTION ARRAY_SUBSCRIPT_ERROR CHARACTER_NOT_IN_REPERTOIRE DATETIME_FIELD_OVERFLOW ' + 'DIVISION_BY_ZERO ERROR_IN_ASSIGNMENT ESCAPE_CHARACTER_CONFLICT INDICATOR_OVERFLOW ' + 'INTERVAL_FIELD_OVERFLOW INVALID_ARGUMENT_FOR_LOGARITHM INVALID_ARGUMENT_FOR_NTILE_FUNCTION ' + 'INVALID_ARGUMENT_FOR_NTH_VALUE_FUNCTION INVALID_ARGUMENT_FOR_POWER_FUNCTION ' + 'INVALID_ARGUMENT_FOR_WIDTH_BUCKET_FUNCTION INVALID_CHARACTER_VALUE_FOR_CAST ' + 'INVALID_DATETIME_FORMAT INVALID_ESCAPE_CHARACTER INVALID_ESCAPE_OCTET INVALID_ESCAPE_SEQUENCE ' + 'NONSTANDARD_USE_OF_ESCAPE_CHARACTER INVALID_INDICATOR_PARAMETER_VALUE INVALID_PARAMETER_VALUE ' + 'INVALID_REGULAR_EXPRESSION INVALID_ROW_COUNT_IN_LIMIT_CLAUSE ' + 'INVALID_ROW_COUNT_IN_RESULT_OFFSET_CLAUSE INVALID_TABLESAMPLE_ARGUMENT INVALID_TABLESAMPLE_REPEAT ' + 'INVALID_TIME_ZONE_DISPLACEMENT_VALUE INVALID_USE_OF_ESCAPE_CHARACTER MOST_SPECIFIC_TYPE_MISMATCH ' + 'NULL_VALUE_NOT_ALLOWED NULL_VALUE_NO_INDICATOR_PARAMETER NUMERIC_VALUE_OUT_OF_RANGE ' + 'SEQUENCE_GENERATOR_LIMIT_EXCEEDED STRING_DATA_LENGTH_MISMATCH STRING_DATA_RIGHT_TRUNCATION ' + 'SUBSTRING_ERROR TRIM_ERROR UNTERMINATED_C_STRING ZERO_LENGTH_CHARACTER_STRING ' + 'FLOATING_POINT_EXCEPTION INVALID_TEXT_REPRESENTATION INVALID_BINARY_REPRESENTATION ' + 'BAD_COPY_FILE_FORMAT UNTRANSLATABLE_CHARACTER NOT_AN_XML_DOCUMENT INVALID_XML_DOCUMENT ' + 'INVALID_XML_CONTENT INVALID_XML_COMMENT INVALID_XML_PROCESSING_INSTRUCTION ' + 'INTEGRITY_CONSTRAINT_VIOLATION RESTRICT_VIOLATION NOT_NULL_VIOLATION FOREIGN_KEY_VIOLATION ' + 'UNIQUE_VIOLATION CHECK_VIOLATION EXCLUSION_VIOLATION INVALID_CURSOR_STATE ' + 'INVALID_TRANSACTION_STATE ACTIVE_SQL_TRANSACTION BRANCH_TRANSACTION_ALREADY_ACTIVE ' + 'HELD_CURSOR_REQUIRES_SAME_ISOLATION_LEVEL INAPPROPRIATE_ACCESS_MODE_FOR_BRANCH_TRANSACTION ' + 'INAPPROPRIATE_ISOLATION_LEVEL_FOR_BRANCH_TRANSACTION ' + 'NO_ACTIVE_SQL_TRANSACTION_FOR_BRANCH_TRANSACTION READ_ONLY_SQL_TRANSACTION ' + 'SCHEMA_AND_DATA_STATEMENT_MIXING_NOT_SUPPORTED NO_ACTIVE_SQL_TRANSACTION ' + 'IN_FAILED_SQL_TRANSACTION IDLE_IN_TRANSACTION_SESSION_TIMEOUT INVALID_SQL_STATEMENT_NAME ' + 'TRIGGERED_DATA_CHANGE_VIOLATION INVALID_AUTHORIZATION_SPECIFICATION INVALID_PASSWORD ' + 'DEPENDENT_PRIVILEGE_DESCRIPTORS_STILL_EXIST DEPENDENT_OBJECTS_STILL_EXIST ' + 'INVALID_TRANSACTION_TERMINATION SQL_ROUTINE_EXCEPTION FUNCTION_EXECUTED_NO_RETURN_STATEMENT ' + 'MODIFYING_SQL_DATA_NOT_PERMITTED PROHIBITED_SQL_STATEMENT_ATTEMPTED ' + 'READING_SQL_DATA_NOT_PERMITTED INVALID_CURSOR_NAME EXTERNAL_ROUTINE_EXCEPTION ' + 'CONTAINING_SQL_NOT_PERMITTED MODIFYING_SQL_DATA_NOT_PERMITTED ' + 'PROHIBITED_SQL_STATEMENT_ATTEMPTED READING_SQL_DATA_NOT_PERMITTED ' + 'EXTERNAL_ROUTINE_INVOCATION_EXCEPTION INVALID_SQLSTATE_RETURNED NULL_VALUE_NOT_ALLOWED ' + 'TRIGGER_PROTOCOL_VIOLATED SRF_PROTOCOL_VIOLATED EVENT_TRIGGER_PROTOCOL_VIOLATED ' + 'SAVEPOINT_EXCEPTION INVALID_SAVEPOINT_SPECIFICATION INVALID_CATALOG_NAME ' + 'INVALID_SCHEMA_NAME TRANSACTION_ROLLBACK TRANSACTION_INTEGRITY_CONSTRAINT_VIOLATION ' + 'SERIALIZATION_FAILURE STATEMENT_COMPLETION_UNKNOWN DEADLOCK_DETECTED ' + 'SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION SYNTAX_ERROR INSUFFICIENT_PRIVILEGE CANNOT_COERCE ' + 'GROUPING_ERROR WINDOWING_ERROR INVALID_RECURSION INVALID_FOREIGN_KEY INVALID_NAME ' + 'NAME_TOO_LONG RESERVED_NAME DATATYPE_MISMATCH INDETERMINATE_DATATYPE COLLATION_MISMATCH ' + 'INDETERMINATE_COLLATION WRONG_OBJECT_TYPE GENERATED_ALWAYS UNDEFINED_COLUMN ' + 'UNDEFINED_FUNCTION UNDEFINED_TABLE UNDEFINED_PARAMETER UNDEFINED_OBJECT ' + 'DUPLICATE_COLUMN DUPLICATE_CURSOR DUPLICATE_DATABASE DUPLICATE_FUNCTION ' + 'DUPLICATE_PREPARED_STATEMENT DUPLICATE_SCHEMA DUPLICATE_TABLE DUPLICATE_ALIAS ' + 'DUPLICATE_OBJECT AMBIGUOUS_COLUMN AMBIGUOUS_FUNCTION AMBIGUOUS_PARAMETER AMBIGUOUS_ALIAS ' + 'INVALID_COLUMN_REFERENCE INVALID_COLUMN_DEFINITION INVALID_CURSOR_DEFINITION ' + 'INVALID_DATABASE_DEFINITION INVALID_FUNCTION_DEFINITION ' + 'INVALID_PREPARED_STATEMENT_DEFINITION INVALID_SCHEMA_DEFINITION INVALID_TABLE_DEFINITION ' + 'INVALID_OBJECT_DEFINITION WITH_CHECK_OPTION_VIOLATION INSUFFICIENT_RESOURCES DISK_FULL ' + 'OUT_OF_MEMORY TOO_MANY_CONNECTIONS CONFIGURATION_LIMIT_EXCEEDED PROGRAM_LIMIT_EXCEEDED ' + 'STATEMENT_TOO_COMPLEX TOO_MANY_COLUMNS TOO_MANY_ARGUMENTS OBJECT_NOT_IN_PREREQUISITE_STATE ' + 'OBJECT_IN_USE CANT_CHANGE_RUNTIME_PARAM LOCK_NOT_AVAILABLE OPERATOR_INTERVENTION ' + 'QUERY_CANCELED ADMIN_SHUTDOWN CRASH_SHUTDOWN CANNOT_CONNECT_NOW DATABASE_DROPPED ' + 'SYSTEM_ERROR IO_ERROR UNDEFINED_FILE DUPLICATE_FILE SNAPSHOT_TOO_OLD CONFIG_FILE_ERROR ' + 'LOCK_FILE_EXISTS FDW_ERROR FDW_COLUMN_NAME_NOT_FOUND FDW_DYNAMIC_PARAMETER_VALUE_NEEDED ' + 'FDW_FUNCTION_SEQUENCE_ERROR FDW_INCONSISTENT_DESCRIPTOR_INFORMATION ' + 'FDW_INVALID_ATTRIBUTE_VALUE FDW_INVALID_COLUMN_NAME FDW_INVALID_COLUMN_NUMBER ' + 'FDW_INVALID_DATA_TYPE FDW_INVALID_DATA_TYPE_DESCRIPTORS ' + 'FDW_INVALID_DESCRIPTOR_FIELD_IDENTIFIER FDW_INVALID_HANDLE FDW_INVALID_OPTION_INDEX ' + 'FDW_INVALID_OPTION_NAME FDW_INVALID_STRING_LENGTH_OR_BUFFER_LENGTH ' + 'FDW_INVALID_STRING_FORMAT FDW_INVALID_USE_OF_NULL_POINTER FDW_TOO_MANY_HANDLES ' + 'FDW_OUT_OF_MEMORY FDW_NO_SCHEMAS FDW_OPTION_NAME_NOT_FOUND FDW_REPLY_HANDLE ' + 'FDW_SCHEMA_NOT_FOUND FDW_TABLE_NOT_FOUND FDW_UNABLE_TO_CREATE_EXECUTION ' + 'FDW_UNABLE_TO_CREATE_REPLY FDW_UNABLE_TO_ESTABLISH_CONNECTION PLPGSQL_ERROR ' + 'RAISE_EXCEPTION NO_DATA_FOUND TOO_MANY_ROWS ASSERT_FAILURE INTERNAL_ERROR DATA_CORRUPTED ' + 'INDEX_CORRUPTED '; var FUNCTIONS = // https://www.postgresql.org/docs/11/static/functions-aggregate.html 'ARRAY_AGG AVG BIT_AND BIT_OR BOOL_AND BOOL_OR COUNT EVERY JSON_AGG JSONB_AGG JSON_OBJECT_AGG ' + 'JSONB_OBJECT_AGG MAX MIN MODE STRING_AGG SUM XMLAGG ' + 'CORR COVAR_POP COVAR_SAMP REGR_AVGX REGR_AVGY REGR_COUNT REGR_INTERCEPT REGR_R2 REGR_SLOPE ' + 'REGR_SXX REGR_SXY REGR_SYY STDDEV STDDEV_POP STDDEV_SAMP VARIANCE VAR_POP VAR_SAMP ' + 'PERCENTILE_CONT PERCENTILE_DISC ' + // https://www.postgresql.org/docs/11/static/functions-window.html 'ROW_NUMBER RANK DENSE_RANK PERCENT_RANK CUME_DIST NTILE LAG LEAD FIRST_VALUE LAST_VALUE NTH_VALUE ' + // https://www.postgresql.org/docs/11/static/functions-comparison.html 'NUM_NONNULLS NUM_NULLS ' + // https://www.postgresql.org/docs/11/static/functions-math.html 'ABS CBRT CEIL CEILING DEGREES DIV EXP FLOOR LN LOG MOD PI POWER RADIANS ROUND SCALE SIGN SQRT ' + 'TRUNC WIDTH_BUCKET ' + 'RANDOM SETSEED ' + 'ACOS ACOSD ASIN ASIND ATAN ATAND ATAN2 ATAN2D COS COSD COT COTD SIN SIND TAN TAND ' + // https://www.postgresql.org/docs/11/static/functions-string.html 'BIT_LENGTH CHAR_LENGTH CHARACTER_LENGTH LOWER OCTET_LENGTH OVERLAY POSITION SUBSTRING TREAT TRIM UPPER ' + 'ASCII BTRIM CHR CONCAT CONCAT_WS CONVERT CONVERT_FROM CONVERT_TO DECODE ENCODE INITCAP' + 'LEFT LENGTH LPAD LTRIM MD5 PARSE_IDENT PG_CLIENT_ENCODING QUOTE_IDENT|10 QUOTE_LITERAL|10 ' + 'QUOTE_NULLABLE|10 REGEXP_MATCH REGEXP_MATCHES REGEXP_REPLACE REGEXP_SPLIT_TO_ARRAY ' + 'REGEXP_SPLIT_TO_TABLE REPEAT REPLACE REVERSE RIGHT RPAD RTRIM SPLIT_PART STRPOS SUBSTR ' + 'TO_ASCII TO_HEX TRANSLATE ' + // https://www.postgresql.org/docs/11/static/functions-binarystring.html 'OCTET_LENGTH GET_BIT GET_BYTE SET_BIT SET_BYTE ' + // https://www.postgresql.org/docs/11/static/functions-formatting.html 'TO_CHAR TO_DATE TO_NUMBER TO_TIMESTAMP ' + // https://www.postgresql.org/docs/11/static/functions-datetime.html 'AGE CLOCK_TIMESTAMP|10 DATE_PART DATE_TRUNC ISFINITE JUSTIFY_DAYS JUSTIFY_HOURS JUSTIFY_INTERVAL ' + 'MAKE_DATE MAKE_INTERVAL|10 MAKE_TIME MAKE_TIMESTAMP|10 MAKE_TIMESTAMPTZ|10 NOW STATEMENT_TIMESTAMP|10 ' + 'TIMEOFDAY TRANSACTION_TIMESTAMP|10 ' + // https://www.postgresql.org/docs/11/static/functions-enum.html 'ENUM_FIRST ENUM_LAST ENUM_RANGE ' + // https://www.postgresql.org/docs/11/static/functions-geometry.html 'AREA CENTER DIAMETER HEIGHT ISCLOSED ISOPEN NPOINTS PCLOSE POPEN RADIUS WIDTH ' + 'BOX BOUND_BOX CIRCLE LINE LSEG PATH POLYGON ' + // https://www.postgresql.org/docs/11/static/functions-net.html 'ABBREV BROADCAST HOST HOSTMASK MASKLEN NETMASK NETWORK SET_MASKLEN TEXT INET_SAME_FAMILY' + 'INET_MERGE MACADDR8_SET7BIT ' + // https://www.postgresql.org/docs/11/static/functions-textsearch.html 'ARRAY_TO_TSVECTOR GET_CURRENT_TS_CONFIG NUMNODE PLAINTO_TSQUERY PHRASETO_TSQUERY WEBSEARCH_TO_TSQUERY ' + 'QUERYTREE SETWEIGHT STRIP TO_TSQUERY TO_TSVECTOR JSON_TO_TSVECTOR JSONB_TO_TSVECTOR TS_DELETE ' + 'TS_FILTER TS_HEADLINE TS_RANK TS_RANK_CD TS_REWRITE TSQUERY_PHRASE TSVECTOR_TO_ARRAY ' + 'TSVECTOR_UPDATE_TRIGGER TSVECTOR_UPDATE_TRIGGER_COLUMN ' + // https://www.postgresql.org/docs/11/static/functions-xml.html 'XMLCOMMENT XMLCONCAT XMLELEMENT XMLFOREST XMLPI XMLROOT ' + 'XMLEXISTS XML_IS_WELL_FORMED XML_IS_WELL_FORMED_DOCUMENT XML_IS_WELL_FORMED_CONTENT ' + 'XPATH XPATH_EXISTS XMLTABLE XMLNAMESPACES ' + 'TABLE_TO_XML TABLE_TO_XMLSCHEMA TABLE_TO_XML_AND_XMLSCHEMA ' + 'QUERY_TO_XML QUERY_TO_XMLSCHEMA QUERY_TO_XML_AND_XMLSCHEMA ' + 'CURSOR_TO_XML CURSOR_TO_XMLSCHEMA ' + 'SCHEMA_TO_XML SCHEMA_TO_XMLSCHEMA SCHEMA_TO_XML_AND_XMLSCHEMA ' + 'DATABASE_TO_XML DATABASE_TO_XMLSCHEMA DATABASE_TO_XML_AND_XMLSCHEMA ' + 'XMLATTRIBUTES ' + // https://www.postgresql.org/docs/11/static/functions-json.html 'TO_JSON TO_JSONB ARRAY_TO_JSON ROW_TO_JSON JSON_BUILD_ARRAY JSONB_BUILD_ARRAY JSON_BUILD_OBJECT ' + 'JSONB_BUILD_OBJECT JSON_OBJECT JSONB_OBJECT JSON_ARRAY_LENGTH JSONB_ARRAY_LENGTH JSON_EACH ' + 'JSONB_EACH JSON_EACH_TEXT JSONB_EACH_TEXT JSON_EXTRACT_PATH JSONB_EXTRACT_PATH ' + 'JSON_OBJECT_KEYS JSONB_OBJECT_KEYS JSON_POPULATE_RECORD JSONB_POPULATE_RECORD JSON_POPULATE_RECORDSET ' + 'JSONB_POPULATE_RECORDSET JSON_ARRAY_ELEMENTS JSONB_ARRAY_ELEMENTS JSON_ARRAY_ELEMENTS_TEXT ' + 'JSONB_ARRAY_ELEMENTS_TEXT JSON_TYPEOF JSONB_TYPEOF JSON_TO_RECORD JSONB_TO_RECORD JSON_TO_RECORDSET ' + 'JSONB_TO_RECORDSET JSON_STRIP_NULLS JSONB_STRIP_NULLS JSONB_SET JSONB_INSERT JSONB_PRETTY ' + // https://www.postgresql.org/docs/11/static/functions-sequence.html 'CURRVAL LASTVAL NEXTVAL SETVAL ' + // https://www.postgresql.org/docs/11/static/functions-conditional.html 'COALESCE NULLIF GREATEST LEAST ' + // https://www.postgresql.org/docs/11/static/functions-array.html 'ARRAY_APPEND ARRAY_CAT ARRAY_NDIMS ARRAY_DIMS ARRAY_FILL ARRAY_LENGTH ARRAY_LOWER ARRAY_POSITION ' + 'ARRAY_POSITIONS ARRAY_PREPEND ARRAY_REMOVE ARRAY_REPLACE ARRAY_TO_STRING ARRAY_UPPER CARDINALITY ' + 'STRING_TO_ARRAY UNNEST ' + // https://www.postgresql.org/docs/11/static/functions-range.html 'ISEMPTY LOWER_INC UPPER_INC LOWER_INF UPPER_INF RANGE_MERGE ' + // https://www.postgresql.org/docs/11/static/functions-srf.html 'GENERATE_SERIES GENERATE_SUBSCRIPTS ' + // https://www.postgresql.org/docs/11/static/functions-info.html 'CURRENT_DATABASE CURRENT_QUERY CURRENT_SCHEMA|10 CURRENT_SCHEMAS|10 INET_CLIENT_ADDR INET_CLIENT_PORT ' + 'INET_SERVER_ADDR INET_SERVER_PORT ROW_SECURITY_ACTIVE FORMAT_TYPE ' + 'TO_REGCLASS TO_REGPROC TO_REGPROCEDURE TO_REGOPER TO_REGOPERATOR TO_REGTYPE TO_REGNAMESPACE TO_REGROLE ' + 'COL_DESCRIPTION OBJ_DESCRIPTION SHOBJ_DESCRIPTION ' + 'TXID_CURRENT TXID_CURRENT_IF_ASSIGNED TXID_CURRENT_SNAPSHOT TXID_SNAPSHOT_XIP TXID_SNAPSHOT_XMAX ' + 'TXID_SNAPSHOT_XMIN TXID_VISIBLE_IN_SNAPSHOT TXID_STATUS ' + // https://www.postgresql.org/docs/11/static/functions-admin.html 'CURRENT_SETTING SET_CONFIG BRIN_SUMMARIZE_NEW_VALUES BRIN_SUMMARIZE_RANGE BRIN_DESUMMARIZE_RANGE ' + 'GIN_CLEAN_PENDING_LIST ' + // https://www.postgresql.org/docs/11/static/functions-trigger.html 'SUPPRESS_REDUNDANT_UPDATES_TRIGGER ' + // ihttps://www.postgresql.org/docs/devel/static/lo-funcs.html 'LO_FROM_BYTEA LO_PUT LO_GET LO_CREAT LO_CREATE LO_UNLINK LO_IMPORT LO_EXPORT LOREAD LOWRITE ' + // 'GROUPING CAST '; var FUNCTIONS_RE = FUNCTIONS.trim() .split(' ') .map( function(val) { return val.split('|')[0]; } ) .join('|'); return { aliases: ['postgres','postgresql'], case_insensitive: true, keywords: { keyword: SQL_KW + PLPGSQL_KW + ROLE_ATTRS, built_in: SQL_BI + PLPGSQL_BI + PLPGSQL_EXCEPTIONS, }, // Forbid some cunstructs from other languages to improve autodetect. In fact // "[a-z]:" is legal (as part of array slice), but improbabal. illegal: /:==|\W\s*\(\*|(^|\s)\$[a-z]|{{|[a-z]:\s*$|\.\.\.|TO:|DO:/, contains: [ // special handling of some words, which are reserved only in some contexts { className: 'keyword', variants: [ { begin: /\bTEXT\s*SEARCH\b/ }, { begin: /\b(PRIMARY|FOREIGN|FOR(\s+NO)?)\s+KEY\b/ }, { begin: /\bPARALLEL\s+(UNSAFE|RESTRICTED|SAFE)\b/ }, { begin: /\bSTORAGE\s+(PLAIN|EXTERNAL|EXTENDED|MAIN)\b/ }, { begin: /\bMATCH\s+(FULL|PARTIAL|SIMPLE)\b/ }, { begin: /\bNULLS\s+(FIRST|LAST)\b/ }, { begin: /\bEVENT\s+TRIGGER\b/ }, { begin: /\b(MAPPING|OR)\s+REPLACE\b/ }, { begin: /\b(FROM|TO)\s+(PROGRAM|STDIN|STDOUT)\b/ }, { begin: /\b(SHARE|EXCLUSIVE)\s+MODE\b/ }, { begin: /\b(LEFT|RIGHT)\s+(OUTER\s+)?JOIN\b/ }, { begin: /\b(FETCH|MOVE)\s+(NEXT|PRIOR|FIRST|LAST|ABSOLUTE|RELATIVE|FORWARD|BACKWARD)\b/ }, { begin: /\bPRESERVE\s+ROWS\b/ }, { begin: /\bDISCARD\s+PLANS\b/ }, { begin: /\bREFERENCING\s+(OLD|NEW)\b/ }, { begin: /\bSKIP\s+LOCKED\b/ }, { begin: /\bGROUPING\s+SETS\b/ }, { begin: /\b(BINARY|INSENSITIVE|SCROLL|NO\s+SCROLL)\s+(CURSOR|FOR)\b/ }, { begin: /\b(WITH|WITHOUT)\s+HOLD\b/ }, { begin: /\bWITH\s+(CASCADED|LOCAL)\s+CHECK\s+OPTION\b/ }, { begin: /\bEXCLUDE\s+(TIES|NO\s+OTHERS)\b/ }, { begin: /\bFORMAT\s+(TEXT|XML|JSON|YAML)\b/ }, { begin: /\bSET\s+((SESSION|LOCAL)\s+)?NAMES\b/ }, { begin: /\bIS\s+(NOT\s+)?UNKNOWN\b/ }, { begin: /\bSECURITY\s+LABEL\b/ }, { begin: /\bSTANDALONE\s+(YES|NO|NO\s+VALUE)\b/ }, { begin: /\bWITH\s+(NO\s+)?DATA\b/ }, { begin: /\b(FOREIGN|SET)\s+DATA\b/ }, { begin: /\bSET\s+(CATALOG|CONSTRAINTS)\b/ }, { begin: /\b(WITH|FOR)\s+ORDINALITY\b/ }, { begin: /\bIS\s+(NOT\s+)?DOCUMENT\b/ }, { begin: /\bXML\s+OPTION\s+(DOCUMENT|CONTENT)\b/ }, { begin: /\b(STRIP|PRESERVE)\s+WHITESPACE\b/ }, { begin: /\bNO\s+(ACTION|MAXVALUE|MINVALUE)\b/ }, { begin: /\bPARTITION\s+BY\s+(RANGE|LIST|HASH)\b/ }, { begin: /\bAT\s+TIME\s+ZONE\b/ }, { begin: /\bGRANTED\s+BY\b/ }, { begin: /\bRETURN\s+(QUERY|NEXT)\b/ }, { begin: /\b(ATTACH|DETACH)\s+PARTITION\b/ }, { begin: /\bFORCE\s+ROW\s+LEVEL\s+SECURITY\b/ }, { begin: /\b(INCLUDING|EXCLUDING)\s+(COMMENTS|CONSTRAINTS|DEFAULTS|IDENTITY|INDEXES|STATISTICS|STORAGE|ALL)\b/ }, { begin: /\bAS\s+(ASSIGNMENT|IMPLICIT|PERMISSIVE|RESTRICTIVE|ENUM|RANGE)\b/ } ] }, // functions named as keywords, followed by '(' { begin: /\b(FORMAT|FAMILY|VERSION)\s*\(/, //keywords: { built_in: 'FORMAT FAMILY VERSION' } }, // INCLUDE ( ... ) in index_parameters in CREATE TABLE { begin: /\bINCLUDE\s*\(/, keywords: 'INCLUDE' }, // not highlight RANGE if not in frame_clause (not 100% correct, but seems satisfactory) { begin: /\bRANGE(?!\s*(BETWEEN|UNBOUNDED|CURRENT|[-0-9]+))/ }, // disable highlighting in commands CREATE AGGREGATE/COLLATION/DATABASE/OPERTOR/TEXT SEARCH .../TYPE // and in PL/pgSQL RAISE ... USING { begin: /\b(VERSION|OWNER|TEMPLATE|TABLESPACE|CONNECTION\s+LIMIT|PROCEDURE|RESTRICT|JOIN|PARSER|COPY|START|END|COLLATION|INPUT|ANALYZE|STORAGE|LIKE|DEFAULT|DELIMITER|ENCODING|COLUMN|CONSTRAINT|TABLE|SCHEMA)\s*=/ }, // PG_smth; HAS_some_PRIVILEGE { //className: 'built_in', begin: /\b(PG_\w+?|HAS_[A-Z_]+_PRIVILEGE)\b/, relevance: 10 }, // extract { begin: /\bEXTRACT\s*\(/, end: /\bFROM\b/, returnEnd: true, keywords: { //built_in: 'EXTRACT', type: 'CENTURY DAY DECADE DOW DOY EPOCH HOUR ISODOW ISOYEAR MICROSECONDS ' + 'MILLENNIUM MILLISECONDS MINUTE MONTH QUARTER SECOND TIMEZONE TIMEZONE_HOUR ' + 'TIMEZONE_MINUTE WEEK YEAR' } }, // xmlelement, xmlpi - special NAME { begin: /\b(XMLELEMENT|XMLPI)\s*\(\s*NAME/, keywords: { //built_in: 'XMLELEMENT XMLPI', keyword: 'NAME' } }, // xmlparse, xmlserialize { begin: /\b(XMLPARSE|XMLSERIALIZE)\s*\(\s*(DOCUMENT|CONTENT)/, keywords: { //built_in: 'XMLPARSE XMLSERIALIZE', keyword: 'DOCUMENT CONTENT' } }, // Sequences. We actually skip everything between CACHE|INCREMENT|MAXVALUE|MINVALUE and // nearest following numeric constant. Without with trick we find a lot of "keywords" // in 'avrasm' autodetection test... { beginKeywords: 'CACHE INCREMENT MAXVALUE MINVALUE', end: hljs.C_NUMBER_RE, returnEnd: true, keywords: 'BY CACHE INCREMENT MAXVALUE MINVALUE' }, // WITH|WITHOUT TIME ZONE as part of datatype { className: 'type', begin: /\b(WITH|WITHOUT)\s+TIME\s+ZONE\b/ }, // INTERVAL optional fields { className: 'type', begin: /\bINTERVAL\s+(YEAR|MONTH|DAY|HOUR|MINUTE|SECOND)(\s+TO\s+(MONTH|HOUR|MINUTE|SECOND))?\b/ }, // Pseudo-types which allowed only as return type { begin: /\bRETURNS\s+(LANGUAGE_HANDLER|TRIGGER|EVENT_TRIGGER|FDW_HANDLER|INDEX_AM_HANDLER|TSM_HANDLER)\b/, keywords: { keyword: 'RETURNS', type: 'LANGUAGE_HANDLER TRIGGER EVENT_TRIGGER FDW_HANDLER INDEX_AM_HANDLER TSM_HANDLER' } }, // Known functions - only when followed by '(' { begin: '\\b(' + FUNCTIONS_RE + ')\\s*\\(' //keywords: { built_in: FUNCTIONS } }, // Types { begin: '\\.(' + TYPES_RE + ')\\b' // prevent highlight as type, say, 'oid' in 'pgclass.oid' }, { begin: '\\b(' + TYPES_RE + ')\\s+PATH\\b', // in XMLTABLE keywords: { keyword: 'PATH', // hopefully no one would use PATH type in XMLTABLE... type: TYPES.replace('PATH ','') } }, { className: 'type', begin: '\\b(' + TYPES_RE + ')\\b' }, // Strings, see https://www.postgresql.org/docs/11/static/sql-syntax-lexical.html#SQL-SYNTAX-CONSTANTS { className: 'string', begin: '\'', end: '\'', contains: [{begin: '\'\''}] }, { className: 'string', begin: '(e|E|u&|U&)\'', end: '\'', contains: [{begin: '\\\\.'}], relevance: 10 }, { begin: DOLLAR_STRING, endSameAsBegin: true, contains: [ { // actually we want them all except SQL; listed are those with known implementations // and XML + JSON just in case subLanguage: ['pgsql','perl','python','tcl','r','lua','java','php','ruby','bash','scheme','xml','json'], endsWithParent: true } ] }, // identifiers in quotes { begin: '"', end: '"', contains: [{begin: '""'}] }, // numbers hljs.C_NUMBER_MODE, // comments hljs.C_BLOCK_COMMENT_MODE, COMMENT_MODE, // PL/pgSQL staff // %ROWTYPE, %TYPE, $n { className: 'meta', variants: [ {begin: '%(ROW)?TYPE', relevance: 10}, // %TYPE, %ROWTYPE {begin: '\\$\\d+'}, // $n {begin: '^#\\w', end: '$'} // #compiler option ] }, // <> { className: 'symbol', begin: LABEL, relevance: 10 } ] }; } },{name:"php",create:/* Language: PHP Author: Victor Karamzin Contributors: Evgeny Stepanischev , Ivan Sagalaev Category: common */ function(hljs) { var VARIABLE = { begin: '\\$+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*' }; var PREPROCESSOR = { className: 'meta', begin: /<\?(php)?|\?>/ }; var STRING = { className: 'string', contains: [hljs.BACKSLASH_ESCAPE, PREPROCESSOR], variants: [ { begin: 'b"', end: '"' }, { begin: 'b\'', end: '\'' }, hljs.inherit(hljs.APOS_STRING_MODE, {illegal: null}), hljs.inherit(hljs.QUOTE_STRING_MODE, {illegal: null}) ] }; var NUMBER = {variants: [hljs.BINARY_NUMBER_MODE, hljs.C_NUMBER_MODE]}; return { aliases: ['php', 'php3', 'php4', 'php5', 'php6', 'php7'], case_insensitive: true, keywords: 'and include_once list abstract global private echo interface as static endswitch ' + 'array null if endwhile or const for endforeach self var while isset public ' + 'protected exit foreach throw elseif include __FILE__ empty require_once do xor ' + 'return parent clone use __CLASS__ __LINE__ else break print eval new ' + 'catch __METHOD__ case exception default die require __FUNCTION__ ' + 'enddeclare final try switch continue endfor endif declare unset true false ' + 'trait goto instanceof insteadof __DIR__ __NAMESPACE__ ' + 'yield finally', contains: [ hljs.HASH_COMMENT_MODE, hljs.COMMENT('//', '$', {contains: [PREPROCESSOR]}), hljs.COMMENT( '/\\*', '\\*/', { contains: [ { className: 'doctag', begin: '@[A-Za-z]+' } ] } ), hljs.COMMENT( '__halt_compiler.+?;', false, { endsWithParent: true, keywords: '__halt_compiler', lexemes: hljs.UNDERSCORE_IDENT_RE } ), { className: 'string', begin: /<<<['"]?\w+['"]?$/, end: /^\w+;?$/, contains: [ hljs.BACKSLASH_ESCAPE, { className: 'subst', variants: [ {begin: /\$\w+/}, {begin: /\{\$/, end: /\}/} ] } ] }, PREPROCESSOR, { className: 'keyword', begin: /\$this\b/ }, VARIABLE, { // swallow composed identifiers to avoid parsing them as keywords begin: /(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/ }, { className: 'function', beginKeywords: 'function', end: /[;{]/, excludeEnd: true, illegal: '\\$|\\[|%', contains: [ hljs.UNDERSCORE_TITLE_MODE, { className: 'params', begin: '\\(', end: '\\)', contains: [ 'self', VARIABLE, hljs.C_BLOCK_COMMENT_MODE, STRING, NUMBER ] } ] }, { className: 'class', beginKeywords: 'class interface', end: '{', excludeEnd: true, illegal: /[:\(\$"]/, contains: [ {beginKeywords: 'extends implements'}, hljs.UNDERSCORE_TITLE_MODE ] }, { beginKeywords: 'namespace', end: ';', illegal: /[\.']/, contains: [hljs.UNDERSCORE_TITLE_MODE] }, { beginKeywords: 'use', end: ';', contains: [hljs.UNDERSCORE_TITLE_MODE] }, { begin: '=>' // No markup, just a relevance booster }, STRING, NUMBER ] }; } },{name:"plaintext",create:/* Language: plaintext Author: Egor Rogov (e.rogov@postgrespro.ru) Description: Plain text without any highlighting. */ function(hljs) { return { disableAutodetect: true }; } },{name:"pony",create:/* Language: Pony Author: Joe Eli McIlvain Description: Pony is an open-source, object-oriented, actor-model, capabilities-secure, high performance programming language. */ function(hljs) { var KEYWORDS = { keyword: 'actor addressof and as be break class compile_error compile_intrinsic ' + 'consume continue delegate digestof do else elseif embed end error ' + 'for fun if ifdef in interface is isnt lambda let match new not object ' + 'or primitive recover repeat return struct then trait try type until ' + 'use var where while with xor', meta: 'iso val tag trn box ref', literal: 'this false true' }; var TRIPLE_QUOTE_STRING_MODE = { className: 'string', begin: '"""', end: '"""', relevance: 10 }; var QUOTE_STRING_MODE = { className: 'string', begin: '"', end: '"', contains: [hljs.BACKSLASH_ESCAPE] }; var SINGLE_QUOTE_CHAR_MODE = { className: 'string', begin: '\'', end: '\'', contains: [hljs.BACKSLASH_ESCAPE], relevance: 0 }; var TYPE_NAME = { className: 'type', begin: '\\b_?[A-Z][\\w]*', relevance: 0 }; var PRIMED_NAME = { begin: hljs.IDENT_RE + '\'', relevance: 0 }; /** * The `FUNCTION` and `CLASS` modes were intentionally removed to simplify * highlighting and fix cases like * ``` * interface Iterator[A: A] * fun has_next(): Bool * fun next(): A? * ``` * where it is valid to have a function head without a body */ return { keywords: KEYWORDS, contains: [ TYPE_NAME, TRIPLE_QUOTE_STRING_MODE, QUOTE_STRING_MODE, SINGLE_QUOTE_CHAR_MODE, PRIMED_NAME, hljs.C_NUMBER_MODE, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE ] }; } },{name:"powershell",create:/* Language: PowerShell Author: David Mohundro Contributors: Nicholas Blumhardt , Victor Zhou , Nicolas Le Gall */ function(hljs){ var BACKTICK_ESCAPE = { begin: "`[\\s\\S]", relevance: 0, }; var VAR = { className: "variable", variants: [{ begin: /\$[\w\d][\w\d_:]*/ }], }; var LITERAL = { className: "literal", begin: /\$(null|true|false)\b/, }; var QUOTE_STRING = { className: "string", variants: [{ begin: /"/, end: /"/ }, { begin: /@"/, end: /^"@/ }], contains: [ BACKTICK_ESCAPE, VAR, { className: "variable", begin: /\$[A-z]/, end: /[^A-z]/, }, ], }; var APOS_STRING = { className: "string", variants: [{ begin: /'/, end: /'/ }, { begin: /@'/, end: /^'@/ }], }; var PS_HELPTAGS = { className: "doctag", variants: [ /* no paramater help tags */ { begin: /\.(synopsis|description|example|inputs|outputs|notes|link|component|role|functionality)/, }, /* one parameter help tags */ { begin: /\.(parameter|forwardhelptargetname|forwardhelpcategory|remotehelprunspace|externalhelp)\s+\S+/, }, ], }; var PS_COMMENT = hljs.inherit(hljs.COMMENT(null, null), { variants: [ /* single-line comment */ { begin: /#/, end: /$/ }, /* multi-line comment */ { begin: /<#/, end: /#>/ }, ], contains: [PS_HELPTAGS], }); return { aliases: ["ps"], lexemes: /-?[A-z\.\-]+/, case_insensitive: true, keywords: { keyword: "if else foreach return function do while until elseif begin for trap data dynamicparam end break throw param continue finally in switch exit filter try process catch" + "ValidateNoCircleInNodeResources ValidateNodeExclusiveResources ValidateNodeManager ValidateNodeResources ValidateNodeResourceSource ValidateNoNameNodeResources ThrowError IsHiddenResource" + "IsPatternMatched ", built_in: "Add-Computer Add-Content Add-History Add-JobTrigger Add-Member Add-PSSnapin Add-Type Checkpoint-Computer Clear-Content " + "Clear-EventLog Clear-History Clear-Host Clear-Item Clear-ItemProperty Clear-Variable Compare-Object Complete-Transaction Connect-PSSession " + "Connect-WSMan Convert-Path ConvertFrom-Csv ConvertFrom-Json ConvertFrom-SecureString ConvertFrom-StringData ConvertTo-Csv ConvertTo-Html " + "ConvertTo-Json ConvertTo-SecureString ConvertTo-Xml Copy-Item Copy-ItemProperty Debug-Process Disable-ComputerRestore Disable-JobTrigger " + "Disable-PSBreakpoint Disable-PSRemoting Disable-PSSessionConfiguration Disable-WSManCredSSP Disconnect-PSSession Disconnect-WSMan " + "Disable-ScheduledJob Enable-ComputerRestore Enable-JobTrigger Enable-PSBreakpoint Enable-PSRemoting Enable-PSSessionConfiguration " + "Enable-ScheduledJob Enable-WSManCredSSP Enter-PSSession Exit-PSSession Export-Alias Export-Clixml Export-Console Export-Counter Export-Csv " + "Export-FormatData Export-ModuleMember Export-PSSession ForEach-Object Format-Custom Format-List Format-Table Format-Wide Get-Acl Get-Alias " + "Get-AuthenticodeSignature Get-ChildItem Get-Command Get-ComputerRestorePoint Get-Content Get-ControlPanelItem Get-Counter Get-Credential " + "Get-Culture Get-Date Get-Event Get-EventLog Get-EventSubscriber Get-ExecutionPolicy Get-FormatData Get-Host Get-HotFix Get-Help Get-History " + "Get-IseSnippet Get-Item Get-ItemProperty Get-Job Get-JobTrigger Get-Location Get-Member Get-Module Get-PfxCertificate Get-Process " + "Get-PSBreakpoint Get-PSCallStack Get-PSDrive Get-PSProvider Get-PSSession Get-PSSessionConfiguration Get-PSSnapin Get-Random Get-ScheduledJob " + "Get-ScheduledJobOption Get-Service Get-TraceSource Get-Transaction Get-TypeData Get-UICulture Get-Unique Get-Variable Get-Verb Get-WinEvent " + "Get-WmiObject Get-WSManCredSSP Get-WSManInstance Group-Object Import-Alias Import-Clixml Import-Counter Import-Csv Import-IseSnippet " + "Import-LocalizedData Import-PSSession Import-Module Invoke-AsWorkflow Invoke-Command Invoke-Expression Invoke-History Invoke-Item " + "Invoke-RestMethod Invoke-WebRequest Invoke-WmiMethod Invoke-WSManAction Join-Path Limit-EventLog Measure-Command Measure-Object Move-Item " + "Move-ItemProperty New-Alias New-Event New-EventLog New-IseSnippet New-Item New-ItemProperty New-JobTrigger New-Object New-Module " + "New-ModuleManifest New-PSDrive New-PSSession New-PSSessionConfigurationFile New-PSSessionOption New-PSTransportOption " + "New-PSWorkflowExecutionOption New-PSWorkflowSession New-ScheduledJobOption New-Service New-TimeSpan New-Variable New-WebServiceProxy " + "New-WinEvent New-WSManInstance New-WSManSessionOption Out-Default Out-File Out-GridView Out-Host Out-Null Out-Printer Out-String Pop-Location " + "Push-Location Read-Host Receive-Job Register-EngineEvent Register-ObjectEvent Register-PSSessionConfiguration Register-ScheduledJob " + "Register-WmiEvent Remove-Computer Remove-Event Remove-EventLog Remove-Item Remove-ItemProperty Remove-Job Remove-JobTrigger Remove-Module " + "Remove-PSBreakpoint Remove-PSDrive Remove-PSSession Remove-PSSnapin Remove-TypeData Remove-Variable Remove-WmiObject Remove-WSManInstance " + "Rename-Computer Rename-Item Rename-ItemProperty Reset-ComputerMachinePassword Resolve-Path Restart-Computer Restart-Service Restore-Computer " + "Resume-Job Resume-Service Save-Help Select-Object Select-String Select-Xml Send-MailMessage Set-Acl Set-Alias Set-AuthenticodeSignature " + "Set-Content Set-Date Set-ExecutionPolicy Set-Item Set-ItemProperty Set-JobTrigger Set-Location Set-PSBreakpoint Set-PSDebug " + "Set-PSSessionConfiguration Set-ScheduledJob Set-ScheduledJobOption Set-Service Set-StrictMode Set-TraceSource Set-Variable Set-WmiInstance " + "Set-WSManInstance Set-WSManQuickConfig Show-Command Show-ControlPanelItem Show-EventLog Sort-Object Split-Path Start-Job Start-Process " + "Start-Service Start-Sleep Start-Transaction Start-Transcript Stop-Computer Stop-Job Stop-Process Stop-Service Stop-Transcript Suspend-Job " + "Suspend-Service Tee-Object Test-ComputerSecureChannel Test-Connection Test-ModuleManifest Test-Path Test-PSSessionConfigurationFile " + "Trace-Command Unblock-File Undo-Transaction Unregister-Event Unregister-PSSessionConfiguration Unregister-ScheduledJob Update-FormatData " + "Update-Help Update-List Update-TypeData Use-Transaction Wait-Event Wait-Job Wait-Process Where-Object Write-Debug Write-Error Write-EventLog " + "Write-Host Write-Output Write-Progress Write-Verbose Write-Warning Add-MDTPersistentDrive Disable-MDTMonitorService Enable-MDTMonitorService " + "Get-MDTDeploymentShareStatistics Get-MDTMonitorData Get-MDTOperatingSystemCatalog Get-MDTPersistentDrive Import-MDTApplication " + "Import-MDTDriver Import-MDTOperatingSystem Import-MDTPackage Import-MDTTaskSequence New-MDTDatabase Remove-MDTMonitorData " + "Remove-MDTPersistentDrive Restore-MDTPersistentDrive Set-MDTMonitorData Test-MDTDeploymentShare Test-MDTMonitorData Update-MDTDatabaseSchema " + "Update-MDTDeploymentShare Update-MDTLinkedDS Update-MDTMedia Add-VamtProductKey Export-VamtData Find-VamtManagedMachine " + "Get-VamtConfirmationId Get-VamtProduct Get-VamtProductKey Import-VamtData Initialize-VamtData Install-VamtConfirmationId " + "Install-VamtProductActivation Install-VamtProductKey Update-VamtProduct Add-CIDatastore Add-KeyManagementServer Add-NodeKeys " + "Add-NsxDynamicCriteria Add-NsxDynamicMemberSet Add-NsxEdgeInterfaceAddress Add-NsxFirewallExclusionListMember Add-NsxFirewallRuleMember " + "Add-NsxIpSetMember Add-NsxLicense Add-NsxLoadBalancerPoolMember Add-NsxLoadBalancerVip Add-NsxSecondaryManager Add-NsxSecurityGroupMember " + "Add-NsxSecurityPolicyRule Add-NsxSecurityPolicyRuleGroup Add-NsxSecurityPolicyRuleService Add-NsxServiceGroupMember " + "Add-NsxTransportZoneMember Add-PassthroughDevice Add-VDSwitchPhysicalNetworkAdapter Add-VDSwitchVMHost Add-VMHost Add-VMHostNtpServer " + "Add-VirtualSwitchPhysicalNetworkAdapter Add-XmlElement Add-vRACustomForm Add-vRAPrincipalToTenantRole Add-vRAReservationNetwork " + "Add-vRAReservationStorage Clear-NsxEdgeInterface Clear-NsxManagerTimeSettings Compress-Archive Connect-CIServer Connect-CisServer " + "Connect-HCXServer Connect-NIServer Connect-NsxLogicalSwitch Connect-NsxServer Connect-NsxtServer Connect-SrmServer Connect-VIServer " + "Connect-Vmc Connect-vRAServer Connect-vRNIServer ConvertFrom-Markdown ConvertTo-MOFInstance Copy-DatastoreItem Copy-HardDisk Copy-NsxEdge " + "Copy-VDisk Copy-VMGuestFile Debug-Runspace Disable-NsxEdgeSsh Disable-RunspaceDebug Disable-vRNIDataSource Disconnect-CIServer " + "Disconnect-CisServer Disconnect-HCXServer Disconnect-NsxLogicalSwitch Disconnect-NsxServer Disconnect-NsxtServer Disconnect-SrmServer " + "Disconnect-VIServer Disconnect-Vmc Disconnect-vRAServer Disconnect-vRNIServer Dismount-Tools Enable-NsxEdgeSsh Enable-RunspaceDebug " + "Enable-vRNIDataSource Expand-Archive Export-NsxObject Export-SpbmStoragePolicy Export-VApp Export-VDPortGroup Export-VDSwitch " + "Export-VMHostProfile Export-vRAIcon Export-vRAPackage Find-Command Find-DscResource Find-Module Find-NsxWhereVMUsed Find-Package " + "Find-PackageProvider Find-RoleCapability Find-Script Format-Hex Format-VMHostDiskPartition Format-XML Generate-VersionInfo " + "Get-AdvancedSetting Get-AlarmAction Get-AlarmActionTrigger Get-AlarmDefinition Get-Annotation Get-CDDrive Get-CIAccessControlRule " + "Get-CIDatastore Get-CINetworkAdapter Get-CIRole Get-CIUser Get-CIVApp Get-CIVAppNetwork Get-CIVAppStartRule Get-CIVAppTemplate Get-CIVM " + "Get-CIVMTemplate Get-CIView Get-Catalog Get-CisCommand Get-CisService Get-CloudCommand Get-Cluster Get-CompatibleVersionAddtionaPropertiesStr " + "Get-ComplexResourceQualifier Get-ConfigurationErrorCount Get-ContentLibraryItem Get-CustomAttribute Get-DSCResourceModules Get-Datacenter " + "Get-Datastore Get-DatastoreCluster Get-DrsClusterGroup Get-DrsRecommendation Get-DrsRule Get-DrsVMHostRule Get-DscResource Get-EdgeGateway " + "Get-EncryptedPassword Get-ErrorReport Get-EsxCli Get-EsxTop Get-ExternalNetwork Get-FileHash Get-FloppyDrive Get-Folder Get-HAPrimaryVMHost " + "Get-HCXAppliance Get-HCXApplianceCompute Get-HCXApplianceDVS Get-HCXApplianceDatastore Get-HCXApplianceNetwork Get-HCXContainer " + "Get-HCXDatastore Get-HCXGateway Get-HCXInterconnectStatus Get-HCXJob Get-HCXMigration Get-HCXNetwork Get-HCXNetworkExtension " + "Get-HCXReplication Get-HCXReplicationSnapshot Get-HCXService Get-HCXSite Get-HCXSitePairing Get-HCXVM Get-HardDisk Get-IScsiHbaTarget " + "Get-InnerMostErrorRecord Get-InstallPath Get-InstalledModule Get-InstalledScript Get-Inventory Get-ItemPropertyValue Get-KeyManagementServer " + "Get-KmipClientCertificate Get-KmsCluster Get-Log Get-LogType Get-MarkdownOption Get-Media Get-MofInstanceName Get-MofInstanceText Get-NetworkAdapter Get-NetworkPool " + "Get-NfsUser Get-NicTeamingPolicy Get-NsxApplicableMember Get-NsxApplicableSecurityAction Get-NsxBackingDVSwitch Get-NsxBackingPortGroup Get-NsxCliDfwAddrSet " + "Get-NsxCliDfwFilter Get-NsxCliDfwRule Get-NsxClusterStatus Get-NsxController Get-NsxDynamicCriteria Get-NsxDynamicMemberSet Get-NsxEdge Get-NsxEdgeBgp " + "Get-NsxEdgeBgpNeighbour Get-NsxEdgeCertificate Get-NsxEdgeCsr Get-NsxEdgeFirewall Get-NsxEdgeFirewallRule Get-NsxEdgeInterface Get-NsxEdgeInterfaceAddress " + "Get-NsxEdgeNat Get-NsxEdgeNatRule Get-NsxEdgeOspf Get-NsxEdgeOspfArea Get-NsxEdgeOspfInterface Get-NsxEdgePrefix Get-NsxEdgeRedistributionRule Get-NsxEdgeRouting " + "Get-NsxEdgeStaticRoute Get-NsxEdgeSubInterface Get-NsxFirewallExclusionListMember Get-NsxFirewallGlobalConfiguration Get-NsxFirewallPublishStatus Get-NsxFirewallRule " + "Get-NsxFirewallRuleMember Get-NsxFirewallSavedConfiguration Get-NsxFirewallSection Get-NsxFirewallThreshold Get-NsxIpPool Get-NsxIpSet Get-NsxLicense Get-NsxLoadBalancer " + "Get-NsxLoadBalancerApplicationProfile Get-NsxLoadBalancerApplicationRule Get-NsxLoadBalancerMonitor Get-NsxLoadBalancerPool Get-NsxLoadBalancerPoolMember Get-NsxLoadBalancerStats " + "Get-NsxLoadBalancerVip Get-NsxLogicalRouter Get-NsxLogicalRouterBgp Get-NsxLogicalRouterBgpNeighbour Get-NsxLogicalRouterBridge Get-NsxLogicalRouterBridging " + "Get-NsxLogicalRouterInterface Get-NsxLogicalRouterOspf Get-NsxLogicalRouterOspfArea Get-NsxLogicalRouterOspfInterface Get-NsxLogicalRouterPrefix " + "Get-NsxLogicalRouterRedistributionRule Get-NsxLogicalRouterRouting Get-NsxLogicalRouterStaticRoute Get-NsxLogicalSwitch Get-NsxMacSet Get-NsxManagerBackup " + "Get-NsxManagerCertificate Get-NsxManagerComponentSummary Get-NsxManagerNetwork Get-NsxManagerRole Get-NsxManagerSsoConfig Get-NsxManagerSyncStatus Get-NsxManagerSyslogServer " + "Get-NsxManagerSystemSummary Get-NsxManagerTimeSettings Get-NsxManagerVcenterConfig Get-NsxSecondaryManager Get-NsxSecurityGroup Get-NsxSecurityGroupEffectiveIpAddress " + "Get-NsxSecurityGroupEffectiveMacAddress Get-NsxSecurityGroupEffectiveMember Get-NsxSecurityGroupEffectiveVirtualMachine Get-NsxSecurityGroupEffectiveVnic " + "Get-NsxSecurityGroupMemberTypes Get-NsxSecurityPolicy Get-NsxSecurityPolicyHighestUsedPrecedence Get-NsxSecurityPolicyRule Get-NsxSecurityTag Get-NsxSecurityTagAssignment " + "Get-NsxSegmentIdRange Get-NsxService Get-NsxServiceDefinition Get-NsxServiceGroup Get-NsxServiceGroupMember Get-NsxServiceProfile Get-NsxSpoofguardNic Get-NsxSpoofguardPolicy " + "Get-NsxSslVpn Get-NsxSslVpnAuthServer Get-NsxSslVpnClientInstallationPackage Get-NsxSslVpnIpPool Get-NsxSslVpnPrivateNetwork Get-NsxSslVpnUser Get-NsxTransportZone " + "Get-NsxUserRole Get-NsxVdsContext Get-NsxtPolicyService Get-NsxtService Get-OSCustomizationNicMapping Get-OSCustomizationSpec Get-Org Get-OrgNetwork Get-OrgVdc " + "Get-OrgVdcNetwork Get-OvfConfiguration Get-PSCurrentConfigurationNode Get-PSDefaultConfigurationDocument Get-PSMetaConfigDocumentInstVersionInfo Get-PSMetaConfigurationProcessed " + "Get-PSReadLineKeyHandler Get-PSReadLineOption Get-PSRepository Get-PSTopConfigurationName Get-PSVersion Get-Package Get-PackageProvider Get-PackageSource Get-PassthroughDevice " + "Get-PositionInfo Get-PowerCLICommunity Get-PowerCLIConfiguration Get-PowerCLIHelp Get-PowerCLIVersion Get-PowerNsxVersion Get-ProviderVdc Get-PublicKeyFromFile " + "Get-PublicKeyFromStore Get-ResourcePool Get-Runspace Get-RunspaceDebug Get-ScsiController Get-ScsiLun Get-ScsiLunPath Get-SecurityInfo Get-SecurityPolicy Get-Snapshot " + "Get-SpbmCapability Get-SpbmCompatibleStorage Get-SpbmEntityConfiguration Get-SpbmFaultDomain Get-SpbmPointInTimeReplica Get-SpbmReplicationGroup Get-SpbmReplicationPair " + "Get-SpbmStoragePolicy Get-Stat Get-StatInterval Get-StatType Get-Tag Get-TagAssignment Get-TagCategory Get-Task Get-Template Get-TimeZone Get-Uptime Get-UsbDevice Get-VAIOFilter " + "Get-VApp Get-VDBlockedPolicy Get-VDPort Get-VDPortgroup Get-VDPortgroupOverridePolicy Get-VDSecurityPolicy Get-VDSwitch Get-VDSwitchPrivateVlan Get-VDTrafficShapingPolicy " + "Get-VDUplinkLacpPolicy Get-VDUplinkTeamingPolicy Get-VDisk Get-VIAccount Get-VICommand Get-VICredentialStoreItem Get-VIEvent Get-VIObjectByVIView Get-VIPermission Get-VIPrivilege " + "Get-VIProperty Get-VIRole Get-VM Get-VMGuest Get-VMHost Get-VMHostAccount Get-VMHostAdvancedConfiguration Get-VMHostAuthentication Get-VMHostAvailableTimeZone " + "Get-VMHostDiagnosticPartition Get-VMHostDisk Get-VMHostDiskPartition Get-VMHostFirewallDefaultPolicy Get-VMHostFirewallException Get-VMHostFirmware Get-VMHostHardware " + "Get-VMHostHba Get-VMHostModule Get-VMHostNetwork Get-VMHostNetworkAdapter Get-VMHostNtpServer Get-VMHostPatch Get-VMHostPciDevice Get-VMHostProfile " + "Get-VMHostProfileImageCacheConfiguration Get-VMHostProfileRequiredInput Get-VMHostProfileStorageDeviceConfiguration Get-VMHostProfileUserConfiguration " + "Get-VMHostProfileVmPortGroupConfiguration Get-VMHostRoute Get-VMHostService Get-VMHostSnmp Get-VMHostStartPolicy Get-VMHostStorage Get-VMHostSysLogServer Get-VMQuestion " + "Get-VMResourceConfiguration Get-VMStartPolicy Get-VTpm Get-VTpmCSR Get-VTpmCertificate Get-VasaProvider Get-VasaStorageArray Get-View Get-VirtualPortGroup Get-VirtualSwitch " + "Get-VmcSddcNetworkService Get-VmcService Get-VsanClusterConfiguration Get-VsanComponent Get-VsanDisk Get-VsanDiskGroup Get-VsanEvacuationPlan Get-VsanFaultDomain " + "Get-VsanIscsiInitiatorGroup Get-VsanIscsiInitiatorGroupTargetAssociation Get-VsanIscsiLun Get-VsanIscsiTarget Get-VsanObject Get-VsanResyncingComponent Get-VsanRuntimeInfo " + "Get-VsanSpaceUsage Get-VsanStat Get-VsanView Get-vRAApplianceServiceStatus Get-vRAAuthorizationRole Get-vRABlueprint Get-vRABusinessGroup Get-vRACatalogItem " + "Get-vRACatalogItemRequestTemplate Get-vRACatalogPrincipal Get-vRAComponentRegistryService Get-vRAComponentRegistryServiceEndpoint Get-vRAComponentRegistryServiceStatus " + "Get-vRAContent Get-vRAContentData Get-vRAContentType Get-vRACustomForm Get-vRAEntitledCatalogItem Get-vRAEntitledService Get-vRAEntitlement Get-vRAExternalNetworkProfile " + "Get-vRAGroupPrincipal Get-vRAIcon Get-vRANATNetworkProfile Get-vRANetworkProfileIPAddressList Get-vRANetworkProfileIPRangeSummary Get-vRAPackage Get-vRAPackageContent " + "Get-vRAPropertyDefinition Get-vRAPropertyGroup Get-vRARequest Get-vRARequestDetail Get-vRAReservation Get-vRAReservationComputeResource Get-vRAReservationComputeResourceMemory " + "Get-vRAReservationComputeResourceNetwork Get-vRAReservationComputeResourceResourcePool Get-vRAReservationComputeResourceStorage Get-vRAReservationPolicy " + "Get-vRAReservationTemplate Get-vRAReservationType Get-vRAResource Get-vRAResourceAction Get-vRAResourceActionRequestTemplate Get-vRAResourceMetric Get-vRAResourceOperation " + "Get-vRAResourceType Get-vRARoutedNetworkProfile Get-vRAService Get-vRAServiceBlueprint Get-vRASourceMachine Get-vRAStorageReservationPolicy Get-vRATenant Get-vRATenantDirectory " + "Get-vRATenantDirectoryStatus Get-vRATenantRole Get-vRAUserPrincipal Get-vRAUserPrincipalGroupMembership Get-vRAVersion Get-vRNIAPIVersion Get-vRNIApplication " + "Get-vRNIApplicationTier Get-vRNIDataSource Get-vRNIDataSourceSNMPConfig Get-vRNIDatastore Get-vRNIDistributedSwitch Get-vRNIDistributedSwitchPortGroup Get-vRNIEntity " + "Get-vRNIEntityName Get-vRNIFirewallRule Get-vRNIFlow Get-vRNIHost Get-vRNIHostVMKNic Get-vRNIIPSet Get-vRNIL2Network Get-vRNINSXManager Get-vRNINodes Get-vRNIProblem " + "Get-vRNIRecommendedRules Get-vRNIRecommendedRulesNsxBundle Get-vRNISecurityGroup Get-vRNISecurityTag Get-vRNIService Get-vRNIServiceGroup Get-vRNIVM Get-vRNIVMvNIC " + "Get-vRNIvCenter Get-vRNIvCenterCluster Get-vRNIvCenterDatacenter Get-vRNIvCenterFolder Grant-NsxSpoofguardNicApproval Import-CIVApp Import-CIVAppTemplate Import-NsxObject " + "Import-PackageProvider Import-PowerShellDataFile Import-SpbmStoragePolicy Import-VApp Import-VMHostProfile Import-vRAContentData Import-vRAIcon Import-vRAPackage " + "Initialize-ConfigurationRuntimeState Install-Module Install-NsxCluster Install-Package Install-PackageProvider Install-Script Install-VMHostPatch Invoke-DrsRecommendation " + "Invoke-NsxCli Invoke-NsxClusterResolveAll Invoke-NsxManagerSync Invoke-NsxRestMethod Invoke-NsxWebRequest Invoke-VMHostProfile Invoke-VMScript Invoke-XpathQuery " + "Invoke-vRADataCollection Invoke-vRARestMethod Invoke-vRATenantDirectorySync Invoke-vRNIRestMethod Join-String Mount-Tools Move-Cluster Move-Datacenter Move-Datastore Move-Folder " + "Move-HardDisk Move-Inventory Move-NsxSecurityPolicyRule Move-ResourcePool Move-Template Move-VApp Move-VDisk Move-VM Move-VMHost New-AdvancedSetting New-AlarmAction " + "New-AlarmActionTrigger New-CDDrive New-CIAccessControlRule New-CIVApp New-CIVAppNetwork New-CIVAppTemplate New-CIVM New-Cluster New-CustomAttribute New-Datacenter New-Datastore " + "New-DatastoreCluster New-DatastoreDrive New-DrsClusterGroup New-DrsRule New-DrsVMHostRule New-DscChecksum New-FloppyDrive New-Folder New-Guid New-HCXAppliance New-HCXMigration " + "New-HCXNetworkExtension New-HCXNetworkMapping New-HCXReplication New-HCXSitePairing New-HCXStaticRoute New-HardDisk New-IScsiHbaTarget New-KmipClientCertificate " + "New-NetworkAdapter New-NfsUser New-NsxAddressSpec New-NsxClusterVxlanConfig New-NsxController New-NsxDynamicCriteriaSpec New-NsxEdge New-NsxEdgeBgpNeighbour New-NsxEdgeCsr " + "New-NsxEdgeFirewallRule New-NsxEdgeInterfaceSpec New-NsxEdgeNatRule New-NsxEdgeOspfArea New-NsxEdgeOspfInterface New-NsxEdgePrefix New-NsxEdgeRedistributionRule " + "New-NsxEdgeSelfSignedCertificate New-NsxEdgeStaticRoute New-NsxEdgeSubInterface New-NsxEdgeSubInterfaceSpec New-NsxFirewallRule New-NsxFirewallSavedConfiguration " + "New-NsxFirewallSection New-NsxIpPool New-NsxIpSet New-NsxLoadBalancerApplicationProfile New-NsxLoadBalancerApplicationRule New-NsxLoadBalancerMemberSpec " + "New-NsxLoadBalancerMonitor New-NsxLoadBalancerPool New-NsxLogicalRouter New-NsxLogicalRouterBgpNeighbour New-NsxLogicalRouterBridge New-NsxLogicalRouterInterface " + "New-NsxLogicalRouterInterfaceSpec New-NsxLogicalRouterOspfArea New-NsxLogicalRouterOspfInterface New-NsxLogicalRouterPrefix New-NsxLogicalRouterRedistributionRule " + "New-NsxLogicalRouterStaticRoute New-NsxLogicalSwitch New-NsxMacSet New-NsxManager New-NsxSecurityGroup New-NsxSecurityPolicy New-NsxSecurityPolicyAssignment " + "New-NsxSecurityPolicyFirewallRuleSpec New-NsxSecurityPolicyGuestIntrospectionSpec New-NsxSecurityPolicyNetworkIntrospectionSpec New-NsxSecurityTag New-NsxSecurityTagAssignment " + "New-NsxSegmentIdRange New-NsxService New-NsxServiceGroup New-NsxSpoofguardPolicy New-NsxSslVpnAuthServer New-NsxSslVpnClientInstallationPackage New-NsxSslVpnIpPool " + "New-NsxSslVpnPrivateNetwork New-NsxSslVpnUser New-NsxTransportZone New-NsxVdsContext New-OSCustomizationNicMapping New-OSCustomizationSpec New-Org New-OrgNetwork New-OrgVdc " + "New-OrgVdcNetwork New-ResourcePool New-ScriptFileInfo New-ScsiController New-Snapshot New-SpbmRule New-SpbmRuleSet New-SpbmStoragePolicy New-StatInterval New-Tag " + "New-TagAssignment New-TagCategory New-Template New-TemporaryFile New-VAIOFilter New-VApp New-VDPortgroup New-VDSwitch New-VDSwitchPrivateVlan New-VDisk " + "New-VICredentialStoreItem New-VIInventoryDrive New-VIPermission New-VIProperty New-VIRole New-VISamlSecurityContext New-VM New-VMHostAccount New-VMHostNetworkAdapter " + "New-VMHostProfile New-VMHostProfileVmPortGroupConfiguration New-VMHostRoute New-VTpm New-VasaProvider New-VcsOAuthSecurityContext New-VirtualPortGroup New-VirtualSwitch " + "New-VsanDisk New-VsanDiskGroup New-VsanFaultDomain New-VsanIscsiInitiatorGroup New-VsanIscsiInitiatorGroupTargetAssociation New-VsanIscsiLun New-VsanIscsiTarget " + "New-vRABusinessGroup New-vRAEntitlement New-vRAExternalNetworkProfile New-vRAGroupPrincipal New-vRANATNetworkProfile New-vRANetworkProfileIPRangeDefinition New-vRAPackage " + "New-vRAPropertyDefinition New-vRAPropertyGroup New-vRAReservation New-vRAReservationNetworkDefinition New-vRAReservationPolicy New-vRAReservationStorageDefinition " + "New-vRARoutedNetworkProfile New-vRAService New-vRAStorageReservationPolicy New-vRATenant New-vRATenantDirectory New-vRAUserPrincipal New-vRNIApplication New-vRNIApplicationTier " + "New-vRNIDataSource Open-VMConsoleWindow Publish-Module Publish-NsxSpoofguardPolicy Publish-Script Register-PSRepository Register-PackageSource Remove-AdvancedSetting " + "Remove-AlarmAction Remove-AlarmActionTrigger Remove-Alias Remove-CDDrive Remove-CIAccessControlRule Remove-CIVApp Remove-CIVAppNetwork Remove-CIVAppTemplate Remove-Cluster " + "Remove-CustomAttribute Remove-Datacenter Remove-Datastore Remove-DatastoreCluster Remove-DrsClusterGroup Remove-DrsRule Remove-DrsVMHostRule Remove-FloppyDrive Remove-Folder " + "Remove-HCXAppliance Remove-HCXNetworkExtension Remove-HCXReplication Remove-HCXSitePairing Remove-HardDisk Remove-IScsiHbaTarget Remove-Inventory Remove-KeyManagementServer " + "Remove-NetworkAdapter Remove-NfsUser Remove-NsxCluster Remove-NsxClusterVxlanConfig Remove-NsxController Remove-NsxDynamicCriteria Remove-NsxDynamicMemberSet Remove-NsxEdge " + "Remove-NsxEdgeBgpNeighbour Remove-NsxEdgeCertificate Remove-NsxEdgeCsr Remove-NsxEdgeFirewallRule Remove-NsxEdgeInterfaceAddress Remove-NsxEdgeNatRule Remove-NsxEdgeOspfArea " + "Remove-NsxEdgeOspfInterface Remove-NsxEdgePrefix Remove-NsxEdgeRedistributionRule Remove-NsxEdgeStaticRoute Remove-NsxEdgeSubInterface Remove-NsxFirewallExclusionListMember " + "Remove-NsxFirewallRule Remove-NsxFirewallRuleMember Remove-NsxFirewallSavedConfiguration Remove-NsxFirewallSection Remove-NsxIpPool Remove-NsxIpSet Remove-NsxIpSetMember " + "Remove-NsxLoadBalancerApplicationProfile Remove-NsxLoadBalancerMonitor Remove-NsxLoadBalancerPool Remove-NsxLoadBalancerPoolMember Remove-NsxLoadBalancerVip " + "Remove-NsxLogicalRouter Remove-NsxLogicalRouterBgpNeighbour Remove-NsxLogicalRouterBridge Remove-NsxLogicalRouterInterface Remove-NsxLogicalRouterOspfArea " + "Remove-NsxLogicalRouterOspfInterface Remove-NsxLogicalRouterPrefix Remove-NsxLogicalRouterRedistributionRule Remove-NsxLogicalRouterStaticRoute Remove-NsxLogicalSwitch " + "Remove-NsxMacSet Remove-NsxSecondaryManager Remove-NsxSecurityGroup Remove-NsxSecurityGroupMember Remove-NsxSecurityPolicy Remove-NsxSecurityPolicyAssignment " + "Remove-NsxSecurityPolicyRule Remove-NsxSecurityPolicyRuleGroup Remove-NsxSecurityPolicyRuleService Remove-NsxSecurityTag Remove-NsxSecurityTagAssignment " + "Remove-NsxSegmentIdRange Remove-NsxService Remove-NsxServiceGroup Remove-NsxSpoofguardPolicy Remove-NsxSslVpnClientInstallationPackage Remove-NsxSslVpnIpPool " + "Remove-NsxSslVpnPrivateNetwork Remove-NsxSslVpnUser Remove-NsxTransportZone Remove-NsxTransportZoneMember Remove-NsxVdsContext Remove-OSCustomizationNicMapping " + "Remove-OSCustomizationSpec Remove-Org Remove-OrgNetwork Remove-OrgVdc Remove-OrgVdcNetwork Remove-PSReadLineKeyHandler Remove-PassthroughDevice Remove-ResourcePool " + "Remove-Snapshot Remove-SpbmStoragePolicy Remove-StatInterval Remove-Tag Remove-TagAssignment Remove-TagCategory Remove-Template Remove-UsbDevice Remove-VAIOFilter Remove-VApp " + "Remove-VDPortGroup Remove-VDSwitch Remove-VDSwitchPhysicalNetworkAdapter Remove-VDSwitchPrivateVlan Remove-VDSwitchVMHost Remove-VDisk Remove-VICredentialStoreItem " + "Remove-VIPermission Remove-VIProperty Remove-VIRole Remove-VM Remove-VMHost Remove-VMHostAccount Remove-VMHostNetworkAdapter Remove-VMHostNtpServer Remove-VMHostProfile " + "Remove-VMHostProfileVmPortGroupConfiguration Remove-VMHostRoute Remove-VTpm Remove-VasaProvider Remove-VirtualPortGroup Remove-VirtualSwitch " + "Remove-VirtualSwitchPhysicalNetworkAdapter Remove-VsanDisk Remove-VsanDiskGroup Remove-VsanFaultDomain Remove-VsanIscsiInitiatorGroup " + "Remove-VsanIscsiInitiatorGroupTargetAssociation Remove-VsanIscsiLun Remove-VsanIscsiTarget Remove-vRABusinessGroup Remove-vRACustomForm Remove-vRAExternalNetworkProfile " + "Remove-vRAGroupPrincipal Remove-vRAIcon Remove-vRANATNetworkProfile Remove-vRAPackage Remove-vRAPrincipalFromTenantRole Remove-vRAPropertyDefinition Remove-vRAPropertyGroup " + "Remove-vRAReservation Remove-vRAReservationNetwork Remove-vRAReservationPolicy Remove-vRAReservationStorage Remove-vRARoutedNetworkProfile Remove-vRAService " + "Remove-vRAStorageReservationPolicy Remove-vRATenant Remove-vRATenantDirectory Remove-vRAUserPrincipal Remove-vRNIApplication Remove-vRNIApplicationTier Remove-vRNIDataSource " + "Repair-NsxEdge Repair-VsanObject Request-vRACatalogItem Request-vRAResourceAction Restart-CIVApp Restart-CIVAppGuest Restart-CIVM Restart-CIVMGuest Restart-VM Restart-VMGuest " + "Restart-VMHost Restart-VMHostService Resume-HCXReplication Revoke-NsxSpoofguardNicApproval Save-Module Save-Package Save-Script Search-Cloud Set-AdvancedSetting " + "Set-AlarmDefinition Set-Annotation Set-CDDrive Set-CIAccessControlRule Set-CINetworkAdapter Set-CIVApp Set-CIVAppNetwork Set-CIVAppStartRule Set-CIVAppTemplate Set-Cluster " + "Set-CustomAttribute Set-Datacenter Set-Datastore Set-DatastoreCluster Set-DrsClusterGroup Set-DrsRule Set-DrsVMHostRule Set-FloppyDrive Set-Folder Set-HCXAppliance " + "Set-HCXMigration Set-HCXReplication Set-HardDisk Set-IScsiHbaTarget Set-KeyManagementServer Set-KmsCluster Set-MarkdownOption Set-NetworkAdapter Set-NfsUser Set-NicTeamingPolicy " + "Set-NodeExclusiveResources Set-NodeManager Set-NodeResourceSource Set-NodeResources Set-NsxEdge Set-NsxEdgeBgp Set-NsxEdgeFirewall Set-NsxEdgeInterface Set-NsxEdgeNat " + "Set-NsxEdgeOspf Set-NsxEdgeRouting Set-NsxFirewallGlobalConfiguration Set-NsxFirewallRule Set-NsxFirewallSavedConfiguration Set-NsxFirewallThreshold Set-NsxLoadBalancer " + "Set-NsxLoadBalancerPoolMember Set-NsxLogicalRouter Set-NsxLogicalRouterBgp Set-NsxLogicalRouterBridging Set-NsxLogicalRouterInterface Set-NsxLogicalRouterOspf " + "Set-NsxLogicalRouterRouting Set-NsxManager Set-NsxManagerRole Set-NsxManagerTimeSettings Set-NsxSecurityPolicy Set-NsxSecurityPolicyFirewallRule Set-NsxSslVpn " + "Set-OSCustomizationNicMapping Set-OSCustomizationSpec Set-Org Set-OrgNetwork Set-OrgVdc Set-OrgVdcNetwork Set-PSCurrentConfigurationNode Set-PSDefaultConfigurationDocument " + "Set-PSMetaConfigDocInsProcessedBeforeMeta Set-PSMetaConfigVersionInfoV2 Set-PSReadLineKeyHandler Set-PSReadLineOption Set-PSRepository Set-PSTopConfigurationName " + "Set-PackageSource Set-PowerCLIConfiguration Set-ResourcePool Set-ScsiController Set-ScsiLun Set-ScsiLunPath Set-SecurityPolicy Set-Snapshot Set-SpbmEntityConfiguration " + "Set-SpbmStoragePolicy Set-StatInterval Set-Tag Set-TagCategory Set-Template Set-VAIOFilter Set-VApp Set-VDBlockedPolicy Set-VDPort Set-VDPortgroup Set-VDPortgroupOverridePolicy " + "Set-VDSecurityPolicy Set-VDSwitch Set-VDTrafficShapingPolicy Set-VDUplinkLacpPolicy Set-VDUplinkTeamingPolicy Set-VDVlanConfiguration Set-VDisk Set-VIPermission Set-VIRole Set-VM " + "Set-VMHost Set-VMHostAccount Set-VMHostAdvancedConfiguration Set-VMHostAuthentication Set-VMHostDiagnosticPartition Set-VMHostFirewallDefaultPolicy Set-VMHostFirewallException " + "Set-VMHostFirmware Set-VMHostHba Set-VMHostModule Set-VMHostNetwork Set-VMHostNetworkAdapter Set-VMHostProfile Set-VMHostProfileImageCacheConfiguration " + "Set-VMHostProfileStorageDeviceConfiguration Set-VMHostProfileUserConfiguration Set-VMHostProfileVmPortGroupConfiguration Set-VMHostRoute Set-VMHostService Set-VMHostSnmp " + "Set-VMHostStartPolicy Set-VMHostStorage Set-VMHostSysLogServer Set-VMQuestion Set-VMResourceConfiguration Set-VMStartPolicy Set-VTpm Set-VirtualPortGroup Set-VirtualSwitch " + "Set-VsanClusterConfiguration Set-VsanFaultDomain Set-VsanIscsiInitiatorGroup Set-VsanIscsiLun Set-VsanIscsiTarget Set-vRABusinessGroup Set-vRACatalogItem Set-vRACustomForm " + "Set-vRAEntitlement Set-vRAExternalNetworkProfile Set-vRANATNetworkProfile Set-vRAReservation Set-vRAReservationNetwork Set-vRAReservationPolicy Set-vRAReservationStorage " + "Set-vRARoutedNetworkProfile Set-vRAService Set-vRAStorageReservationPolicy Set-vRATenant Set-vRATenantDirectory Set-vRAUserPrincipal Set-vRNIDataSourceSNMPConfig Show-Markdown " + "Start-CIVApp Start-CIVM Start-HCXMigration Start-HCXReplication Start-SpbmReplicationFailover Start-SpbmReplicationPrepareFailover Start-SpbmReplicationPromote " + "Start-SpbmReplicationReverse Start-SpbmReplicationTestFailover Start-ThreadJob Start-VApp Start-VM Start-VMHost Start-VMHostService Start-VsanClusterDiskUpdate " + "Start-VsanClusterRebalance Start-VsanEncryptionConfiguration Stop-CIVApp Stop-CIVAppGuest Stop-CIVM Stop-CIVMGuest Stop-SpbmReplicationTestFailover Stop-Task Stop-VApp Stop-VM " + "Stop-VMGuest Stop-VMHost Stop-VMHostService Stop-VsanClusterRebalance Suspend-CIVApp Suspend-CIVM Suspend-HCXReplication Suspend-VM Suspend-VMGuest Suspend-VMHost " + "Sync-SpbmReplicationGroup Test-ConflictingResources Test-HCXMigration Test-HCXReplication Test-Json Test-ModuleReloadRequired Test-MofInstanceText Test-NodeManager " + "Test-NodeResourceSource Test-NodeResources Test-ScriptFileInfo Test-VMHostProfileCompliance Test-VMHostSnmp Test-VsanClusterHealth Test-VsanNetworkPerformance " + "Test-VsanStoragePerformance Test-VsanVMCreation Test-vRAPackage Uninstall-Module Uninstall-Package Uninstall-Script Unlock-VM Unregister-PSRepository Unregister-PackageSource " + "Update-ConfigurationDocumentRef Update-ConfigurationErrorCount Update-DependsOn Update-LocalConfigManager Update-Module Update-ModuleManifest Update-ModuleVersion Update-PowerNsx " + "Update-Script Update-ScriptFileInfo Update-Tools Update-VsanHclDatabase ValidateUpdate-ConfigurationData Wait-Debugger Wait-NsxControllerJob Wait-NsxGenericJob Wait-NsxJob " + "Wait-Task Wait-Tools Write-Information Write-Log Write-MetaConfigFile Write-NodeMOFFile", nomarkup: "-ne -eq -lt -gt -ge -le -not -like -notlike -match -notmatch -contains -notcontains -in -notin -replace", }, contains: [ BACKTICK_ESCAPE, hljs.NUMBER_MODE, QUOTE_STRING, APOS_STRING, LITERAL, VAR, PS_COMMENT, ], }; } },{name:"processing",create:/* Language: Processing Author: Erik Paluka Category: graphics */ function(hljs) { return { keywords: { keyword: 'BufferedReader PVector PFont PImage PGraphics HashMap boolean byte char color ' + 'double float int long String Array FloatDict FloatList IntDict IntList JSONArray JSONObject ' + 'Object StringDict StringList Table TableRow XML ' + // Java keywords 'false synchronized int abstract float private char boolean static null if const ' + 'for true while long throw strictfp finally protected import native final return void ' + 'enum else break transient new catch instanceof byte super volatile case assert short ' + 'package default double public try this switch continue throws protected public private', literal: 'P2D P3D HALF_PI PI QUARTER_PI TAU TWO_PI', title: 'setup draw', built_in: 'displayHeight displayWidth mouseY mouseX mousePressed pmouseX pmouseY key ' + 'keyCode pixels focused frameCount frameRate height width ' + 'size createGraphics beginDraw createShape loadShape PShape arc ellipse line point ' + 'quad rect triangle bezier bezierDetail bezierPoint bezierTangent curve curveDetail curvePoint ' + 'curveTangent curveTightness shape shapeMode beginContour beginShape bezierVertex curveVertex ' + 'endContour endShape quadraticVertex vertex ellipseMode noSmooth rectMode smooth strokeCap ' + 'strokeJoin strokeWeight mouseClicked mouseDragged mouseMoved mousePressed mouseReleased ' + 'mouseWheel keyPressed keyPressedkeyReleased keyTyped print println save saveFrame day hour ' + 'millis minute month second year background clear colorMode fill noFill noStroke stroke alpha ' + 'blue brightness color green hue lerpColor red saturation modelX modelY modelZ screenX screenY ' + 'screenZ ambient emissive shininess specular add createImage beginCamera camera endCamera frustum ' + 'ortho perspective printCamera printProjection cursor frameRate noCursor exit loop noLoop popStyle ' + 'pushStyle redraw binary boolean byte char float hex int str unbinary unhex join match matchAll nf ' + 'nfc nfp nfs split splitTokens trim append arrayCopy concat expand reverse shorten sort splice subset ' + 'box sphere sphereDetail createInput createReader loadBytes loadJSONArray loadJSONObject loadStrings ' + 'loadTable loadXML open parseXML saveTable selectFolder selectInput beginRaw beginRecord createOutput ' + 'createWriter endRaw endRecord PrintWritersaveBytes saveJSONArray saveJSONObject saveStream saveStrings ' + 'saveXML selectOutput popMatrix printMatrix pushMatrix resetMatrix rotate rotateX rotateY rotateZ scale ' + 'shearX shearY translate ambientLight directionalLight lightFalloff lights lightSpecular noLights normal ' + 'pointLight spotLight image imageMode loadImage noTint requestImage tint texture textureMode textureWrap ' + 'blend copy filter get loadPixels set updatePixels blendMode loadShader PShaderresetShader shader createFont ' + 'loadFont text textFont textAlign textLeading textMode textSize textWidth textAscent textDescent abs ceil ' + 'constrain dist exp floor lerp log mag map max min norm pow round sq sqrt acos asin atan atan2 cos degrees ' + 'radians sin tan noise noiseDetail noiseSeed random randomGaussian randomSeed' }, contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, hljs.C_NUMBER_MODE ] }; } },{name:"profile",create:/* Language: Python profile Description: Python profiler results Author: Brian Beck */ function(hljs) { return { contains: [ hljs.C_NUMBER_MODE, { begin: '[a-zA-Z_][\\da-zA-Z_]+\\.[\\da-zA-Z_]{1,3}', end: ':', excludeEnd: true }, { begin: '(ncalls|tottime|cumtime)', end: '$', keywords: 'ncalls tottime|10 cumtime|10 filename', relevance: 10 }, { begin: 'function calls', end: '$', contains: [hljs.C_NUMBER_MODE], relevance: 10 }, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, { className: 'string', begin: '\\(', end: '\\)$', excludeBegin: true, excludeEnd: true, relevance: 0 } ] }; } },{name:"prolog",create:/* Language: Prolog Description: Prolog is a general purpose logic programming language associated with artificial intelligence and computational linguistics. Author: Raivo Laanemets */ function(hljs) { var ATOM = { begin: /[a-z][A-Za-z0-9_]*/, relevance: 0 }; var VAR = { className: 'symbol', variants: [ {begin: /[A-Z][a-zA-Z0-9_]*/}, {begin: /_[A-Za-z0-9_]*/}, ], relevance: 0 }; var PARENTED = { begin: /\(/, end: /\)/, relevance: 0 }; var LIST = { begin: /\[/, end: /\]/ }; var LINE_COMMENT = { className: 'comment', begin: /%/, end: /$/, contains: [hljs.PHRASAL_WORDS_MODE] }; var BACKTICK_STRING = { className: 'string', begin: /`/, end: /`/, contains: [hljs.BACKSLASH_ESCAPE] }; var CHAR_CODE = { className: 'string', // 0'a etc. begin: /0\'(\\\'|.)/ }; var SPACE_CODE = { className: 'string', begin: /0\'\\s/ // 0'\s }; var PRED_OP = { // relevance booster begin: /:-/ }; var inner = [ ATOM, VAR, PARENTED, PRED_OP, LIST, LINE_COMMENT, hljs.C_BLOCK_COMMENT_MODE, hljs.QUOTE_STRING_MODE, hljs.APOS_STRING_MODE, BACKTICK_STRING, CHAR_CODE, SPACE_CODE, hljs.C_NUMBER_MODE ]; PARENTED.contains = inner; LIST.contains = inner; return { contains: inner.concat([ {begin: /\.$/} // relevance booster ]) }; } },{name:"properties",create:/* Language: Properties Contributors: Valentin Aitken , Egor Rogov Category: common, config */ function(hljs) { // whitespaces: space, tab, formfeed var WS0 = '[ \\t\\f]*'; var WS1 = '[ \\t\\f]+'; // delimiter var DELIM = '(' + WS0+'[:=]'+WS0+ '|' + WS1 + ')'; var KEY_ALPHANUM = '([^\\\\\\W:= \\t\\f\\n]|\\\\.)+'; var KEY_OTHER = '([^\\\\:= \\t\\f\\n]|\\\\.)+'; var DELIM_AND_VALUE = { // skip DELIM end: DELIM, relevance: 0, starts: { // value: everything until end of line (again, taking into account backslashes) className: 'string', end: /$/, relevance: 0, contains: [ { begin: '\\\\\\n' } ] } }; return { case_insensitive: true, illegal: /\S/, contains: [ hljs.COMMENT('^\\s*[!#]', '$'), // key: everything until whitespace or = or : (taking into account backslashes) // case of a "normal" key { begin: KEY_ALPHANUM + DELIM, returnBegin: true, contains: [ { className: 'attr', begin: KEY_ALPHANUM, endsParent: true, relevance: 0 } ], starts: DELIM_AND_VALUE }, // case of key containing non-alphanumeric chars => relevance = 0 { begin: KEY_OTHER + DELIM, returnBegin: true, relevance: 0, contains: [ { className: 'meta', begin: KEY_OTHER, endsParent: true, relevance: 0 } ], starts: DELIM_AND_VALUE }, // case of an empty key { className: 'attr', relevance: 0, begin: KEY_OTHER + WS0 + '$' } ] }; } },{name:"protobuf",create:/* Language: Protocol Buffers Author: Dan Tao Description: Protocol buffer message definition format Category: protocols */ function(hljs) { return { keywords: { keyword: 'package import option optional required repeated group oneof', built_in: 'double float int32 int64 uint32 uint64 sint32 sint64 ' + 'fixed32 fixed64 sfixed32 sfixed64 bool string bytes', literal: 'true false' }, contains: [ hljs.QUOTE_STRING_MODE, hljs.NUMBER_MODE, hljs.C_LINE_COMMENT_MODE, { className: 'class', beginKeywords: 'message enum service', end: /\{/, illegal: /\n/, contains: [ hljs.inherit(hljs.TITLE_MODE, { starts: {endsWithParent: true, excludeEnd: true} // hack: eating everything after the first title }) ] }, { className: 'function', beginKeywords: 'rpc', end: /;/, excludeEnd: true, keywords: 'rpc returns' }, { begin: /^\s*[A-Z_]+/, end: /\s*=/, excludeEnd: true } ] }; } },{name:"puppet",create:/* Language: Puppet Author: Jose Molina Colmenero Category: config */ function(hljs) { var PUPPET_KEYWORDS = { keyword: /* language keywords */ 'and case default else elsif false if in import enherits node or true undef unless main settings $string ', literal: /* metaparameters */ 'alias audit before loglevel noop require subscribe tag ' + /* normal attributes */ 'owner ensure group mode name|0 changes context force incl lens load_path onlyif provider returns root show_diff type_check ' + 'en_address ip_address realname command environment hour monute month monthday special target weekday '+ 'creates cwd ogoutput refresh refreshonly tries try_sleep umask backup checksum content ctime force ignore ' + 'links mtime purge recurse recurselimit replace selinux_ignore_defaults selrange selrole seltype seluser source ' + 'souirce_permissions sourceselect validate_cmd validate_replacement allowdupe attribute_membership auth_membership forcelocal gid '+ 'ia_load_module members system host_aliases ip allowed_trunk_vlans description device_url duplex encapsulation etherchannel ' + 'native_vlan speed principals allow_root auth_class auth_type authenticate_user k_of_n mechanisms rule session_owner shared options ' + 'device fstype enable hasrestart directory present absent link atboot blockdevice device dump pass remounts poller_tag use ' + 'message withpath adminfile allow_virtual allowcdrom category configfiles flavor install_options instance package_settings platform ' + 'responsefile status uninstall_options vendor unless_system_user unless_uid binary control flags hasstatus manifest pattern restart running ' + 'start stop allowdupe auths expiry gid groups home iterations key_membership keys managehome membership password password_max_age ' + 'password_min_age profile_membership profiles project purge_ssh_keys role_membership roles salt shell uid baseurl cost descr enabled ' + 'enablegroups exclude failovermethod gpgcheck gpgkey http_caching include includepkgs keepalive metadata_expire metalink mirrorlist ' + 'priority protect proxy proxy_password proxy_username repo_gpgcheck s3_enabled skip_if_unavailable sslcacert sslclientcert sslclientkey ' + 'sslverify mounted', built_in: /* core facts */ 'architecture augeasversion blockdevices boardmanufacturer boardproductname boardserialnumber cfkey dhcp_servers ' + 'domain ec2_ ec2_userdata facterversion filesystems ldom fqdn gid hardwareisa hardwaremodel hostname id|0 interfaces '+ 'ipaddress ipaddress_ ipaddress6 ipaddress6_ iphostnumber is_virtual kernel kernelmajversion kernelrelease kernelversion ' + 'kernelrelease kernelversion lsbdistcodename lsbdistdescription lsbdistid lsbdistrelease lsbmajdistrelease lsbminordistrelease ' + 'lsbrelease macaddress macaddress_ macosx_buildversion macosx_productname macosx_productversion macosx_productverson_major ' + 'macosx_productversion_minor manufacturer memoryfree memorysize netmask metmask_ network_ operatingsystem operatingsystemmajrelease '+ 'operatingsystemrelease osfamily partitions path physicalprocessorcount processor processorcount productname ps puppetversion '+ 'rubysitedir rubyversion selinux selinux_config_mode selinux_config_policy selinux_current_mode selinux_current_mode selinux_enforced '+ 'selinux_policyversion serialnumber sp_ sshdsakey sshecdsakey sshrsakey swapencrypted swapfree swapsize timezone type uniqueid uptime '+ 'uptime_days uptime_hours uptime_seconds uuid virtual vlans xendomains zfs_version zonenae zones zpool_version' }; var COMMENT = hljs.COMMENT('#', '$'); var IDENT_RE = '([A-Za-z_]|::)(\\w|::)*'; var TITLE = hljs.inherit(hljs.TITLE_MODE, {begin: IDENT_RE}); var VARIABLE = {className: 'variable', begin: '\\$' + IDENT_RE}; var STRING = { className: 'string', contains: [hljs.BACKSLASH_ESCAPE, VARIABLE], variants: [ {begin: /'/, end: /'/}, {begin: /"/, end: /"/} ] }; return { aliases: ['pp'], contains: [ COMMENT, VARIABLE, STRING, { beginKeywords: 'class', end: '\\{|;', illegal: /=/, contains: [TITLE, COMMENT] }, { beginKeywords: 'define', end: /\{/, contains: [ { className: 'section', begin: hljs.IDENT_RE, endsParent: true } ] }, { begin: hljs.IDENT_RE + '\\s+\\{', returnBegin: true, end: /\S/, contains: [ { className: 'keyword', begin: hljs.IDENT_RE }, { begin: /\{/, end: /\}/, keywords: PUPPET_KEYWORDS, relevance: 0, contains: [ STRING, COMMENT, { begin:'[a-zA-Z_]+\\s*=>', returnBegin: true, end: '=>', contains: [ { className: 'attr', begin: hljs.IDENT_RE, } ] }, { className: 'number', begin: '(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b', relevance: 0 }, VARIABLE ] } ], relevance: 0 } ] } } },{name:"purebasic",create:/* Language: PureBASIC Author: Tristano Ajmone Description: Syntax highlighting for PureBASIC (v.5.00-5.60). No inline ASM highlighting. (v.1.2, May 2017) Credits: I've taken inspiration from the PureBasic language file for GeSHi, created by Gustavo Julio Fiorenza (GuShH). */ // Base deafult colors in PB IDE: background: #FFFFDF; foreground: #000000; function(hljs) { var STRINGS = { // PB IDE color: #0080FF (Azure Radiance) className: 'string', begin: '(~)?"', end: '"', illegal: '\\n' }; var CONSTANTS = { // PB IDE color: #924B72 (Cannon Pink) // "#" + a letter or underscore + letters, digits or underscores + (optional) "$" className: 'symbol', begin: '#[a-zA-Z_]\\w*\\$?' }; return { aliases: ['pb', 'pbi'], keywords: // PB IDE color: #006666 (Blue Stone) + Bold // Keywords from all version of PureBASIC 5.00 upward ... 'Align And Array As Break CallDebugger Case CompilerCase CompilerDefault ' + 'CompilerElse CompilerElseIf CompilerEndIf CompilerEndSelect CompilerError ' + 'CompilerIf CompilerSelect CompilerWarning Continue Data DataSection Debug ' + 'DebugLevel Declare DeclareC DeclareCDLL DeclareDLL DeclareModule Default ' + 'Define Dim DisableASM DisableDebugger DisableExplicit Else ElseIf EnableASM ' + 'EnableDebugger EnableExplicit End EndDataSection EndDeclareModule EndEnumeration ' + 'EndIf EndImport EndInterface EndMacro EndModule EndProcedure EndSelect ' + 'EndStructure EndStructureUnion EndWith Enumeration EnumerationBinary Extends ' + 'FakeReturn For ForEach ForEver Global Gosub Goto If Import ImportC ' + 'IncludeBinary IncludeFile IncludePath Interface List Macro MacroExpandedCount ' + 'Map Module NewList NewMap Next Not Or Procedure ProcedureC ' + 'ProcedureCDLL ProcedureDLL ProcedureReturn Protected Prototype PrototypeC ReDim ' + 'Read Repeat Restore Return Runtime Select Shared Static Step Structure ' + 'StructureUnion Swap Threaded To UndefineMacro Until Until UnuseModule ' + 'UseModule Wend While With XIncludeFile XOr', contains: [ // COMMENTS | PB IDE color: #00AAAA (Persian Green) hljs.COMMENT(';', '$', {relevance: 0}), { // PROCEDURES DEFINITIONS className: 'function', begin: '\\b(Procedure|Declare)(C|CDLL|DLL)?\\b', end: '\\(', excludeEnd: true, returnBegin: true, contains: [ { // PROCEDURE KEYWORDS | PB IDE color: #006666 (Blue Stone) + Bold className: 'keyword', begin: '(Procedure|Declare)(C|CDLL|DLL)?', excludeEnd: true }, { // PROCEDURE RETURN TYPE SETTING | PB IDE color: #000000 (Black) className: 'type', begin: '\\.\\w*' // end: ' ', }, hljs.UNDERSCORE_TITLE_MODE // PROCEDURE NAME | PB IDE color: #006666 (Blue Stone) ] }, STRINGS, CONSTANTS ] }; } /* ============================================================================== CHANGELOG ============================================================================== - v.1.2 (2017-05-12) -- BUG-FIX: Some keywords were accidentally joyned together. Now fixed. - v.1.1 (2017-04-30) -- Updated to PureBASIC 5.60. -- Keywords list now built by extracting them from the PureBASIC SDK's "SyntaxHilighting.dll" (from each PureBASIC version). Tokens from each version are added to the list, and renamed or removed tokens are kept for the sake of covering all versions of the language from PureBASIC v5.00 upward. (NOTE: currently, there are no renamed or deprecated tokens in the keywords list). For more info, see: -- http://www.purebasic.fr/english/viewtopic.php?&p=506269 -- https://github.com/tajmone/purebasic-archives/tree/master/syntax-highlighting/guidelines - v.1.0 (April 2016) -- First release -- Keywords list taken and adapted from GuShH's (Gustavo Julio Fiorenza) PureBasic language file for GeSHi: -- https://github.com/easybook/geshi/blob/master/geshi/purebasic.php */},{name:"python",create:/* Language: Python Category: common */ function(hljs) { var KEYWORDS = { keyword: 'and elif is global as in if from raise for except finally print import pass return ' + 'exec else break not with class assert yield try while continue del or def lambda ' + 'async await nonlocal|10', built_in: 'Ellipsis NotImplemented', literal: 'False None True' }; var PROMPT = { className: 'meta', begin: /^(>>>|\.\.\.) / }; var SUBST = { className: 'subst', begin: /\{/, end: /\}/, keywords: KEYWORDS, illegal: /#/ }; var STRING = { className: 'string', contains: [hljs.BACKSLASH_ESCAPE], variants: [ { begin: /(u|b)?r?'''/, end: /'''/, contains: [hljs.BACKSLASH_ESCAPE, PROMPT], relevance: 10 }, { begin: /(u|b)?r?"""/, end: /"""/, contains: [hljs.BACKSLASH_ESCAPE, PROMPT], relevance: 10 }, { begin: /(fr|rf|f)'''/, end: /'''/, contains: [hljs.BACKSLASH_ESCAPE, PROMPT, SUBST] }, { begin: /(fr|rf|f)"""/, end: /"""/, contains: [hljs.BACKSLASH_ESCAPE, PROMPT, SUBST] }, { begin: /(u|r|ur)'/, end: /'/, relevance: 10 }, { begin: /(u|r|ur)"/, end: /"/, relevance: 10 }, { begin: /(b|br)'/, end: /'/ }, { begin: /(b|br)"/, end: /"/ }, { begin: /(fr|rf|f)'/, end: /'/, contains: [hljs.BACKSLASH_ESCAPE, SUBST] }, { begin: /(fr|rf|f)"/, end: /"/, contains: [hljs.BACKSLASH_ESCAPE, SUBST] }, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE ] }; var NUMBER = { className: 'number', relevance: 0, variants: [ {begin: hljs.BINARY_NUMBER_RE + '[lLjJ]?'}, {begin: '\\b(0o[0-7]+)[lLjJ]?'}, {begin: hljs.C_NUMBER_RE + '[lLjJ]?'} ] }; var PARAMS = { className: 'params', begin: /\(/, end: /\)/, contains: ['self', PROMPT, NUMBER, STRING] }; SUBST.contains = [STRING, NUMBER, PROMPT]; return { aliases: ['py', 'gyp', 'ipython'], keywords: KEYWORDS, illegal: /(<\/|->|\?)|=>/, contains: [ PROMPT, NUMBER, STRING, hljs.HASH_COMMENT_MODE, { variants: [ {className: 'function', beginKeywords: 'def'}, {className: 'class', beginKeywords: 'class'} ], end: /:/, illegal: /[${=;\n,]/, contains: [ hljs.UNDERSCORE_TITLE_MODE, PARAMS, { begin: /->/, endsWithParent: true, keywords: 'None' } ] }, { className: 'meta', begin: /^[\t ]*@/, end: /$/ }, { begin: /\b(print|exec)\(/ // don’t highlight keywords-turned-functions in Python 3 } ] }; } },{name:"q",create:/* Language: Q Author: Sergey Vidyuk Description: K/Q/Kdb+ from Kx Systems */ function(hljs) { var Q_KEYWORDS = { keyword: 'do while select delete by update from', literal: '0b 1b', built_in: 'neg not null string reciprocal floor ceiling signum mod xbar xlog and or each scan over prior mmu lsq inv md5 ltime gtime count first var dev med cov cor all any rand sums prds mins maxs fills deltas ratios avgs differ prev next rank reverse iasc idesc asc desc msum mcount mavg mdev xrank mmin mmax xprev rotate distinct group where flip type key til get value attr cut set upsert raze union inter except cross sv vs sublist enlist read0 read1 hopen hclose hdel hsym hcount peach system ltrim rtrim trim lower upper ssr view tables views cols xcols keys xkey xcol xasc xdesc fkeys meta lj aj aj0 ij pj asof uj ww wj wj1 fby xgroup ungroup ej save load rsave rload show csv parse eval min max avg wavg wsum sin cos tan sum', type: '`float `double int `timestamp `timespan `datetime `time `boolean `symbol `char `byte `short `long `real `month `date `minute `second `guid' }; return { aliases:['k', 'kdb'], keywords: Q_KEYWORDS, lexemes: /(`?)[A-Za-z0-9_]+\b/, contains: [ hljs.C_LINE_COMMENT_MODE, hljs.QUOTE_STRING_MODE, hljs.C_NUMBER_MODE ] }; } },{name:"qml",create:/* Language: QML Requires: javascript.js, xml.js Author: John Foster Description: Syntax highlighting for the Qt Quick QML scripting language, based mostly off the JavaScript parser. Category: scripting */ function(hljs) { var KEYWORDS = { keyword: 'in of on if for while finally var new function do return void else break catch ' + 'instanceof with throw case default try this switch continue typeof delete ' + 'let yield const export super debugger as async await import', literal: 'true false null undefined NaN Infinity', built_in: 'eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent ' + 'encodeURI encodeURIComponent escape unescape Object Function Boolean Error ' + 'EvalError InternalError RangeError ReferenceError StopIteration SyntaxError ' + 'TypeError URIError Number Math Date String RegExp Array Float32Array ' + 'Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array ' + 'Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require ' + 'module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect ' + 'Behavior bool color coordinate date double enumeration font geocircle georectangle ' + 'geoshape int list matrix4x4 parent point quaternion real rect ' + 'size string url variant vector2d vector3d vector4d' + 'Promise' }; var QML_IDENT_RE = '[a-zA-Z_][a-zA-Z0-9\\._]*'; // Isolate property statements. Ends at a :, =, ;, ,, a comment or end of line. // Use property class. var PROPERTY = { className: 'keyword', begin: '\\bproperty\\b', starts: { className: 'string', end: '(:|=|;|,|//|/\\*|$)', returnEnd: true } }; // Isolate signal statements. Ends at a ) a comment or end of line. // Use property class. var SIGNAL = { className: 'keyword', begin: '\\bsignal\\b', starts: { className: 'string', end: '(\\(|:|=|;|,|//|/\\*|$)', returnEnd: true } }; // id: is special in QML. When we see id: we want to mark the id: as attribute and // emphasize the token following. var ID_ID = { className: 'attribute', begin: '\\bid\\s*:', starts: { className: 'string', end: QML_IDENT_RE, returnEnd: false } }; // Find QML object attribute. An attribute is a QML identifier followed by :. // Unfortunately it's hard to know where it ends, as it may contain scalars, // objects, object definitions, or javascript. The true end is either when the parent // ends or the next attribute is detected. var QML_ATTRIBUTE = { begin: QML_IDENT_RE + '\\s*:', returnBegin: true, contains: [ { className: 'attribute', begin: QML_IDENT_RE, end: '\\s*:', excludeEnd: true, relevance: 0 } ], relevance: 0 }; // Find QML object. A QML object is a QML identifier followed by { and ends at the matching }. // All we really care about is finding IDENT followed by { and just mark up the IDENT and ignore the {. var QML_OBJECT = { begin: QML_IDENT_RE + '\\s*{', end: '{', returnBegin: true, relevance: 0, contains: [ hljs.inherit(hljs.TITLE_MODE, {begin: QML_IDENT_RE}) ] }; return { aliases: ['qt'], case_insensitive: false, keywords: KEYWORDS, contains: [ { className: 'meta', begin: /^\s*['"]use (strict|asm)['"]/ }, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, { // template string className: 'string', begin: '`', end: '`', contains: [ hljs.BACKSLASH_ESCAPE, { className: 'subst', begin: '\\$\\{', end: '\\}' } ] }, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, { className: 'number', variants: [ { begin: '\\b(0[bB][01]+)' }, { begin: '\\b(0[oO][0-7]+)' }, { begin: hljs.C_NUMBER_RE } ], relevance: 0 }, { // "value" container begin: '(' + hljs.RE_STARTERS_RE + '|\\b(case|return|throw)\\b)\\s*', keywords: 'return throw case', contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.REGEXP_MODE, { // E4X / JSX begin: /\s*[);\]]/, relevance: 0, subLanguage: 'xml' } ], relevance: 0 }, SIGNAL, PROPERTY, { className: 'function', beginKeywords: 'function', end: /\{/, excludeEnd: true, contains: [ hljs.inherit(hljs.TITLE_MODE, {begin: /[A-Za-z$_][0-9A-Za-z$_]*/}), { className: 'params', begin: /\(/, end: /\)/, excludeBegin: true, excludeEnd: true, contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE ] } ], illegal: /\[|%/ }, { begin: '\\.' + hljs.IDENT_RE, relevance: 0 // hack: prevents detection of keywords after dots }, ID_ID, QML_ATTRIBUTE, QML_OBJECT ], illegal: /#/ }; } },{name:"r",create:/* Language: R Author: Joe Cheng Category: scientific */ function(hljs) { var IDENT_RE = '([a-zA-Z]|\\.[a-zA-Z.])[a-zA-Z0-9._]*'; return { contains: [ hljs.HASH_COMMENT_MODE, { begin: IDENT_RE, lexemes: IDENT_RE, keywords: { keyword: 'function if in break next repeat else for return switch while try tryCatch ' + 'stop warning require library attach detach source setMethod setGeneric ' + 'setGroupGeneric setClass ...', literal: 'NULL NA TRUE FALSE T F Inf NaN NA_integer_|10 NA_real_|10 NA_character_|10 ' + 'NA_complex_|10' }, relevance: 0 }, { // hex value className: 'number', begin: "0[xX][0-9a-fA-F]+[Li]?\\b", relevance: 0 }, { // explicit integer className: 'number', begin: "\\d+(?:[eE][+\\-]?\\d*)?L\\b", relevance: 0 }, { // number with trailing decimal className: 'number', begin: "\\d+\\.(?!\\d)(?:i\\b)?", relevance: 0 }, { // number className: 'number', begin: "\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d*)?i?\\b", relevance: 0 }, { // number with leading decimal className: 'number', begin: "\\.\\d+(?:[eE][+\\-]?\\d*)?i?\\b", relevance: 0 }, { // escaped identifier begin: '`', end: '`', relevance: 0 }, { className: 'string', contains: [hljs.BACKSLASH_ESCAPE], variants: [ {begin: '"', end: '"'}, {begin: "'", end: "'"} ] } ] }; } },{name:"reasonml",create:/* Language: ReasonML Author: Gidi Meir Morris Category: functional */ function(hljs) { function orReValues(ops){ return ops .map(function(op) { return op .split('') .map(function(char) { return '\\' + char; }) .join(''); }) .join('|'); } var RE_IDENT = '~?[a-z$_][0-9a-zA-Z$_]*'; var RE_MODULE_IDENT = '`?[A-Z$_][0-9a-zA-Z$_]*'; var RE_PARAM_TYPEPARAM = '\'?[a-z$_][0-9a-z$_]*'; var RE_PARAM_TYPE = '\s*:\s*[a-z$_][0-9a-z$_]*(\(\s*(' + RE_PARAM_TYPEPARAM + '\s*(,' + RE_PARAM_TYPEPARAM + ')*)?\s*\))?'; var RE_PARAM = RE_IDENT + '(' + RE_PARAM_TYPE + ')?(' + RE_PARAM_TYPE + ')?'; var RE_OPERATOR = "(" + orReValues(['||', '&&', '++', '**', '+.', '*', '/', '*.', '/.', '...', '|>']) + "|==|===)"; var RE_OPERATOR_SPACED = "\\s+" + RE_OPERATOR + "\\s+"; var KEYWORDS = { keyword: 'and as asr assert begin class constraint do done downto else end exception external' + 'for fun function functor if in include inherit initializer' + 'land lazy let lor lsl lsr lxor match method mod module mutable new nonrec' + 'object of open or private rec sig struct then to try type val virtual when while with', built_in: 'array bool bytes char exn|5 float int int32 int64 list lazy_t|5 nativeint|5 ref string unit ', literal: 'true false' }; var RE_NUMBER = '\\b(0[xX][a-fA-F0-9_]+[Lln]?|' + '0[oO][0-7_]+[Lln]?|' + '0[bB][01_]+[Lln]?|' + '[0-9][0-9_]*([Lln]|(\\.[0-9_]*)?([eE][-+]?[0-9_]+)?)?)'; var NUMBER_MODE = { className: 'number', relevance: 0, variants: [ { begin: RE_NUMBER }, { begin: '\\(\\-' + RE_NUMBER + '\\)' } ] }; var OPERATOR_MODE = { className: 'operator', relevance: 0, begin: RE_OPERATOR }; var LIST_CONTENTS_MODES = [ { className: 'identifier', relevance: 0, begin: RE_IDENT }, OPERATOR_MODE, NUMBER_MODE ]; var MODULE_ACCESS_CONTENTS = [ hljs.QUOTE_STRING_MODE, OPERATOR_MODE, { className: 'module', begin: "\\b" + RE_MODULE_IDENT, returnBegin: true, end: "\.", contains: [ { className: 'identifier', begin: RE_MODULE_IDENT, relevance: 0 } ] } ]; var PARAMS_CONTENTS = [ { className: 'module', begin: "\\b" + RE_MODULE_IDENT, returnBegin: true, end: "\.", relevance: 0, contains: [ { className: 'identifier', begin: RE_MODULE_IDENT, relevance: 0 } ] } ]; var PARAMS_MODE = { begin: RE_IDENT, end: '(,|\\n|\\))', relevance: 0, contains: [ OPERATOR_MODE, { className: 'typing', begin: ':', end: '(,|\\n)', returnBegin: true, relevance: 0, contains: PARAMS_CONTENTS } ] }; var FUNCTION_BLOCK_MODE = { className: 'function', relevance: 0, keywords: KEYWORDS, variants: [ { begin: '\\s(\\(\\.?.*?\\)|' + RE_IDENT + ')\\s*=>', end: '\\s*=>', returnBegin: true, relevance: 0, contains: [ { className: 'params', variants: [ { begin: RE_IDENT }, { begin: RE_PARAM }, { begin: /\(\s*\)/, } ] } ] }, { begin: '\\s\\(\\.?[^;\\|]*\\)\\s*=>', end: '\\s=>', returnBegin: true, relevance: 0, contains: [ { className: 'params', relevance: 0, variants: [ PARAMS_MODE ] } ] }, { begin: '\\(\\.\\s' + RE_IDENT + '\\)\\s*=>' } ] }; MODULE_ACCESS_CONTENTS.push(FUNCTION_BLOCK_MODE); var CONSTRUCTOR_MODE = { className: 'constructor', begin: RE_MODULE_IDENT + '\\(', end: '\\)', illegal: '\\n', keywords: KEYWORDS, contains: [ hljs.QUOTE_STRING_MODE, OPERATOR_MODE, { className: 'params', begin: '\\b' + RE_IDENT } ] }; var PATTERN_MATCH_BLOCK_MODE = { className: 'pattern-match', begin: '\\|', returnBegin: true, keywords: KEYWORDS, end: '=>', relevance: 0, contains: [ CONSTRUCTOR_MODE, OPERATOR_MODE, { relevance: 0, className: 'constructor', begin: RE_MODULE_IDENT } ] }; var MODULE_ACCESS_MODE = { className: 'module-access', keywords: KEYWORDS, returnBegin: true, variants: [ { begin: "\\b(" + RE_MODULE_IDENT + "\\.)+" + RE_IDENT }, { begin: "\\b(" + RE_MODULE_IDENT + "\\.)+\\(", end: "\\)", returnBegin: true, contains: [ FUNCTION_BLOCK_MODE, { begin: '\\(', end: '\\)', skip: true } ].concat(MODULE_ACCESS_CONTENTS) }, { begin: "\\b(" + RE_MODULE_IDENT + "\\.)+{", end: "}" } ], contains: MODULE_ACCESS_CONTENTS }; PARAMS_CONTENTS.push(MODULE_ACCESS_MODE); return { aliases: ['re'], keywords: KEYWORDS, illegal: '(:\\-|:=|\\${|\\+=)', contains: [ hljs.COMMENT('/\\*', '\\*/', { illegal: '^(\\#,\\/\\/)' }), { className: 'character', begin: '\'(\\\\[^\']+|[^\'])\'', illegal: '\\n', relevance: 0 }, hljs.QUOTE_STRING_MODE, { className: 'literal', begin: '\\(\\)', relevance: 0 }, { className: 'literal', begin: '\\[\\|', end: '\\|\\]', relevance: 0, contains: LIST_CONTENTS_MODES }, { className: 'literal', begin: '\\[', end: '\\]', relevance: 0, contains: LIST_CONTENTS_MODES }, CONSTRUCTOR_MODE, { className: 'operator', begin: RE_OPERATOR_SPACED, illegal: '\\-\\->', relevance: 0 }, NUMBER_MODE, hljs.C_LINE_COMMENT_MODE, PATTERN_MATCH_BLOCK_MODE, FUNCTION_BLOCK_MODE, { className: 'module-def', begin: "\\bmodule\\s+" + RE_IDENT + "\\s+" + RE_MODULE_IDENT + "\\s+=\\s+{", end: "}", returnBegin: true, keywords: KEYWORDS, relevance: 0, contains: [ { className: 'module', relevance: 0, begin: RE_MODULE_IDENT }, { begin: '{', end: '}', skip: true } ].concat(MODULE_ACCESS_CONTENTS) }, MODULE_ACCESS_MODE ] }; } },{name:"rib",create:/* Language: RenderMan RIB Author: Konstantin Evdokimenko Contributors: Shuen-Huei Guan Category: graphics */ function(hljs) { return { keywords: 'ArchiveRecord AreaLightSource Atmosphere Attribute AttributeBegin AttributeEnd Basis ' + 'Begin Blobby Bound Clipping ClippingPlane Color ColorSamples ConcatTransform Cone ' + 'CoordinateSystem CoordSysTransform CropWindow Curves Cylinder DepthOfField Detail ' + 'DetailRange Disk Displacement Display End ErrorHandler Exposure Exterior Format ' + 'FrameAspectRatio FrameBegin FrameEnd GeneralPolygon GeometricApproximation Geometry ' + 'Hider Hyperboloid Identity Illuminate Imager Interior LightSource ' + 'MakeCubeFaceEnvironment MakeLatLongEnvironment MakeShadow MakeTexture Matte ' + 'MotionBegin MotionEnd NuPatch ObjectBegin ObjectEnd ObjectInstance Opacity Option ' + 'Orientation Paraboloid Patch PatchMesh Perspective PixelFilter PixelSamples ' + 'PixelVariance Points PointsGeneralPolygons PointsPolygons Polygon Procedural Projection ' + 'Quantize ReadArchive RelativeDetail ReverseOrientation Rotate Scale ScreenWindow ' + 'ShadingInterpolation ShadingRate Shutter Sides Skew SolidBegin SolidEnd Sphere ' + 'SubdivisionMesh Surface TextureCoordinates Torus Transform TransformBegin TransformEnd ' + 'TransformPoints Translate TrimCurve WorldBegin WorldEnd', illegal: ' Website: http://roboconf.net Description: Syntax highlighting for Roboconf's DSL Category: config */ function(hljs) { var IDENTIFIER = '[a-zA-Z-_][^\\n{]+\\{'; var PROPERTY = { className: 'attribute', begin: /[a-zA-Z-_]+/, end: /\s*:/, excludeEnd: true, starts: { end: ';', relevance: 0, contains: [ { className: 'variable', begin: /\.[a-zA-Z-_]+/ }, { className: 'keyword', begin: /\(optional\)/ } ] } }; return { aliases: ['graph', 'instances'], case_insensitive: true, keywords: 'import', contains: [ // Facet sections { begin: '^facet ' + IDENTIFIER, end: '}', keywords: 'facet', contains: [ PROPERTY, hljs.HASH_COMMENT_MODE ] }, // Instance sections { begin: '^\\s*instance of ' + IDENTIFIER, end: '}', keywords: 'name count channels instance-data instance-state instance of', illegal: /\S/, contains: [ 'self', PROPERTY, hljs.HASH_COMMENT_MODE ] }, // Component sections { begin: '^' + IDENTIFIER, end: '}', contains: [ PROPERTY, hljs.HASH_COMMENT_MODE ] }, // Comments hljs.HASH_COMMENT_MODE ] }; } },{name:"routeros",create:/* Language: Microtik RouterOS script Author: Ivan Dementev Description: Scripting host provides a way to automate some router maintenance tasks by means of executing user-defined scripts bounded to some event occurrence URL: https://wiki.mikrotik.com/wiki/Manual:Scripting */ // Colors from RouterOS terminal: // green - #0E9A00 // teal - #0C9A9A // purple - #99069A // light-brown - #9A9900 function(hljs) { var STATEMENTS = 'foreach do while for if from to step else on-error and or not in'; // Global commands: Every global command should start with ":" token, otherwise it will be treated as variable. var GLOBAL_COMMANDS = 'global local beep delay put len typeof pick log time set find environment terminal error execute parse resolve toarray tobool toid toip toip6 tonum tostr totime'; // Common commands: Following commands available from most sub-menus: var COMMON_COMMANDS = 'add remove enable disable set get print export edit find run debug error info warning'; var LITERALS = 'true false yes no nothing nil null'; var OBJECTS = 'traffic-flow traffic-generator firewall scheduler aaa accounting address-list address align area bandwidth-server bfd bgp bridge client clock community config connection console customer default dhcp-client dhcp-server discovery dns e-mail ethernet filter firewall firmware gps graphing group hardware health hotspot identity igmp-proxy incoming instance interface ip ipsec ipv6 irq l2tp-server lcd ldp logging mac-server mac-winbox mangle manual mirror mme mpls nat nd neighbor network note ntp ospf ospf-v3 ovpn-server page peer pim ping policy pool port ppp pppoe-client pptp-server prefix profile proposal proxy queue radius resource rip ripng route routing screen script security-profiles server service service-port settings shares smb sms sniffer snmp snooper socks sstp-server system tool tracking type upgrade upnp user-manager users user vlan secret vrrp watchdog web-access wireless pptp pppoe lan wan layer7-protocol lease simple raw'; // print parameters // Several parameters are available for print command: // ToDo: var PARAMETERS_PRINT = 'append as-value brief detail count-only file follow follow-only from interval terse value-list without-paging where info'; // ToDo: var OPERATORS = '&& and ! not || or in ~ ^ & << >> + - * /'; // ToDo: var TYPES = 'num number bool boolean str string ip ip6-prefix id time array'; // ToDo: The following tokens serve as delimiters in the grammar: () [] {} : ; $ / var VAR_PREFIX = 'global local set for foreach'; var VAR = { className: 'variable', variants: [ {begin: /\$[\w\d#@][\w\d_]*/}, {begin: /\$\{(.*?)}/} ] }; var QUOTE_STRING = { className: 'string', begin: /"/, end: /"/, contains: [ hljs.BACKSLASH_ESCAPE, VAR, { className: 'variable', begin: /\$\(/, end: /\)/, contains: [hljs.BACKSLASH_ESCAPE] } ] }; var APOS_STRING = { className: 'string', begin: /'/, end: /'/ }; var IPADDR = '((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\b'; var IPADDR_wBITMASK = IPADDR+'/(3[0-2]|[1-2][0-9]|\\d)'; ////////////////////////////////////////////////////////////////////// return { aliases: ['routeros', 'mikrotik'], case_insensitive: true, lexemes: /:?[\w-]+/, keywords: { literal: LITERALS, keyword: STATEMENTS + ' :' + STATEMENTS.split(' ').join(' :') + ' :' + GLOBAL_COMMANDS.split(' ').join(' :'), }, contains: [ { // недопустимые конструкции variants: [ { begin: /^@/, end: /$/, }, // dns { begin: /\/\*/, end: /\*\//, }, // -- comment { begin: /%%/, end: /$/, }, // -- comment { begin: /^'/, end: /$/, }, // Monkey one line comment { begin: /^\s*\/[\w-]+=/, end: /$/, }, // jboss-cli { begin: /\/\//, end: /$/, }, // Stan comment { begin: /^\[\\]$/, }, // F# class declaration? { begin: /<\//, end: />/, }, // HTML tags { begin: /^facet /, end: /\}/, }, // roboconf - лютый костыль ))) { begin: '^1\\.\\.(\\d+)$', end: /$/, }, // tap ], illegal: /./, }, hljs.COMMENT('^#', '$'), QUOTE_STRING, APOS_STRING, VAR, { // attribute=value begin: /[\w-]+\=([^\s\{\}\[\]\(\)]+)/, relevance: 0, returnBegin: true, contains: [ { className: 'attribute', begin: /[^=]+/ }, { begin: /=/, endsWithParent: true, relevance: 0, contains: [ QUOTE_STRING, APOS_STRING, VAR, { className: 'literal', begin: '\\b(' + LITERALS.split(' ').join('|') + ')\\b', }, /*{ // IPv4 addresses and subnets className: 'number', variants: [ {begin: IPADDR_wBITMASK+'(,'+IPADDR_wBITMASK+')*'}, //192.168.0.0/24,1.2.3.0/24 {begin: IPADDR+'-'+IPADDR}, // 192.168.0.1-192.168.0.3 {begin: IPADDR+'(,'+IPADDR+')*'}, // 192.168.0.1,192.168.0.34,192.168.24.1,192.168.0.1 ] }, // */ /*{ // MAC addresses and DHCP Client IDs className: 'number', begin: /\b(1:)?([0-9A-Fa-f]{1,2}[:-]){5}([0-9A-Fa-f]){1,2}\b/, }, //*/ { // Не форматировать не классифицированные значения. Необходимо для исключения подсветки значений как built_in. // className: 'number', begin: /("[^"]*"|[^\s\{\}\[\]]+)/, }, //*/ ] } //*/ ] },//*/ { // HEX values className: 'number', begin: /\*[0-9a-fA-F]+/, }, //*/ { begin: '\\b(' + COMMON_COMMANDS.split(' ').join('|') + ')([\\s\[\(]|\])', returnBegin: true, contains: [ { className: 'builtin-name', //'function', begin: /\w+/, }, ], }, { className: 'built_in', variants: [ {begin: '(\\.\\./|/|\\s)((' + OBJECTS.split(' ').join('|') + ');?\\s)+',relevance: 10,}, {begin: /\.\./,}, ], },//*/ ] }; } },{name:"rsl",create:/* Language: RenderMan RSL Author: Konstantin Evdokimenko Contributors: Shuen-Huei Guan Category: graphics */ function(hljs) { return { keywords: { keyword: 'float color point normal vector matrix while for if do return else break extern continue', built_in: 'abs acos ambient area asin atan atmosphere attribute calculatenormal ceil cellnoise ' + 'clamp comp concat cos degrees depth Deriv diffuse distance Du Dv environment exp ' + 'faceforward filterstep floor format fresnel incident length lightsource log match ' + 'max min mod noise normalize ntransform opposite option phong pnoise pow printf ' + 'ptlined radians random reflect refract renderinfo round setcomp setxcomp setycomp ' + 'setzcomp shadow sign sin smoothstep specular specularbrdf spline sqrt step tan ' + 'texture textureinfo trace transform vtransform xcomp ycomp zcomp' }, illegal: ' Contributors: Peter Leonov , Vasily Polovnyov , Loren Segal , Pascal Hurni , Cedric Sohrauer Category: common */ function(hljs) { var RUBY_METHOD_RE = '[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?'; var RUBY_KEYWORDS = { keyword: 'and then defined module in return redo if BEGIN retry end for self when ' + 'next until do begin unless END rescue else break undef not super class case ' + 'require yield alias while ensure elsif or include attr_reader attr_writer attr_accessor', literal: 'true false nil' }; var YARDOCTAG = { className: 'doctag', begin: '@[A-Za-z]+' }; var IRB_OBJECT = { begin: '#<', end: '>' }; var COMMENT_MODES = [ hljs.COMMENT( '#', '$', { contains: [YARDOCTAG] } ), hljs.COMMENT( '^\\=begin', '^\\=end', { contains: [YARDOCTAG], relevance: 10 } ), hljs.COMMENT('^__END__', '\\n$') ]; var SUBST = { className: 'subst', begin: '#\\{', end: '}', keywords: RUBY_KEYWORDS }; var STRING = { className: 'string', contains: [hljs.BACKSLASH_ESCAPE, SUBST], variants: [ {begin: /'/, end: /'/}, {begin: /"/, end: /"/}, {begin: /`/, end: /`/}, {begin: '%[qQwWx]?\\(', end: '\\)'}, {begin: '%[qQwWx]?\\[', end: '\\]'}, {begin: '%[qQwWx]?{', end: '}'}, {begin: '%[qQwWx]?<', end: '>'}, {begin: '%[qQwWx]?/', end: '/'}, {begin: '%[qQwWx]?%', end: '%'}, {begin: '%[qQwWx]?-', end: '-'}, {begin: '%[qQwWx]?\\|', end: '\\|'}, { // \B in the beginning suppresses recognition of ?-sequences where ? // is the last character of a preceding identifier, as in: `func?4` begin: /\B\?(\\\d{1,3}|\\x[A-Fa-f0-9]{1,2}|\\u[A-Fa-f0-9]{4}|\\?\S)\b/ }, { // heredocs begin: /<<[-~]?'?(\w+)(?:.|\n)*?\n\s*\1\b/, returnBegin: true, contains: [ { begin: /<<[-~]?'?/ }, { begin: /\w+/, endSameAsBegin: true, contains: [hljs.BACKSLASH_ESCAPE, SUBST], } ] } ] }; var PARAMS = { className: 'params', begin: '\\(', end: '\\)', endsParent: true, keywords: RUBY_KEYWORDS }; var RUBY_DEFAULT_CONTAINS = [ STRING, IRB_OBJECT, { className: 'class', beginKeywords: 'class module', end: '$|;', illegal: /=/, contains: [ hljs.inherit(hljs.TITLE_MODE, {begin: '[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?'}), { begin: '<\\s*', contains: [{ begin: '(' + hljs.IDENT_RE + '::)?' + hljs.IDENT_RE }] } ].concat(COMMENT_MODES) }, { className: 'function', beginKeywords: 'def', end: '$|;', contains: [ hljs.inherit(hljs.TITLE_MODE, {begin: RUBY_METHOD_RE}), PARAMS ].concat(COMMENT_MODES) }, { // swallow namespace qualifiers before symbols begin: hljs.IDENT_RE + '::' }, { className: 'symbol', begin: hljs.UNDERSCORE_IDENT_RE + '(\\!|\\?)?:', relevance: 0 }, { className: 'symbol', begin: ':(?!\\s)', contains: [STRING, {begin: RUBY_METHOD_RE}], relevance: 0 }, { className: 'number', begin: '(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b', relevance: 0 }, { begin: '(\\$\\W)|((\\$|\\@\\@?)(\\w+))' // variables }, { className: 'params', begin: /\|/, end: /\|/, keywords: RUBY_KEYWORDS }, { // regexp container begin: '(' + hljs.RE_STARTERS_RE + '|unless)\\s*', keywords: 'unless', contains: [ IRB_OBJECT, { className: 'regexp', contains: [hljs.BACKSLASH_ESCAPE, SUBST], illegal: /\n/, variants: [ {begin: '/', end: '/[a-z]*'}, {begin: '%r{', end: '}[a-z]*'}, {begin: '%r\\(', end: '\\)[a-z]*'}, {begin: '%r!', end: '![a-z]*'}, {begin: '%r\\[', end: '\\][a-z]*'} ] } ].concat(COMMENT_MODES), relevance: 0 } ].concat(COMMENT_MODES); SUBST.contains = RUBY_DEFAULT_CONTAINS; PARAMS.contains = RUBY_DEFAULT_CONTAINS; var SIMPLE_PROMPT = "[>?]>"; var DEFAULT_PROMPT = "[\\w#]+\\(\\w+\\):\\d+:\\d+>"; var RVM_PROMPT = "(\\w+-)?\\d+\\.\\d+\\.\\d(p\\d+)?[^>]+>"; var IRB_DEFAULT = [ { begin: /^\s*=>/, starts: { end: '$', contains: RUBY_DEFAULT_CONTAINS } }, { className: 'meta', begin: '^('+SIMPLE_PROMPT+"|"+DEFAULT_PROMPT+'|'+RVM_PROMPT+')', starts: { end: '$', contains: RUBY_DEFAULT_CONTAINS } } ]; return { aliases: ['rb', 'gemspec', 'podspec', 'thor', 'irb'], keywords: RUBY_KEYWORDS, illegal: /\/\*/, contains: COMMENT_MODES.concat(IRB_DEFAULT).concat(RUBY_DEFAULT_CONTAINS) }; } },{name:"ruleslanguage",create:/* Language: Oracle Rules Language Author: Jason Jacobson Description: The Oracle Utilities Rules Language is used to program the Oracle Utilities Applications acquired from LODESTAR Corporation. The products include Billing Component, LPSS, Pricing Component etc. through version 1.6.1. Category: enterprise */ function(hljs) { return { keywords: { keyword: 'BILL_PERIOD BILL_START BILL_STOP RS_EFFECTIVE_START RS_EFFECTIVE_STOP RS_JURIS_CODE RS_OPCO_CODE ' + 'INTDADDATTRIBUTE|5 INTDADDVMSG|5 INTDBLOCKOP|5 INTDBLOCKOPNA|5 INTDCLOSE|5 INTDCOUNT|5 ' + 'INTDCOUNTSTATUSCODE|5 INTDCREATEMASK|5 INTDCREATEDAYMASK|5 INTDCREATEFACTORMASK|5 ' + 'INTDCREATEHANDLE|5 INTDCREATEOVERRIDEDAYMASK|5 INTDCREATEOVERRIDEMASK|5 ' + 'INTDCREATESTATUSCODEMASK|5 INTDCREATETOUPERIOD|5 INTDDELETE|5 INTDDIPTEST|5 INTDEXPORT|5 ' + 'INTDGETERRORCODE|5 INTDGETERRORMESSAGE|5 INTDISEQUAL|5 INTDJOIN|5 INTDLOAD|5 INTDLOADACTUALCUT|5 ' + 'INTDLOADDATES|5 INTDLOADHIST|5 INTDLOADLIST|5 INTDLOADLISTDATES|5 INTDLOADLISTENERGY|5 ' + 'INTDLOADLISTHIST|5 INTDLOADRELATEDCHANNEL|5 INTDLOADSP|5 INTDLOADSTAGING|5 INTDLOADUOM|5 ' + 'INTDLOADUOMDATES|5 INTDLOADUOMHIST|5 INTDLOADVERSION|5 INTDOPEN|5 INTDREADFIRST|5 INTDREADNEXT|5 ' + 'INTDRECCOUNT|5 INTDRELEASE|5 INTDREPLACE|5 INTDROLLAVG|5 INTDROLLPEAK|5 INTDSCALAROP|5 INTDSCALE|5 ' + 'INTDSETATTRIBUTE|5 INTDSETDSTPARTICIPANT|5 INTDSETSTRING|5 INTDSETVALUE|5 INTDSETVALUESTATUS|5 ' + 'INTDSHIFTSTARTTIME|5 INTDSMOOTH|5 INTDSORT|5 INTDSPIKETEST|5 INTDSUBSET|5 INTDTOU|5 ' + 'INTDTOURELEASE|5 INTDTOUVALUE|5 INTDUPDATESTATS|5 INTDVALUE|5 STDEV INTDDELETEEX|5 ' + 'INTDLOADEXACTUAL|5 INTDLOADEXCUT|5 INTDLOADEXDATES|5 INTDLOADEX|5 INTDLOADEXRELATEDCHANNEL|5 ' + 'INTDSAVEEX|5 MVLOAD|5 MVLOADACCT|5 MVLOADACCTDATES|5 MVLOADACCTHIST|5 MVLOADDATES|5 MVLOADHIST|5 ' + 'MVLOADLIST|5 MVLOADLISTDATES|5 MVLOADLISTHIST|5 IF FOR NEXT DONE SELECT END CALL ABORT CLEAR CHANNEL FACTOR LIST NUMBER ' + 'OVERRIDE SET WEEK DISTRIBUTIONNODE ELSE WHEN THEN OTHERWISE IENUM CSV INCLUDE LEAVE RIDER SAVE DELETE ' + 'NOVALUE SECTION WARN SAVE_UPDATE DETERMINANT LABEL REPORT REVENUE EACH ' + 'IN FROM TOTAL CHARGE BLOCK AND OR CSV_FILE RATE_CODE AUXILIARY_DEMAND ' + 'UIDACCOUNT RS BILL_PERIOD_SELECT HOURS_PER_MONTH INTD_ERROR_STOP SEASON_SCHEDULE_NAME ' + 'ACCOUNTFACTOR ARRAYUPPERBOUND CALLSTOREDPROC GETADOCONNECTION GETCONNECT GETDATASOURCE ' + 'GETQUALIFIER GETUSERID HASVALUE LISTCOUNT LISTOP LISTUPDATE LISTVALUE PRORATEFACTOR RSPRORATE ' + 'SETBINPATH SETDBMONITOR WQ_OPEN BILLINGHOURS DATE DATEFROMFLOAT DATETIMEFROMSTRING ' + 'DATETIMETOSTRING DATETOFLOAT DAY DAYDIFF DAYNAME DBDATETIME HOUR MINUTE MONTH MONTHDIFF ' + 'MONTHHOURS MONTHNAME ROUNDDATE SAMEWEEKDAYLASTYEAR SECOND WEEKDAY WEEKDIFF YEAR YEARDAY ' + 'YEARSTR COMPSUM HISTCOUNT HISTMAX HISTMIN HISTMINNZ HISTVALUE MAXNRANGE MAXRANGE MINRANGE ' + 'COMPIKVA COMPKVA COMPKVARFROMKQKW COMPLF IDATTR FLAG LF2KW LF2KWH MAXKW POWERFACTOR ' + 'READING2USAGE AVGSEASON MAXSEASON MONTHLYMERGE SEASONVALUE SUMSEASON ACCTREADDATES ' + 'ACCTTABLELOAD CONFIGADD CONFIGGET CREATEOBJECT CREATEREPORT EMAILCLIENT EXPBLKMDMUSAGE ' + 'EXPMDMUSAGE EXPORT_USAGE FACTORINEFFECT GETUSERSPECIFIEDSTOP INEFFECT ISHOLIDAY RUNRATE ' + 'SAVE_PROFILE SETREPORTTITLE USEREXIT WATFORRUNRATE TO TABLE ACOS ASIN ATAN ATAN2 BITAND CEIL ' + 'COS COSECANT COSH COTANGENT DIVQUOT DIVREM EXP FABS FLOOR FMOD FREPM FREXPN LOG LOG10 MAX MAXN ' + 'MIN MINNZ MODF POW ROUND ROUND2VALUE ROUNDINT SECANT SIN SINH SQROOT TAN TANH FLOAT2STRING ' + 'FLOAT2STRINGNC INSTR LEFT LEN LTRIM MID RIGHT RTRIM STRING STRINGNC TOLOWER TOUPPER TRIM ' + 'NUMDAYS READ_DATE STAGING', built_in: 'IDENTIFIER OPTIONS XML_ELEMENT XML_OP XML_ELEMENT_OF DOMDOCCREATE DOMDOCLOADFILE DOMDOCLOADXML ' + 'DOMDOCSAVEFILE DOMDOCGETROOT DOMDOCADDPI DOMNODEGETNAME DOMNODEGETTYPE DOMNODEGETVALUE DOMNODEGETCHILDCT ' + 'DOMNODEGETFIRSTCHILD DOMNODEGETSIBLING DOMNODECREATECHILDELEMENT DOMNODESETATTRIBUTE ' + 'DOMNODEGETCHILDELEMENTCT DOMNODEGETFIRSTCHILDELEMENT DOMNODEGETSIBLINGELEMENT DOMNODEGETATTRIBUTECT ' + 'DOMNODEGETATTRIBUTEI DOMNODEGETATTRIBUTEBYNAME DOMNODEGETBYNAME' }, contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, hljs.C_NUMBER_MODE, { className: 'literal', variants: [ {begin: '#\\s+[a-zA-Z\\ \\.]*', relevance: 0}, // looks like #-comment {begin: '#[a-zA-Z\\ \\.]+'} ] } ] }; } },{name:"rust",create:/* Language: Rust Author: Andrey Vlasovskikh Contributors: Roman Shmatov , Kasper Andersen Category: system */ function(hljs) { var NUM_SUFFIX = '([ui](8|16|32|64|128|size)|f(32|64))\?'; var KEYWORDS = 'abstract as async await become box break const continue crate do dyn ' + 'else enum extern false final fn for if impl in let loop macro match mod ' + 'move mut override priv pub ref return self Self static struct super ' + 'trait true try type typeof unsafe unsized use virtual where while yield'; var BUILTINS = // functions 'drop ' + // types 'i8 i16 i32 i64 i128 isize ' + 'u8 u16 u32 u64 u128 usize ' + 'f32 f64 ' + 'str char bool ' + 'Box Option Result String Vec ' + // traits 'Copy Send Sized Sync Drop Fn FnMut FnOnce ToOwned Clone Debug ' + 'PartialEq PartialOrd Eq Ord AsRef AsMut Into From Default Iterator ' + 'Extend IntoIterator DoubleEndedIterator ExactSizeIterator ' + 'SliceConcatExt ToString ' + // macros 'assert! assert_eq! bitflags! bytes! cfg! col! concat! concat_idents! ' + 'debug_assert! debug_assert_eq! env! panic! file! format! format_args! ' + 'include_bin! include_str! line! local_data_key! module_path! ' + 'option_env! print! println! select! stringify! try! unimplemented! ' + 'unreachable! vec! write! writeln! macro_rules! assert_ne! debug_assert_ne!'; return { aliases: ['rs'], keywords: { keyword: KEYWORDS, literal: 'true false Some None Ok Err', built_in: BUILTINS }, lexemes: hljs.IDENT_RE + '!?', illegal: '' } ] }; } },{name:"sas",create:/* Language: SAS Author: Mauricio Caceres Description: Syntax Highlighting for SAS */ function(hljs) { // Data step and PROC SQL statements var SAS_KEYWORDS = ''+ 'do if then else end until while '+ ''+ 'abort array attrib by call cards cards4 catname continue '+ 'datalines datalines4 delete delim delimiter display dm drop '+ 'endsas error file filename footnote format goto in infile '+ 'informat input keep label leave length libname link list '+ 'lostcard merge missing modify options output out page put '+ 'redirect remove rename replace retain return select set skip '+ 'startsas stop title update waitsas where window x systask '+ ''+ 'add and alter as cascade check create delete describe '+ 'distinct drop foreign from group having index insert into in '+ 'key like message modify msgtype not null on or order primary '+ 'references reset restrict select set table unique update '+ 'validate view where'; // Built-in SAS functions var SAS_FUN = ''+ 'abs|addr|airy|arcos|arsin|atan|attrc|attrn|band|'+ 'betainv|blshift|bnot|bor|brshift|bxor|byte|cdf|ceil|'+ 'cexist|cinv|close|cnonct|collate|compbl|compound|'+ 'compress|cos|cosh|css|curobs|cv|daccdb|daccdbsl|'+ 'daccsl|daccsyd|dacctab|dairy|date|datejul|datepart|'+ 'datetime|day|dclose|depdb|depdbsl|depdbsl|depsl|'+ 'depsl|depsyd|depsyd|deptab|deptab|dequote|dhms|dif|'+ 'digamma|dim|dinfo|dnum|dopen|doptname|doptnum|dread|'+ 'dropnote|dsname|erf|erfc|exist|exp|fappend|fclose|'+ 'fcol|fdelete|fetch|fetchobs|fexist|fget|fileexist|'+ 'filename|fileref|finfo|finv|fipname|fipnamel|'+ 'fipstate|floor|fnonct|fnote|fopen|foptname|foptnum|'+ 'fpoint|fpos|fput|fread|frewind|frlen|fsep|fuzz|'+ 'fwrite|gaminv|gamma|getoption|getvarc|getvarn|hbound|'+ 'hms|hosthelp|hour|ibessel|index|indexc|indexw|input|'+ 'inputc|inputn|int|intck|intnx|intrr|irr|jbessel|'+ 'juldate|kurtosis|lag|lbound|left|length|lgamma|'+ 'libname|libref|log|log10|log2|logpdf|logpmf|logsdf|'+ 'lowcase|max|mdy|mean|min|minute|mod|month|mopen|'+ 'mort|n|netpv|nmiss|normal|note|npv|open|ordinal|'+ 'pathname|pdf|peek|peekc|pmf|point|poisson|poke|'+ 'probbeta|probbnml|probchi|probf|probgam|probhypr|'+ 'probit|probnegb|probnorm|probt|put|putc|putn|qtr|'+ 'quote|ranbin|rancau|ranexp|rangam|range|rank|rannor|'+ 'ranpoi|rantbl|rantri|ranuni|repeat|resolve|reverse|'+ 'rewind|right|round|saving|scan|sdf|second|sign|'+ 'sin|sinh|skewness|soundex|spedis|sqrt|std|stderr|'+ 'stfips|stname|stnamel|substr|sum|symget|sysget|'+ 'sysmsg|sysprod|sysrc|system|tan|tanh|time|timepart|'+ 'tinv|tnonct|today|translate|tranwrd|trigamma|'+ 'trim|trimn|trunc|uniform|upcase|uss|var|varfmt|'+ 'varinfmt|varlabel|varlen|varname|varnum|varray|'+ 'varrayx|vartype|verify|vformat|vformatd|vformatdx|'+ 'vformatn|vformatnx|vformatw|vformatwx|vformatx|'+ 'vinarray|vinarrayx|vinformat|vinformatd|vinformatdx|'+ 'vinformatn|vinformatnx|vinformatw|vinformatwx|'+ 'vinformatx|vlabel|vlabelx|vlength|vlengthx|vname|'+ 'vnamex|vtype|vtypex|weekday|year|yyq|zipfips|zipname|'+ 'zipnamel|zipstate'; // Built-in macro functions var SAS_MACRO_FUN = 'bquote|nrbquote|cmpres|qcmpres|compstor|'+ 'datatyp|display|do|else|end|eval|global|goto|'+ 'if|index|input|keydef|label|left|length|let|'+ 'local|lowcase|macro|mend|nrbquote|nrquote|'+ 'nrstr|put|qcmpres|qleft|qlowcase|qscan|'+ 'qsubstr|qsysfunc|qtrim|quote|qupcase|scan|str|'+ 'substr|superq|syscall|sysevalf|sysexec|sysfunc|'+ 'sysget|syslput|sysprod|sysrc|sysrput|then|to|'+ 'trim|unquote|until|upcase|verify|while|window'; return { aliases: ['sas', 'SAS'], case_insensitive: true, // SAS is case-insensitive keywords: { literal: 'null missing _all_ _automatic_ _character_ _infile_ '+ '_n_ _name_ _null_ _numeric_ _user_ _webout_', meta: SAS_KEYWORDS }, contains: [ { // Distinct highlight for proc , data, run, quit className: 'keyword', begin: /^\s*(proc [\w\d_]+|data|run|quit)[\s\;]/ }, { // Macro variables className: 'variable', begin: /\&[a-zA-Z_\&][a-zA-Z0-9_]*\.?/ }, { // Special emphasis for datalines|cards className: 'emphasis', begin: /^\s*datalines|cards.*;/, end: /^\s*;\s*$/ }, { // Built-in macro variables take precedence className: 'built_in', begin: '%(' + SAS_MACRO_FUN + ')' }, { // User-defined macro functions highlighted after className: 'name', begin: /%[a-zA-Z_][a-zA-Z_0-9]*/ }, { className: 'meta', begin: '[^%](' + SAS_FUN + ')[\(]' }, { className: 'string', variants: [ hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE ] }, hljs.COMMENT('\\*', ';'), hljs.C_BLOCK_COMMENT_MODE ] }; } },{name:"scala",create:/* Language: Scala Category: functional Author: Jan Berkel Contributors: Erik Osheim */ function(hljs) { var ANNOTATION = { className: 'meta', begin: '@[A-Za-z]+' }; // used in strings for escaping/interpolation/substitution var SUBST = { className: 'subst', variants: [ {begin: '\\$[A-Za-z0-9_]+'}, {begin: '\\${', end: '}'} ] }; var STRING = { className: 'string', variants: [ { begin: '"', end: '"', illegal: '\\n', contains: [hljs.BACKSLASH_ESCAPE] }, { begin: '"""', end: '"""', relevance: 10 }, { begin: '[a-z]+"', end: '"', illegal: '\\n', contains: [hljs.BACKSLASH_ESCAPE, SUBST] }, { className: 'string', begin: '[a-z]+"""', end: '"""', contains: [SUBST], relevance: 10 } ] }; var SYMBOL = { className: 'symbol', begin: '\'\\w[\\w\\d_]*(?!\')' }; var TYPE = { className: 'type', begin: '\\b[A-Z][A-Za-z0-9_]*', relevance: 0 }; var NAME = { className: 'title', begin: /[^0-9\n\t "'(),.`{}\[\]:;][^\n\t "'(),.`{}\[\]:;]+|[^0-9\n\t "'(),.`{}\[\]:;=]/, relevance: 0 }; var CLASS = { className: 'class', beginKeywords: 'class object trait type', end: /[:={\[\n;]/, excludeEnd: true, contains: [ { beginKeywords: 'extends with', relevance: 10 }, { begin: /\[/, end: /\]/, excludeBegin: true, excludeEnd: true, relevance: 0, contains: [TYPE] }, { className: 'params', begin: /\(/, end: /\)/, excludeBegin: true, excludeEnd: true, relevance: 0, contains: [TYPE] }, NAME ] }; var METHOD = { className: 'function', beginKeywords: 'def', end: /[:={\[(\n;]/, excludeEnd: true, contains: [NAME] }; return { keywords: { literal: 'true false null', keyword: 'type yield lazy override def with val var sealed abstract private trait object if forSome for while throw finally protected extends import final return else break new catch super class case package default try this match continue throws implicit' }, contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, STRING, SYMBOL, TYPE, METHOD, CLASS, hljs.C_NUMBER_MODE, ANNOTATION ] }; } },{name:"scheme",create:/* Language: Scheme Description: Keywords based on http://community.schemewiki.org/?scheme-keywords Author: JP Verkamp Contributors: Ivan Sagalaev Origin: clojure.js Category: lisp */ function(hljs) { var SCHEME_IDENT_RE = '[^\\(\\)\\[\\]\\{\\}",\'`;#|\\\\\\s]+'; var SCHEME_SIMPLE_NUMBER_RE = '(\\-|\\+)?\\d+([./]\\d+)?'; var SCHEME_COMPLEX_NUMBER_RE = SCHEME_SIMPLE_NUMBER_RE + '[+\\-]' + SCHEME_SIMPLE_NUMBER_RE + 'i'; var BUILTINS = { 'builtin-name': 'case-lambda call/cc class define-class exit-handler field import ' + 'inherit init-field interface let*-values let-values let/ec mixin ' + 'opt-lambda override protect provide public rename require ' + 'require-for-syntax syntax syntax-case syntax-error unit/sig unless ' + 'when with-syntax and begin call-with-current-continuation ' + 'call-with-input-file call-with-output-file case cond define ' + 'define-syntax delay do dynamic-wind else for-each if lambda let let* ' + 'let-syntax letrec letrec-syntax map or syntax-rules \' * + , ,@ - ... / ' + '; < <= = => > >= ` abs acos angle append apply asin assoc assq assv atan ' + 'boolean? caar cadr call-with-input-file call-with-output-file ' + 'call-with-values car cdddar cddddr cdr ceiling char->integer ' + 'char-alphabetic? char-ci<=? char-ci=? char-ci>? ' + 'char-downcase char-lower-case? char-numeric? char-ready? char-upcase ' + 'char-upper-case? char-whitespace? char<=? char=? char>? ' + 'char? close-input-port close-output-port complex? cons cos ' + 'current-input-port current-output-port denominator display eof-object? ' + 'eq? equal? eqv? eval even? exact->inexact exact? exp expt floor ' + 'force gcd imag-part inexact->exact inexact? input-port? integer->char ' + 'integer? interaction-environment lcm length list list->string ' + 'list->vector list-ref list-tail list? load log magnitude make-polar ' + 'make-rectangular make-string make-vector max member memq memv min ' + 'modulo negative? newline not null-environment null? number->string ' + 'number? numerator odd? open-input-file open-output-file output-port? ' + 'pair? peek-char port? positive? procedure? quasiquote quote quotient ' + 'rational? rationalize read read-char real-part real? remainder reverse ' + 'round scheme-report-environment set! set-car! set-cdr! sin sqrt string ' + 'string->list string->number string->symbol string-append string-ci<=? ' + 'string-ci=? string-ci>? string-copy ' + 'string-fill! string-length string-ref string-set! string<=? string=? string>? string? substring symbol->string symbol? ' + 'tan transcript-off transcript-on truncate values vector ' + 'vector->list vector-fill! vector-length vector-ref vector-set! ' + 'with-input-from-file with-output-to-file write write-char zero?' }; var SHEBANG = { className: 'meta', begin: '^#!', end: '$' }; var LITERAL = { className: 'literal', begin: '(#t|#f|#\\\\' + SCHEME_IDENT_RE + '|#\\\\.)' }; var NUMBER = { className: 'number', variants: [ { begin: SCHEME_SIMPLE_NUMBER_RE, relevance: 0 }, { begin: SCHEME_COMPLEX_NUMBER_RE, relevance: 0 }, { begin: '#b[0-1]+(/[0-1]+)?' }, { begin: '#o[0-7]+(/[0-7]+)?' }, { begin: '#x[0-9a-f]+(/[0-9a-f]+)?' } ] }; var STRING = hljs.QUOTE_STRING_MODE; var REGULAR_EXPRESSION = { className: 'regexp', begin: '#[pr]x"', end: '[^\\\\]"' }; var COMMENT_MODES = [ hljs.COMMENT( ';', '$', { relevance: 0 } ), hljs.COMMENT('#\\|', '\\|#') ]; var IDENT = { begin: SCHEME_IDENT_RE, relevance: 0 }; var QUOTED_IDENT = { className: 'symbol', begin: '\'' + SCHEME_IDENT_RE }; var BODY = { endsWithParent: true, relevance: 0 }; var QUOTED_LIST = { variants: [ { begin: /'/ }, { begin: '`' } ], contains: [ { begin: '\\(', end: '\\)', contains: ['self', LITERAL, STRING, NUMBER, IDENT, QUOTED_IDENT] } ] }; var NAME = { className: 'name', begin: SCHEME_IDENT_RE, lexemes: SCHEME_IDENT_RE, keywords: BUILTINS }; var LAMBDA = { begin: /lambda/, endsWithParent: true, returnBegin: true, contains: [ NAME, { begin: /\(/, end: /\)/, endsParent: true, contains: [IDENT], } ] }; var LIST = { variants: [ { begin: '\\(', end: '\\)' }, { begin: '\\[', end: '\\]' } ], contains: [LAMBDA, NAME, BODY] }; BODY.contains = [LITERAL, NUMBER, STRING, IDENT, QUOTED_IDENT, QUOTED_LIST, LIST].concat(COMMENT_MODES); return { illegal: /\S/, contains: [SHEBANG, NUMBER, STRING, QUOTED_IDENT, QUOTED_LIST, LIST].concat(COMMENT_MODES) }; } },{name:"scilab",create:/* Language: Scilab Author: Sylvestre Ledru Origin: matlab.js Description: Scilab is a port from Matlab Category: scientific */ function(hljs) { var COMMON_CONTAINS = [ hljs.C_NUMBER_MODE, { className: 'string', begin: '\'|\"', end: '\'|\"', contains: [hljs.BACKSLASH_ESCAPE, {begin: '\'\''}] } ]; return { aliases: ['sci'], lexemes: /%?\w+/, keywords: { keyword: 'abort break case clear catch continue do elseif else endfunction end for function '+ 'global if pause return resume select try then while', literal: '%f %F %t %T %pi %eps %inf %nan %e %i %z %s', built_in: // Scilab has more than 2000 functions. Just list the most commons 'abs and acos asin atan ceil cd chdir clearglobal cosh cos cumprod deff disp error '+ 'exec execstr exists exp eye gettext floor fprintf fread fsolve imag isdef isempty '+ 'isinfisnan isvector lasterror length load linspace list listfiles log10 log2 log '+ 'max min msprintf mclose mopen ones or pathconvert poly printf prod pwd rand real '+ 'round sinh sin size gsort sprintf sqrt strcat strcmps tring sum system tanh tan '+ 'type typename warning zeros matrix' }, illegal: '("|#|/\\*|\\s+/\\w+)', contains: [ { className: 'function', beginKeywords: 'function', end: '$', contains: [ hljs.UNDERSCORE_TITLE_MODE, { className: 'params', begin: '\\(', end: '\\)' } ] }, { begin: '[a-zA-Z_][a-zA-Z_0-9]*(\'+[\\.\']*|[\\.\']+)', end: '', relevance: 0 }, { begin: '\\[', end: '\\]\'*[\\.\']*', relevance: 0, contains: COMMON_CONTAINS }, hljs.COMMENT('//', '$') ].concat(COMMON_CONTAINS) }; } },{name:"scss",create:/* Language: SCSS Author: Kurt Emch Category: css */ function(hljs) { var IDENT_RE = '[a-zA-Z-][a-zA-Z0-9_-]*'; var VARIABLE = { className: 'variable', begin: '(\\$' + IDENT_RE + ')\\b' }; var HEXCOLOR = { className: 'number', begin: '#[0-9A-Fa-f]+' }; var DEF_INTERNALS = { className: 'attribute', begin: '[A-Z\\_\\.\\-]+', end: ':', excludeEnd: true, illegal: '[^\\s]', starts: { endsWithParent: true, excludeEnd: true, contains: [ HEXCOLOR, hljs.CSS_NUMBER_MODE, hljs.QUOTE_STRING_MODE, hljs.APOS_STRING_MODE, hljs.C_BLOCK_COMMENT_MODE, { className: 'meta', begin: '!important' } ] } }; return { case_insensitive: true, illegal: '[=/|\']', contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, { className: 'selector-id', begin: '\\#[A-Za-z0-9_-]+', relevance: 0 }, { className: 'selector-class', begin: '\\.[A-Za-z0-9_-]+', relevance: 0 }, { className: 'selector-attr', begin: '\\[', end: '\\]', illegal: '$' }, { className: 'selector-tag', // begin: IDENT_RE, end: '[,|\\s]' begin: '\\b(a|abbr|acronym|address|area|article|aside|audio|b|base|big|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|command|datalist|dd|del|details|dfn|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|frame|frameset|(h[1-6])|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|map|mark|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|p|param|pre|progress|q|rp|rt|ruby|samp|script|section|select|small|span|strike|strong|style|sub|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|tt|ul|var|video)\\b', relevance: 0 }, { begin: ':(visited|valid|root|right|required|read-write|read-only|out-range|optional|only-of-type|only-child|nth-of-type|nth-last-of-type|nth-last-child|nth-child|not|link|left|last-of-type|last-child|lang|invalid|indeterminate|in-range|hover|focus|first-of-type|first-line|first-letter|first-child|first|enabled|empty|disabled|default|checked|before|after|active)' }, { begin: '::(after|before|choices|first-letter|first-line|repeat-index|repeat-item|selection|value)' }, VARIABLE, { className: 'attribute', begin: '\\b(z-index|word-wrap|word-spacing|word-break|width|widows|white-space|visibility|vertical-align|unicode-bidi|transition-timing-function|transition-property|transition-duration|transition-delay|transition|transform-style|transform-origin|transform|top|text-underline-position|text-transform|text-shadow|text-rendering|text-overflow|text-indent|text-decoration-style|text-decoration-line|text-decoration-color|text-decoration|text-align-last|text-align|tab-size|table-layout|right|resize|quotes|position|pointer-events|perspective-origin|perspective|page-break-inside|page-break-before|page-break-after|padding-top|padding-right|padding-left|padding-bottom|padding|overflow-y|overflow-x|overflow-wrap|overflow|outline-width|outline-style|outline-offset|outline-color|outline|orphans|order|opacity|object-position|object-fit|normal|none|nav-up|nav-right|nav-left|nav-index|nav-down|min-width|min-height|max-width|max-height|mask|marks|margin-top|margin-right|margin-left|margin-bottom|margin|list-style-type|list-style-position|list-style-image|list-style|line-height|letter-spacing|left|justify-content|initial|inherit|ime-mode|image-orientation|image-resolution|image-rendering|icon|hyphens|height|font-weight|font-variant-ligatures|font-variant|font-style|font-stretch|font-size-adjust|font-size|font-language-override|font-kerning|font-feature-settings|font-family|font|float|flex-wrap|flex-shrink|flex-grow|flex-flow|flex-direction|flex-basis|flex|filter|empty-cells|display|direction|cursor|counter-reset|counter-increment|content|column-width|column-span|column-rule-width|column-rule-style|column-rule-color|column-rule|column-gap|column-fill|column-count|columns|color|clip-path|clip|clear|caption-side|break-inside|break-before|break-after|box-sizing|box-shadow|box-decoration-break|bottom|border-width|border-top-width|border-top-style|border-top-right-radius|border-top-left-radius|border-top-color|border-top|border-style|border-spacing|border-right-width|border-right-style|border-right-color|border-right|border-radius|border-left-width|border-left-style|border-left-color|border-left|border-image-width|border-image-source|border-image-slice|border-image-repeat|border-image-outset|border-image|border-color|border-collapse|border-bottom-width|border-bottom-style|border-bottom-right-radius|border-bottom-left-radius|border-bottom-color|border-bottom|border|background-size|background-repeat|background-position|background-origin|background-image|background-color|background-clip|background-attachment|background-blend-mode|background|backface-visibility|auto|animation-timing-function|animation-play-state|animation-name|animation-iteration-count|animation-fill-mode|animation-duration|animation-direction|animation-delay|animation|align-self|align-items|align-content)\\b', illegal: '[^\\s]' }, { begin: '\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b' }, { begin: ':', end: ';', contains: [ VARIABLE, HEXCOLOR, hljs.CSS_NUMBER_MODE, hljs.QUOTE_STRING_MODE, hljs.APOS_STRING_MODE, { className: 'meta', begin: '!important' } ] }, { begin: '@', end: '[{;]', keywords: 'mixin include extend for if else each while charset import debug media page content font-face namespace warn', contains: [ VARIABLE, hljs.QUOTE_STRING_MODE, hljs.APOS_STRING_MODE, HEXCOLOR, hljs.CSS_NUMBER_MODE, { begin: '\\s[A-Za-z0-9_.-]+', relevance: 0 } ] } ] }; } },{name:"shell",create:/* Language: Shell Session Requires: bash.js Author: TSUYUSATO Kitsune Category: common */ function(hljs) { return { aliases: ['console'], contains: [ { className: 'meta', begin: '^\\s{0,3}[\\w\\d\\[\\]()@-]*[>%$#]', starts: { end: '$', subLanguage: 'bash' } } ] } } },{name:"smali",create:/* Language: Smali Author: Dennis Titze Description: Basic Smali highlighting */ function(hljs) { var smali_instr_low_prio = ['add', 'and', 'cmp', 'cmpg', 'cmpl', 'const', 'div', 'double', 'float', 'goto', 'if', 'int', 'long', 'move', 'mul', 'neg', 'new', 'nop', 'not', 'or', 'rem', 'return', 'shl', 'shr', 'sput', 'sub', 'throw', 'ushr', 'xor']; var smali_instr_high_prio = ['aget', 'aput', 'array', 'check', 'execute', 'fill', 'filled', 'goto/16', 'goto/32', 'iget', 'instance', 'invoke', 'iput', 'monitor', 'packed', 'sget', 'sparse']; var smali_keywords = ['transient', 'constructor', 'abstract', 'final', 'synthetic', 'public', 'private', 'protected', 'static', 'bridge', 'system']; return { aliases: ['smali'], contains: [ { className: 'string', begin: '"', end: '"', relevance: 0 }, hljs.COMMENT( '#', '$', { relevance: 0 } ), { className: 'keyword', variants: [ {begin: '\\s*\\.end\\s[a-zA-Z0-9]*'}, {begin: '^[ ]*\\.[a-zA-Z]*', relevance: 0}, {begin: '\\s:[a-zA-Z_0-9]*', relevance: 0}, {begin: '\\s(' + smali_keywords.join('|') + ')'} ] }, { className: 'built_in', variants : [ { begin: '\\s('+smali_instr_low_prio.join('|')+')\\s' }, { begin: '\\s('+smali_instr_low_prio.join('|')+')((\\-|/)[a-zA-Z0-9]+)+\\s', relevance: 10 }, { begin: '\\s('+smali_instr_high_prio.join('|')+')((\\-|/)[a-zA-Z0-9]+)*\\s', relevance: 10 }, ] }, { className: 'class', begin: 'L[^\(;:\n]*;', relevance: 0 }, { begin: '[vp][0-9]+', } ] }; } },{name:"smalltalk",create:/* Language: Smalltalk Author: Vladimir Gubarkov */ function(hljs) { var VAR_IDENT_RE = '[a-z][a-zA-Z0-9_]*'; var CHAR = { className: 'string', begin: '\\$.{1}' }; var SYMBOL = { className: 'symbol', begin: '#' + hljs.UNDERSCORE_IDENT_RE }; return { aliases: ['st'], keywords: 'self super nil true false thisContext', // only 6 contains: [ hljs.COMMENT('"', '"'), hljs.APOS_STRING_MODE, { className: 'type', begin: '\\b[A-Z][A-Za-z0-9_]*', relevance: 0 }, { begin: VAR_IDENT_RE + ':', relevance: 0 }, hljs.C_NUMBER_MODE, SYMBOL, CHAR, { // This looks more complicated than needed to avoid combinatorial // explosion under V8. It effectively means `| var1 var2 ... |` with // whitespace adjacent to `|` being optional. begin: '\\|[ ]*' + VAR_IDENT_RE + '([ ]+' + VAR_IDENT_RE + ')*[ ]*\\|', returnBegin: true, end: /\|/, illegal: /\S/, contains: [{begin: '(\\|[ ]*)?' + VAR_IDENT_RE}] }, { begin: '\\#\\(', end: '\\)', contains: [ hljs.APOS_STRING_MODE, CHAR, hljs.C_NUMBER_MODE, SYMBOL ] } ] }; } },{name:"sml",create:/* Language: SML Author: Edwin Dalorzo Description: SML language definition. Origin: ocaml.js Category: functional */ function(hljs) { return { aliases: ['ml'], keywords: { keyword: /* according to Definition of Standard ML 97 */ 'abstype and andalso as case datatype do else end eqtype ' + 'exception fn fun functor handle if in include infix infixr ' + 'let local nonfix of op open orelse raise rec sharing sig ' + 'signature struct structure then type val with withtype where while', built_in: /* built-in types according to basis library */ 'array bool char exn int list option order real ref string substring vector unit word', literal: 'true false NONE SOME LESS EQUAL GREATER nil' }, illegal: /\/\/|>>/, lexemes: '[a-z_]\\w*!?', contains: [ { className: 'literal', begin: /\[(\|\|)?\]|\(\)/, relevance: 0 }, hljs.COMMENT( '\\(\\*', '\\*\\)', { contains: ['self'] } ), { /* type variable */ className: 'symbol', begin: '\'[A-Za-z_](?!\')[\\w\']*' /* the grammar is ambiguous on how 'a'b should be interpreted but not the compiler */ }, { /* polymorphic variant */ className: 'type', begin: '`[A-Z][\\w\']*' }, { /* module or constructor */ className: 'type', begin: '\\b[A-Z][\\w\']*', relevance: 0 }, { /* don't color identifiers, but safely catch all identifiers with '*/ begin: '[a-z_]\\w*\'[\\w\']*' }, hljs.inherit(hljs.APOS_STRING_MODE, {className: 'string', relevance: 0}), hljs.inherit(hljs.QUOTE_STRING_MODE, {illegal: null}), { className: 'number', begin: '\\b(0[xX][a-fA-F0-9_]+[Lln]?|' + '0[oO][0-7_]+[Lln]?|' + '0[bB][01_]+[Lln]?|' + '[0-9][0-9_]*([Lln]|(\\.[0-9_]*)?([eE][-+]?[0-9_]+)?)?)', relevance: 0 }, { begin: /[-=]>/ // relevance booster } ] }; } },{name:"sqf",create:/* Language: SQF Author: Søren Enevoldsen Contributors: Marvin Saignat , Dedmen Miller Description: Scripting language for the Arma game series Requires: cpp.js */ function(hljs) { var CPP = hljs.getLanguage('cpp').exports; // In SQF, a variable start with _ var VARIABLE = { className: 'variable', begin: /\b_+[a-zA-Z_]\w*/ }; // In SQF, a function should fit myTag_fnc_myFunction pattern // https://community.bistudio.com/wiki/Functions_Library_(Arma_3)#Adding_a_Function var FUNCTION = { className: 'title', begin: /[a-zA-Z][a-zA-Z0-9]+_fnc_\w*/ }; // In SQF strings, quotes matching the start are escaped by adding a consecutive. // Example of single escaped quotes: " "" " and ' '' '. var STRINGS = { className: 'string', variants: [ { begin: '"', end: '"', contains: [{begin: '""', relevance: 0}] }, { begin: '\'', end: '\'', contains: [{begin: '\'\'', relevance: 0}] } ] }; return { aliases: ['sqf'], case_insensitive: true, keywords: { keyword: 'case catch default do else exit exitWith for forEach from if ' + 'private switch then throw to try waitUntil while with', built_in: 'abs accTime acos action actionIDs actionKeys actionKeysImages actionKeysNames ' + 'actionKeysNamesArray actionName actionParams activateAddons activatedAddons activateKey ' + 'add3DENConnection add3DENEventHandler add3DENLayer addAction addBackpack addBackpackCargo ' + 'addBackpackCargoGlobal addBackpackGlobal addCamShake addCuratorAddons addCuratorCameraArea ' + 'addCuratorEditableObjects addCuratorEditingArea addCuratorPoints addEditorObject addEventHandler ' + 'addForce addGoggles addGroupIcon addHandgunItem addHeadgear addItem addItemCargo ' + 'addItemCargoGlobal addItemPool addItemToBackpack addItemToUniform addItemToVest addLiveStats ' + 'addMagazine addMagazineAmmoCargo addMagazineCargo addMagazineCargoGlobal addMagazineGlobal ' + 'addMagazinePool addMagazines addMagazineTurret addMenu addMenuItem addMissionEventHandler ' + 'addMPEventHandler addMusicEventHandler addOwnedMine addPlayerScores addPrimaryWeaponItem ' + 'addPublicVariableEventHandler addRating addResources addScore addScoreSide addSecondaryWeaponItem ' + 'addSwitchableUnit addTeamMember addToRemainsCollector addTorque addUniform addVehicle addVest ' + 'addWaypoint addWeapon addWeaponCargo addWeaponCargoGlobal addWeaponGlobal addWeaponItem ' + 'addWeaponPool addWeaponTurret admin agent agents AGLToASL aimedAtTarget aimPos airDensityRTD ' + 'airplaneThrottle airportSide AISFinishHeal alive all3DENEntities allAirports allControls ' + 'allCurators allCutLayers allDead allDeadMen allDisplays allGroups allMapMarkers allMines ' + 'allMissionObjects allow3DMode allowCrewInImmobile allowCuratorLogicIgnoreAreas allowDamage ' + 'allowDammage allowFileOperations allowFleeing allowGetIn allowSprint allPlayers allSimpleObjects ' + 'allSites allTurrets allUnits allUnitsUAV allVariables ammo ammoOnPylon and animate animateBay ' + 'animateDoor animatePylon animateSource animationNames animationPhase animationSourcePhase ' + 'animationState append apply armoryPoints arrayIntersect asin ASLToAGL ASLToATL assert ' + 'assignAsCargo assignAsCargoIndex assignAsCommander assignAsDriver assignAsGunner assignAsTurret ' + 'assignCurator assignedCargo assignedCommander assignedDriver assignedGunner assignedItems ' + 'assignedTarget assignedTeam assignedVehicle assignedVehicleRole assignItem assignTeam ' + 'assignToAirport atan atan2 atg ATLToASL attachedObject attachedObjects attachedTo attachObject ' + 'attachTo attackEnabled backpack backpackCargo backpackContainer backpackItems backpackMagazines ' + 'backpackSpaceFor behaviour benchmark binocular boundingBox boundingBoxReal boundingCenter ' + 'breakOut breakTo briefingName buildingExit buildingPos buttonAction buttonSetAction cadetMode ' + 'call callExtension camCommand camCommit camCommitPrepared camCommitted camConstuctionSetParams ' + 'camCreate camDestroy cameraEffect cameraEffectEnableHUD cameraInterest cameraOn cameraView ' + 'campaignConfigFile camPreload camPreloaded camPrepareBank camPrepareDir camPrepareDive ' + 'camPrepareFocus camPrepareFov camPrepareFovRange camPreparePos camPrepareRelPos camPrepareTarget ' + 'camSetBank camSetDir camSetDive camSetFocus camSetFov camSetFovRange camSetPos camSetRelPos ' + 'camSetTarget camTarget camUseNVG canAdd canAddItemToBackpack canAddItemToUniform canAddItemToVest ' + 'cancelSimpleTaskDestination canFire canMove canSlingLoad canStand canSuspend ' + 'canTriggerDynamicSimulation canUnloadInCombat canVehicleCargo captive captiveNum cbChecked ' + 'cbSetChecked ceil channelEnabled cheatsEnabled checkAIFeature checkVisibility className ' + 'clearAllItemsFromBackpack clearBackpackCargo clearBackpackCargoGlobal clearGroupIcons ' + 'clearItemCargo clearItemCargoGlobal clearItemPool clearMagazineCargo clearMagazineCargoGlobal ' + 'clearMagazinePool clearOverlay clearRadio clearWeaponCargo clearWeaponCargoGlobal clearWeaponPool ' + 'clientOwner closeDialog closeDisplay closeOverlay collapseObjectTree collect3DENHistory ' + 'collectiveRTD combatMode commandArtilleryFire commandChat commander commandFire commandFollow ' + 'commandFSM commandGetOut commandingMenu commandMove commandRadio commandStop ' + 'commandSuppressiveFire commandTarget commandWatch comment commitOverlay compile compileFinal ' + 'completedFSM composeText configClasses configFile configHierarchy configName configProperties ' + 'configSourceAddonList configSourceMod configSourceModList confirmSensorTarget ' + 'connectTerminalToUAV controlsGroupCtrl copyFromClipboard copyToClipboard copyWaypoints cos count ' + 'countEnemy countFriendly countSide countType countUnknown create3DENComposition create3DENEntity ' + 'createAgent createCenter createDialog createDiaryLink createDiaryRecord createDiarySubject ' + 'createDisplay createGearDialog createGroup createGuardedPoint createLocation createMarker ' + 'createMarkerLocal createMenu createMine createMissionDisplay createMPCampaignDisplay ' + 'createSimpleObject createSimpleTask createSite createSoundSource createTask createTeam ' + 'createTrigger createUnit createVehicle createVehicleCrew createVehicleLocal crew ctAddHeader ' + 'ctAddRow ctClear ctCurSel ctData ctFindHeaderRows ctFindRowHeader ctHeaderControls ctHeaderCount ' + 'ctRemoveHeaders ctRemoveRows ctrlActivate ctrlAddEventHandler ctrlAngle ctrlAutoScrollDelay ' + 'ctrlAutoScrollRewind ctrlAutoScrollSpeed ctrlChecked ctrlClassName ctrlCommit ctrlCommitted ' + 'ctrlCreate ctrlDelete ctrlEnable ctrlEnabled ctrlFade ctrlHTMLLoaded ctrlIDC ctrlIDD ' + 'ctrlMapAnimAdd ctrlMapAnimClear ctrlMapAnimCommit ctrlMapAnimDone ctrlMapCursor ctrlMapMouseOver ' + 'ctrlMapScale ctrlMapScreenToWorld ctrlMapWorldToScreen ctrlModel ctrlModelDirAndUp ctrlModelScale ' + 'ctrlParent ctrlParentControlsGroup ctrlPosition ctrlRemoveAllEventHandlers ctrlRemoveEventHandler ' + 'ctrlScale ctrlSetActiveColor ctrlSetAngle ctrlSetAutoScrollDelay ctrlSetAutoScrollRewind ' + 'ctrlSetAutoScrollSpeed ctrlSetBackgroundColor ctrlSetChecked ctrlSetEventHandler ctrlSetFade ' + 'ctrlSetFocus ctrlSetFont ctrlSetFontH1 ctrlSetFontH1B ctrlSetFontH2 ctrlSetFontH2B ctrlSetFontH3 ' + 'ctrlSetFontH3B ctrlSetFontH4 ctrlSetFontH4B ctrlSetFontH5 ctrlSetFontH5B ctrlSetFontH6 ' + 'ctrlSetFontH6B ctrlSetFontHeight ctrlSetFontHeightH1 ctrlSetFontHeightH2 ctrlSetFontHeightH3 ' + 'ctrlSetFontHeightH4 ctrlSetFontHeightH5 ctrlSetFontHeightH6 ctrlSetFontHeightSecondary ' + 'ctrlSetFontP ctrlSetFontPB ctrlSetFontSecondary ctrlSetForegroundColor ctrlSetModel ' + 'ctrlSetModelDirAndUp ctrlSetModelScale ctrlSetPixelPrecision ctrlSetPosition ctrlSetScale ' + 'ctrlSetStructuredText ctrlSetText ctrlSetTextColor ctrlSetTooltip ctrlSetTooltipColorBox ' + 'ctrlSetTooltipColorShade ctrlSetTooltipColorText ctrlShow ctrlShown ctrlText ctrlTextHeight ' + 'ctrlTextWidth ctrlType ctrlVisible ctRowControls ctRowCount ctSetCurSel ctSetData ' + 'ctSetHeaderTemplate ctSetRowTemplate ctSetValue ctValue curatorAddons curatorCamera ' + 'curatorCameraArea curatorCameraAreaCeiling curatorCoef curatorEditableObjects curatorEditingArea ' + 'curatorEditingAreaType curatorMouseOver curatorPoints curatorRegisteredObjects curatorSelected ' + 'curatorWaypointCost current3DENOperation currentChannel currentCommand currentMagazine ' + 'currentMagazineDetail currentMagazineDetailTurret currentMagazineTurret currentMuzzle ' + 'currentNamespace currentTask currentTasks currentThrowable currentVisionMode currentWaypoint ' + 'currentWeapon currentWeaponMode currentWeaponTurret currentZeroing cursorObject cursorTarget ' + 'customChat customRadio cutFadeOut cutObj cutRsc cutText damage date dateToNumber daytime ' + 'deActivateKey debriefingText debugFSM debugLog deg delete3DENEntities deleteAt deleteCenter ' + 'deleteCollection deleteEditorObject deleteGroup deleteGroupWhenEmpty deleteIdentity ' + 'deleteLocation deleteMarker deleteMarkerLocal deleteRange deleteResources deleteSite deleteStatus ' + 'deleteTeam deleteVehicle deleteVehicleCrew deleteWaypoint detach detectedMines ' + 'diag_activeMissionFSMs diag_activeScripts diag_activeSQFScripts diag_activeSQSScripts ' + 'diag_captureFrame diag_captureFrameToFile diag_captureSlowFrame diag_codePerformance ' + 'diag_drawMode diag_enable diag_enabled diag_fps diag_fpsMin diag_frameNo diag_lightNewLoad ' + 'diag_list diag_log diag_logSlowFrame diag_mergeConfigFile diag_recordTurretLimits ' + 'diag_setLightNew diag_tickTime diag_toggle dialog diarySubjectExists didJIP didJIPOwner ' + 'difficulty difficultyEnabled difficultyEnabledRTD difficultyOption direction directSay disableAI ' + 'disableCollisionWith disableConversation disableDebriefingStats disableMapIndicators ' + 'disableNVGEquipment disableRemoteSensors disableSerialization disableTIEquipment ' + 'disableUAVConnectability disableUserInput displayAddEventHandler displayCtrl displayParent ' + 'displayRemoveAllEventHandlers displayRemoveEventHandler displaySetEventHandler dissolveTeam ' + 'distance distance2D distanceSqr distributionRegion do3DENAction doArtilleryFire doFire doFollow ' + 'doFSM doGetOut doMove doorPhase doStop doSuppressiveFire doTarget doWatch drawArrow drawEllipse ' + 'drawIcon drawIcon3D drawLine drawLine3D drawLink drawLocation drawPolygon drawRectangle ' + 'drawTriangle driver drop dynamicSimulationDistance dynamicSimulationDistanceCoef ' + 'dynamicSimulationEnabled dynamicSimulationSystemEnabled echo edit3DENMissionAttributes editObject ' + 'editorSetEventHandler effectiveCommander emptyPositions enableAI enableAIFeature ' + 'enableAimPrecision enableAttack enableAudioFeature enableAutoStartUpRTD enableAutoTrimRTD ' + 'enableCamShake enableCaustics enableChannel enableCollisionWith enableCopilot ' + 'enableDebriefingStats enableDiagLegend enableDynamicSimulation enableDynamicSimulationSystem ' + 'enableEndDialog enableEngineArtillery enableEnvironment enableFatigue enableGunLights ' + 'enableInfoPanelComponent enableIRLasers enableMimics enablePersonTurret enableRadio enableReload ' + 'enableRopeAttach enableSatNormalOnDetail enableSaving enableSentences enableSimulation ' + 'enableSimulationGlobal enableStamina enableTeamSwitch enableTraffic enableUAVConnectability ' + 'enableUAVWaypoints enableVehicleCargo enableVehicleSensor enableWeaponDisassembly ' + 'endLoadingScreen endMission engineOn enginesIsOnRTD enginesRpmRTD enginesTorqueRTD entities ' + 'environmentEnabled estimatedEndServerTime estimatedTimeLeft evalObjectArgument everyBackpack ' + 'everyContainer exec execEditorScript execFSM execVM exp expectedDestination exportJIPMessages ' + 'eyeDirection eyePos face faction fadeMusic fadeRadio fadeSound fadeSpeech failMission ' + 'fillWeaponsFromPool find findCover findDisplay findEditorObject findEmptyPosition ' + 'findEmptyPositionReady findIf findNearestEnemy finishMissionInit finite fire fireAtTarget ' + 'firstBackpack flag flagAnimationPhase flagOwner flagSide flagTexture fleeing floor flyInHeight ' + 'flyInHeightASL fog fogForecast fogParams forceAddUniform forcedMap forceEnd forceFlagTexture ' + 'forceFollowRoad forceMap forceRespawn forceSpeed forceWalk forceWeaponFire forceWeatherChange ' + 'forEachMember forEachMemberAgent forEachMemberTeam forgetTarget format formation ' + 'formationDirection formationLeader formationMembers formationPosition formationTask formatText ' + 'formLeader freeLook fromEditor fuel fullCrew gearIDCAmmoCount gearSlotAmmoCount gearSlotData ' + 'get3DENActionState get3DENAttribute get3DENCamera get3DENConnections get3DENEntity ' + 'get3DENEntityID get3DENGrid get3DENIconsVisible get3DENLayerEntities get3DENLinesVisible ' + 'get3DENMissionAttribute get3DENMouseOver get3DENSelected getAimingCoef getAllEnvSoundControllers ' + 'getAllHitPointsDamage getAllOwnedMines getAllSoundControllers getAmmoCargo getAnimAimPrecision ' + 'getAnimSpeedCoef getArray getArtilleryAmmo getArtilleryComputerSettings getArtilleryETA ' + 'getAssignedCuratorLogic getAssignedCuratorUnit getBackpackCargo getBleedingRemaining ' + 'getBurningValue getCameraViewDirection getCargoIndex getCenterOfMass getClientState ' + 'getClientStateNumber getCompatiblePylonMagazines getConnectedUAV getContainerMaxLoad ' + 'getCursorObjectParams getCustomAimCoef getDammage getDescription getDir getDirVisual ' + 'getDLCAssetsUsage getDLCAssetsUsageByName getDLCs getEditorCamera getEditorMode ' + 'getEditorObjectScope getElevationOffset getEnvSoundController getFatigue getForcedFlagTexture ' + 'getFriend getFSMVariable getFuelCargo getGroupIcon getGroupIconParams getGroupIcons getHideFrom ' + 'getHit getHitIndex getHitPointDamage getItemCargo getMagazineCargo getMarkerColor getMarkerPos ' + 'getMarkerSize getMarkerType getMass getMissionConfig getMissionConfigValue getMissionDLCs ' + 'getMissionLayerEntities getModelInfo getMousePosition getMusicPlayedTime getNumber ' + 'getObjectArgument getObjectChildren getObjectDLC getObjectMaterials getObjectProxy ' + 'getObjectTextures getObjectType getObjectViewDistance getOxygenRemaining getPersonUsedDLCs ' + 'getPilotCameraDirection getPilotCameraPosition getPilotCameraRotation getPilotCameraTarget ' + 'getPlateNumber getPlayerChannel getPlayerScores getPlayerUID getPos getPosASL getPosASLVisual ' + 'getPosASLW getPosATL getPosATLVisual getPosVisual getPosWorld getPylonMagazines getRelDir ' + 'getRelPos getRemoteSensorsDisabled getRepairCargo getResolution getShadowDistance getShotParents ' + 'getSlingLoad getSoundController getSoundControllerResult getSpeed getStamina getStatValue ' + 'getSuppression getTerrainGrid getTerrainHeightASL getText getTotalDLCUsageTime getUnitLoadout ' + 'getUnitTrait getUserMFDText getUserMFDvalue getVariable getVehicleCargo getWeaponCargo ' + 'getWeaponSway getWingsOrientationRTD getWingsPositionRTD getWPPos glanceAt globalChat globalRadio ' + 'goggles goto group groupChat groupFromNetId groupIconSelectable groupIconsVisible groupId ' + 'groupOwner groupRadio groupSelectedUnits groupSelectUnit gunner gusts halt handgunItems ' + 'handgunMagazine handgunWeapon handsHit hasInterface hasPilotCamera hasWeapon hcAllGroups ' + 'hcGroupParams hcLeader hcRemoveAllGroups hcRemoveGroup hcSelected hcSelectGroup hcSetGroup ' + 'hcShowBar hcShownBar headgear hideBody hideObject hideObjectGlobal hideSelection hint hintC ' + 'hintCadet hintSilent hmd hostMission htmlLoad HUDMovementLevels humidity image importAllGroups ' + 'importance in inArea inAreaArray incapacitatedState inflame inflamed infoPanel ' + 'infoPanelComponentEnabled infoPanelComponents infoPanels inGameUISetEventHandler inheritsFrom ' + 'initAmbientLife inPolygon inputAction inRangeOfArtillery insertEditorObject intersect is3DEN ' + 'is3DENMultiplayer isAbleToBreathe isAgent isArray isAutoHoverOn isAutonomous isAutotest ' + 'isBleeding isBurning isClass isCollisionLightOn isCopilotEnabled isDamageAllowed isDedicated ' + 'isDLCAvailable isEngineOn isEqualTo isEqualType isEqualTypeAll isEqualTypeAny isEqualTypeArray ' + 'isEqualTypeParams isFilePatchingEnabled isFlashlightOn isFlatEmpty isForcedWalk isFormationLeader ' + 'isGroupDeletedWhenEmpty isHidden isInRemainsCollector isInstructorFigureEnabled isIRLaserOn ' + 'isKeyActive isKindOf isLaserOn isLightOn isLocalized isManualFire isMarkedForCollection ' + 'isMultiplayer isMultiplayerSolo isNil isNull isNumber isObjectHidden isObjectRTD isOnRoad ' + 'isPipEnabled isPlayer isRealTime isRemoteExecuted isRemoteExecutedJIP isServer isShowing3DIcons ' + 'isSimpleObject isSprintAllowed isStaminaEnabled isSteamMission isStreamFriendlyUIEnabled isText ' + 'isTouchingGround isTurnedOut isTutHintsEnabled isUAVConnectable isUAVConnected isUIContext ' + 'isUniformAllowed isVehicleCargo isVehicleRadarOn isVehicleSensorEnabled isWalking ' + 'isWeaponDeployed isWeaponRested itemCargo items itemsWithMagazines join joinAs joinAsSilent ' + 'joinSilent joinString kbAddDatabase kbAddDatabaseTargets kbAddTopic kbHasTopic kbReact ' + 'kbRemoveTopic kbTell kbWasSaid keyImage keyName knowsAbout land landAt landResult language ' + 'laserTarget lbAdd lbClear lbColor lbColorRight lbCurSel lbData lbDelete lbIsSelected lbPicture ' + 'lbPictureRight lbSelection lbSetColor lbSetColorRight lbSetCurSel lbSetData lbSetPicture ' + 'lbSetPictureColor lbSetPictureColorDisabled lbSetPictureColorSelected lbSetPictureRight ' + 'lbSetPictureRightColor lbSetPictureRightColorDisabled lbSetPictureRightColorSelected ' + 'lbSetSelectColor lbSetSelectColorRight lbSetSelected lbSetText lbSetTextRight lbSetTooltip ' + 'lbSetValue lbSize lbSort lbSortByValue lbText lbTextRight lbValue leader leaderboardDeInit ' + 'leaderboardGetRows leaderboardInit leaderboardRequestRowsFriends leaderboardsRequestUploadScore ' + 'leaderboardsRequestUploadScoreKeepBest leaderboardState leaveVehicle libraryCredits ' + 'libraryDisclaimers lifeState lightAttachObject lightDetachObject lightIsOn lightnings limitSpeed ' + 'linearConversion lineIntersects lineIntersectsObjs lineIntersectsSurfaces lineIntersectsWith ' + 'linkItem list listObjects listRemoteTargets listVehicleSensors ln lnbAddArray lnbAddColumn ' + 'lnbAddRow lnbClear lnbColor lnbCurSelRow lnbData lnbDeleteColumn lnbDeleteRow ' + 'lnbGetColumnsPosition lnbPicture lnbSetColor lnbSetColumnsPos lnbSetCurSelRow lnbSetData ' + 'lnbSetPicture lnbSetText lnbSetValue lnbSize lnbSort lnbSortByValue lnbText lnbValue load loadAbs ' + 'loadBackpack loadFile loadGame loadIdentity loadMagazine loadOverlay loadStatus loadUniform ' + 'loadVest local localize locationPosition lock lockCameraTo lockCargo lockDriver locked ' + 'lockedCargo lockedDriver lockedTurret lockIdentity lockTurret lockWP log logEntities logNetwork ' + 'logNetworkTerminate lookAt lookAtPos magazineCargo magazines magazinesAllTurrets magazinesAmmo ' + 'magazinesAmmoCargo magazinesAmmoFull magazinesDetail magazinesDetailBackpack ' + 'magazinesDetailUniform magazinesDetailVest magazinesTurret magazineTurretAmmo mapAnimAdd ' + 'mapAnimClear mapAnimCommit mapAnimDone mapCenterOnCamera mapGridPosition markAsFinishedOnSteam ' + 'markerAlpha markerBrush markerColor markerDir markerPos markerShape markerSize markerText ' + 'markerType max members menuAction menuAdd menuChecked menuClear menuCollapse menuData menuDelete ' + 'menuEnable menuEnabled menuExpand menuHover menuPicture menuSetAction menuSetCheck menuSetData ' + 'menuSetPicture menuSetValue menuShortcut menuShortcutText menuSize menuSort menuText menuURL ' + 'menuValue min mineActive mineDetectedBy missionConfigFile missionDifficulty missionName ' + 'missionNamespace missionStart missionVersion mod modelToWorld modelToWorldVisual ' + 'modelToWorldVisualWorld modelToWorldWorld modParams moonIntensity moonPhase morale move ' + 'move3DENCamera moveInAny moveInCargo moveInCommander moveInDriver moveInGunner moveInTurret ' + 'moveObjectToEnd moveOut moveTime moveTo moveToCompleted moveToFailed musicVolume name nameSound ' + 'nearEntities nearestBuilding nearestLocation nearestLocations nearestLocationWithDubbing ' + 'nearestObject nearestObjects nearestTerrainObjects nearObjects nearObjectsReady nearRoads ' + 'nearSupplies nearTargets needReload netId netObjNull newOverlay nextMenuItemIndex ' + 'nextWeatherChange nMenuItems not numberOfEnginesRTD numberToDate objectCurators objectFromNetId ' + 'objectParent objStatus onBriefingGroup onBriefingNotes onBriefingPlan onBriefingTeamSwitch ' + 'onCommandModeChanged onDoubleClick onEachFrame onGroupIconClick onGroupIconOverEnter ' + 'onGroupIconOverLeave onHCGroupSelectionChanged onMapSingleClick onPlayerConnected ' + 'onPlayerDisconnected onPreloadFinished onPreloadStarted onShowNewObject onTeamSwitch ' + 'openCuratorInterface openDLCPage openMap openSteamApp openYoutubeVideo or orderGetIn overcast ' + 'overcastForecast owner param params parseNumber parseSimpleArray parseText parsingNamespace ' + 'particlesQuality pickWeaponPool pitch pixelGrid pixelGridBase pixelGridNoUIScale pixelH pixelW ' + 'playableSlotsNumber playableUnits playAction playActionNow player playerRespawnTime playerSide ' + 'playersNumber playGesture playMission playMove playMoveNow playMusic playScriptedMission ' + 'playSound playSound3D position positionCameraToWorld posScreenToWorld posWorldToScreen ' + 'ppEffectAdjust ppEffectCommit ppEffectCommitted ppEffectCreate ppEffectDestroy ppEffectEnable ' + 'ppEffectEnabled ppEffectForceInNVG precision preloadCamera preloadObject preloadSound ' + 'preloadTitleObj preloadTitleRsc preprocessFile preprocessFileLineNumbers primaryWeapon ' + 'primaryWeaponItems primaryWeaponMagazine priority processDiaryLink productVersion profileName ' + 'profileNamespace profileNameSteam progressLoadingScreen progressPosition progressSetPosition ' + 'publicVariable publicVariableClient publicVariableServer pushBack pushBackUnique putWeaponPool ' + 'queryItemsPool queryMagazinePool queryWeaponPool rad radioChannelAdd radioChannelCreate ' + 'radioChannelRemove radioChannelSetCallSign radioChannelSetLabel radioVolume rain rainbow random ' + 'rank rankId rating rectangular registeredTasks registerTask reload reloadEnabled remoteControl ' + 'remoteExec remoteExecCall remoteExecutedOwner remove3DENConnection remove3DENEventHandler ' + 'remove3DENLayer removeAction removeAll3DENEventHandlers removeAllActions removeAllAssignedItems ' + 'removeAllContainers removeAllCuratorAddons removeAllCuratorCameraAreas ' + 'removeAllCuratorEditingAreas removeAllEventHandlers removeAllHandgunItems removeAllItems ' + 'removeAllItemsWithMagazines removeAllMissionEventHandlers removeAllMPEventHandlers ' + 'removeAllMusicEventHandlers removeAllOwnedMines removeAllPrimaryWeaponItems removeAllWeapons ' + 'removeBackpack removeBackpackGlobal removeCuratorAddons removeCuratorCameraArea ' + 'removeCuratorEditableObjects removeCuratorEditingArea removeDrawIcon removeDrawLinks ' + 'removeEventHandler removeFromRemainsCollector removeGoggles removeGroupIcon removeHandgunItem ' + 'removeHeadgear removeItem removeItemFromBackpack removeItemFromUniform removeItemFromVest ' + 'removeItems removeMagazine removeMagazineGlobal removeMagazines removeMagazinesTurret ' + 'removeMagazineTurret removeMenuItem removeMissionEventHandler removeMPEventHandler ' + 'removeMusicEventHandler removeOwnedMine removePrimaryWeaponItem removeSecondaryWeaponItem ' + 'removeSimpleTask removeSwitchableUnit removeTeamMember removeUniform removeVest removeWeapon ' + 'removeWeaponAttachmentCargo removeWeaponCargo removeWeaponGlobal removeWeaponTurret ' + 'reportRemoteTarget requiredVersion resetCamShake resetSubgroupDirection resize resources ' + 'respawnVehicle restartEditorCamera reveal revealMine reverse reversedMouseY roadAt ' + 'roadsConnectedTo roleDescription ropeAttachedObjects ropeAttachedTo ropeAttachEnabled ' + 'ropeAttachTo ropeCreate ropeCut ropeDestroy ropeDetach ropeEndPosition ropeLength ropes ' + 'ropeUnwind ropeUnwound rotorsForcesRTD rotorsRpmRTD round runInitScript safeZoneH safeZoneW ' + 'safeZoneWAbs safeZoneX safeZoneXAbs safeZoneY save3DENInventory saveGame saveIdentity ' + 'saveJoysticks saveOverlay saveProfileNamespace saveStatus saveVar savingEnabled say say2D say3D ' + 'scopeName score scoreSide screenshot screenToWorld scriptDone scriptName scudState ' + 'secondaryWeapon secondaryWeaponItems secondaryWeaponMagazine select selectBestPlaces ' + 'selectDiarySubject selectedEditorObjects selectEditorObject selectionNames selectionPosition ' + 'selectLeader selectMax selectMin selectNoPlayer selectPlayer selectRandom selectRandomWeighted ' + 'selectWeapon selectWeaponTurret sendAUMessage sendSimpleCommand sendTask sendTaskResult ' + 'sendUDPMessage serverCommand serverCommandAvailable serverCommandExecutable serverName serverTime ' + 'set set3DENAttribute set3DENAttributes set3DENGrid set3DENIconsVisible set3DENLayer ' + 'set3DENLinesVisible set3DENLogicType set3DENMissionAttribute set3DENMissionAttributes ' + 'set3DENModelsVisible set3DENObjectType set3DENSelected setAccTime setActualCollectiveRTD ' + 'setAirplaneThrottle setAirportSide setAmmo setAmmoCargo setAmmoOnPylon setAnimSpeedCoef ' + 'setAperture setApertureNew setArmoryPoints setAttributes setAutonomous setBehaviour ' + 'setBleedingRemaining setBrakesRTD setCameraInterest setCamShakeDefParams setCamShakeParams ' + 'setCamUseTI setCaptive setCenterOfMass setCollisionLight setCombatMode setCompassOscillation ' + 'setConvoySeparation setCuratorCameraAreaCeiling setCuratorCoef setCuratorEditingAreaType ' + 'setCuratorWaypointCost setCurrentChannel setCurrentTask setCurrentWaypoint setCustomAimCoef ' + 'setCustomWeightRTD setDamage setDammage setDate setDebriefingText setDefaultCamera setDestination ' + 'setDetailMapBlendPars setDir setDirection setDrawIcon setDriveOnPath setDropInterval ' + 'setDynamicSimulationDistance setDynamicSimulationDistanceCoef setEditorMode setEditorObjectScope ' + 'setEffectCondition setEngineRPMRTD setFace setFaceAnimation setFatigue setFeatureType ' + 'setFlagAnimationPhase setFlagOwner setFlagSide setFlagTexture setFog setFormation ' + 'setFormationTask setFormDir setFriend setFromEditor setFSMVariable setFuel setFuelCargo ' + 'setGroupIcon setGroupIconParams setGroupIconsSelectable setGroupIconsVisible setGroupId ' + 'setGroupIdGlobal setGroupOwner setGusts setHideBehind setHit setHitIndex setHitPointDamage ' + 'setHorizonParallaxCoef setHUDMovementLevels setIdentity setImportance setInfoPanel setLeader ' + 'setLightAmbient setLightAttenuation setLightBrightness setLightColor setLightDayLight ' + 'setLightFlareMaxDistance setLightFlareSize setLightIntensity setLightnings setLightUseFlare ' + 'setLocalWindParams setMagazineTurretAmmo setMarkerAlpha setMarkerAlphaLocal setMarkerBrush ' + 'setMarkerBrushLocal setMarkerColor setMarkerColorLocal setMarkerDir setMarkerDirLocal ' + 'setMarkerPos setMarkerPosLocal setMarkerShape setMarkerShapeLocal setMarkerSize ' + 'setMarkerSizeLocal setMarkerText setMarkerTextLocal setMarkerType setMarkerTypeLocal setMass ' + 'setMimic setMousePosition setMusicEffect setMusicEventHandler setName setNameSound ' + 'setObjectArguments setObjectMaterial setObjectMaterialGlobal setObjectProxy setObjectTexture ' + 'setObjectTextureGlobal setObjectViewDistance setOvercast setOwner setOxygenRemaining ' + 'setParticleCircle setParticleClass setParticleFire setParticleParams setParticleRandom ' + 'setPilotCameraDirection setPilotCameraRotation setPilotCameraTarget setPilotLight setPiPEffect ' + 'setPitch setPlateNumber setPlayable setPlayerRespawnTime setPos setPosASL setPosASL2 setPosASLW ' + 'setPosATL setPosition setPosWorld setPylonLoadOut setPylonsPriority setRadioMsg setRain ' + 'setRainbow setRandomLip setRank setRectangular setRepairCargo setRotorBrakeRTD setShadowDistance ' + 'setShotParents setSide setSimpleTaskAlwaysVisible setSimpleTaskCustomData ' + 'setSimpleTaskDescription setSimpleTaskDestination setSimpleTaskTarget setSimpleTaskType ' + 'setSimulWeatherLayers setSize setSkill setSlingLoad setSoundEffect setSpeaker setSpeech ' + 'setSpeedMode setStamina setStaminaScheme setStatValue setSuppression setSystemOfUnits ' + 'setTargetAge setTaskMarkerOffset setTaskResult setTaskState setTerrainGrid setText ' + 'setTimeMultiplier setTitleEffect setTrafficDensity setTrafficDistance setTrafficGap ' + 'setTrafficSpeed setTriggerActivation setTriggerArea setTriggerStatements setTriggerText ' + 'setTriggerTimeout setTriggerType setType setUnconscious setUnitAbility setUnitLoadout setUnitPos ' + 'setUnitPosWeak setUnitRank setUnitRecoilCoefficient setUnitTrait setUnloadInCombat ' + 'setUserActionText setUserMFDText setUserMFDvalue setVariable setVectorDir setVectorDirAndUp ' + 'setVectorUp setVehicleAmmo setVehicleAmmoDef setVehicleArmor setVehicleCargo setVehicleId ' + 'setVehicleLock setVehiclePosition setVehicleRadar setVehicleReceiveRemoteTargets ' + 'setVehicleReportOwnPosition setVehicleReportRemoteTargets setVehicleTIPars setVehicleVarName ' + 'setVelocity setVelocityModelSpace setVelocityTransformation setViewDistance ' + 'setVisibleIfTreeCollapsed setWantedRPMRTD setWaves setWaypointBehaviour setWaypointCombatMode ' + 'setWaypointCompletionRadius setWaypointDescription setWaypointForceBehaviour setWaypointFormation ' + 'setWaypointHousePosition setWaypointLoiterRadius setWaypointLoiterType setWaypointName ' + 'setWaypointPosition setWaypointScript setWaypointSpeed setWaypointStatements setWaypointTimeout ' + 'setWaypointType setWaypointVisible setWeaponReloadingTime setWind setWindDir setWindForce ' + 'setWindStr setWingForceScaleRTD setWPPos show3DIcons showChat showCinemaBorder showCommandingMenu ' + 'showCompass showCuratorCompass showGPS showHUD showLegend showMap shownArtilleryComputer ' + 'shownChat shownCompass shownCuratorCompass showNewEditorObject shownGPS shownHUD shownMap ' + 'shownPad shownRadio shownScoretable shownUAVFeed shownWarrant shownWatch showPad showRadio ' + 'showScoretable showSubtitles showUAVFeed showWarrant showWatch showWaypoint showWaypoints side ' + 'sideChat sideEnemy sideFriendly sideRadio simpleTasks simulationEnabled simulCloudDensity ' + 'simulCloudOcclusion simulInClouds simulWeatherSync sin size sizeOf skill skillFinal skipTime ' + 'sleep sliderPosition sliderRange sliderSetPosition sliderSetRange sliderSetSpeed sliderSpeed ' + 'slingLoadAssistantShown soldierMagazines someAmmo sort soundVolume spawn speaker speed speedMode ' + 'splitString sqrt squadParams stance startLoadingScreen step stop stopEngineRTD stopped str ' + 'sunOrMoon supportInfo suppressFor surfaceIsWater surfaceNormal surfaceType swimInDepth ' + 'switchableUnits switchAction switchCamera switchGesture switchLight switchMove ' + 'synchronizedObjects synchronizedTriggers synchronizedWaypoints synchronizeObjectsAdd ' + 'synchronizeObjectsRemove synchronizeTrigger synchronizeWaypoint systemChat systemOfUnits tan ' + 'targetKnowledge targets targetsAggregate targetsQuery taskAlwaysVisible taskChildren ' + 'taskCompleted taskCustomData taskDescription taskDestination taskHint taskMarkerOffset taskParent ' + 'taskResult taskState taskType teamMember teamName teams teamSwitch teamSwitchEnabled teamType ' + 'terminate terrainIntersect terrainIntersectASL terrainIntersectAtASL text textLog textLogFormat ' + 'tg time timeMultiplier titleCut titleFadeOut titleObj titleRsc titleText toArray toFixed toLower ' + 'toString toUpper triggerActivated triggerActivation triggerArea triggerAttachedVehicle ' + 'triggerAttachObject triggerAttachVehicle triggerDynamicSimulation triggerStatements triggerText ' + 'triggerTimeout triggerTimeoutCurrent triggerType turretLocal turretOwner turretUnit tvAdd tvClear ' + 'tvCollapse tvCollapseAll tvCount tvCurSel tvData tvDelete tvExpand tvExpandAll tvPicture ' + 'tvSetColor tvSetCurSel tvSetData tvSetPicture tvSetPictureColor tvSetPictureColorDisabled ' + 'tvSetPictureColorSelected tvSetPictureRight tvSetPictureRightColor tvSetPictureRightColorDisabled ' + 'tvSetPictureRightColorSelected tvSetText tvSetTooltip tvSetValue tvSort tvSortByValue tvText ' + 'tvTooltip tvValue type typeName typeOf UAVControl uiNamespace uiSleep unassignCurator ' + 'unassignItem unassignTeam unassignVehicle underwater uniform uniformContainer uniformItems ' + 'uniformMagazines unitAddons unitAimPosition unitAimPositionVisual unitBackpack unitIsUAV unitPos ' + 'unitReady unitRecoilCoefficient units unitsBelowHeight unlinkItem unlockAchievement ' + 'unregisterTask updateDrawIcon updateMenuItem updateObjectTree useAISteeringComponent ' + 'useAudioTimeForMoves userInputDisabled vectorAdd vectorCos vectorCrossProduct vectorDiff ' + 'vectorDir vectorDirVisual vectorDistance vectorDistanceSqr vectorDotProduct vectorFromTo ' + 'vectorMagnitude vectorMagnitudeSqr vectorModelToWorld vectorModelToWorldVisual vectorMultiply ' + 'vectorNormalized vectorUp vectorUpVisual vectorWorldToModel vectorWorldToModelVisual vehicle ' + 'vehicleCargoEnabled vehicleChat vehicleRadio vehicleReceiveRemoteTargets vehicleReportOwnPosition ' + 'vehicleReportRemoteTargets vehicles vehicleVarName velocity velocityModelSpace verifySignature ' + 'vest vestContainer vestItems vestMagazines viewDistance visibleCompass visibleGPS visibleMap ' + 'visiblePosition visiblePositionASL visibleScoretable visibleWatch waves waypointAttachedObject ' + 'waypointAttachedVehicle waypointAttachObject waypointAttachVehicle waypointBehaviour ' + 'waypointCombatMode waypointCompletionRadius waypointDescription waypointForceBehaviour ' + 'waypointFormation waypointHousePosition waypointLoiterRadius waypointLoiterType waypointName ' + 'waypointPosition waypoints waypointScript waypointsEnabledUAV waypointShow waypointSpeed ' + 'waypointStatements waypointTimeout waypointTimeoutCurrent waypointType waypointVisible ' + 'weaponAccessories weaponAccessoriesCargo weaponCargo weaponDirection weaponInertia weaponLowered ' + 'weapons weaponsItems weaponsItemsCargo weaponState weaponsTurret weightRTD WFSideText wind ', literal: 'blufor civilian configNull controlNull displayNull east endl false grpNull independent lineBreak ' + 'locationNull nil objNull opfor pi resistance scriptNull sideAmbientLife sideEmpty sideLogic ' + 'sideUnknown taskNull teamMemberNull true west', }, contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.NUMBER_MODE, VARIABLE, FUNCTION, STRINGS, CPP.preprocessor ], illegal: /#|^\$ / }; } },{name:"sql",create:/* Language: SQL Contributors: Nikolay Lisienko , Heiko August , Travis Odom , Vadimtro , Benjamin Auder Category: common */ function(hljs) { var COMMENT_MODE = hljs.COMMENT('--', '$'); return { case_insensitive: true, illegal: /[<>{}*]/, contains: [ { beginKeywords: 'begin end start commit rollback savepoint lock alter create drop rename call ' + 'delete do handler insert load replace select truncate update set show pragma grant ' + 'merge describe use explain help declare prepare execute deallocate release ' + 'unlock purge reset change stop analyze cache flush optimize repair kill ' + 'install uninstall checksum restore check backup revoke comment values with', end: /;/, endsWithParent: true, lexemes: /[\w\.]+/, keywords: { keyword: 'as abort abs absolute acc acce accep accept access accessed accessible account acos action activate add ' + 'addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias ' + 'all allocate allow alter always analyze ancillary and anti any anydata anydataset anyschema anytype apply ' + 'archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan ' + 'atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid ' + 'authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile ' + 'before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float ' + 'binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound ' + 'bucket buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel ' + 'capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base ' + 'char_length character_length characters characterset charindex charset charsetform charsetid check ' + 'checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close ' + 'cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation ' + 'collect colu colum column column_value columns columns_updated comment commit compact compatibility ' + 'compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn ' + 'connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection ' + 'consider consistent constant constraint constraints constructor container content contents context ' + 'contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost ' + 'count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation ' + 'critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user ' + 'cursor curtime customdatum cycle data database databases datafile datafiles datalength date_add ' + 'date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts ' + 'day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate ' + 'declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults ' + 'deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank ' + 'depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor ' + 'deterministic diagnostics difference dimension direct_load directory disable disable_all ' + 'disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div ' + 'do document domain dotnet double downgrade drop dumpfile duplicate duration each edition editionable ' + 'editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt ' + 'end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors ' + 'escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding ' + 'execu execut execute exempt exists exit exp expire explain explode export export_set extended extent external ' + 'external_1 external_2 externally extract failed failed_login_attempts failover failure far fast ' + 'feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final ' + 'finish first first_value fixed flash_cache flashback floor flush following follows for forall force foreign ' + 'form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ' + 'ftp full function general generated get get_format get_lock getdate getutcdate global global_name ' + 'globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups ' + 'gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex ' + 'hierarchy high high_priority hosts hour hours http id ident_current ident_incr ident_seed identified ' + 'identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment ' + 'index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile ' + 'initial initialized initially initrans inmemory inner innodb input insert install instance instantiable ' + 'instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat ' + 'is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists ' + 'keep keep_duplicates key keys kill language large last last_day last_insert_id last_value lateral lax lcase ' + 'lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit ' + 'lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate ' + 'locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call ' + 'logoff logon logs long loop low low_priority lower lpad lrtrim ltrim main make_set makedate maketime ' + 'managed management manual map mapping mask master master_pos_wait match matched materialized max ' + 'maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans ' + 'md5 measures median medium member memcompress memory merge microsecond mid migration min minextents ' + 'minimum mining minus minute minutes minvalue missing mod mode model modification modify module monitoring month ' + 'months mount move movement multiset mutex name name_const names nan national native natural nav nchar ' + 'nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile ' + 'nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile ' + 'nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder ' + 'nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck ' + 'noswitch not nothing notice notnull notrim novalidate now nowait nth_value nullif nulls num numb numbe ' + 'nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ' + 'ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old ' + 'on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date ' + 'oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary ' + 'out outer outfile outline output over overflow overriding package pad parallel parallel_enable ' + 'parameters parent parse partial partition partitions pascal passing password password_grace_time ' + 'password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex ' + 'pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc ' + 'performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin ' + 'policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction ' + 'prediction_cost prediction_details prediction_probability prediction_set prepare present preserve ' + 'prior priority private private_sga privileges procedural procedure procedure_analyze processlist ' + 'profiles project prompt protection public publishingservername purge quarter query quick quiesce quota ' + 'quotename radians raise rand range rank raw read reads readsize rebuild record records ' + 'recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh ' + 'regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy ' + 'reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename ' + 'repair repeat replace replicate replication required reset resetlogs resize resource respect restore ' + 'restricted result result_cache resumable resume retention return returning returns reuse reverse revoke ' + 'right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows ' + 'rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll ' + 'sdo_georaster sdo_topo_geometry search sec_to_time second seconds section securefile security seed segment select ' + 'self semi sequence sequential serializable server servererror session session_user sessions_per_user set ' + 'sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor ' + 'si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin ' + 'size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex ' + 'source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows ' + 'sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone ' + 'standby start starting startup statement static statistics stats_binomial_test stats_crosstab ' + 'stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep ' + 'stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev ' + 'stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate ' + 'subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum ' + 'suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate ' + 'sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime table tables tablespace tablesample tan tdo ' + 'template temporary terminated tertiary_weights test than then thread through tier ties time time_format ' + 'time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr ' + 'timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking ' + 'transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate ' + 'try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress ' + 'under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unnest unpivot ' + 'unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert ' + 'url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date ' + 'utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var ' + 'var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray ' + 'verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear ' + 'wellformed when whene whenev wheneve whenever where while whitespace window with within without work wrapped ' + 'xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces ' + 'xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek', literal: 'true false null unknown', built_in: 'array bigint binary bit blob bool boolean char character date dec decimal float int int8 integer interval number ' + 'numeric real record serial serial8 smallint text time timestamp tinyint varchar varying void' }, contains: [ { className: 'string', begin: '\'', end: '\'', contains: [hljs.BACKSLASH_ESCAPE, {begin: '\'\''}] }, { className: 'string', begin: '"', end: '"', contains: [hljs.BACKSLASH_ESCAPE, {begin: '""'}] }, { className: 'string', begin: '`', end: '`', contains: [hljs.BACKSLASH_ESCAPE] }, hljs.C_NUMBER_MODE, hljs.C_BLOCK_COMMENT_MODE, COMMENT_MODE, hljs.HASH_COMMENT_MODE ] }, hljs.C_BLOCK_COMMENT_MODE, COMMENT_MODE, hljs.HASH_COMMENT_MODE ] }; } },{name:"stan",create:/* Language: Stan Author: Brendan Rocks Category: scientific Description: The Stan probabilistic programming language (http://mc-stan.org/). */ function(hljs) { return { contains: [ hljs.HASH_COMMENT_MODE, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, { begin: hljs.UNDERSCORE_IDENT_RE, lexemes: hljs.UNDERSCORE_IDENT_RE, keywords: { // Stan's keywords name: 'for in while repeat until if then else', // Stan's probablity distributions (less beta and gamma, as commonly // used for parameter names). So far, _log and _rng variants are not // included symbol: 'bernoulli bernoulli_logit binomial binomial_logit ' + 'beta_binomial hypergeometric categorical categorical_logit ' + 'ordered_logistic neg_binomial neg_binomial_2 ' + 'neg_binomial_2_log poisson poisson_log multinomial normal ' + 'exp_mod_normal skew_normal student_t cauchy double_exponential ' + 'logistic gumbel lognormal chi_square inv_chi_square ' + 'scaled_inv_chi_square exponential inv_gamma weibull frechet ' + 'rayleigh wiener pareto pareto_type_2 von_mises uniform ' + 'multi_normal multi_normal_prec multi_normal_cholesky multi_gp ' + 'multi_gp_cholesky multi_student_t gaussian_dlm_obs dirichlet ' + 'lkj_corr lkj_corr_cholesky wishart inv_wishart', // Stan's data types 'selector-tag': 'int real vector simplex unit_vector ordered positive_ordered ' + 'row_vector matrix cholesky_factor_corr cholesky_factor_cov ' + 'corr_matrix cov_matrix', // Stan's model blocks title: 'functions model data parameters quantities transformed ' + 'generated', literal: 'true false' }, relevance: 0 }, // The below is all taken from the R language definition { // hex value className: 'number', begin: "0[xX][0-9a-fA-F]+[Li]?\\b", relevance: 0 }, { // hex value className: 'number', begin: "0[xX][0-9a-fA-F]+[Li]?\\b", relevance: 0 }, { // explicit integer className: 'number', begin: "\\d+(?:[eE][+\\-]?\\d*)?L\\b", relevance: 0 }, { // number with trailing decimal className: 'number', begin: "\\d+\\.(?!\\d)(?:i\\b)?", relevance: 0 }, { // number className: 'number', begin: "\\d+(?:\\.\\d*)?(?:[eE][+\\-]?\\d*)?i?\\b", relevance: 0 }, { // number with leading decimal className: 'number', begin: "\\.\\d+(?:[eE][+\\-]?\\d*)?i?\\b", relevance: 0 } ] }; } },{name:"stata",create:/* Language: Stata Author: Brian Quistorff Contributors: Drew McDonald Description: Syntax highlighting for Stata code. This is a fork and modification of Drew McDonald's file (https://github.com/drewmcdonald/stata-highlighting). I have also included a list of builtin commands from https://bugs.kde.org/show_bug.cgi?id=135646. Category: scientific */ function(hljs) { return { aliases: ['do', 'ado'], case_insensitive: true, keywords: 'if else in foreach for forv forva forval forvalu forvalue forvalues by bys bysort xi quietly qui capture about ac ac_7 acprplot acprplot_7 adjust ado adopath adoupdate alpha ameans an ano anov anova anova_estat anova_terms anovadef aorder ap app appe appen append arch arch_dr arch_estat arch_p archlm areg areg_p args arima arima_dr arima_estat arima_p as asmprobit asmprobit_estat asmprobit_lf asmprobit_mfx__dlg asmprobit_p ass asse asser assert avplot avplot_7 avplots avplots_7 bcskew0 bgodfrey bias binreg bip0_lf biplot bipp_lf bipr_lf bipr_p biprobit bitest bitesti bitowt blogit bmemsize boot bootsamp bootstrap bootstrap_8 boxco_l boxco_p boxcox boxcox_6 boxcox_p bprobit br break brier bro brow brows browse brr brrstat bs bs_7 bsampl_w bsample bsample_7 bsqreg bstat bstat_7 bstat_8 bstrap bstrap_7 bubble bubbleplot ca ca_estat ca_p cabiplot camat canon canon_8 canon_8_p canon_estat canon_p cap caprojection capt captu captur capture cat cc cchart cchart_7 cci cd censobs_table centile cf char chdir checkdlgfiles checkestimationsample checkhlpfiles checksum chelp ci cii cl class classutil clear cli clis clist clo clog clog_lf clog_p clogi clogi_sw clogit clogit_lf clogit_p clogitp clogl_sw cloglog clonevar clslistarray cluster cluster_measures cluster_stop cluster_tree cluster_tree_8 clustermat cmdlog cnr cnre cnreg cnreg_p cnreg_sw cnsreg codebook collaps4 collapse colormult_nb colormult_nw compare compress conf confi confir confirm conren cons const constr constra constrai constrain constraint continue contract copy copyright copysource cor corc corr corr2data corr_anti corr_kmo corr_smc corre correl correla correlat correlate corrgram cou coun count cox cox_p cox_sw coxbase coxhaz coxvar cprplot cprplot_7 crc cret cretu cretur creturn cross cs cscript cscript_log csi ct ct_is ctset ctst_5 ctst_st cttost cumsp cumsp_7 cumul cusum cusum_7 cutil d|0 datasig datasign datasigna datasignat datasignatu datasignatur datasignature datetof db dbeta de dec deco decod decode deff des desc descr descri describ describe destring dfbeta dfgls dfuller di di_g dir dirstats dis discard disp disp_res disp_s displ displa display distinct do doe doed doedi doedit dotplot dotplot_7 dprobit drawnorm drop ds ds_util dstdize duplicates durbina dwstat dydx e|0 ed edi edit egen eivreg emdef en enc enco encod encode eq erase ereg ereg_lf ereg_p ereg_sw ereghet ereghet_glf ereghet_glf_sh ereghet_gp ereghet_ilf ereghet_ilf_sh ereghet_ip eret eretu eretur ereturn err erro error esize est est_cfexist est_cfname est_clickable est_expand est_hold est_table est_unhold est_unholdok estat estat_default estat_summ estat_vce_only esti estimates etodow etof etomdy ex exi exit expand expandcl fac fact facto factor factor_estat factor_p factor_pca_rotated factor_rotate factormat fcast fcast_compute fcast_graph fdades fdadesc fdadescr fdadescri fdadescrib fdadescribe fdasav fdasave fdause fh_st file open file read file close file filefilter fillin find_hlp_file findfile findit findit_7 fit fl fli flis flist for5_0 forest forestplot form forma format fpredict frac_154 frac_adj frac_chk frac_cox frac_ddp frac_dis frac_dv frac_in frac_mun frac_pp frac_pq frac_pv frac_wgt frac_xo fracgen fracplot fracplot_7 fracpoly fracpred fron_ex fron_hn fron_p fron_tn fron_tn2 frontier ftodate ftoe ftomdy ftowdate funnel funnelplot g|0 gamhet_glf gamhet_gp gamhet_ilf gamhet_ip gamma gamma_d2 gamma_p gamma_sw gammahet gdi_hexagon gdi_spokes ge gen gene gener genera generat generate genrank genstd genvmean gettoken gl gladder gladder_7 glim_l01 glim_l02 glim_l03 glim_l04 glim_l05 glim_l06 glim_l07 glim_l08 glim_l09 glim_l10 glim_l11 glim_l12 glim_lf glim_mu glim_nw1 glim_nw2 glim_nw3 glim_p glim_v1 glim_v2 glim_v3 glim_v4 glim_v5 glim_v6 glim_v7 glm glm_6 glm_p glm_sw glmpred glo glob globa global glogit glogit_8 glogit_p gmeans gnbre_lf gnbreg gnbreg_5 gnbreg_p gomp_lf gompe_sw gomper_p gompertz gompertzhet gomphet_glf gomphet_glf_sh gomphet_gp gomphet_ilf gomphet_ilf_sh gomphet_ip gphdot gphpen gphprint gprefs gprobi_p gprobit gprobit_8 gr gr7 gr_copy gr_current gr_db gr_describe gr_dir gr_draw gr_draw_replay gr_drop gr_edit gr_editviewopts gr_example gr_example2 gr_export gr_print gr_qscheme gr_query gr_read gr_rename gr_replay gr_save gr_set gr_setscheme gr_table gr_undo gr_use graph graph7 grebar greigen greigen_7 greigen_8 grmeanby grmeanby_7 gs_fileinfo gs_filetype gs_graphinfo gs_stat gsort gwood h|0 hadimvo hareg hausman haver he heck_d2 heckma_p heckman heckp_lf heckpr_p heckprob hel help hereg hetpr_lf hetpr_p hetprob hettest hexdump hilite hist hist_7 histogram hlogit hlu hmeans hotel hotelling hprobit hreg hsearch icd9 icd9_ff icd9p iis impute imtest inbase include inf infi infil infile infix inp inpu input ins insheet insp inspe inspec inspect integ inten intreg intreg_7 intreg_p intrg2_ll intrg_ll intrg_ll2 ipolate iqreg ir irf irf_create irfm iri is_svy is_svysum isid istdize ivprob_1_lf ivprob_lf ivprobit ivprobit_p ivreg ivreg_footnote ivtob_1_lf ivtob_lf ivtobit ivtobit_p jackknife jacknife jknife jknife_6 jknife_8 jkstat joinby kalarma1 kap kap_3 kapmeier kappa kapwgt kdensity kdensity_7 keep ksm ksmirnov ktau kwallis l|0 la lab labbe labbeplot labe label labelbook ladder levels levelsof leverage lfit lfit_p li lincom line linktest lis list lloghet_glf lloghet_glf_sh lloghet_gp lloghet_ilf lloghet_ilf_sh lloghet_ip llogi_sw llogis_p llogist llogistic llogistichet lnorm_lf lnorm_sw lnorma_p lnormal lnormalhet lnormhet_glf lnormhet_glf_sh lnormhet_gp lnormhet_ilf lnormhet_ilf_sh lnormhet_ip lnskew0 loadingplot loc loca local log logi logis_lf logistic logistic_p logit logit_estat logit_p loglogs logrank loneway lookfor lookup lowess lowess_7 lpredict lrecomp lroc lroc_7 lrtest ls lsens lsens_7 lsens_x lstat ltable ltable_7 ltriang lv lvr2plot lvr2plot_7 m|0 ma mac macr macro makecns man manova manova_estat manova_p manovatest mantel mark markin markout marksample mat mat_capp mat_order mat_put_rr mat_rapp mata mata_clear mata_describe mata_drop mata_matdescribe mata_matsave mata_matuse mata_memory mata_mlib mata_mosave mata_rename mata_which matalabel matcproc matlist matname matr matri matrix matrix_input__dlg matstrik mcc mcci md0_ md1_ md1debug_ md2_ md2debug_ mds mds_estat mds_p mdsconfig mdslong mdsmat mdsshepard mdytoe mdytof me_derd mean means median memory memsize menl meqparse mer merg merge meta mfp mfx mhelp mhodds minbound mixed_ll mixed_ll_reparm mkassert mkdir mkmat mkspline ml ml_5 ml_adjs ml_bhhhs ml_c_d ml_check ml_clear ml_cnt ml_debug ml_defd ml_e0 ml_e0_bfgs ml_e0_cycle ml_e0_dfp ml_e0i ml_e1 ml_e1_bfgs ml_e1_bhhh ml_e1_cycle ml_e1_dfp ml_e2 ml_e2_cycle ml_ebfg0 ml_ebfr0 ml_ebfr1 ml_ebh0q ml_ebhh0 ml_ebhr0 ml_ebr0i ml_ecr0i ml_edfp0 ml_edfr0 ml_edfr1 ml_edr0i ml_eds ml_eer0i ml_egr0i ml_elf ml_elf_bfgs ml_elf_bhhh ml_elf_cycle ml_elf_dfp ml_elfi ml_elfs ml_enr0i ml_enrr0 ml_erdu0 ml_erdu0_bfgs ml_erdu0_bhhh ml_erdu0_bhhhq ml_erdu0_cycle ml_erdu0_dfp ml_erdu0_nrbfgs ml_exde ml_footnote ml_geqnr ml_grad0 ml_graph ml_hbhhh ml_hd0 ml_hold ml_init ml_inv ml_log ml_max ml_mlout ml_mlout_8 ml_model ml_nb0 ml_opt ml_p ml_plot ml_query ml_rdgrd ml_repor ml_s_e ml_score ml_searc ml_technique ml_unhold mleval mlf_ mlmatbysum mlmatsum mlog mlogi mlogit mlogit_footnote mlogit_p mlopts mlsum mlvecsum mnl0_ mor more mov move mprobit mprobit_lf mprobit_p mrdu0_ mrdu1_ mvdecode mvencode mvreg mvreg_estat n|0 nbreg nbreg_al nbreg_lf nbreg_p nbreg_sw nestreg net newey newey_7 newey_p news nl nl_7 nl_9 nl_9_p nl_p nl_p_7 nlcom nlcom_p nlexp2 nlexp2_7 nlexp2a nlexp2a_7 nlexp3 nlexp3_7 nlgom3 nlgom3_7 nlgom4 nlgom4_7 nlinit nllog3 nllog3_7 nllog4 nllog4_7 nlog_rd nlogit nlogit_p nlogitgen nlogittree nlpred no nobreak noi nois noisi noisil noisily note notes notes_dlg nptrend numlabel numlist odbc old_ver olo olog ologi ologi_sw ologit ologit_p ologitp on one onew onewa oneway op_colnm op_comp op_diff op_inv op_str opr opro oprob oprob_sw oprobi oprobi_p oprobit oprobitp opts_exclusive order orthog orthpoly ou out outf outfi outfil outfile outs outsh outshe outshee outsheet ovtest pac pac_7 palette parse parse_dissim pause pca pca_8 pca_display pca_estat pca_p pca_rotate pcamat pchart pchart_7 pchi pchi_7 pcorr pctile pentium pergram pergram_7 permute permute_8 personal peto_st pkcollapse pkcross pkequiv pkexamine pkexamine_7 pkshape pksumm pksumm_7 pl plo plot plugin pnorm pnorm_7 poisgof poiss_lf poiss_sw poisso_p poisson poisson_estat post postclose postfile postutil pperron pr prais prais_e prais_e2 prais_p predict predictnl preserve print pro prob probi probit probit_estat probit_p proc_time procoverlay procrustes procrustes_estat procrustes_p profiler prog progr progra program prop proportion prtest prtesti pwcorr pwd q\\s qby qbys qchi qchi_7 qladder qladder_7 qnorm qnorm_7 qqplot qqplot_7 qreg qreg_c qreg_p qreg_sw qu quadchk quantile quantile_7 que quer query range ranksum ratio rchart rchart_7 rcof recast reclink recode reg reg3 reg3_p regdw regr regre regre_p2 regres regres_p regress regress_estat regriv_p remap ren rena renam rename renpfix repeat replace report reshape restore ret retu retur return rm rmdir robvar roccomp roccomp_7 roccomp_8 rocf_lf rocfit rocfit_8 rocgold rocplot rocplot_7 roctab roctab_7 rolling rologit rologit_p rot rota rotat rotate rotatemat rreg rreg_p ru run runtest rvfplot rvfplot_7 rvpplot rvpplot_7 sa safesum sample sampsi sav save savedresults saveold sc sca scal scala scalar scatter scm_mine sco scob_lf scob_p scobi_sw scobit scor score scoreplot scoreplot_help scree screeplot screeplot_help sdtest sdtesti se search separate seperate serrbar serrbar_7 serset set set_defaults sfrancia sh she shel shell shewhart shewhart_7 signestimationsample signrank signtest simul simul_7 simulate simulate_8 sktest sleep slogit slogit_d2 slogit_p smooth snapspan so sor sort spearman spikeplot spikeplot_7 spikeplt spline_x split sqreg sqreg_p sret sretu sretur sreturn ssc st st_ct st_hc st_hcd st_hcd_sh st_is st_issys st_note st_promo st_set st_show st_smpl st_subid stack statsby statsby_8 stbase stci stci_7 stcox stcox_estat stcox_fr stcox_fr_ll stcox_p stcox_sw stcoxkm stcoxkm_7 stcstat stcurv stcurve stcurve_7 stdes stem stepwise stereg stfill stgen stir stjoin stmc stmh stphplot stphplot_7 stphtest stphtest_7 stptime strate strate_7 streg streg_sw streset sts sts_7 stset stsplit stsum sttocc sttoct stvary stweib su suest suest_8 sum summ summa summar summari summariz summarize sunflower sureg survcurv survsum svar svar_p svmat svy svy_disp svy_dreg svy_est svy_est_7 svy_estat svy_get svy_gnbreg_p svy_head svy_header svy_heckman_p svy_heckprob_p svy_intreg_p svy_ivreg_p svy_logistic_p svy_logit_p svy_mlogit_p svy_nbreg_p svy_ologit_p svy_oprobit_p svy_poisson_p svy_probit_p svy_regress_p svy_sub svy_sub_7 svy_x svy_x_7 svy_x_p svydes svydes_8 svygen svygnbreg svyheckman svyheckprob svyintreg svyintreg_7 svyintrg svyivreg svylc svylog_p svylogit svymarkout svymarkout_8 svymean svymlog svymlogit svynbreg svyolog svyologit svyoprob svyoprobit svyopts svypois svypois_7 svypoisson svyprobit svyprobt svyprop svyprop_7 svyratio svyreg svyreg_p svyregress svyset svyset_7 svyset_8 svytab svytab_7 svytest svytotal sw sw_8 swcnreg swcox swereg swilk swlogis swlogit swologit swoprbt swpois swprobit swqreg swtobit swweib symmetry symmi symplot symplot_7 syntax sysdescribe sysdir sysuse szroeter ta tab tab1 tab2 tab_or tabd tabdi tabdis tabdisp tabi table tabodds tabodds_7 tabstat tabu tabul tabula tabulat tabulate te tempfile tempname tempvar tes test testnl testparm teststd tetrachoric time_it timer tis tob tobi tobit tobit_p tobit_sw token tokeni tokeniz tokenize tostring total translate translator transmap treat_ll treatr_p treatreg trim trimfill trnb_cons trnb_mean trpoiss_d2 trunc_ll truncr_p truncreg tsappend tset tsfill tsline tsline_ex tsreport tsrevar tsrline tsset tssmooth tsunab ttest ttesti tut_chk tut_wait tutorial tw tware_st two twoway twoway__fpfit_serset twoway__function_gen twoway__histogram_gen twoway__ipoint_serset twoway__ipoints_serset twoway__kdensity_gen twoway__lfit_serset twoway__normgen_gen twoway__pci_serset twoway__qfit_serset twoway__scatteri_serset twoway__sunflower_gen twoway_ksm_serset ty typ type typeof u|0 unab unabbrev unabcmd update us use uselabel var var_mkcompanion var_p varbasic varfcast vargranger varirf varirf_add varirf_cgraph varirf_create varirf_ctable varirf_describe varirf_dir varirf_drop varirf_erase varirf_graph varirf_ograph varirf_rename varirf_set varirf_table varlist varlmar varnorm varsoc varstable varstable_w varstable_w2 varwle vce vec vec_fevd vec_mkphi vec_p vec_p_w vecirf_create veclmar veclmar_w vecnorm vecnorm_w vecrank vecstable verinst vers versi versio version view viewsource vif vwls wdatetof webdescribe webseek webuse weib1_lf weib2_lf weib_lf weib_lf0 weibhet_glf weibhet_glf_sh weibhet_glfa weibhet_glfa_sh weibhet_gp weibhet_ilf weibhet_ilf_sh weibhet_ilfa weibhet_ilfa_sh weibhet_ip weibu_sw weibul_p weibull weibull_c weibull_s weibullhet wh whelp whi which whil while wilc_st wilcoxon win wind windo window winexec wntestb wntestb_7 wntestq xchart xchart_7 xcorr xcorr_7 xi xi_6 xmlsav xmlsave xmluse xpose xsh xshe xshel xshell xt_iis xt_tis xtab_p xtabond xtbin_p xtclog xtcloglog xtcloglog_8 xtcloglog_d2 xtcloglog_pa_p xtcloglog_re_p xtcnt_p xtcorr xtdata xtdes xtfront_p xtfrontier xtgee xtgee_elink xtgee_estat xtgee_makeivar xtgee_p xtgee_plink xtgls xtgls_p xthaus xthausman xtht_p xthtaylor xtile xtint_p xtintreg xtintreg_8 xtintreg_d2 xtintreg_p xtivp_1 xtivp_2 xtivreg xtline xtline_ex xtlogit xtlogit_8 xtlogit_d2 xtlogit_fe_p xtlogit_pa_p xtlogit_re_p xtmixed xtmixed_estat xtmixed_p xtnb_fe xtnb_lf xtnbreg xtnbreg_pa_p xtnbreg_refe_p xtpcse xtpcse_p xtpois xtpoisson xtpoisson_d2 xtpoisson_pa_p xtpoisson_refe_p xtpred xtprobit xtprobit_8 xtprobit_d2 xtprobit_re_p xtps_fe xtps_lf xtps_ren xtps_ren_8 xtrar_p xtrc xtrc_p xtrchh xtrefe_p xtreg xtreg_be xtreg_fe xtreg_ml xtreg_pa_p xtreg_re xtregar xtrere_p xtset xtsf_ll xtsf_llti xtsum xttab xttest0 xttobit xttobit_8 xttobit_p xttrans yx yxview__barlike_draw yxview_area_draw yxview_bar_draw yxview_dot_draw yxview_dropline_draw yxview_function_draw yxview_iarrow_draw yxview_ilabels_draw yxview_normal_draw yxview_pcarrow_draw yxview_pcbarrow_draw yxview_pccapsym_draw yxview_pcscatter_draw yxview_pcspike_draw yxview_rarea_draw yxview_rbar_draw yxview_rbarm_draw yxview_rcap_draw yxview_rcapsym_draw yxview_rconnected_draw yxview_rline_draw yxview_rscatter_draw yxview_rspike_draw yxview_spike_draw yxview_sunflower_draw zap_s zinb zinb_llf zinb_plf zip zip_llf zip_p zip_plf zt_ct_5 zt_hc_5 zt_hcd_5 zt_is_5 zt_iss_5 zt_sho_5 zt_smp_5 ztbase_5 ztcox_5 ztdes_5 ztereg_5 ztfill_5 ztgen_5 ztir_5 ztjoin_5 ztnb ztnb_p ztp ztp_p zts_5 ztset_5 ztspli_5 ztsum_5 zttoct_5 ztvary_5 ztweib_5', contains: [ { className: 'symbol', begin: /`[a-zA-Z0-9_]+'/ }, { className: 'variable', begin: /\$\{?[a-zA-Z0-9_]+\}?/ }, { className: 'string', variants: [ {begin: '`"[^\r\n]*?"\''}, {begin: '"[^\r\n"]*"'} ] }, { className: 'built_in', variants: [ { begin: '\\b(abs|acos|asin|atan|atan2|atanh|ceil|cloglog|comb|cos|digamma|exp|floor|invcloglog|invlogit|ln|lnfact|lnfactorial|lngamma|log|log10|max|min|mod|reldif|round|sign|sin|sqrt|sum|tan|tanh|trigamma|trunc|betaden|Binomial|binorm|binormal|chi2|chi2tail|dgammapda|dgammapdada|dgammapdadx|dgammapdx|dgammapdxdx|F|Fden|Ftail|gammaden|gammap|ibeta|invbinomial|invchi2|invchi2tail|invF|invFtail|invgammap|invibeta|invnchi2|invnFtail|invnibeta|invnorm|invnormal|invttail|nbetaden|nchi2|nFden|nFtail|nibeta|norm|normal|normalden|normd|npnchi2|tden|ttail|uniform|abbrev|char|index|indexnot|length|lower|ltrim|match|plural|proper|real|regexm|regexr|regexs|reverse|rtrim|string|strlen|strlower|strltrim|strmatch|strofreal|strpos|strproper|strreverse|strrtrim|strtrim|strupper|subinstr|subinword|substr|trim|upper|word|wordcount|_caller|autocode|byteorder|chop|clip|cond|e|epsdouble|epsfloat|group|inlist|inrange|irecode|matrix|maxbyte|maxdouble|maxfloat|maxint|maxlong|mi|minbyte|mindouble|minfloat|minint|minlong|missing|r|recode|replay|return|s|scalar|d|date|day|dow|doy|halfyear|mdy|month|quarter|week|year|d|daily|dofd|dofh|dofm|dofq|dofw|dofy|h|halfyearly|hofd|m|mofd|monthly|q|qofd|quarterly|tin|twithin|w|weekly|wofd|y|yearly|yh|ym|yofd|yq|yw|cholesky|colnumb|colsof|corr|det|diag|diag0cnt|el|get|hadamard|I|inv|invsym|issym|issymmetric|J|matmissing|matuniform|mreldif|nullmat|rownumb|rowsof|sweep|syminv|trace|vec|vecdiag)(?=\\(|$)' } ] }, hljs.COMMENT('^[ \t]*\\*.*$', false), hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE ] }; } },{name:"step21",create:/* Language: STEP Part 21 Contributors: Adam Joseph Cook Description: Syntax highlighter for STEP Part 21 files (ISO 10303-21). */ function(hljs) { var STEP21_IDENT_RE = '[A-Z_][A-Z0-9_.]*'; var STEP21_KEYWORDS = { keyword: 'HEADER ENDSEC DATA' }; var STEP21_START = { className: 'meta', begin: 'ISO-10303-21;', relevance: 10 }; var STEP21_CLOSE = { className: 'meta', begin: 'END-ISO-10303-21;', relevance: 10 }; return { aliases: ['p21', 'step', 'stp'], case_insensitive: true, // STEP 21 is case insensitive in theory, in practice all non-comments are capitalized. lexemes: STEP21_IDENT_RE, keywords: STEP21_KEYWORDS, contains: [ STEP21_START, STEP21_CLOSE, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.COMMENT('/\\*\\*!', '\\*/'), hljs.C_NUMBER_MODE, hljs.inherit(hljs.APOS_STRING_MODE, {illegal: null}), hljs.inherit(hljs.QUOTE_STRING_MODE, {illegal: null}), { className: 'string', begin: "'", end: "'" }, { className: 'symbol', variants: [ { begin: '#', end: '\\d+', illegal: '\\W' } ] } ] }; } },{name:"stylus",create:/* Language: Stylus Author: Bryant Williams Description: Stylus (https://github.com/LearnBoost/stylus/) Category: css */ function(hljs) { var VARIABLE = { className: 'variable', begin: '\\$' + hljs.IDENT_RE }; var HEX_COLOR = { className: 'number', begin: '#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})' }; var AT_KEYWORDS = [ 'charset', 'css', 'debug', 'extend', 'font-face', 'for', 'import', 'include', 'media', 'mixin', 'page', 'warn', 'while' ]; var PSEUDO_SELECTORS = [ 'after', 'before', 'first-letter', 'first-line', 'active', 'first-child', 'focus', 'hover', 'lang', 'link', 'visited' ]; var TAGS = [ 'a', 'abbr', 'address', 'article', 'aside', 'audio', 'b', 'blockquote', 'body', 'button', 'canvas', 'caption', 'cite', 'code', 'dd', 'del', 'details', 'dfn', 'div', 'dl', 'dt', 'em', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'mark', 'menu', 'nav', 'object', 'ol', 'p', 'q', 'quote', 'samp', 'section', 'span', 'strong', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'ul', 'var', 'video' ]; var TAG_END = '[\\.\\s\\n\\[\\:,]'; var ATTRIBUTES = [ 'align-content', 'align-items', 'align-self', 'animation', 'animation-delay', 'animation-direction', 'animation-duration', 'animation-fill-mode', 'animation-iteration-count', 'animation-name', 'animation-play-state', 'animation-timing-function', 'auto', 'backface-visibility', 'background', 'background-attachment', 'background-clip', 'background-color', 'background-image', 'background-origin', 'background-position', 'background-repeat', 'background-size', 'border', 'border-bottom', 'border-bottom-color', 'border-bottom-left-radius', 'border-bottom-right-radius', 'border-bottom-style', 'border-bottom-width', 'border-collapse', 'border-color', 'border-image', 'border-image-outset', 'border-image-repeat', 'border-image-slice', 'border-image-source', 'border-image-width', 'border-left', 'border-left-color', 'border-left-style', 'border-left-width', 'border-radius', 'border-right', 'border-right-color', 'border-right-style', 'border-right-width', 'border-spacing', 'border-style', 'border-top', 'border-top-color', 'border-top-left-radius', 'border-top-right-radius', 'border-top-style', 'border-top-width', 'border-width', 'bottom', 'box-decoration-break', 'box-shadow', 'box-sizing', 'break-after', 'break-before', 'break-inside', 'caption-side', 'clear', 'clip', 'clip-path', 'color', 'column-count', 'column-fill', 'column-gap', 'column-rule', 'column-rule-color', 'column-rule-style', 'column-rule-width', 'column-span', 'column-width', 'columns', 'content', 'counter-increment', 'counter-reset', 'cursor', 'direction', 'display', 'empty-cells', 'filter', 'flex', 'flex-basis', 'flex-direction', 'flex-flow', 'flex-grow', 'flex-shrink', 'flex-wrap', 'float', 'font', 'font-family', 'font-feature-settings', 'font-kerning', 'font-language-override', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-variant-ligatures', 'font-weight', 'height', 'hyphens', 'icon', 'image-orientation', 'image-rendering', 'image-resolution', 'ime-mode', 'inherit', 'initial', 'justify-content', 'left', 'letter-spacing', 'line-height', 'list-style', 'list-style-image', 'list-style-position', 'list-style-type', 'margin', 'margin-bottom', 'margin-left', 'margin-right', 'margin-top', 'marks', 'mask', 'max-height', 'max-width', 'min-height', 'min-width', 'nav-down', 'nav-index', 'nav-left', 'nav-right', 'nav-up', 'none', 'normal', 'object-fit', 'object-position', 'opacity', 'order', 'orphans', 'outline', 'outline-color', 'outline-offset', 'outline-style', 'outline-width', 'overflow', 'overflow-wrap', 'overflow-x', 'overflow-y', 'padding', 'padding-bottom', 'padding-left', 'padding-right', 'padding-top', 'page-break-after', 'page-break-before', 'page-break-inside', 'perspective', 'perspective-origin', 'pointer-events', 'position', 'quotes', 'resize', 'right', 'tab-size', 'table-layout', 'text-align', 'text-align-last', 'text-decoration', 'text-decoration-color', 'text-decoration-line', 'text-decoration-style', 'text-indent', 'text-overflow', 'text-rendering', 'text-shadow', 'text-transform', 'text-underline-position', 'top', 'transform', 'transform-origin', 'transform-style', 'transition', 'transition-delay', 'transition-duration', 'transition-property', 'transition-timing-function', 'unicode-bidi', 'vertical-align', 'visibility', 'white-space', 'widows', 'width', 'word-break', 'word-spacing', 'word-wrap', 'z-index' ]; // illegals var ILLEGAL = [ '\\?', '(\\bReturn\\b)', // monkey '(\\bEnd\\b)', // monkey '(\\bend\\b)', // vbscript '(\\bdef\\b)', // gradle ';', // a whole lot of languages '#\\s', // markdown '\\*\\s', // markdown '===\\s', // markdown '\\|', '%', // prolog ]; return { aliases: ['styl'], case_insensitive: false, keywords: 'if else for in', illegal: '(' + ILLEGAL.join('|') + ')', contains: [ // strings hljs.QUOTE_STRING_MODE, hljs.APOS_STRING_MODE, // comments hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, // hex colors HEX_COLOR, // class tag { begin: '\\.[a-zA-Z][a-zA-Z0-9_-]*' + TAG_END, returnBegin: true, contains: [ {className: 'selector-class', begin: '\\.[a-zA-Z][a-zA-Z0-9_-]*'} ] }, // id tag { begin: '\\#[a-zA-Z][a-zA-Z0-9_-]*' + TAG_END, returnBegin: true, contains: [ {className: 'selector-id', begin: '\\#[a-zA-Z][a-zA-Z0-9_-]*'} ] }, // tags { begin: '\\b(' + TAGS.join('|') + ')' + TAG_END, returnBegin: true, contains: [ {className: 'selector-tag', begin: '\\b[a-zA-Z][a-zA-Z0-9_-]*'} ] }, // psuedo selectors { begin: '&?:?:\\b(' + PSEUDO_SELECTORS.join('|') + ')' + TAG_END }, // @ keywords { begin: '\@(' + AT_KEYWORDS.join('|') + ')\\b' }, // variables VARIABLE, // dimension hljs.CSS_NUMBER_MODE, // number hljs.NUMBER_MODE, // functions // - only from beginning of line + whitespace { className: 'function', begin: '^[a-zA-Z][a-zA-Z0-9_\-]*\\(.*\\)', illegal: '[\\n]', returnBegin: true, contains: [ {className: 'title', begin: '\\b[a-zA-Z][a-zA-Z0-9_\-]*'}, { className: 'params', begin: /\(/, end: /\)/, contains: [ HEX_COLOR, VARIABLE, hljs.APOS_STRING_MODE, hljs.CSS_NUMBER_MODE, hljs.NUMBER_MODE, hljs.QUOTE_STRING_MODE ] } ] }, // attributes // - only from beginning of line + whitespace // - must have whitespace after it { className: 'attribute', begin: '\\b(' + ATTRIBUTES.reverse().join('|') + ')\\b', starts: { // value container end: /;|$/, contains: [ HEX_COLOR, VARIABLE, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, hljs.CSS_NUMBER_MODE, hljs.NUMBER_MODE, hljs.C_BLOCK_COMMENT_MODE ], illegal: /\./, relevance: 0 } } ] }; } },{name:"subunit",create:/* Language: SubUnit Author: Sergey Bronnikov Website: https://bronevichok.ru/ */ function(hljs) { var DETAILS = { className: 'string', begin: '\\[\n(multipart)?', end: '\\]\n' }; var TIME = { className: 'string', begin: '\\d{4}-\\d{2}-\\d{2}(\\s+)\\d{2}:\\d{2}:\\d{2}\.\\d+Z' }; var PROGRESSVALUE = { className: 'string', begin: '(\\+|-)\\d+' }; var KEYWORDS = { className: 'keyword', relevance: 10, variants: [ { begin: '^(test|testing|success|successful|failure|error|skip|xfail|uxsuccess)(:?)\\s+(test)?' }, { begin: '^progress(:?)(\\s+)?(pop|push)?' }, { begin: '^tags:' }, { begin: '^time:' } ], }; return { case_insensitive: true, contains: [ DETAILS, TIME, PROGRESSVALUE, KEYWORDS ] }; } },{name:"swift",create:/* Language: Swift Author: Chris Eidhof Contributors: Nate Cook , Alexander Lichter Category: system */ function(hljs) { var SWIFT_KEYWORDS = { keyword: '#available #colorLiteral #column #else #elseif #endif #file ' + '#fileLiteral #function #if #imageLiteral #line #selector #sourceLocation ' + '_ __COLUMN__ __FILE__ __FUNCTION__ __LINE__ Any as as! as? associatedtype ' + 'associativity break case catch class continue convenience default defer deinit didSet do ' + 'dynamic dynamicType else enum extension fallthrough false fileprivate final for func ' + 'get guard if import in indirect infix init inout internal is lazy left let ' + 'mutating nil none nonmutating open operator optional override postfix precedence ' + 'prefix private protocol Protocol public repeat required rethrows return ' + 'right self Self set static struct subscript super switch throw throws true ' + 'try try! try? Type typealias unowned var weak where while willSet', literal: 'true false nil', built_in: 'abs advance alignof alignofValue anyGenerator assert assertionFailure ' + 'bridgeFromObjectiveC bridgeFromObjectiveCUnconditional bridgeToObjectiveC ' + 'bridgeToObjectiveCUnconditional c contains count countElements countLeadingZeros ' + 'debugPrint debugPrintln distance dropFirst dropLast dump encodeBitsAsWords ' + 'enumerate equal fatalError filter find getBridgedObjectiveCType getVaList ' + 'indices insertionSort isBridgedToObjectiveC isBridgedVerbatimToObjectiveC ' + 'isUniquelyReferenced isUniquelyReferencedNonObjC join lazy lexicographicalCompare ' + 'map max maxElement min minElement numericCast overlaps partition posix ' + 'precondition preconditionFailure print println quickSort readLine reduce reflect ' + 'reinterpretCast reverse roundUpToAlignment sizeof sizeofValue sort split ' + 'startsWith stride strideof strideofValue swap toString transcode ' + 'underestimateCount unsafeAddressOf unsafeBitCast unsafeDowncast unsafeUnwrap ' + 'unsafeReflect withExtendedLifetime withObjectAtPlusZero withUnsafePointer ' + 'withUnsafePointerToObject withUnsafeMutablePointer withUnsafeMutablePointers ' + 'withUnsafePointer withUnsafePointers withVaList zip' }; var TYPE = { className: 'type', begin: '\\b[A-Z][\\w\u00C0-\u02B8\']*', relevance: 0 }; // slightly more special to swift var OPTIONAL_USING_TYPE = { className: 'type', begin: '\\b[A-Z][\\w\u00C0-\u02B8\']*[!?]' } var BLOCK_COMMENT = hljs.COMMENT( '/\\*', '\\*/', { contains: ['self'] } ); var SUBST = { className: 'subst', begin: /\\\(/, end: '\\)', keywords: SWIFT_KEYWORDS, contains: [] // assigned later }; var STRING = { className: 'string', contains: [hljs.BACKSLASH_ESCAPE, SUBST], variants: [ {begin: /"""/, end: /"""/}, {begin: /"/, end: /"/}, ] }; var NUMBERS = { className: 'number', begin: '\\b([\\d_]+(\\.[\\deE_]+)?|0x[a-fA-F0-9_]+(\\.[a-fA-F0-9p_]+)?|0b[01_]+|0o[0-7_]+)\\b', relevance: 0 }; SUBST.contains = [NUMBERS]; return { keywords: SWIFT_KEYWORDS, contains: [ STRING, hljs.C_LINE_COMMENT_MODE, BLOCK_COMMENT, OPTIONAL_USING_TYPE, TYPE, NUMBERS, { className: 'function', beginKeywords: 'func', end: '{', excludeEnd: true, contains: [ hljs.inherit(hljs.TITLE_MODE, { begin: /[A-Za-z$_][0-9A-Za-z$_]*/ }), { begin: // }, { className: 'params', begin: /\(/, end: /\)/, endsParent: true, keywords: SWIFT_KEYWORDS, contains: [ 'self', NUMBERS, STRING, hljs.C_BLOCK_COMMENT_MODE, {begin: ':'} // relevance booster ], illegal: /["']/ } ], illegal: /\[|%/ }, { className: 'class', beginKeywords: 'struct protocol class extension enum', keywords: SWIFT_KEYWORDS, end: '\\{', excludeEnd: true, contains: [ hljs.inherit(hljs.TITLE_MODE, {begin: /[A-Za-z$_][\u00C0-\u02B80-9A-Za-z$_]*/}) ] }, { className: 'meta', // @attributes begin: '(@discardableResult|@warn_unused_result|@exported|@lazy|@noescape|' + '@NSCopying|@NSManaged|@objc|@objcMembers|@convention|@required|' + '@noreturn|@IBAction|@IBDesignable|@IBInspectable|@IBOutlet|' + '@infix|@prefix|@postfix|@autoclosure|@testable|@available|' + '@nonobjc|@NSApplicationMain|@UIApplicationMain)' }, { beginKeywords: 'import', end: /$/, contains: [hljs.C_LINE_COMMENT_MODE, BLOCK_COMMENT] } ] }; } },{name:"taggerscript",create:/* Language: Tagger Script Author: Philipp Wolfer Description: Syntax Highlighting for the Tagger Script as used by MusicBrainz Picard. */ function(hljs) { var COMMENT = { className: 'comment', begin: /\$noop\(/, end: /\)/, contains: [{ begin: /\(/, end: /\)/, contains: ['self', { begin: /\\./ }] }], relevance: 10 }; var FUNCTION = { className: 'keyword', begin: /\$(?!noop)[a-zA-Z][_a-zA-Z0-9]*/, end: /\(/, excludeEnd: true }; var VARIABLE = { className: 'variable', begin: /%[_a-zA-Z0-9:]*/, end: '%' }; var ESCAPE_SEQUENCE = { className: 'symbol', begin: /\\./ }; return { contains: [ COMMENT, FUNCTION, VARIABLE, ESCAPE_SEQUENCE ] }; } },{name:"tap",create:/* Language: Test Anything Protocol Requires: yaml.js Author: Sergey Bronnikov Website: https://bronevichok.ru/ */ function(hljs) { return { case_insensitive: true, contains: [ hljs.HASH_COMMENT_MODE, // version of format and total amount of testcases { className: 'meta', variants: [ { begin: '^TAP version (\\d+)$' }, { begin: '^1\\.\\.(\\d+)$' } ], }, // YAML block { begin: '(\s+)?---$', end: '\\.\\.\\.$', subLanguage: 'yaml', relevance: 0 }, // testcase number { className: 'number', begin: ' (\\d+) ' }, // testcase status and description { className: 'symbol', variants: [ { begin: '^ok' }, { begin: '^not ok' } ], }, ] }; } },{name:"tcl",create:/* Language: Tcl Author: Radek Liska */ function(hljs) { return { aliases: ['tk'], keywords: 'after append apply array auto_execok auto_import auto_load auto_mkindex ' + 'auto_mkindex_old auto_qualify auto_reset bgerror binary break catch cd chan clock ' + 'close concat continue dde dict encoding eof error eval exec exit expr fblocked ' + 'fconfigure fcopy file fileevent filename flush for foreach format gets glob global ' + 'history http if incr info interp join lappend|10 lassign|10 lindex|10 linsert|10 list ' + 'llength|10 load lrange|10 lrepeat|10 lreplace|10 lreverse|10 lsearch|10 lset|10 lsort|10 '+ 'mathfunc mathop memory msgcat namespace open package parray pid pkg::create pkg_mkIndex '+ 'platform platform::shell proc puts pwd read refchan regexp registry regsub|10 rename '+ 'return safe scan seek set socket source split string subst switch tcl_endOfWord '+ 'tcl_findLibrary tcl_startOfNextWord tcl_startOfPreviousWord tcl_wordBreakAfter '+ 'tcl_wordBreakBefore tcltest tclvars tell time tm trace unknown unload unset update '+ 'uplevel upvar variable vwait while', contains: [ hljs.COMMENT(';[ \\t]*#', '$'), hljs.COMMENT('^[ \\t]*#', '$'), { beginKeywords: 'proc', end: '[\\{]', excludeEnd: true, contains: [ { className: 'title', begin: '[ \\t\\n\\r]+(::)?[a-zA-Z_]((::)?[a-zA-Z0-9_])*', end: '[ \\t\\n\\r]', endsWithParent: true, excludeEnd: true } ] }, { excludeEnd: true, variants: [ { begin: '\\$(\\{)?(::)?[a-zA-Z_]((::)?[a-zA-Z0-9_])*\\(([a-zA-Z0-9_])*\\)', end: '[^a-zA-Z0-9_\\}\\$]' }, { begin: '\\$(\\{)?(::)?[a-zA-Z_]((::)?[a-zA-Z0-9_])*', end: '(\\))?[^a-zA-Z0-9_\\}\\$]' } ] }, { className: 'string', contains: [hljs.BACKSLASH_ESCAPE], variants: [ hljs.inherit(hljs.QUOTE_STRING_MODE, {illegal: null}) ] }, { className: 'number', variants: [hljs.BINARY_NUMBER_MODE, hljs.C_NUMBER_MODE] } ] } } },{name:"tex",create:/* Language: TeX Author: Vladimir Moskva Website: http://fulc.ru/ Category: markup */ function(hljs) { var COMMAND = { className: 'tag', begin: /\\/, relevance: 0, contains: [ { className: 'name', variants: [ {begin: /[a-zA-Z\u0430-\u044f\u0410-\u042f]+[*]?/}, {begin: /[^a-zA-Z\u0430-\u044f\u0410-\u042f0-9]/} ], starts: { endsWithParent: true, relevance: 0, contains: [ { className: 'string', // because it looks like attributes in HTML tags variants: [ {begin: /\[/, end: /\]/}, {begin: /\{/, end: /\}/} ] }, { begin: /\s*=\s*/, endsWithParent: true, relevance: 0, contains: [ { className: 'number', begin: /-?\d*\.?\d+(pt|pc|mm|cm|in|dd|cc|ex|em)?/ } ] } ] } } ] }; return { contains: [ COMMAND, { className: 'formula', contains: [COMMAND], relevance: 0, variants: [ {begin: /\$\$/, end: /\$\$/}, {begin: /\$/, end: /\$/} ] }, hljs.COMMENT( '%', '$', { relevance: 0 } ) ] }; } },{name:"thrift",create:/* Language: Thrift Author: Oleg Efimov Description: Thrift message definition format Category: protocols */ function(hljs) { var BUILT_IN_TYPES = 'bool byte i16 i32 i64 double string binary'; return { keywords: { keyword: 'namespace const typedef struct enum service exception void oneway set list map required optional', built_in: BUILT_IN_TYPES, literal: 'true false' }, contains: [ hljs.QUOTE_STRING_MODE, hljs.NUMBER_MODE, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, { className: 'class', beginKeywords: 'struct enum service exception', end: /\{/, illegal: /\n/, contains: [ hljs.inherit(hljs.TITLE_MODE, { starts: {endsWithParent: true, excludeEnd: true} // hack: eating everything after the first title }) ] }, { begin: '\\b(set|list|map)\\s*<', end: '>', keywords: BUILT_IN_TYPES, contains: ['self'] } ] }; } },{name:"tp",create:/* Language: TP Author: Jay Strybis Description: FANUC TP programming language (TPP). */ function(hljs) { var TPID = { className: 'number', begin: '[1-9][0-9]*', /* no leading zeros */ relevance: 0 }; var TPLABEL = { className: 'symbol', begin: ':[^\\]]+' }; var TPDATA = { className: 'built_in', begin: '(AR|P|PAYLOAD|PR|R|SR|RSR|LBL|VR|UALM|MESSAGE|UTOOL|UFRAME|TIMER|' + 'TIMER_OVERFLOW|JOINT_MAX_SPEED|RESUME_PROG|DIAG_REC)\\[', end: '\\]', contains: [ 'self', TPID, TPLABEL ] }; var TPIO = { className: 'built_in', begin: '(AI|AO|DI|DO|F|RI|RO|UI|UO|GI|GO|SI|SO)\\[', end: '\\]', contains: [ 'self', TPID, hljs.QUOTE_STRING_MODE, /* for pos section at bottom */ TPLABEL ] }; return { keywords: { keyword: 'ABORT ACC ADJUST AND AP_LD BREAK CALL CNT COL CONDITION CONFIG DA DB ' + 'DIV DETECT ELSE END ENDFOR ERR_NUM ERROR_PROG FINE FOR GP GUARD INC ' + 'IF JMP LINEAR_MAX_SPEED LOCK MOD MONITOR OFFSET Offset OR OVERRIDE ' + 'PAUSE PREG PTH RT_LD RUN SELECT SKIP Skip TA TB TO TOOL_OFFSET ' + 'Tool_Offset UF UT UFRAME_NUM UTOOL_NUM UNLOCK WAIT X Y Z W P R STRLEN ' + 'SUBSTR FINDSTR VOFFSET PROG ATTR MN POS', literal: 'ON OFF max_speed LPOS JPOS ENABLE DISABLE START STOP RESET' }, contains: [ TPDATA, TPIO, { className: 'keyword', begin: '/(PROG|ATTR|MN|POS|END)\\b' }, { /* this is for cases like ,CALL */ className: 'keyword', begin: '(CALL|RUN|POINT_LOGIC|LBL)\\b' }, { /* this is for cases like CNT100 where the default lexemes do not * separate the keyword and the number */ className: 'keyword', begin: '\\b(ACC|CNT|Skip|Offset|PSPD|RT_LD|AP_LD|Tool_Offset)' }, { /* to catch numbers that do not have a word boundary on the left */ className: 'number', begin: '\\d+(sec|msec|mm/sec|cm/min|inch/min|deg/sec|mm|in|cm)?\\b', relevance: 0 }, hljs.COMMENT('//', '[;$]'), hljs.COMMENT('!', '[;$]'), hljs.COMMENT('--eg:', '$'), hljs.QUOTE_STRING_MODE, { className: 'string', begin: '\'', end: '\'' }, hljs.C_NUMBER_MODE, { className: 'variable', begin: '\\$[A-Za-z0-9_]+' } ] }; } },{name:"twig",create:/* Language: Twig Requires: xml.js Author: Luke Holder Description: Twig is a templating language for PHP Category: template */ function(hljs) { var PARAMS = { className: 'params', begin: '\\(', end: '\\)' }; var FUNCTION_NAMES = 'attribute block constant cycle date dump include ' + 'max min parent random range source template_from_string'; var FUNCTIONS = { beginKeywords: FUNCTION_NAMES, keywords: {name: FUNCTION_NAMES}, relevance: 0, contains: [ PARAMS ] }; var FILTER = { begin: /\|[A-Za-z_]+:?/, keywords: 'abs batch capitalize convert_encoding date date_modify default ' + 'escape first format join json_encode keys last length lower ' + 'merge nl2br number_format raw replace reverse round slice sort split ' + 'striptags title trim upper url_encode', contains: [ FUNCTIONS ] }; var TAGS = 'autoescape block do embed extends filter flush for ' + 'if import include macro sandbox set spaceless use verbatim'; TAGS = TAGS + ' ' + TAGS.split(' ').map(function(t){return 'end' + t}).join(' '); return { aliases: ['craftcms'], case_insensitive: true, subLanguage: 'xml', contains: [ hljs.COMMENT(/\{#/, /#}/), { className: 'template-tag', begin: /\{%/, end: /%}/, contains: [ { className: 'name', begin: /\w+/, keywords: TAGS, starts: { endsWithParent: true, contains: [FILTER, FUNCTIONS], relevance: 0 } } ] }, { className: 'template-variable', begin: /\{\{/, end: /}}/, contains: ['self', FILTER, FUNCTIONS] } ] }; } },{name:"typescript",create:/* Language: TypeScript Author: Panu Horsmalahti Contributors: Ike Ku Description: TypeScript is a strict superset of JavaScript Category: scripting */ function(hljs) { var JS_IDENT_RE = '[A-Za-z$_][0-9A-Za-z$_]*'; var KEYWORDS = { keyword: 'in if for while finally var new function do return void else break catch ' + 'instanceof with throw case default try this switch continue typeof delete ' + 'let yield const class public private protected get set super ' + 'static implements enum export import declare type namespace abstract ' + 'as from extends async await', literal: 'true false null undefined NaN Infinity', built_in: 'eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent ' + 'encodeURI encodeURIComponent escape unescape Object Function Boolean Error ' + 'EvalError InternalError RangeError ReferenceError StopIteration SyntaxError ' + 'TypeError URIError Number Math Date String RegExp Array Float32Array ' + 'Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array ' + 'Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require ' + 'module console window document any number boolean string void Promise' }; var DECORATOR = { className: 'meta', begin: '@' + JS_IDENT_RE, }; var ARGS = { begin: '\\(', end: /\)/, keywords: KEYWORDS, contains: [ 'self', hljs.QUOTE_STRING_MODE, hljs.APOS_STRING_MODE, hljs.NUMBER_MODE ] }; var PARAMS = { className: 'params', begin: /\(/, end: /\)/, excludeBegin: true, excludeEnd: true, keywords: KEYWORDS, contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, DECORATOR, ARGS ] }; var NUMBER = { className: 'number', variants: [ { begin: '\\b(0[bB][01]+)' }, { begin: '\\b(0[oO][0-7]+)' }, { begin: hljs.C_NUMBER_RE } ], relevance: 0 }; var SUBST = { className: 'subst', begin: '\\$\\{', end: '\\}', keywords: KEYWORDS, contains: [] // defined later }; var HTML_TEMPLATE = { begin: 'html`', end: '', starts: { end: '`', returnEnd: false, contains: [ hljs.BACKSLASH_ESCAPE, SUBST ], subLanguage: 'xml', } }; var CSS_TEMPLATE = { begin: 'css`', end: '', starts: { end: '`', returnEnd: false, contains: [ hljs.BACKSLASH_ESCAPE, SUBST ], subLanguage: 'css', } }; var TEMPLATE_STRING = { className: 'string', begin: '`', end: '`', contains: [ hljs.BACKSLASH_ESCAPE, SUBST ] }; SUBST.contains = [ hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, HTML_TEMPLATE, CSS_TEMPLATE, TEMPLATE_STRING, NUMBER, hljs.REGEXP_MODE ]; return { aliases: ['ts'], keywords: KEYWORDS, contains: [ { className: 'meta', begin: /^\s*['"]use strict['"]/ }, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, HTML_TEMPLATE, CSS_TEMPLATE, TEMPLATE_STRING, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, NUMBER, { // "value" container begin: '(' + hljs.RE_STARTERS_RE + '|\\b(case|return|throw)\\b)\\s*', keywords: 'return throw case', contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.REGEXP_MODE, { className: 'function', begin: '(\\(.*?\\)|' + hljs.IDENT_RE + ')\\s*=>', returnBegin: true, end: '\\s*=>', contains: [ { className: 'params', variants: [ { begin: hljs.IDENT_RE }, { begin: /\(\s*\)/, }, { begin: /\(/, end: /\)/, excludeBegin: true, excludeEnd: true, keywords: KEYWORDS, contains: [ 'self', hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE ] } ] } ] } ], relevance: 0 }, { className: 'function', begin: 'function', end: /[\{;]/, excludeEnd: true, keywords: KEYWORDS, contains: [ 'self', hljs.inherit(hljs.TITLE_MODE, { begin: JS_IDENT_RE }), PARAMS ], illegal: /%/, relevance: 0 // () => {} is more typical in TypeScript }, { beginKeywords: 'constructor', end: /\{/, excludeEnd: true, contains: [ 'self', PARAMS ] }, { // prevent references like module.id from being higlighted as module definitions begin: /module\./, keywords: { built_in: 'module' }, relevance: 0 }, { beginKeywords: 'module', end: /\{/, excludeEnd: true }, { beginKeywords: 'interface', end: /\{/, excludeEnd: true, keywords: 'interface extends' }, { begin: /\$[(.]/ // relevance booster for a pattern common to JS libs: `$(something)` and `$.something` }, { begin: '\\.' + hljs.IDENT_RE, relevance: 0 // hack: prevents detection of keywords after dots }, DECORATOR, ARGS ] }; } },{name:"vala",create:/* Language: Vala Author: Antono Vasiljev Description: Vala is a new programming language that aims to bring modern programming language features to GNOME developers without imposing any additional runtime requirements and without using a different ABI compared to applications and libraries written in C. */ function(hljs) { return { keywords: { keyword: // Value types 'char uchar unichar int uint long ulong short ushort int8 int16 int32 int64 uint8 ' + 'uint16 uint32 uint64 float double bool struct enum string void ' + // Reference types 'weak unowned owned ' + // Modifiers 'async signal static abstract interface override virtual delegate ' + // Control Structures 'if while do for foreach else switch case break default return try catch ' + // Visibility 'public private protected internal ' + // Other 'using new this get set const stdout stdin stderr var', built_in: 'DBus GLib CCode Gee Object Gtk Posix', literal: 'false true null' }, contains: [ { className: 'class', beginKeywords: 'class interface namespace', end: '{', excludeEnd: true, illegal: '[^,:\\n\\s\\.]', contains: [ hljs.UNDERSCORE_TITLE_MODE ] }, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, { className: 'string', begin: '"""', end: '"""', relevance: 5 }, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, hljs.C_NUMBER_MODE, { className: 'meta', begin: '^#', end: '$', relevance: 2 } ] }; } },{name:"vbnet",create:/* Language: VB.NET Author: Poren Chiang */ function(hljs) { return { aliases: ['vb'], case_insensitive: true, keywords: { keyword: 'addhandler addressof alias and andalso aggregate ansi as assembly auto binary by byref byval ' + /* a-b */ 'call case catch class compare const continue custom declare default delegate dim distinct do ' + /* c-d */ 'each equals else elseif end enum erase error event exit explicit finally for friend from function ' + /* e-f */ 'get global goto group handles if implements imports in inherits interface into is isfalse isnot istrue ' + /* g-i */ 'join key let lib like loop me mid mod module mustinherit mustoverride mybase myclass ' + /* j-m */ 'namespace narrowing new next not notinheritable notoverridable ' + /* n */ 'of off on operator option optional or order orelse overloads overridable overrides ' + /* o */ 'paramarray partial preserve private property protected public ' + /* p */ 'raiseevent readonly redim rem removehandler resume return ' + /* r */ 'select set shadows shared skip static step stop structure strict sub synclock ' + /* s */ 'take text then throw to try unicode until using when where while widening with withevents writeonly xor', /* t-x */ built_in: 'boolean byte cbool cbyte cchar cdate cdec cdbl char cint clng cobj csbyte cshort csng cstr ctype ' + /* b-c */ 'date decimal directcast double gettype getxmlnamespace iif integer long object ' + /* d-o */ 'sbyte short single string trycast typeof uinteger ulong ushort', /* s-u */ literal: 'true false nothing' }, illegal: '//|{|}|endif|gosub|variant|wend|^\\$ ', /* reserved deprecated keywords */ contains: [ hljs.inherit(hljs.QUOTE_STRING_MODE, {contains: [{begin: '""'}]}), hljs.COMMENT( '\'', '$', { returnBegin: true, contains: [ { className: 'doctag', begin: '\'\'\'|', contains: [hljs.PHRASAL_WORDS_MODE] }, { className: 'doctag', begin: '', contains: [hljs.PHRASAL_WORDS_MODE] } ] } ), hljs.C_NUMBER_MODE, { className: 'meta', begin: '#', end: '$', keywords: {'meta-keyword': 'if else elseif end region externalsource'} } ] }; } },{name:"vbscript-html",create:/* Language: VBScript in HTML Requires: xml.js, vbscript.js Author: Ivan Sagalaev Description: "Bridge" language defining fragments of VBScript in HTML within <% .. %> Category: scripting */ function(hljs) { return { subLanguage: 'xml', contains: [ { begin: '<%', end: '%>', subLanguage: 'vbscript' } ] }; } },{name:"vbscript",create:/* Language: VBScript Author: Nikita Ledyaev Contributors: Michal Gabrukiewicz Category: scripting */ function(hljs) { return { aliases: ['vbs'], case_insensitive: true, keywords: { keyword: 'call class const dim do loop erase execute executeglobal exit for each next function ' + 'if then else on error option explicit new private property let get public randomize ' + 'redim rem select case set stop sub while wend with end to elseif is or xor and not ' + 'class_initialize class_terminate default preserve in me byval byref step resume goto', built_in: 'lcase month vartype instrrev ubound setlocale getobject rgb getref string ' + 'weekdayname rnd dateadd monthname now day minute isarray cbool round formatcurrency ' + 'conversions csng timevalue second year space abs clng timeserial fixs len asc ' + 'isempty maths dateserial atn timer isobject filter weekday datevalue ccur isdate ' + 'instr datediff formatdatetime replace isnull right sgn array snumeric log cdbl hex ' + 'chr lbound msgbox ucase getlocale cos cdate cbyte rtrim join hour oct typename trim ' + 'strcomp int createobject loadpicture tan formatnumber mid scriptenginebuildversion ' + 'scriptengine split scriptengineminorversion cint sin datepart ltrim sqr ' + 'scriptenginemajorversion time derived eval date formatpercent exp inputbox left ascw ' + 'chrw regexp server response request cstr err', literal: 'true false null nothing empty' }, illegal: '//', contains: [ hljs.inherit(hljs.QUOTE_STRING_MODE, {contains: [{begin: '""'}]}), hljs.COMMENT( /'/, /$/, { relevance: 0 } ), hljs.C_NUMBER_MODE ] }; } },{name:"verilog",create:/* Language: Verilog Author: Jon Evans Contributors: Boone Severson Description: Verilog is a hardware description language used in electronic design automation to describe digital and mixed-signal systems. This highlighter supports Verilog and SystemVerilog through IEEE 1800-2012. */ function(hljs) { var SV_KEYWORDS = { keyword: 'accept_on alias always always_comb always_ff always_latch and assert assign ' + 'assume automatic before begin bind bins binsof bit break buf|0 bufif0 bufif1 ' + 'byte case casex casez cell chandle checker class clocking cmos config const ' + 'constraint context continue cover covergroup coverpoint cross deassign default ' + 'defparam design disable dist do edge else end endcase endchecker endclass ' + 'endclocking endconfig endfunction endgenerate endgroup endinterface endmodule ' + 'endpackage endprimitive endprogram endproperty endspecify endsequence endtable ' + 'endtask enum event eventually expect export extends extern final first_match for ' + 'force foreach forever fork forkjoin function generate|5 genvar global highz0 highz1 ' + 'if iff ifnone ignore_bins illegal_bins implements implies import incdir include ' + 'initial inout input inside instance int integer interconnect interface intersect ' + 'join join_any join_none large let liblist library local localparam logic longint ' + 'macromodule matches medium modport module nand negedge nettype new nexttime nmos ' + 'nor noshowcancelled not notif0 notif1 or output package packed parameter pmos ' + 'posedge primitive priority program property protected pull0 pull1 pulldown pullup ' + 'pulsestyle_ondetect pulsestyle_onevent pure rand randc randcase randsequence rcmos ' + 'real realtime ref reg reject_on release repeat restrict return rnmos rpmos rtran ' + 'rtranif0 rtranif1 s_always s_eventually s_nexttime s_until s_until_with scalared ' + 'sequence shortint shortreal showcancelled signed small soft solve specify specparam ' + 'static string strong strong0 strong1 struct super supply0 supply1 sync_accept_on ' + 'sync_reject_on table tagged task this throughout time timeprecision timeunit tran ' + 'tranif0 tranif1 tri tri0 tri1 triand trior trireg type typedef union unique unique0 ' + 'unsigned until until_with untyped use uwire var vectored virtual void wait wait_order ' + 'wand weak weak0 weak1 while wildcard wire with within wor xnor xor', literal: 'null', built_in: '$finish $stop $exit $fatal $error $warning $info $realtime $time $printtimescale ' + '$bitstoreal $bitstoshortreal $itor $signed $cast $bits $stime $timeformat ' + '$realtobits $shortrealtobits $rtoi $unsigned $asserton $assertkill $assertpasson ' + '$assertfailon $assertnonvacuouson $assertoff $assertcontrol $assertpassoff ' + '$assertfailoff $assertvacuousoff $isunbounded $sampled $fell $changed $past_gclk ' + '$fell_gclk $changed_gclk $rising_gclk $steady_gclk $coverage_control ' + '$coverage_get $coverage_save $set_coverage_db_name $rose $stable $past ' + '$rose_gclk $stable_gclk $future_gclk $falling_gclk $changing_gclk $display ' + '$coverage_get_max $coverage_merge $get_coverage $load_coverage_db $typename ' + '$unpacked_dimensions $left $low $increment $clog2 $ln $log10 $exp $sqrt $pow ' + '$floor $ceil $sin $cos $tan $countbits $onehot $isunknown $fatal $warning ' + '$dimensions $right $high $size $asin $acos $atan $atan2 $hypot $sinh $cosh ' + '$tanh $asinh $acosh $atanh $countones $onehot0 $error $info $random ' + '$dist_chi_square $dist_erlang $dist_exponential $dist_normal $dist_poisson ' + '$dist_t $dist_uniform $q_initialize $q_remove $q_exam $async$and$array ' + '$async$nand$array $async$or$array $async$nor$array $sync$and$array ' + '$sync$nand$array $sync$or$array $sync$nor$array $q_add $q_full $psprintf ' + '$async$and$plane $async$nand$plane $async$or$plane $async$nor$plane ' + '$sync$and$plane $sync$nand$plane $sync$or$plane $sync$nor$plane $system ' + '$display $displayb $displayh $displayo $strobe $strobeb $strobeh $strobeo ' + '$write $readmemb $readmemh $writememh $value$plusargs ' + '$dumpvars $dumpon $dumplimit $dumpports $dumpportson $dumpportslimit ' + '$writeb $writeh $writeo $monitor $monitorb $monitorh $monitoro $writememb ' + '$dumpfile $dumpoff $dumpall $dumpflush $dumpportsoff $dumpportsall ' + '$dumpportsflush $fclose $fdisplay $fdisplayb $fdisplayh $fdisplayo ' + '$fstrobe $fstrobeb $fstrobeh $fstrobeo $swrite $swriteb $swriteh ' + '$swriteo $fscanf $fread $fseek $fflush $feof $fopen $fwrite $fwriteb ' + '$fwriteh $fwriteo $fmonitor $fmonitorb $fmonitorh $fmonitoro $sformat ' + '$sformatf $fgetc $ungetc $fgets $sscanf $rewind $ftell $ferror' }; return { aliases: ['v', 'sv', 'svh'], case_insensitive: false, keywords: SV_KEYWORDS, lexemes: /[\w\$]+/, contains: [ hljs.C_BLOCK_COMMENT_MODE, hljs.C_LINE_COMMENT_MODE, hljs.QUOTE_STRING_MODE, { className: 'number', contains: [hljs.BACKSLASH_ESCAPE], variants: [ {begin: '\\b((\\d+\'(b|h|o|d|B|H|O|D))[0-9xzXZa-fA-F_]+)'}, {begin: '\\B((\'(b|h|o|d|B|H|O|D))[0-9xzXZa-fA-F_]+)'}, {begin: '\\b([0-9_])+', relevance: 0} ] }, /* parameters to instances */ { className: 'variable', variants: [ {begin: '#\\((?!parameter).+\\)'}, {begin: '\\.\\w+', relevance: 0}, ] }, { className: 'meta', begin: '`', end: '$', keywords: {'meta-keyword': 'define __FILE__ ' + '__LINE__ begin_keywords celldefine default_nettype define ' + 'else elsif end_keywords endcelldefine endif ifdef ifndef ' + 'include line nounconnected_drive pragma resetall timescale ' + 'unconnected_drive undef undefineall'}, relevance: 0 } ] }; // return } },{name:"vhdl",create:/* Language: VHDL Author: Igor Kalnitsky Contributors: Daniel C.K. Kho , Guillaume Savaton Description: VHDL is a hardware description language used in electronic design automation to describe digital and mixed-signal systems. */ function(hljs) { // Regular expression for VHDL numeric literals. // Decimal literal: var INTEGER_RE = '\\d(_|\\d)*'; var EXPONENT_RE = '[eE][-+]?' + INTEGER_RE; var DECIMAL_LITERAL_RE = INTEGER_RE + '(\\.' + INTEGER_RE + ')?' + '(' + EXPONENT_RE + ')?'; // Based literal: var BASED_INTEGER_RE = '\\w+'; var BASED_LITERAL_RE = INTEGER_RE + '#' + BASED_INTEGER_RE + '(\\.' + BASED_INTEGER_RE + ')?' + '#' + '(' + EXPONENT_RE + ')?'; var NUMBER_RE = '\\b(' + BASED_LITERAL_RE + '|' + DECIMAL_LITERAL_RE + ')'; return { case_insensitive: true, keywords: { keyword: 'abs access after alias all and architecture array assert assume assume_guarantee attribute ' + 'begin block body buffer bus case component configuration constant context cover disconnect ' + 'downto default else elsif end entity exit fairness file for force function generate ' + 'generic group guarded if impure in inertial inout is label library linkage literal ' + 'loop map mod nand new next nor not null of on open or others out package parameter port ' + 'postponed procedure process property protected pure range record register reject ' + 'release rem report restrict restrict_guarantee return rol ror select sequence ' + 'severity shared signal sla sll sra srl strong subtype then to transport type ' + 'unaffected units until use variable view vmode vprop vunit wait when while with xnor xor', built_in: 'boolean bit character ' + 'integer time delay_length natural positive ' + 'string bit_vector file_open_kind file_open_status ' + 'std_logic std_logic_vector unsigned signed boolean_vector integer_vector ' + 'std_ulogic std_ulogic_vector unresolved_unsigned u_unsigned unresolved_signed u_signed ' + 'real_vector time_vector', literal: 'false true note warning error failure ' + // severity_level 'line text side width' // textio }, illegal: '{', contains: [ hljs.C_BLOCK_COMMENT_MODE, // VHDL-2008 block commenting. hljs.COMMENT('--', '$'), hljs.QUOTE_STRING_MODE, { className: 'number', begin: NUMBER_RE, relevance: 0 }, { className: 'string', begin: '\'(U|X|0|1|Z|W|L|H|-)\'', contains: [hljs.BACKSLASH_ESCAPE] }, { className: 'symbol', begin: '\'[A-Za-z](_?[A-Za-z0-9])*', contains: [hljs.BACKSLASH_ESCAPE] } ] }; } },{name:"vim",create:/* Language: Vim Script Author: Jun Yang Description: full keyword and built-in from http://vimdoc.sourceforge.net/htmldoc/ Category: scripting */ function(hljs) { return { lexemes: /[!#@\w]+/, keywords: { keyword: // express version except: ! & * < = > !! # @ @@ 'N|0 P|0 X|0 a|0 ab abc abo al am an|0 ar arga argd arge argdo argg argl argu as au aug aun b|0 bN ba bad bd be bel bf bl bm bn bo bp br brea breaka breakd breakl bro bufdo buffers bun bw c|0 cN cNf ca cabc caddb cad caddf cal cat cb cc ccl cd ce cex cf cfir cgetb cgete cg changes chd che checkt cl cla clo cm cmapc cme cn cnew cnf cno cnorea cnoreme co col colo com comc comp con conf cope '+ 'cp cpf cq cr cs cst cu cuna cunme cw delm deb debugg delc delf dif diffg diffo diffp diffpu diffs diffthis dig di dl dell dj dli do doautoa dp dr ds dsp e|0 ea ec echoe echoh echom echon el elsei em en endfo endf endt endw ene ex exe exi exu f|0 files filet fin fina fini fir fix fo foldc foldd folddoc foldo for fu go gr grepa gu gv ha helpf helpg helpt hi hid his ia iabc if ij il im imapc '+ 'ime ino inorea inoreme int is isp iu iuna iunme j|0 ju k|0 keepa kee keepj lN lNf l|0 lad laddb laddf la lan lat lb lc lch lcl lcs le lefta let lex lf lfir lgetb lgete lg lgr lgrepa lh ll lla lli lmak lm lmapc lne lnew lnf ln loadk lo loc lockv lol lope lp lpf lr ls lt lu lua luad luaf lv lvimgrepa lw m|0 ma mak map mapc marks mat me menut mes mk mks mksp mkv mkvie mod mz mzf nbc nb nbs new nm nmapc nme nn nnoreme noa no noh norea noreme norm nu nun nunme ol o|0 om omapc ome on ono onoreme opt ou ounme ow p|0 '+ 'profd prof pro promptr pc ped pe perld po popu pp pre prev ps pt ptN ptf ptj ptl ptn ptp ptr pts pu pw py3 python3 py3d py3f py pyd pyf quita qa rec red redi redr redraws reg res ret retu rew ri rightb rub rubyd rubyf rund ru rv sN san sa sal sav sb sbN sba sbf sbl sbm sbn sbp sbr scrip scripte scs se setf setg setl sf sfir sh sim sig sil sl sla sm smap smapc sme sn sni sno snor snoreme sor '+ 'so spelld spe spelli spellr spellu spellw sp spr sre st sta startg startr star stopi stj sts sun sunm sunme sus sv sw sy synti sync tN tabN tabc tabdo tabe tabf tabfir tabl tabm tabnew '+ 'tabn tabo tabp tabr tabs tab ta tags tc tcld tclf te tf th tj tl tm tn to tp tr try ts tu u|0 undoj undol una unh unl unlo unm unme uns up ve verb vert vim vimgrepa vi viu vie vm vmapc vme vne vn vnoreme vs vu vunme windo w|0 wN wa wh wi winc winp wn wp wq wqa ws wu wv x|0 xa xmapc xm xme xn xnoreme xu xunme y|0 z|0 ~ '+ // full version 'Next Print append abbreviate abclear aboveleft all amenu anoremenu args argadd argdelete argedit argglobal arglocal argument ascii autocmd augroup aunmenu buffer bNext ball badd bdelete behave belowright bfirst blast bmodified bnext botright bprevious brewind break breakadd breakdel breaklist browse bunload '+ 'bwipeout change cNext cNfile cabbrev cabclear caddbuffer caddexpr caddfile call catch cbuffer cclose center cexpr cfile cfirst cgetbuffer cgetexpr cgetfile chdir checkpath checktime clist clast close cmap cmapclear cmenu cnext cnewer cnfile cnoremap cnoreabbrev cnoremenu copy colder colorscheme command comclear compiler continue confirm copen cprevious cpfile cquit crewind cscope cstag cunmap '+ 'cunabbrev cunmenu cwindow delete delmarks debug debuggreedy delcommand delfunction diffupdate diffget diffoff diffpatch diffput diffsplit digraphs display deletel djump dlist doautocmd doautoall deletep drop dsearch dsplit edit earlier echo echoerr echohl echomsg else elseif emenu endif endfor '+ 'endfunction endtry endwhile enew execute exit exusage file filetype find finally finish first fixdel fold foldclose folddoopen folddoclosed foldopen function global goto grep grepadd gui gvim hardcopy help helpfind helpgrep helptags highlight hide history insert iabbrev iabclear ijump ilist imap '+ 'imapclear imenu inoremap inoreabbrev inoremenu intro isearch isplit iunmap iunabbrev iunmenu join jumps keepalt keepmarks keepjumps lNext lNfile list laddexpr laddbuffer laddfile last language later lbuffer lcd lchdir lclose lcscope left leftabove lexpr lfile lfirst lgetbuffer lgetexpr lgetfile lgrep lgrepadd lhelpgrep llast llist lmake lmap lmapclear lnext lnewer lnfile lnoremap loadkeymap loadview '+ 'lockmarks lockvar lolder lopen lprevious lpfile lrewind ltag lunmap luado luafile lvimgrep lvimgrepadd lwindow move mark make mapclear match menu menutranslate messages mkexrc mksession mkspell mkvimrc mkview mode mzscheme mzfile nbclose nbkey nbsart next nmap nmapclear nmenu nnoremap '+ 'nnoremenu noautocmd noremap nohlsearch noreabbrev noremenu normal number nunmap nunmenu oldfiles open omap omapclear omenu only onoremap onoremenu options ounmap ounmenu ownsyntax print profdel profile promptfind promptrepl pclose pedit perl perldo pop popup ppop preserve previous psearch ptag ptNext '+ 'ptfirst ptjump ptlast ptnext ptprevious ptrewind ptselect put pwd py3do py3file python pydo pyfile quit quitall qall read recover redo redir redraw redrawstatus registers resize retab return rewind right rightbelow ruby rubydo rubyfile rundo runtime rviminfo substitute sNext sandbox sargument sall saveas sbuffer sbNext sball sbfirst sblast sbmodified sbnext sbprevious sbrewind scriptnames scriptencoding '+ 'scscope set setfiletype setglobal setlocal sfind sfirst shell simalt sign silent sleep slast smagic smapclear smenu snext sniff snomagic snoremap snoremenu sort source spelldump spellgood spellinfo spellrepall spellundo spellwrong split sprevious srewind stop stag startgreplace startreplace '+ 'startinsert stopinsert stjump stselect sunhide sunmap sunmenu suspend sview swapname syntax syntime syncbind tNext tabNext tabclose tabedit tabfind tabfirst tablast tabmove tabnext tabonly tabprevious tabrewind tag tcl tcldo tclfile tearoff tfirst throw tjump tlast tmenu tnext topleft tprevious '+'trewind tselect tunmenu undo undojoin undolist unabbreviate unhide unlet unlockvar unmap unmenu unsilent update vglobal version verbose vertical vimgrep vimgrepadd visual viusage view vmap vmapclear vmenu vnew '+ 'vnoremap vnoremenu vsplit vunmap vunmenu write wNext wall while winsize wincmd winpos wnext wprevious wqall wsverb wundo wviminfo xit xall xmapclear xmap xmenu xnoremap xnoremenu xunmap xunmenu yank', built_in: //built in func 'synIDtrans atan2 range matcharg did_filetype asin feedkeys xor argv ' + 'complete_check add getwinposx getqflist getwinposy screencol ' + 'clearmatches empty extend getcmdpos mzeval garbagecollect setreg ' + 'ceil sqrt diff_hlID inputsecret get getfperm getpid filewritable ' + 'shiftwidth max sinh isdirectory synID system inputrestore winline ' + 'atan visualmode inputlist tabpagewinnr round getregtype mapcheck ' + 'hasmapto histdel argidx findfile sha256 exists toupper getcmdline ' + 'taglist string getmatches bufnr strftime winwidth bufexists ' + 'strtrans tabpagebuflist setcmdpos remote_read printf setloclist ' + 'getpos getline bufwinnr float2nr len getcmdtype diff_filler luaeval ' + 'resolve libcallnr foldclosedend reverse filter has_key bufname ' + 'str2float strlen setline getcharmod setbufvar index searchpos ' + 'shellescape undofile foldclosed setqflist buflisted strchars str2nr ' + 'virtcol floor remove undotree remote_expr winheight gettabwinvar ' + 'reltime cursor tabpagenr finddir localtime acos getloclist search ' + 'tanh matchend rename gettabvar strdisplaywidth type abs py3eval ' + 'setwinvar tolower wildmenumode log10 spellsuggest bufloaded ' + 'synconcealed nextnonblank server2client complete settabwinvar ' + 'executable input wincol setmatches getftype hlID inputsave ' + 'searchpair or screenrow line settabvar histadd deepcopy strpart ' + 'remote_peek and eval getftime submatch screenchar winsaveview ' + 'matchadd mkdir screenattr getfontname libcall reltimestr getfsize ' + 'winnr invert pow getbufline byte2line soundfold repeat fnameescape ' + 'tagfiles sin strwidth spellbadword trunc maparg log lispindent ' + 'hostname setpos globpath remote_foreground getchar synIDattr ' + 'fnamemodify cscope_connection stridx winbufnr indent min ' + 'complete_add nr2char searchpairpos inputdialog values matchlist ' + 'items hlexists strridx browsedir expand fmod pathshorten line2byte ' + 'argc count getwinvar glob foldtextresult getreg foreground cosh ' + 'matchdelete has char2nr simplify histget searchdecl iconv ' + 'winrestcmd pumvisible writefile foldlevel haslocaldir keys cos ' + 'matchstr foldtext histnr tan tempname getcwd byteidx getbufvar ' + 'islocked escape eventhandler remote_send serverlist winrestview ' + 'synstack pyeval prevnonblank readfile cindent filereadable changenr ' + 'exp' }, illegal: /;/, contains: [ hljs.NUMBER_MODE, { className: 'string', begin: '\'', end: '\'', illegal: '\\n' }, /* A double quote can start either a string or a line comment. Strings are ended before the end of a line by another double quote and can contain escaped double-quotes and post-escaped line breaks. Also, any double quote at the beginning of a line is a comment but we don't handle that properly at the moment: any double quote inside will turn them into a string. Handling it properly will require a smarter parser. */ { className: 'string', begin: /"(\\"|\n\\|[^"\n])*"/ }, hljs.COMMENT('"', '$'), { className: 'variable', begin: /[bwtglsav]:[\w\d_]*/ }, { className: 'function', beginKeywords: 'function function!', end: '$', relevance: 0, contains: [ hljs.TITLE_MODE, { className: 'params', begin: '\\(', end: '\\)' } ] }, { className: 'symbol', begin: /<[\w-]+>/ } ] }; } },{name:"x86asm",create:/* Language: Intel x86 Assembly Author: innocenat Description: x86 assembly language using Intel's mnemonic and NASM syntax Category: assembler */ function(hljs) { return { case_insensitive: true, lexemes: '[.%]?' + hljs.IDENT_RE, keywords: { keyword: 'lock rep repe repz repne repnz xaquire xrelease bnd nobnd ' + 'aaa aad aam aas adc add and arpl bb0_reset bb1_reset bound bsf bsr bswap bt btc btr bts call cbw cdq cdqe clc cld cli clts cmc cmp cmpsb cmpsd cmpsq cmpsw cmpxchg cmpxchg486 cmpxchg8b cmpxchg16b cpuid cpu_read cpu_write cqo cwd cwde daa das dec div dmint emms enter equ f2xm1 fabs fadd faddp fbld fbstp fchs fclex fcmovb fcmovbe fcmove fcmovnb fcmovnbe fcmovne fcmovnu fcmovu fcom fcomi fcomip fcomp fcompp fcos fdecstp fdisi fdiv fdivp fdivr fdivrp femms feni ffree ffreep fiadd ficom ficomp fidiv fidivr fild fimul fincstp finit fist fistp fisttp fisub fisubr fld fld1 fldcw fldenv fldl2e fldl2t fldlg2 fldln2 fldpi fldz fmul fmulp fnclex fndisi fneni fninit fnop fnsave fnstcw fnstenv fnstsw fpatan fprem fprem1 fptan frndint frstor fsave fscale fsetpm fsin fsincos fsqrt fst fstcw fstenv fstp fstsw fsub fsubp fsubr fsubrp ftst fucom fucomi fucomip fucomp fucompp fxam fxch fxtract fyl2x fyl2xp1 hlt ibts icebp idiv imul in inc incbin insb insd insw int int01 int1 int03 int3 into invd invpcid invlpg invlpga iret iretd iretq iretw jcxz jecxz jrcxz jmp jmpe lahf lar lds lea leave les lfence lfs lgdt lgs lidt lldt lmsw loadall loadall286 lodsb lodsd lodsq lodsw loop loope loopne loopnz loopz lsl lss ltr mfence monitor mov movd movq movsb movsd movsq movsw movsx movsxd movzx mul mwait neg nop not or out outsb outsd outsw packssdw packsswb packuswb paddb paddd paddsb paddsiw paddsw paddusb paddusw paddw pand pandn pause paveb pavgusb pcmpeqb pcmpeqd pcmpeqw pcmpgtb pcmpgtd pcmpgtw pdistib pf2id pfacc pfadd pfcmpeq pfcmpge pfcmpgt pfmax pfmin pfmul pfrcp pfrcpit1 pfrcpit2 pfrsqit1 pfrsqrt pfsub pfsubr pi2fd pmachriw pmaddwd pmagw pmulhriw pmulhrwa pmulhrwc pmulhw pmullw pmvgezb pmvlzb pmvnzb pmvzb pop popa popad popaw popf popfd popfq popfw por prefetch prefetchw pslld psllq psllw psrad psraw psrld psrlq psrlw psubb psubd psubsb psubsiw psubsw psubusb psubusw psubw punpckhbw punpckhdq punpckhwd punpcklbw punpckldq punpcklwd push pusha pushad pushaw pushf pushfd pushfq pushfw pxor rcl rcr rdshr rdmsr rdpmc rdtsc rdtscp ret retf retn rol ror rdm rsdc rsldt rsm rsts sahf sal salc sar sbb scasb scasd scasq scasw sfence sgdt shl shld shr shrd sidt sldt skinit smi smint smintold smsw stc std sti stosb stosd stosq stosw str sub svdc svldt svts swapgs syscall sysenter sysexit sysret test ud0 ud1 ud2b ud2 ud2a umov verr verw fwait wbinvd wrshr wrmsr xadd xbts xchg xlatb xlat xor cmove cmovz cmovne cmovnz cmova cmovnbe cmovae cmovnb cmovb cmovnae cmovbe cmovna cmovg cmovnle cmovge cmovnl cmovl cmovnge cmovle cmovng cmovc cmovnc cmovo cmovno cmovs cmovns cmovp cmovpe cmovnp cmovpo je jz jne jnz ja jnbe jae jnb jb jnae jbe jna jg jnle jge jnl jl jnge jle jng jc jnc jo jno js jns jpo jnp jpe jp sete setz setne setnz seta setnbe setae setnb setnc setb setnae setcset setbe setna setg setnle setge setnl setl setnge setle setng sets setns seto setno setpe setp setpo setnp addps addss andnps andps cmpeqps cmpeqss cmpleps cmpless cmpltps cmpltss cmpneqps cmpneqss cmpnleps cmpnless cmpnltps cmpnltss cmpordps cmpordss cmpunordps cmpunordss cmpps cmpss comiss cvtpi2ps cvtps2pi cvtsi2ss cvtss2si cvttps2pi cvttss2si divps divss ldmxcsr maxps maxss minps minss movaps movhps movlhps movlps movhlps movmskps movntps movss movups mulps mulss orps rcpps rcpss rsqrtps rsqrtss shufps sqrtps sqrtss stmxcsr subps subss ucomiss unpckhps unpcklps xorps fxrstor fxrstor64 fxsave fxsave64 xgetbv xsetbv xsave xsave64 xsaveopt xsaveopt64 xrstor xrstor64 prefetchnta prefetcht0 prefetcht1 prefetcht2 maskmovq movntq pavgb pavgw pextrw pinsrw pmaxsw pmaxub pminsw pminub pmovmskb pmulhuw psadbw pshufw pf2iw pfnacc pfpnacc pi2fw pswapd maskmovdqu clflush movntdq movnti movntpd movdqa movdqu movdq2q movq2dq paddq pmuludq pshufd pshufhw pshuflw pslldq psrldq psubq punpckhqdq punpcklqdq addpd addsd andnpd andpd cmpeqpd cmpeqsd cmplepd cmplesd cmpltpd cmpltsd cmpneqpd cmpneqsd cmpnlepd cmpnlesd cmpnltpd cmpnltsd cmpordpd cmpordsd cmpunordpd cmpunordsd cmppd comisd cvtdq2pd cvtdq2ps cvtpd2dq cvtpd2pi cvtpd2ps cvtpi2pd cvtps2dq cvtps2pd cvtsd2si cvtsd2ss cvtsi2sd cvtss2sd cvttpd2pi cvttpd2dq cvttps2dq cvttsd2si divpd divsd maxpd maxsd minpd minsd movapd movhpd movlpd movmskpd movupd mulpd mulsd orpd shufpd sqrtpd sqrtsd subpd subsd ucomisd unpckhpd unpcklpd xorpd addsubpd addsubps haddpd haddps hsubpd hsubps lddqu movddup movshdup movsldup clgi stgi vmcall vmclear vmfunc vmlaunch vmload vmmcall vmptrld vmptrst vmread vmresume vmrun vmsave vmwrite vmxoff vmxon invept invvpid pabsb pabsw pabsd palignr phaddw phaddd phaddsw phsubw phsubd phsubsw pmaddubsw pmulhrsw pshufb psignb psignw psignd extrq insertq movntsd movntss lzcnt blendpd blendps blendvpd blendvps dppd dpps extractps insertps movntdqa mpsadbw packusdw pblendvb pblendw pcmpeqq pextrb pextrd pextrq phminposuw pinsrb pinsrd pinsrq pmaxsb pmaxsd pmaxud pmaxuw pminsb pminsd pminud pminuw pmovsxbw pmovsxbd pmovsxbq pmovsxwd pmovsxwq pmovsxdq pmovzxbw pmovzxbd pmovzxbq pmovzxwd pmovzxwq pmovzxdq pmuldq pmulld ptest roundpd roundps roundsd roundss crc32 pcmpestri pcmpestrm pcmpistri pcmpistrm pcmpgtq popcnt getsec pfrcpv pfrsqrtv movbe aesenc aesenclast aesdec aesdeclast aesimc aeskeygenassist vaesenc vaesenclast vaesdec vaesdeclast vaesimc vaeskeygenassist vaddpd vaddps vaddsd vaddss vaddsubpd vaddsubps vandpd vandps vandnpd vandnps vblendpd vblendps vblendvpd vblendvps vbroadcastss vbroadcastsd vbroadcastf128 vcmpeq_ospd vcmpeqpd vcmplt_ospd vcmpltpd vcmple_ospd vcmplepd vcmpunord_qpd vcmpunordpd vcmpneq_uqpd vcmpneqpd vcmpnlt_uspd vcmpnltpd vcmpnle_uspd vcmpnlepd vcmpord_qpd vcmpordpd vcmpeq_uqpd vcmpnge_uspd vcmpngepd vcmpngt_uspd vcmpngtpd vcmpfalse_oqpd vcmpfalsepd vcmpneq_oqpd vcmpge_ospd vcmpgepd vcmpgt_ospd vcmpgtpd vcmptrue_uqpd vcmptruepd vcmplt_oqpd vcmple_oqpd vcmpunord_spd vcmpneq_uspd vcmpnlt_uqpd vcmpnle_uqpd vcmpord_spd vcmpeq_uspd vcmpnge_uqpd vcmpngt_uqpd vcmpfalse_ospd vcmpneq_ospd vcmpge_oqpd vcmpgt_oqpd vcmptrue_uspd vcmppd vcmpeq_osps vcmpeqps vcmplt_osps vcmpltps vcmple_osps vcmpleps vcmpunord_qps vcmpunordps vcmpneq_uqps vcmpneqps vcmpnlt_usps vcmpnltps vcmpnle_usps vcmpnleps vcmpord_qps vcmpordps vcmpeq_uqps vcmpnge_usps vcmpngeps vcmpngt_usps vcmpngtps vcmpfalse_oqps vcmpfalseps vcmpneq_oqps vcmpge_osps vcmpgeps vcmpgt_osps vcmpgtps vcmptrue_uqps vcmptrueps vcmplt_oqps vcmple_oqps vcmpunord_sps vcmpneq_usps vcmpnlt_uqps vcmpnle_uqps vcmpord_sps vcmpeq_usps vcmpnge_uqps vcmpngt_uqps vcmpfalse_osps vcmpneq_osps vcmpge_oqps vcmpgt_oqps vcmptrue_usps vcmpps vcmpeq_ossd vcmpeqsd vcmplt_ossd vcmpltsd vcmple_ossd vcmplesd vcmpunord_qsd vcmpunordsd vcmpneq_uqsd vcmpneqsd vcmpnlt_ussd vcmpnltsd vcmpnle_ussd vcmpnlesd vcmpord_qsd vcmpordsd vcmpeq_uqsd vcmpnge_ussd vcmpngesd vcmpngt_ussd vcmpngtsd vcmpfalse_oqsd vcmpfalsesd vcmpneq_oqsd vcmpge_ossd vcmpgesd vcmpgt_ossd vcmpgtsd vcmptrue_uqsd vcmptruesd vcmplt_oqsd vcmple_oqsd vcmpunord_ssd vcmpneq_ussd vcmpnlt_uqsd vcmpnle_uqsd vcmpord_ssd vcmpeq_ussd vcmpnge_uqsd vcmpngt_uqsd vcmpfalse_ossd vcmpneq_ossd vcmpge_oqsd vcmpgt_oqsd vcmptrue_ussd vcmpsd vcmpeq_osss vcmpeqss vcmplt_osss vcmpltss vcmple_osss vcmpless vcmpunord_qss vcmpunordss vcmpneq_uqss vcmpneqss vcmpnlt_usss vcmpnltss vcmpnle_usss vcmpnless vcmpord_qss vcmpordss vcmpeq_uqss vcmpnge_usss vcmpngess vcmpngt_usss vcmpngtss vcmpfalse_oqss vcmpfalsess vcmpneq_oqss vcmpge_osss vcmpgess vcmpgt_osss vcmpgtss vcmptrue_uqss vcmptruess vcmplt_oqss vcmple_oqss vcmpunord_sss vcmpneq_usss vcmpnlt_uqss vcmpnle_uqss vcmpord_sss vcmpeq_usss vcmpnge_uqss vcmpngt_uqss vcmpfalse_osss vcmpneq_osss vcmpge_oqss vcmpgt_oqss vcmptrue_usss vcmpss vcomisd vcomiss vcvtdq2pd vcvtdq2ps vcvtpd2dq vcvtpd2ps vcvtps2dq vcvtps2pd vcvtsd2si vcvtsd2ss vcvtsi2sd vcvtsi2ss vcvtss2sd vcvtss2si vcvttpd2dq vcvttps2dq vcvttsd2si vcvttss2si vdivpd vdivps vdivsd vdivss vdppd vdpps vextractf128 vextractps vhaddpd vhaddps vhsubpd vhsubps vinsertf128 vinsertps vlddqu vldqqu vldmxcsr vmaskmovdqu vmaskmovps vmaskmovpd vmaxpd vmaxps vmaxsd vmaxss vminpd vminps vminsd vminss vmovapd vmovaps vmovd vmovq vmovddup vmovdqa vmovqqa vmovdqu vmovqqu vmovhlps vmovhpd vmovhps vmovlhps vmovlpd vmovlps vmovmskpd vmovmskps vmovntdq vmovntqq vmovntdqa vmovntpd vmovntps vmovsd vmovshdup vmovsldup vmovss vmovupd vmovups vmpsadbw vmulpd vmulps vmulsd vmulss vorpd vorps vpabsb vpabsw vpabsd vpacksswb vpackssdw vpackuswb vpackusdw vpaddb vpaddw vpaddd vpaddq vpaddsb vpaddsw vpaddusb vpaddusw vpalignr vpand vpandn vpavgb vpavgw vpblendvb vpblendw vpcmpestri vpcmpestrm vpcmpistri vpcmpistrm vpcmpeqb vpcmpeqw vpcmpeqd vpcmpeqq vpcmpgtb vpcmpgtw vpcmpgtd vpcmpgtq vpermilpd vpermilps vperm2f128 vpextrb vpextrw vpextrd vpextrq vphaddw vphaddd vphaddsw vphminposuw vphsubw vphsubd vphsubsw vpinsrb vpinsrw vpinsrd vpinsrq vpmaddwd vpmaddubsw vpmaxsb vpmaxsw vpmaxsd vpmaxub vpmaxuw vpmaxud vpminsb vpminsw vpminsd vpminub vpminuw vpminud vpmovmskb vpmovsxbw vpmovsxbd vpmovsxbq vpmovsxwd vpmovsxwq vpmovsxdq vpmovzxbw vpmovzxbd vpmovzxbq vpmovzxwd vpmovzxwq vpmovzxdq vpmulhuw vpmulhrsw vpmulhw vpmullw vpmulld vpmuludq vpmuldq vpor vpsadbw vpshufb vpshufd vpshufhw vpshuflw vpsignb vpsignw vpsignd vpslldq vpsrldq vpsllw vpslld vpsllq vpsraw vpsrad vpsrlw vpsrld vpsrlq vptest vpsubb vpsubw vpsubd vpsubq vpsubsb vpsubsw vpsubusb vpsubusw vpunpckhbw vpunpckhwd vpunpckhdq vpunpckhqdq vpunpcklbw vpunpcklwd vpunpckldq vpunpcklqdq vpxor vrcpps vrcpss vrsqrtps vrsqrtss vroundpd vroundps vroundsd vroundss vshufpd vshufps vsqrtpd vsqrtps vsqrtsd vsqrtss vstmxcsr vsubpd vsubps vsubsd vsubss vtestps vtestpd vucomisd vucomiss vunpckhpd vunpckhps vunpcklpd vunpcklps vxorpd vxorps vzeroall vzeroupper pclmullqlqdq pclmulhqlqdq pclmullqhqdq pclmulhqhqdq pclmulqdq vpclmullqlqdq vpclmulhqlqdq vpclmullqhqdq vpclmulhqhqdq vpclmulqdq vfmadd132ps vfmadd132pd vfmadd312ps vfmadd312pd vfmadd213ps vfmadd213pd vfmadd123ps vfmadd123pd vfmadd231ps vfmadd231pd vfmadd321ps vfmadd321pd vfmaddsub132ps vfmaddsub132pd vfmaddsub312ps vfmaddsub312pd vfmaddsub213ps vfmaddsub213pd vfmaddsub123ps vfmaddsub123pd vfmaddsub231ps vfmaddsub231pd vfmaddsub321ps vfmaddsub321pd vfmsub132ps vfmsub132pd vfmsub312ps vfmsub312pd vfmsub213ps vfmsub213pd vfmsub123ps vfmsub123pd vfmsub231ps vfmsub231pd vfmsub321ps vfmsub321pd vfmsubadd132ps vfmsubadd132pd vfmsubadd312ps vfmsubadd312pd vfmsubadd213ps vfmsubadd213pd vfmsubadd123ps vfmsubadd123pd vfmsubadd231ps vfmsubadd231pd vfmsubadd321ps vfmsubadd321pd vfnmadd132ps vfnmadd132pd vfnmadd312ps vfnmadd312pd vfnmadd213ps vfnmadd213pd vfnmadd123ps vfnmadd123pd vfnmadd231ps vfnmadd231pd vfnmadd321ps vfnmadd321pd vfnmsub132ps vfnmsub132pd vfnmsub312ps vfnmsub312pd vfnmsub213ps vfnmsub213pd vfnmsub123ps vfnmsub123pd vfnmsub231ps vfnmsub231pd vfnmsub321ps vfnmsub321pd vfmadd132ss vfmadd132sd vfmadd312ss vfmadd312sd vfmadd213ss vfmadd213sd vfmadd123ss vfmadd123sd vfmadd231ss vfmadd231sd vfmadd321ss vfmadd321sd vfmsub132ss vfmsub132sd vfmsub312ss vfmsub312sd vfmsub213ss vfmsub213sd vfmsub123ss vfmsub123sd vfmsub231ss vfmsub231sd vfmsub321ss vfmsub321sd vfnmadd132ss vfnmadd132sd vfnmadd312ss vfnmadd312sd vfnmadd213ss vfnmadd213sd vfnmadd123ss vfnmadd123sd vfnmadd231ss vfnmadd231sd vfnmadd321ss vfnmadd321sd vfnmsub132ss vfnmsub132sd vfnmsub312ss vfnmsub312sd vfnmsub213ss vfnmsub213sd vfnmsub123ss vfnmsub123sd vfnmsub231ss vfnmsub231sd vfnmsub321ss vfnmsub321sd rdfsbase rdgsbase rdrand wrfsbase wrgsbase vcvtph2ps vcvtps2ph adcx adox rdseed clac stac xstore xcryptecb xcryptcbc xcryptctr xcryptcfb xcryptofb montmul xsha1 xsha256 llwpcb slwpcb lwpval lwpins vfmaddpd vfmaddps vfmaddsd vfmaddss vfmaddsubpd vfmaddsubps vfmsubaddpd vfmsubaddps vfmsubpd vfmsubps vfmsubsd vfmsubss vfnmaddpd vfnmaddps vfnmaddsd vfnmaddss vfnmsubpd vfnmsubps vfnmsubsd vfnmsubss vfrczpd vfrczps vfrczsd vfrczss vpcmov vpcomb vpcomd vpcomq vpcomub vpcomud vpcomuq vpcomuw vpcomw vphaddbd vphaddbq vphaddbw vphadddq vphaddubd vphaddubq vphaddubw vphaddudq vphadduwd vphadduwq vphaddwd vphaddwq vphsubbw vphsubdq vphsubwd vpmacsdd vpmacsdqh vpmacsdql vpmacssdd vpmacssdqh vpmacssdql vpmacsswd vpmacssww vpmacswd vpmacsww vpmadcsswd vpmadcswd vpperm vprotb vprotd vprotq vprotw vpshab vpshad vpshaq vpshaw vpshlb vpshld vpshlq vpshlw vbroadcasti128 vpblendd vpbroadcastb vpbroadcastw vpbroadcastd vpbroadcastq vpermd vpermpd vpermps vpermq vperm2i128 vextracti128 vinserti128 vpmaskmovd vpmaskmovq vpsllvd vpsllvq vpsravd vpsrlvd vpsrlvq vgatherdpd vgatherqpd vgatherdps vgatherqps vpgatherdd vpgatherqd vpgatherdq vpgatherqq xabort xbegin xend xtest andn bextr blci blcic blsi blsic blcfill blsfill blcmsk blsmsk blsr blcs bzhi mulx pdep pext rorx sarx shlx shrx tzcnt tzmsk t1mskc valignd valignq vblendmpd vblendmps vbroadcastf32x4 vbroadcastf64x4 vbroadcasti32x4 vbroadcasti64x4 vcompresspd vcompressps vcvtpd2udq vcvtps2udq vcvtsd2usi vcvtss2usi vcvttpd2udq vcvttps2udq vcvttsd2usi vcvttss2usi vcvtudq2pd vcvtudq2ps vcvtusi2sd vcvtusi2ss vexpandpd vexpandps vextractf32x4 vextractf64x4 vextracti32x4 vextracti64x4 vfixupimmpd vfixupimmps vfixupimmsd vfixupimmss vgetexppd vgetexpps vgetexpsd vgetexpss vgetmantpd vgetmantps vgetmantsd vgetmantss vinsertf32x4 vinsertf64x4 vinserti32x4 vinserti64x4 vmovdqa32 vmovdqa64 vmovdqu32 vmovdqu64 vpabsq vpandd vpandnd vpandnq vpandq vpblendmd vpblendmq vpcmpltd vpcmpled vpcmpneqd vpcmpnltd vpcmpnled vpcmpd vpcmpltq vpcmpleq vpcmpneqq vpcmpnltq vpcmpnleq vpcmpq vpcmpequd vpcmpltud vpcmpleud vpcmpnequd vpcmpnltud vpcmpnleud vpcmpud vpcmpequq vpcmpltuq vpcmpleuq vpcmpnequq vpcmpnltuq vpcmpnleuq vpcmpuq vpcompressd vpcompressq vpermi2d vpermi2pd vpermi2ps vpermi2q vpermt2d vpermt2pd vpermt2ps vpermt2q vpexpandd vpexpandq vpmaxsq vpmaxuq vpminsq vpminuq vpmovdb vpmovdw vpmovqb vpmovqd vpmovqw vpmovsdb vpmovsdw vpmovsqb vpmovsqd vpmovsqw vpmovusdb vpmovusdw vpmovusqb vpmovusqd vpmovusqw vpord vporq vprold vprolq vprolvd vprolvq vprord vprorq vprorvd vprorvq vpscatterdd vpscatterdq vpscatterqd vpscatterqq vpsraq vpsravq vpternlogd vpternlogq vptestmd vptestmq vptestnmd vptestnmq vpxord vpxorq vrcp14pd vrcp14ps vrcp14sd vrcp14ss vrndscalepd vrndscaleps vrndscalesd vrndscaless vrsqrt14pd vrsqrt14ps vrsqrt14sd vrsqrt14ss vscalefpd vscalefps vscalefsd vscalefss vscatterdpd vscatterdps vscatterqpd vscatterqps vshuff32x4 vshuff64x2 vshufi32x4 vshufi64x2 kandnw kandw kmovw knotw kortestw korw kshiftlw kshiftrw kunpckbw kxnorw kxorw vpbroadcastmb2q vpbroadcastmw2d vpconflictd vpconflictq vplzcntd vplzcntq vexp2pd vexp2ps vrcp28pd vrcp28ps vrcp28sd vrcp28ss vrsqrt28pd vrsqrt28ps vrsqrt28sd vrsqrt28ss vgatherpf0dpd vgatherpf0dps vgatherpf0qpd vgatherpf0qps vgatherpf1dpd vgatherpf1dps vgatherpf1qpd vgatherpf1qps vscatterpf0dpd vscatterpf0dps vscatterpf0qpd vscatterpf0qps vscatterpf1dpd vscatterpf1dps vscatterpf1qpd vscatterpf1qps prefetchwt1 bndmk bndcl bndcu bndcn bndmov bndldx bndstx sha1rnds4 sha1nexte sha1msg1 sha1msg2 sha256rnds2 sha256msg1 sha256msg2 hint_nop0 hint_nop1 hint_nop2 hint_nop3 hint_nop4 hint_nop5 hint_nop6 hint_nop7 hint_nop8 hint_nop9 hint_nop10 hint_nop11 hint_nop12 hint_nop13 hint_nop14 hint_nop15 hint_nop16 hint_nop17 hint_nop18 hint_nop19 hint_nop20 hint_nop21 hint_nop22 hint_nop23 hint_nop24 hint_nop25 hint_nop26 hint_nop27 hint_nop28 hint_nop29 hint_nop30 hint_nop31 hint_nop32 hint_nop33 hint_nop34 hint_nop35 hint_nop36 hint_nop37 hint_nop38 hint_nop39 hint_nop40 hint_nop41 hint_nop42 hint_nop43 hint_nop44 hint_nop45 hint_nop46 hint_nop47 hint_nop48 hint_nop49 hint_nop50 hint_nop51 hint_nop52 hint_nop53 hint_nop54 hint_nop55 hint_nop56 hint_nop57 hint_nop58 hint_nop59 hint_nop60 hint_nop61 hint_nop62 hint_nop63', built_in: // Instruction pointer 'ip eip rip ' + // 8-bit registers 'al ah bl bh cl ch dl dh sil dil bpl spl r8b r9b r10b r11b r12b r13b r14b r15b ' + // 16-bit registers 'ax bx cx dx si di bp sp r8w r9w r10w r11w r12w r13w r14w r15w ' + // 32-bit registers 'eax ebx ecx edx esi edi ebp esp eip r8d r9d r10d r11d r12d r13d r14d r15d ' + // 64-bit registers 'rax rbx rcx rdx rsi rdi rbp rsp r8 r9 r10 r11 r12 r13 r14 r15 ' + // Segment registers 'cs ds es fs gs ss ' + // Floating point stack registers 'st st0 st1 st2 st3 st4 st5 st6 st7 ' + // MMX Registers 'mm0 mm1 mm2 mm3 mm4 mm5 mm6 mm7 ' + // SSE registers 'xmm0 xmm1 xmm2 xmm3 xmm4 xmm5 xmm6 xmm7 xmm8 xmm9 xmm10 xmm11 xmm12 xmm13 xmm14 xmm15 ' + 'xmm16 xmm17 xmm18 xmm19 xmm20 xmm21 xmm22 xmm23 xmm24 xmm25 xmm26 xmm27 xmm28 xmm29 xmm30 xmm31 ' + // AVX registers 'ymm0 ymm1 ymm2 ymm3 ymm4 ymm5 ymm6 ymm7 ymm8 ymm9 ymm10 ymm11 ymm12 ymm13 ymm14 ymm15 ' + 'ymm16 ymm17 ymm18 ymm19 ymm20 ymm21 ymm22 ymm23 ymm24 ymm25 ymm26 ymm27 ymm28 ymm29 ymm30 ymm31 ' + // AVX-512F registers 'zmm0 zmm1 zmm2 zmm3 zmm4 zmm5 zmm6 zmm7 zmm8 zmm9 zmm10 zmm11 zmm12 zmm13 zmm14 zmm15 ' + 'zmm16 zmm17 zmm18 zmm19 zmm20 zmm21 zmm22 zmm23 zmm24 zmm25 zmm26 zmm27 zmm28 zmm29 zmm30 zmm31 ' + // AVX-512F mask registers 'k0 k1 k2 k3 k4 k5 k6 k7 ' + // Bound (MPX) register 'bnd0 bnd1 bnd2 bnd3 ' + // Special register 'cr0 cr1 cr2 cr3 cr4 cr8 dr0 dr1 dr2 dr3 dr8 tr3 tr4 tr5 tr6 tr7 ' + // NASM altreg package 'r0 r1 r2 r3 r4 r5 r6 r7 r0b r1b r2b r3b r4b r5b r6b r7b ' + 'r0w r1w r2w r3w r4w r5w r6w r7w r0d r1d r2d r3d r4d r5d r6d r7d ' + 'r0h r1h r2h r3h ' + 'r0l r1l r2l r3l r4l r5l r6l r7l r8l r9l r10l r11l r12l r13l r14l r15l ' + 'db dw dd dq dt ddq do dy dz ' + 'resb resw resd resq rest resdq reso resy resz ' + 'incbin equ times ' + 'byte word dword qword nosplit rel abs seg wrt strict near far a32 ptr', meta: '%define %xdefine %+ %undef %defstr %deftok %assign %strcat %strlen %substr %rotate %elif %else %endif ' + '%if %ifmacro %ifctx %ifidn %ifidni %ifid %ifnum %ifstr %iftoken %ifempty %ifenv %error %warning %fatal %rep ' + '%endrep %include %push %pop %repl %pathsearch %depend %use %arg %stacksize %local %line %comment %endcomment ' + '.nolist ' + '__FILE__ __LINE__ __SECT__ __BITS__ __OUTPUT_FORMAT__ __DATE__ __TIME__ __DATE_NUM__ __TIME_NUM__ ' + '__UTC_DATE__ __UTC_TIME__ __UTC_DATE_NUM__ __UTC_TIME_NUM__ __PASS__ struc endstruc istruc at iend ' + 'align alignb sectalign daz nodaz up down zero default option assume public ' + 'bits use16 use32 use64 default section segment absolute extern global common cpu float ' + '__utf16__ __utf16le__ __utf16be__ __utf32__ __utf32le__ __utf32be__ ' + '__float8__ __float16__ __float32__ __float64__ __float80m__ __float80e__ __float128l__ __float128h__ ' + '__Infinity__ __QNaN__ __SNaN__ Inf NaN QNaN SNaN float8 float16 float32 float64 float80m float80e ' + 'float128l float128h __FLOAT_DAZ__ __FLOAT_ROUND__ __FLOAT__' }, contains: [ hljs.COMMENT( ';', '$', { relevance: 0 } ), { className: 'number', variants: [ // Float number and x87 BCD { begin: '\\b(?:([0-9][0-9_]*)?\\.[0-9_]*(?:[eE][+-]?[0-9_]+)?|' + '(0[Xx])?[0-9][0-9_]*\\.?[0-9_]*(?:[pP](?:[+-]?[0-9_]+)?)?)\\b', relevance: 0 }, // Hex number in $ { begin: '\\$[0-9][0-9A-Fa-f]*', relevance: 0 }, // Number in H,D,T,Q,O,B,Y suffix { begin: '\\b(?:[0-9A-Fa-f][0-9A-Fa-f_]*[Hh]|[0-9][0-9_]*[DdTt]?|[0-7][0-7_]*[QqOo]|[0-1][0-1_]*[BbYy])\\b' }, // Number in X,D,T,Q,O,B,Y prefix { begin: '\\b(?:0[Xx][0-9A-Fa-f_]+|0[DdTt][0-9_]+|0[QqOo][0-7_]+|0[BbYy][0-1_]+)\\b'} ] }, // Double quote string hljs.QUOTE_STRING_MODE, { className: 'string', variants: [ // Single-quoted string { begin: '\'', end: '[^\\\\]\'' }, // Backquoted string { begin: '`', end: '[^\\\\]`' } ], relevance: 0 }, { className: 'symbol', variants: [ // Global label and local label { begin: '^\\s*[A-Za-z._?][A-Za-z0-9_$#@~.?]*(:|\\s+label)' }, // Macro-local label { begin: '^\\s*%%[A-Za-z0-9_$#@~.?]*:' } ], relevance: 0 }, // Macro parameter { className: 'subst', begin: '%[0-9]+', relevance: 0 }, // Macro parameter { className: 'subst', begin: '%!\S+', relevance: 0 }, { className: 'meta', begin: /^\s*\.[\w_-]+/ } ] }; } },{name:"xl",create:/* Language: XL Author: Christophe de Dinechin Description: An extensible programming language, based on parse tree rewriting (http://xlr.sf.net) */ function(hljs) { var BUILTIN_MODULES = 'ObjectLoader Animate MovieCredits Slides Filters Shading Materials LensFlare Mapping VLCAudioVideo ' + 'StereoDecoder PointCloud NetworkAccess RemoteControl RegExp ChromaKey Snowfall NodeJS Speech Charts'; var XL_KEYWORDS = { keyword: 'if then else do while until for loop import with is as where when by data constant ' + 'integer real text name boolean symbol infix prefix postfix block tree', literal: 'true false nil', built_in: 'in mod rem and or xor not abs sign floor ceil sqrt sin cos tan asin ' + 'acos atan exp expm1 log log2 log10 log1p pi at text_length text_range ' + 'text_find text_replace contains page slide basic_slide title_slide ' + 'title subtitle fade_in fade_out fade_at clear_color color line_color ' + 'line_width texture_wrap texture_transform texture scale_?x scale_?y ' + 'scale_?z? translate_?x translate_?y translate_?z? rotate_?x rotate_?y ' + 'rotate_?z? rectangle circle ellipse sphere path line_to move_to ' + 'quad_to curve_to theme background contents locally time mouse_?x ' + 'mouse_?y mouse_buttons ' + BUILTIN_MODULES }; var DOUBLE_QUOTE_TEXT = { className: 'string', begin: '"', end: '"', illegal: '\\n' }; var SINGLE_QUOTE_TEXT = { className: 'string', begin: '\'', end: '\'', illegal: '\\n' }; var LONG_TEXT = { className: 'string', begin: '<<', end: '>>' }; var BASED_NUMBER = { className: 'number', begin: '[0-9]+#[0-9A-Z_]+(\\.[0-9-A-Z_]+)?#?([Ee][+-]?[0-9]+)?' }; var IMPORT = { beginKeywords: 'import', end: '$', keywords: XL_KEYWORDS, contains: [DOUBLE_QUOTE_TEXT] }; var FUNCTION_DEFINITION = { className: 'function', begin: /[a-z][^\n]*->/, returnBegin: true, end: /->/, contains: [ hljs.inherit(hljs.TITLE_MODE, {starts: { endsWithParent: true, keywords: XL_KEYWORDS }}) ] }; return { aliases: ['tao'], lexemes: /[a-zA-Z][a-zA-Z0-9_?]*/, keywords: XL_KEYWORDS, contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, DOUBLE_QUOTE_TEXT, SINGLE_QUOTE_TEXT, LONG_TEXT, FUNCTION_DEFINITION, IMPORT, BASED_NUMBER, hljs.NUMBER_MODE ] }; } },{name:"xml",create:/* Language: HTML, XML Category: common */ function(hljs) { var XML_IDENT_RE = '[A-Za-z0-9\\._:-]+'; var TAG_INTERNALS = { endsWithParent: true, illegal: /`]+/} ] } ] } ] }; return { aliases: ['html', 'xhtml', 'rss', 'atom', 'xjb', 'xsd', 'xsl', 'plist', 'wsf'], case_insensitive: true, contains: [ { className: 'meta', begin: '', relevance: 10, contains: [{begin: '\\[', end: '\\]'}] }, hljs.COMMENT( '', { relevance: 10 } ), { begin: '<\\!\\[CDATA\\[', end: '\\]\\]>', relevance: 10 }, { className: 'meta', begin: /<\?xml/, end: /\?>/, relevance: 10 }, { begin: /<\?(php)?/, end: /\?>/, subLanguage: 'php', contains: [ // We don't want the php closing tag ?> to close the PHP block when // inside any of the following blocks: {begin: '/\\*', end: '\\*/', skip: true}, {begin: 'b"', end: '"', skip: true}, {begin: 'b\'', end: '\'', skip: true}, hljs.inherit(hljs.APOS_STRING_MODE, {illegal: null, className: null, contains: null, skip: true}), hljs.inherit(hljs.QUOTE_STRING_MODE, {illegal: null, className: null, contains: null, skip: true}) ] }, { className: 'tag', /* The lookahead pattern (?=...) ensures that 'begin' only matches '|$)', end: '>', keywords: {name: 'style'}, contains: [TAG_INTERNALS], starts: { end: '', returnEnd: true, subLanguage: ['css', 'xml'] } }, { className: 'tag', // See the comment in the $endif$ $for(css)$ $endfor$ $if(math)$ $math$ $endif$ $for(header-includes)$ $header-includes$ $endfor$ $for(include-before)$ $include-before$ $endfor$
    $body$
    $for(include-after)$ $include-after$ $endfor$ liquidsoap-2.3.2/dune000066400000000000000000000012611477303350200145520ustar00rootroot00000000000000(rule (target liquidsoap.config) (package liquidsoap) (alias install) (enabled_if %{env:LIQUIDSOAP_ENABLE_BUILD_CONFIG=true}) (deps src/libs/stdlib.liq (source_tree src/libs)) (mode (promote (only liquidsoap.config) (until-clean))) (action (progn (echo "\nCongratulation on building liquidsoap! Here are the details of your build and configuration:\n") (run %{bin:liquidsoap} --build-config) (with-stdout-to %{target} (run %{bin:liquidsoap} --opam-config))))) (alias (name default) (deps (alias_rec install))) (alias (name runtest) (deps (alias_rec install))) (alias (name citest) (deps (alias_rec perftest) (alias_rec runtest))) liquidsoap-2.3.2/dune-project000066400000000000000000000100311477303350200162110ustar00rootroot00000000000000(lang dune 3.6) (using menhir 2.1) (using dune_site 0.1) (name liquidsoap) (source (github savonet/liquidsoap)) (license GPL-2.0-or-later) (authors "The Savonet Team ") (maintainers "The Savonet Team ") (homepage "https://github.com/savonet/liquidsoap") (bug_reports "https://github.com/savonet/liquidsoap/issues") (version 2.3.2) (generate_opam_files true) (executables_implicit_empty_intf true) (package (name liquidsoap) (depends (ocaml (>= 4.14)) (dtools (>= 0.4.6)) (duppy (>= 0.9.4)) (mm (>= 0.8.6)) (re (>= 1.11.0)) (ocurl (>= 0.9.2)) (cry (>= 1.0.3)) (camomile (>= 2.0.0)) uri fileutils menhirLib (mem_usage (>= 0.1.1)) (metadata (>= 0.3.0)) magic-mime dune-build-info (liquidsoap-lang (= :version)) (ppx_string :build)) (depopts alsa ao bjack camlimages ctypes-foreign dssi faad fdkaac ffmpeg flac frei0r gd graphics imagelib inotify irc-client-unix jemalloc ladspa lame lilv lo mad memtrace ogg opus osc-unix portaudio posix-time2 posix-socket pulseaudio prometheus-liquidsoap samplerate shine soundtouch speex sqlite3 srt ssl tls-liquidsoap theora sdl-liquidsoap vorbis yaml xmlplaylist) (conflicts (alsa (< 0.3.0)) (ao (< 0.2.0)) (bjack (< 0.1.3)) (camomile (< 1.0.0)) (dssi (< 0.1.3)) (faad (< 0.5.0)) (fdkaac (< 0.3.1)) (ffmpeg (< 1.2.0)) (ffmpeg-avutil (< 1.2.0)) (flac (< 1.0.0)) (frei0r (< 0.1.0)) (inotify (< 1.0)) (ladspa (< 0.2.0)) (lame (< 0.3.7)) (lo (< 0.2.0)) (mad (< 0.5.0)) (magic (< 0.6)) (mirage-crypto-rng (< 0.6.2)) (ogg (< 1.0.0)) (opus (< 0.2.0)) (odoc (< 3.0.0~beta1)) (portaudio (< 0.2.0)) (posix-time2 (< 2.0.2)) (posix-socket (< 2.1.0)) (pulseaudio (< 0.1.4)) (samplerate (< 0.1.5)) (shine (< 0.2.0)) (soundtouch (< 0.1.9)) (speex (< 1.0.0)) (srt (< 0.3.2)) (ssl (< 0.7.0)) (tls (< 1.0.2)) (sdl-liquidsoap (< 2)) (theora (< 1.0.0)) (vorbis (< 1.0.0)) (xmlplaylist (< 0.1.3)) (pandoc :with-doc) (pandoc-include :with-doc)) (synopsis "Swiss-army knife for multimedia streaming") (description "\| Liquidsoap is a powerful and flexible language for describing your "\| streams. It offers a rich collection of operators that you can combine "\| at will, giving you more power than you need for creating or "\| transforming streams. But liquidsoap is still very light and easy to "\| use, in the Unix tradition of simple strong components working "\| together. )) (package (name liquidsoap-lang) (depends (ocaml (>= 4.14)) dune-site (re (>= 1.11.0)) (ppx_string :build) (ppx_hash :build) (sedlex (>= 3.2)) (menhir (>= 20240715)) xml-light ) (sites (share libs) (share bin) (share cache) (lib_root lib_root)) (synopsis "Liquidsoap language library")) (package (name liquidsoap-js) (depends (ocaml (>= 4.14)) (liquidsoap-lang (= :version)) js_of_ocaml-ppx (js_of_ocaml (>= 5.7.2))) (conflicts (liquidsoap (<> :version))) (synopsis "Liquidsoap language - javascript wrapper")) (package (name liquidsoap-mode) (depends (liquidsoap (= :version))) (synopsis "Liquidosap emacs mode") ) (package (name tls-liquidsoap) (version 1) (allow_empty) (depends tls ca-certs mirage-crypto-rng cstruct) (synopsis "Virtual package install liquidosap dependencies for TLS optional features") ) (package (name prometheus-liquidsoap) (version 2) (allow_empty) (depends prometheus-app cohttp-lwt-unix) (synopsis "Virtual package installing liquidsoap dependencies for prometheus optional features") ) (package (name sdl-liquidsoap) (version 3) (allow_empty) (depends tsdl (tsdl-image (>= 0.3.2)) tsdl-ttf) (synopsis "Virtual package installing liquidsoap dependencies for SDL optional features") ) liquidsoap-2.3.2/liquidsoap000077500000000000000000000003261477303350200157750ustar00rootroot00000000000000#!/bin/sh # shellcheck disable=SC2068 DIR="$(dirname "$0")" export DIR opam exec dune -- exec --display=quiet --no-print-directory --root="$DIR" src/bin/liquidsoap.exe -- --stdlib "$DIR"/src/libs/stdlib.liq "$@" liquidsoap-2.3.2/liquidsoap-js.opam000066400000000000000000000017271477303350200173450ustar00rootroot00000000000000# This file is generated by dune, edit dune-project instead opam-version: "2.0" version: "2.3.2" synopsis: "Liquidsoap language - javascript wrapper" maintainer: ["The Savonet Team "] authors: ["The Savonet Team "] license: "GPL-2.0-or-later" homepage: "https://github.com/savonet/liquidsoap" bug-reports: "https://github.com/savonet/liquidsoap/issues" depends: [ "dune" {>= "3.6"} "ocaml" {>= "4.14"} "liquidsoap-lang" {= version} "js_of_ocaml-ppx" "js_of_ocaml" {>= "5.7.2"} "odoc" {with-doc} ] conflicts: [ "liquidsoap" {!= version} ] build: [ ["dune" "subst"] {dev} [ "dune" "build" "-p" name "-j" jobs "--promote-install-files=false" "@install" "@runtest" {with-test} "@doc" {with-doc} ] ["dune" "install" "-p" name "--create-install-files" name] ] dev-repo: "git+https://github.com/savonet/liquidsoap.git" x-maintenance-intent: ["(latest)"] liquidsoap-2.3.2/liquidsoap-js.opam.template000066400000000000000000000000431477303350200211450ustar00rootroot00000000000000x-maintenance-intent: ["(latest)"] liquidsoap-2.3.2/liquidsoap-lang.opam000066400000000000000000000017341477303350200176500ustar00rootroot00000000000000# This file is generated by dune, edit dune-project instead opam-version: "2.0" version: "2.3.2" synopsis: "Liquidsoap language library" maintainer: ["The Savonet Team "] authors: ["The Savonet Team "] license: "GPL-2.0-or-later" homepage: "https://github.com/savonet/liquidsoap" bug-reports: "https://github.com/savonet/liquidsoap/issues" depends: [ "dune" {>= "3.6"} "ocaml" {>= "4.14"} "dune-site" "re" {>= "1.11.0"} "ppx_string" {build} "ppx_hash" {build} "sedlex" {>= "3.2"} "menhir" {>= "20240715"} "xml-light" "odoc" {with-doc} ] build: [ ["dune" "subst"] {dev} [ "dune" "build" "-p" name "-j" jobs "--promote-install-files=false" "@install" "@runtest" {with-test} "@doc" {with-doc} ] ["dune" "install" "-p" name "--create-install-files" name] ] dev-repo: "git+https://github.com/savonet/liquidsoap.git" x-maintenance-intent: ["(latest)"] liquidsoap-2.3.2/liquidsoap-lang.opam.template000066400000000000000000000000431477303350200214520ustar00rootroot00000000000000x-maintenance-intent: ["(latest)"] liquidsoap-2.3.2/liquidsoap-mode.opam000066400000000000000000000021201477303350200176410ustar00rootroot00000000000000# This file is generated by dune, edit dune-project instead opam-version: "2.0" version: "2.3.2" synopsis: "Liquidosap emacs mode" maintainer: ["The Savonet Team "] authors: ["The Savonet Team "] license: "GPL-2.0-or-later" homepage: "https://github.com/savonet/liquidsoap" bug-reports: "https://github.com/savonet/liquidsoap/issues" depends: [ "dune" {>= "3.6"} "liquidsoap" {= version} "odoc" {with-doc} ] build: [ ["dune" "subst"] {dev} [ "dune" "build" "-p" name "-j" jobs "--promote-install-files=false" "@install" "@runtest" {with-test} "@doc" {with-doc} ] ["dune" "install" "-p" name "--create-install-files" name] ] dev-repo: "git+https://github.com/savonet/liquidsoap.git" post-messages: [ "This package requires additional configuration for use in editors. Install package 'user-setup', or manually: * for Emacs, add these lines to ~/.emacs: (add-to-list 'load-path \"%{share}%/emacs/site-lisp\") (require 'emacs-mode) " {success & !user-setup:installed} ] liquidsoap-2.3.2/liquidsoap-mode.opam.template000066400000000000000000000004461477303350200214640ustar00rootroot00000000000000post-messages: [ "This package requires additional configuration for use in editors. Install package 'user-setup', or manually: * for Emacs, add these lines to ~/.emacs: (add-to-list 'load-path \"%{share}%/emacs/site-lisp\") (require 'emacs-mode) " {success & !user-setup:installed} ] liquidsoap-2.3.2/liquidsoap.opam000066400000000000000000000075421477303350200167340ustar00rootroot00000000000000# This file is generated by dune, edit dune-project instead opam-version: "2.0" version: "2.3.2" synopsis: "Swiss-army knife for multimedia streaming" description: """ Liquidsoap is a powerful and flexible language for describing your streams. It offers a rich collection of operators that you can combine at will, giving you more power than you need for creating or transforming streams. But liquidsoap is still very light and easy to use, in the Unix tradition of simple strong components working together. """ maintainer: ["The Savonet Team "] authors: ["The Savonet Team "] license: "GPL-2.0-or-later" homepage: "https://github.com/savonet/liquidsoap" bug-reports: "https://github.com/savonet/liquidsoap/issues" depends: [ "dune" {>= "3.6"} "ocaml" {>= "4.14"} "dtools" {>= "0.4.6"} "duppy" {>= "0.9.4"} "mm" {>= "0.8.6"} "re" {>= "1.11.0"} "ocurl" {>= "0.9.2"} "cry" {>= "1.0.3"} "camomile" {>= "2.0.0"} "uri" "fileutils" "menhirLib" "mem_usage" {>= "0.1.1"} "metadata" {>= "0.3.0"} "magic-mime" "dune-build-info" "liquidsoap-lang" {= version} "ppx_string" {build} "odoc" {with-doc} ] depopts: [ "alsa" "ao" "bjack" "camlimages" "ctypes-foreign" "dssi" "faad" "fdkaac" "ffmpeg" "flac" "frei0r" "gd" "graphics" "imagelib" "inotify" "irc-client-unix" "jemalloc" "ladspa" "lame" "lilv" "lo" "mad" "memtrace" "ogg" "opus" "osc-unix" "portaudio" "posix-time2" "posix-socket" "pulseaudio" "prometheus-liquidsoap" "samplerate" "shine" "soundtouch" "speex" "sqlite3" "srt" "ssl" "tls-liquidsoap" "theora" "sdl-liquidsoap" "vorbis" "yaml" "xmlplaylist" ] conflicts: [ "alsa" {< "0.3.0"} "ao" {< "0.2.0"} "bjack" {< "0.1.3"} "camomile" {< "1.0.0"} "dssi" {< "0.1.3"} "faad" {< "0.5.0"} "fdkaac" {< "0.3.1"} "ffmpeg" {< "1.2.0"} "ffmpeg-avutil" {< "1.2.0"} "flac" {< "1.0.0"} "frei0r" {< "0.1.0"} "inotify" {< "1.0"} "ladspa" {< "0.2.0"} "lame" {< "0.3.7"} "lo" {< "0.2.0"} "mad" {< "0.5.0"} "magic" {< "0.6"} "mirage-crypto-rng" {< "0.6.2"} "ogg" {< "1.0.0"} "opus" {< "0.2.0"} "odoc" {< "3.0.0~beta1"} "portaudio" {< "0.2.0"} "posix-time2" {< "2.0.2"} "posix-socket" {< "2.1.0"} "pulseaudio" {< "0.1.4"} "samplerate" {< "0.1.5"} "shine" {< "0.2.0"} "soundtouch" {< "0.1.9"} "speex" {< "1.0.0"} "srt" {< "0.3.2"} "ssl" {< "0.7.0"} "tls" {< "1.0.2"} "sdl-liquidsoap" {< "2"} "theora" {< "1.0.0"} "vorbis" {< "1.0.0"} "xmlplaylist" {< "0.1.3"} "pandoc" {with-doc} "pandoc-include" {with-doc} ] build: [ ["dune" "subst"] {dev} [ "dune" "build" "-p" name "-j" jobs "--promote-install-files=false" "@install" "@runtest" {with-test} "@doc" {with-doc} ] ["dune" "install" "-p" name "--create-install-files" name] ] post-messages: [ """\ We're sorry that your liquidsoap install failed. Check out our installation instructions at: https://www.liquidsoap.info/doc-%{version}%/install.html#opam for more information.""" {failure} "✨ Congratulations on installing liquidsoap! ✨" {success} """\ We noticed that you did not install the ffmpeg package. This package is highly recommended for most users and provides a lot of useful features, including decoding and encoding multiple media format, sending and receiving from various inputs and outputs and more.""" {success & !ffmpeg-enabled} """\ We noticed that you did not install any ssl or tls support. Liquidsoap won't be able to use SSL encryption in its input or output operators. You might want to install one of ssl or tls-liquidsoap package.""" {success & !ssl-enabled & !tls-enabled} ] x-maintenance-intent: ["(latest)"] depexts: ["coreutils"] {os = "macos" & os-distribution = "homebrew"} dev-repo: "git+https://github.com/savonet/liquidsoap.git" liquidsoap-2.3.2/liquidsoap.opam.template000066400000000000000000000017631477303350200205450ustar00rootroot00000000000000post-messages: [ """\ We're sorry that your liquidsoap install failed. Check out our installation instructions at: https://www.liquidsoap.info/doc-%{version}%/install.html#opam for more information.""" {failure} "✨ Congratulations on installing liquidsoap! ✨" {success} """\ We noticed that you did not install the ffmpeg package. This package is highly recommended for most users and provides a lot of useful features, including decoding and encoding multiple media format, sending and receiving from various inputs and outputs and more.""" {success & !ffmpeg-enabled} """\ We noticed that you did not install any ssl or tls support. Liquidsoap won't be able to use SSL encryption in its input or output operators. You might want to install one of ssl or tls-liquidsoap package.""" {success & !ssl-enabled & !tls-enabled} ] x-maintenance-intent: ["(latest)"] depexts: ["coreutils"] {os = "macos" & os-distribution = "homebrew"} dev-repo: "git+https://github.com/savonet/liquidsoap.git" liquidsoap-2.3.2/prometheus-liquidsoap.opam000066400000000000000000000015621477303350200211210ustar00rootroot00000000000000# This file is generated by dune, edit dune-project instead opam-version: "2.0" version: "2" synopsis: "Virtual package installing liquidsoap dependencies for prometheus optional features" maintainer: ["The Savonet Team "] authors: ["The Savonet Team "] license: "GPL-2.0-or-later" homepage: "https://github.com/savonet/liquidsoap" bug-reports: "https://github.com/savonet/liquidsoap/issues" depends: [ "dune" {>= "3.6"} "prometheus-app" "cohttp-lwt-unix" "odoc" {with-doc} ] build: [ ["dune" "subst"] {dev} [ "dune" "build" "-p" name "-j" jobs "--promote-install-files=false" "@install" "@runtest" {with-test} "@doc" {with-doc} ] ["dune" "install" "-p" name "--create-install-files" name] ] dev-repo: "git+https://github.com/savonet/liquidsoap.git" liquidsoap-2.3.2/scripts/000077500000000000000000000000001477303350200153635ustar00rootroot00000000000000liquidsoap-2.3.2/scripts/.gitignore000066400000000000000000000000311477303350200173450ustar00rootroot00000000000000liquidsoap-completion.el liquidsoap-2.3.2/scripts/bash-completion000066400000000000000000000020541477303350200203730ustar00rootroot00000000000000if [ -z "$BASH_VERSION" ]; then return 0; fi _liquidsoap_add() { IFS=$'\n' _liquidsoap_reply+=("$@") } _liquidsoap_add_f() { local cmd cmd=$1; shift _liquidsoap_add "$($cmd "$@" 2>/dev/null)" } _liquidsoap() { local IFS cmd cur compgen_opt cmd=${COMP_WORDS[1]} cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]} compgen_opt=() _liquidsoap_reply=() case "$prev" in "-h"|"-help"|"--help") _liquidsoap_add_f liquidsoap --list-functions ;; *) _liquidsoap_reply+=("-c --check --list-settings -d --daemon --debug -h --help -i --interactive --list-functions --list-functions-md --list-extra-functions-md --list-plugins --list-protocols-md --no-stdlib --parse-only --profile --quiet -r --strict --unsafe --version -v --verbose") compgen_opt+=(-o filenames -f) ;; esac COMPREPLY=($(compgen -W "${_liquidsoap_reply[*]}" "${compgen_opt[@]}" -- "$cur")) unset _liquidsoap_reply return 0 } complete -F _liquidsoap liquidsoap liquidsoap-2.3.2/scripts/dune000066400000000000000000000014301477303350200162370ustar00rootroot00000000000000(executable (name gen_emacs_completion) (link_flags -cclib %{env:LIQ_LDFLAGS=}) (libraries liquidsoap_runtime) (modules gen_emacs_completion)) (rule (target liquidsoap-completions.el) (deps (source_tree ../src/libs) (:stdlib ../src/libs/stdlib.liq) ./gen_emacs_completion.exe) (action (with-stdout-to %{target} (run ./gen_emacs_completion.exe --stdlib %{stdlib})))) (install (section share_root) (package liquidsoap) (files (bash-completion as bash_completion/completions/liquidsoap))) (install (section share_root) (package liquidsoap-mode) (files (liquidsoap-mode.el as emacs/site-lisp/liquidsoap-mode.el) (liquidsoap-completion.el as emacs/site-lisp/liquidsoap-completion.el) (liquidsoap-completions.el as emacs/site-lisp/liquidsoap-completions.el))) liquidsoap-2.3.2/scripts/gen_emacs_completion.ml000066400000000000000000000002761477303350200220740ustar00rootroot00000000000000open Liquidsoap_runtime let () = Main.parse_options (); Main.with_toplevel ~run_streams:false (fun () -> Lang_string.kprint_string ~pager:false Doc.Value.print_emacs_completions) liquidsoap-2.3.2/scripts/liquidsoap-completion.el000066400000000000000000000021731477303350200222310ustar00rootroot00000000000000;; Inspired of ;; http://sixty-north.com/blog/writing-the-simplest-emacs-company-mode-backend ;; http://sixty-north.com/blog/a-more-full-featured-company-mode-backend.html (require 'cl-lib) (require 'company) (require 'liquidsoap-completions) (defun liquidsoap-annotation (s) (format " : %s" (get-text-property 0 :type s)) ) (defun liquidsoap-meta (s) (get-text-property 0 :description s) ) (defun company-liquidsoap-backend (command &optional arg &rest ignored) (interactive (list 'interactive)) (cl-case command (interactive (company-begin-backend 'company-liquidsoap-backend)) (prefix (and (eq major-mode 'liquidsoap-mode) ;; we don't use company-grab-symbol here because we want to match dots (company-grab-line "\\(?:^\\| \\)\\([^ ]*\\)" 1) ) ) (candidates (cl-remove-if-not (lambda (c) (string-prefix-p arg c)) liquidsoap-completions)) (annotation (liquidsoap-annotation arg)) (meta (liquidsoap-meta arg)) ) ) (defun init-liquidsoap-completion () (add-to-list 'company-backends 'company-liquidsoap-backend) ) (provide 'liquidsoap-completion) liquidsoap-2.3.2/scripts/liquidsoap-mode.el000066400000000000000000000064731477303350200210130ustar00rootroot00000000000000;; liquidsoap-mode.el -- Liquidsoap major mode ;; Copyright (C) 2003-2024 Samuel Mimram (require 'liquidsoap-completion) (defvar liquidsoap-font-lock-keywords '( ("#.*" . 'font-lock-comment-face) ("^\\(%ifdef .*\\|%ifndef .*\\|%ifencoder .*\\|%ifnencoder .*\\|%ifversion .*\\|%else .*\\|%endif\\|%include\\)" . 'font-lock-preprocessor-face) ("\\<\\(fun\\|def\\|rec\\|replaces\\|eval\\|begin\\|end\\|if\\|then\\|else\\|elsif\\|let\\|try\\|catch\\|while\\|for\\|in\\|to\\|do\\|open\\)\\>\\|->\\|;" . font-lock-keyword-face) ("\\<\\(and\\|or\\|not\\|mod\\|??\\)\\>\\|:=" . font-lock-builtin-face) ("\\<\\(true\\|false\\)\\>" . font-lock-constant-face) ("\\" st) st) "Syntax table for Liquidsoap major mode.") (defvar liquidsoap-tab-width 2) ;see http://www.emacswiki.org/emacs/ModeTutorial (defun liquidsoap-indent-line () "Indent current Liquidsoap line" (interactive) (beginning-of-line) ; At beginning, no indentation (if (bobp) (indent-line-to 0) ; not-indented is a boolean saying we found a match looking backward ; cur-indent is the current indetation (let ((not-indented t) cur-indent) ; De-indent after end (if (looking-at "^[ \t]*\\(end\\|else\\|elsif\\|then\\|%endif\\|try\\|catch\\)") (progn (save-excursion (forward-line -1) (setq cur-indent (- (current-indentation) liquidsoap-tab-width))) (if (< cur-indent 0) (setq cur-indent 0))) (save-excursion (while not-indented (forward-line -1) ; Indent as much as the last end (if (looking-at "^[ \t]*\\(end\\|%endif\\)") (progn (setq cur-indent (current-indentation)) (setq not-indented nil)) ; Increment if we find that we are in a block (if (looking-at "^[ \t]*\\(def\\|if\\|then\\|else\\|elsif\\|%ifdef\\|.*=$\\|try\\|catch\\|for\\|while\\)") (progn (setq cur-indent (+ (current-indentation) liquidsoap-tab-width)) (setq not-indented nil)) ; Same as previous line otherwise (if (bobp) (setq not-indented nil)) ) ) ) ) ) ; If we didn't see an indentation hint, then allow no indentation (if cur-indent (indent-line-to cur-indent) (indent-line-to 0)) ) ) ) (define-derived-mode liquidsoap-mode fundamental-mode "Liquidsoap" "Major mode for Liquidsoap files." :syntax-table liquidsoap-mode-syntax-table (set (make-local-variable 'comment-start) "#") (set (make-local-variable 'comment-start-skip) "#+\\s-*") (set (make-local-variable 'indent-line-function) 'liquidsoap-indent-line) (set (make-local-variable 'font-lock-defaults) '(liquidsoap-font-lock-keywords)) (setq mode-name "Liquidsoap") ) (add-hook 'liquidsoap-mode-hook 'company-mode) (add-hook 'liquidsoap-mode-hook 'init-liquidsoap-completion) (provide 'liquidsoap-mode) ;;;###autoload (add-to-list 'auto-mode-alist '("\\.liq\\'" . liquidsoap-mode)) liquidsoap-2.3.2/scripts/liquidsoap.xml000066400000000000000000000123421477303350200202610ustar00rootroot00000000000000 ]> def eval replaces rec let fun begin end if then else elsif while for in try catch do open == != and or not ! true false bool int float string @param @category @flag liquidsoap-2.3.2/sdl-liquidsoap.opam000066400000000000000000000015661477303350200175140ustar00rootroot00000000000000# This file is generated by dune, edit dune-project instead opam-version: "2.0" version: "3" synopsis: "Virtual package installing liquidsoap dependencies for SDL optional features" maintainer: ["The Savonet Team "] authors: ["The Savonet Team "] license: "GPL-2.0-or-later" homepage: "https://github.com/savonet/liquidsoap" bug-reports: "https://github.com/savonet/liquidsoap/issues" depends: [ "dune" {>= "3.6"} "tsdl" "tsdl-image" {>= "0.3.2"} "tsdl-ttf" "odoc" {with-doc} ] build: [ ["dune" "subst"] {dev} [ "dune" "build" "-p" name "-j" jobs "--promote-install-files=false" "@install" "@runtest" {with-test} "@doc" {with-doc} ] ["dune" "install" "-p" name "--create-install-files" name] ] dev-repo: "git+https://github.com/savonet/liquidsoap.git" liquidsoap-2.3.2/src/000077500000000000000000000000001477303350200144635ustar00rootroot00000000000000liquidsoap-2.3.2/src/bin/000077500000000000000000000000001477303350200152335ustar00rootroot00000000000000liquidsoap-2.3.2/src/bin/dune000066400000000000000000000010101477303350200161010ustar00rootroot00000000000000(executable (name liquidsoap) (public_name liquidsoap) (package liquidsoap) (link_flags -cclib %{env:LIQ_LDFLAGS=}) (libraries liquidsoap_runtime) (modules liquidsoap)) (rule (target liquidsoap-macos-instruments.exe) (enabled_if %{bin-available:codesign}) (deps ./liquidsoap.exe ./instruments.plist) (action (progn (copy ./liquidsoap.exe ./liquidsoap-macos-instruments.exe) (run codesign -s - -v -f --entitlements instruments.plist liquidsoap-macos-instruments.exe)))) liquidsoap-2.3.2/src/bin/instruments.plist000066400000000000000000000003531477303350200207040ustar00rootroot00000000000000com.apple.security.get-task-allow liquidsoap-2.3.2/src/bin/liquidsoap.ml000066400000000000000000000000521477303350200177340ustar00rootroot00000000000000let () = Liquidsoap_runtime.Runner.run () liquidsoap-2.3.2/src/build/000077500000000000000000000000001477303350200155625ustar00rootroot00000000000000liquidsoap-2.3.2/src/build/build_tools.ml000066400000000000000000000001731477303350200204340ustar00rootroot00000000000000let read_files ~location dir = List.sort Stdlib.compare (Array.to_list (Sys.readdir (Filename.concat location dir))) liquidsoap-2.3.2/src/build/dune000066400000000000000000000001211477303350200164320ustar00rootroot00000000000000(library (name liquidsoap_build_tools) (wrapped false) (modules build_tools)) liquidsoap-2.3.2/src/config/000077500000000000000000000000001477303350200157305ustar00rootroot00000000000000liquidsoap-2.3.2/src/config/alsa_option.disabled.ml000077700000000000000000000000001477303350200254062noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/alsa_option.enabled.ml000077700000000000000000000000001477303350200250542noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/ao_option.disabled.ml000077700000000000000000000000001477303350200250652noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/ao_option.enabled.ml000077700000000000000000000000001477303350200245332noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/bjack_option.disabled.ml000077700000000000000000000000001477303350200255402noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/bjack_option.enabled.ml000077700000000000000000000000001477303350200252062noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/camlimages_option.disabled.ml000077700000000000000000000000001477303350200265702noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/camlimages_option.enabled.ml000077700000000000000000000000001477303350200262362noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/dssi_option.disabled.ml000077700000000000000000000000001477303350200254302noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/dssi_option.enabled.ml000077700000000000000000000000001477303350200250762noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/faad_option.disabled.ml000077700000000000000000000000001477303350200253612noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/faad_option.enabled.ml000077700000000000000000000000001477303350200250272noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/fdkaac_option.disabled.ml000077700000000000000000000000001477303350200256772noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/fdkaac_option.enabled.ml000077700000000000000000000000001477303350200253452noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/ffmpeg_option.disabled.ml000077700000000000000000000000001477303350200257322noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/ffmpeg_option.enabled.ml000077700000000000000000000000001477303350200254002noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/flac_option.disabled.ml000077700000000000000000000000001477303350200253732noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/flac_option.enabled.ml000077700000000000000000000000001477303350200250412noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/frei0r_option.disabled.ml000077700000000000000000000000001477303350200256552noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/frei0r_option.enabled.ml000077700000000000000000000000001477303350200253232noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/gd_option.disabled.ml000077700000000000000000000000001477303350200250602noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/gd_option.enabled.ml000077700000000000000000000000001477303350200245262noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/graphics_option.disabled.ml000077700000000000000000000000001477303350200262662noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/graphics_option.enabled.ml000077700000000000000000000000001477303350200257342noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/imagelib_option.disabled.ml000077700000000000000000000000001477303350200262372noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/imagelib_option.enabled.ml000077700000000000000000000000001477303350200257052noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/inotify_option.disabled.ml000077700000000000000000000000001477303350200261472noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/inotify_option.enabled.ml000077700000000000000000000000001477303350200256152noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/irc_option.disabled.ml000066400000000000000000000001031477303350200221670ustar00rootroot00000000000000let detected = "no (requires irc-client-unix)" let enabled = false liquidsoap-2.3.2/src/config/irc_option.enabled.ml000077700000000000000000000000001477303350200247112noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/jemalloc_option.disabled.ml000077700000000000000000000000001477303350200262542noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/jemalloc_option.enabled.ml000077700000000000000000000000001477303350200257222noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/ladspa_option.disabled.ml000077700000000000000000000000001477303350200257322noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/ladspa_option.enabled.ml000077700000000000000000000000001477303350200254002noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/lame_option.disabled.ml000077700000000000000000000000001477303350200254042noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/lame_option.enabled.ml000077700000000000000000000000001477303350200250522noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/lilv_option.disabled.ml000077700000000000000000000000001477303350200254342noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/lilv_option.enabled.ml000077700000000000000000000000001477303350200251022noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/liquidsoap_paths.default.ml000066400000000000000000000020201477303350200232500ustar00rootroot00000000000000module Sites = Liquidsoap_lang.Sites.Sites type mode = [ `Default | `Standalone | `Posix ] let mode = `Default let get_site name = match name with [] -> "" | s :: _ -> s (* This is a hack. *) let prefix () = Filename.(dirname (get_site Sites.lib_root)) let rundir () = List.fold_left Filename.concat (prefix ()) ["var"; "liquidsoap"; "run"] let rundir_descr = "(set by dune-site)" let logdir () = List.fold_left Filename.concat (prefix ()) ["var"; "liquidsoap"; "log"] let logdir_descr = "(set by dune-site)" let liq_libs_dir () = get_site Sites.libs let liq_libs_dir_descr = "(set by dune-site)" let bin_dir () = get_site Sites.bin let bin_dir_descr = "(set by dune-site)" let camomile_dir () = Filename.dirname CamomileLib.Config.Default.datadir let camomile_dir_descr = "(set by dune-site)" let user_cache_override () = None let user_cache_override_descr = "$HOME/.cache/liquidsoap" let system_cache_override () = match Sites.cache with [] -> None | d :: _ -> Some d let system_cache_override_descr = "(set by dune-site)" liquidsoap-2.3.2/src/config/liquidsoap_paths.posix.ml000066400000000000000000000012431477303350200227740ustar00rootroot00000000000000type mode = [ `Default | `Standalone | `Posix ] let mode = `Posix let rundir () = "/var/run/liquidsoap" let rundir_descr = rundir () let logdir () = "/var/log/liquidsoap" let logdir_descr = logdir () let liq_libs_dir () = "/usr/share/liquidsoap/libs" let liq_libs_dir_descr = liq_libs_dir () let bin_dir () = "/usr/share/liquidsoap/bin" let bin_dir_descr = bin_dir () let camomile_dir () = "/usr/share/liquidsoap/camomile" let camomile_dir_descr = camomile_dir () let user_cache_override () = None let user_cache_override_descr = "$HOME/.cache/liquidsoap" let system_cache_override () = Some "/var/cache/liquidsoap" let system_cache_override_descr = "/var/cache/liquidsoap" liquidsoap-2.3.2/src/config/liquidsoap_paths.standalone.ml000066400000000000000000000014271477303350200237660ustar00rootroot00000000000000type mode = [ `Default | `Standalone | `Posix ] let mode = `Standalone let path = Filename.concat (Filename.dirname Sys.executable_name) let rundir () = path "run" let rundir_descr = "./run" let logdir () = path "log" let logdir_descr = "./log" let liq_libs_dir () = path "libs" let liq_libs_dir_descr = "./libs" let bin_dir () = path "bin" let bin_dir_descr = "./bin" let camomile_dir () = path "camomile" let camomile_dir_descr = "./camomile" let user_cache_override () = let dir = Filename.dirname Sys.executable_name in let cwd = Sys.getcwd () in Sys.chdir dir; let dir = Sys.getcwd () in Sys.chdir cwd; Some (Filename.concat dir ".cache") let user_cache_override_descr = "./cache" let system_cache_override () = Some "./cache" let system_cache_override_descr = "./cache" liquidsoap-2.3.2/src/config/lo_option.disabled.ml000077700000000000000000000000001477303350200251002noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/lo_option.enabled.ml000077700000000000000000000000001477303350200245462noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/mad_option.disabled.ml000077700000000000000000000000001477303350200252272noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/mad_option.enabled.ml000077700000000000000000000000001477303350200246752noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/memtrace_option.disabled.ml000077700000000000000000000000001477303350200262632noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/memtrace_option.enabled.ml000077700000000000000000000000001477303350200257312noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/ndi_option.disabled.ml000066400000000000000000000001021477303350200221630ustar00rootroot00000000000000let detected = "no (requires ctypes-foreign)" let enabled = false liquidsoap-2.3.2/src/config/ndi_option.enabled.ml000077700000000000000000000000001477303350200247062noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/noop.disabled.ml000066400000000000000000000002261477303350200210030ustar00rootroot00000000000000let detected = let dep = Filename.basename (List.hd (String.split_on_char '_' __FILE__)) in [%string "no (requires %{dep})"] let enabled = false liquidsoap-2.3.2/src/config/noop.enabled.ml000066400000000000000000000000501477303350200206210ustar00rootroot00000000000000let detected = "yes" let enabled = true liquidsoap-2.3.2/src/config/ogg_flac_option.disabled.ml000077700000000000000000000000001477303350200262272noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/ogg_flac_option.enabled.ml000077700000000000000000000000001477303350200256752noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/ogg_option.disabled.ml000077700000000000000000000000001477303350200252422noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/ogg_option.enabled.ml000077700000000000000000000000001477303350200247102noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/optionals_option.disabled.ml000077700000000000000000000000001477303350200264762noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/optionals_option.enabled.ml000077700000000000000000000000001477303350200261442noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/opus_option.disabled.ml000077700000000000000000000000001477303350200254542noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/opus_option.enabled.ml000077700000000000000000000000001477303350200251222noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/osc_option.disabled.ml000066400000000000000000000000741477303350200222050ustar00rootroot00000000000000let detected = "no (requires osc-unix)" let enabled = false liquidsoap-2.3.2/src/config/osc_option.enabled.ml000077700000000000000000000000001477303350200247202noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/oss_option.disabled.ml000077700000000000000000000000001477303350200252722noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/oss_option.enabled.ml000077700000000000000000000000001477303350200247402noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/portaudio_option.disabled.ml000077700000000000000000000000001477303350200264742noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/portaudio_option.enabled.ml000077700000000000000000000000001477303350200261422noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/posix_time_option.disabled.ml000077700000000000000000000000001477303350200266462noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/posix_time_option.enabled.ml000077700000000000000000000000001477303350200263142noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/prometheus_option.disabled.ml000077700000000000000000000000001477303350200266612noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/prometheus_option.enabled.ml000077700000000000000000000000001477303350200263272noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/pulseaudio_option.disabled.ml000077700000000000000000000000001477303350200266402noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/pulseaudio_option.enabled.ml000077700000000000000000000000001477303350200263062noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/samplerate_option.disabled.ml000077700000000000000000000000001477303350200266232noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/samplerate_option.enabled.ml000077700000000000000000000000001477303350200262712noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/sdl_option.disabled.ml000066400000000000000000000001111477303350200221730ustar00rootroot00000000000000let detected = "no (requires tsdl-image & tsdl-ttf)" let enabled = false liquidsoap-2.3.2/src/config/sdl_option.enabled.ml000077700000000000000000000000001477303350200247162noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/shine_option.disabled.ml000077700000000000000000000000001477303350200255742noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/shine_option.enabled.ml000077700000000000000000000000001477303350200252422noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/soundtouch_option.disabled.ml000077700000000000000000000000001477303350200266612noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/soundtouch_option.enabled.ml000077700000000000000000000000001477303350200263272noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/speex_option.disabled.ml000077700000000000000000000000001477303350200256122noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/speex_option.enabled.ml000077700000000000000000000000001477303350200252602noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/sqlite3_option.disabled.ml000077700000000000000000000000001477303350200260522noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/sqlite3_option.enabled.ml000077700000000000000000000000001477303350200255202noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/srt_option.disabled.ml000077700000000000000000000000001477303350200252762noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/srt_option.enabled.ml000077700000000000000000000000001477303350200247442noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/ssl_option.disabled.ml000077700000000000000000000000001477303350200252672noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/ssl_option.enabled.ml000077700000000000000000000000001477303350200247352noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/stereotool_option.disabled.ml000066400000000000000000000001021477303350200236100ustar00rootroot00000000000000let detected = "no (requires ctypes-foreign)" let enabled = false liquidsoap-2.3.2/src/config/stereotool_option.enabled.ml000077700000000000000000000000001477303350200263332noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/theora_option.disabled.ml000077700000000000000000000000001477303350200257502noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/theora_option.enabled.ml000077700000000000000000000000001477303350200254162noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/tls_option.disabled.ml000066400000000000000000000001021477303350200222130ustar00rootroot00000000000000let detected = "no (requires tls-liquidsoap)" let enabled = false liquidsoap-2.3.2/src/config/tls_option.enabled.ml000077700000000000000000000000001477303350200247362noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/vorbis_option.disabled.ml000077700000000000000000000000001477303350200257722noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/vorbis_option.enabled.ml000077700000000000000000000000001477303350200254402noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/winsvc_option.disabled.ml000077700000000000000000000000001477303350200257772noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/winsvc_option.enabled.ml000077700000000000000000000000001477303350200254452noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/xmlplaylist_option.disabled.ml000077700000000000000000000000001477303350200270502noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/xmlplaylist_option.enabled.ml000077700000000000000000000000001477303350200265162noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/yaml_option.disabled.ml000077700000000000000000000000001477303350200254302noop.disabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/config/yaml_option.enabled.ml000077700000000000000000000000001477303350200250762noop.enabled.mlustar00rootroot00000000000000liquidsoap-2.3.2/src/console/000077500000000000000000000000001477303350200161255ustar00rootroot00000000000000liquidsoap-2.3.2/src/console/console.ml000066400000000000000000000044641477303350200201310ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (* Some of the code below was borrowed from opam. *) let dumb_term = lazy (try Sys.getenv "TERM" = "dumb" with Not_found -> Sys.win32) type color_conf = [ `Always | `Never | `Auto ] let color_conf : color_conf ref = ref `Auto let color = let auto = lazy (try Unix.isatty Unix.stdout && not (Lazy.force dumb_term) with _ -> false) in fun () -> match !color_conf with | `Always -> true | `Never -> false | `Auto -> Lazy.force auto type text_style = [ `bold | `underline | `crossed | `black | `red | `green | `yellow | `blue | `magenta | `cyan | `white ] let style_code (c : text_style) = match c with | `bold -> "01" | `underline -> "04" | `crossed -> "09" | `black -> "30" | `red -> "31" | `green -> "32" | `yellow -> "33" | `blue -> "1;34" (* most terminals make blue unreadable unless bold *) | `magenta -> "35" | `cyan -> "36" | `white -> "37" let colorize styles s = if not (color ()) then s else Printf.sprintf "\027[%sm%s\027[0m" (String.concat ";" (List.map style_code styles)) s let start_color styles = if not (color ()) then "" else Printf.sprintf "\027[%sm" (String.concat ";" (List.map style_code styles)) let stop_color () = if not (color ()) then "" else "\027[0m" liquidsoap-2.3.2/src/console/console.mli000066400000000000000000000025441477303350200202770ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) type text_style = [ `bold | `underline | `crossed | `black | `red | `green | `yellow | `blue | `magenta | `cyan | `white ] val colorize : text_style list -> string -> string val start_color : text_style list -> string val stop_color : unit -> string type color_conf = [ `Always | `Never | `Auto ] val color_conf : color_conf ref liquidsoap-2.3.2/src/console/dune000066400000000000000000000001461477303350200170040ustar00rootroot00000000000000(library (name console) (public_name liquidsoap-lang.console) (modules console) (libraries unix)) liquidsoap-2.3.2/src/core/000077500000000000000000000000001477303350200154135ustar00rootroot00000000000000liquidsoap-2.3.2/src/core/alsa_settings.ml000066400000000000000000000041671477303350200206150ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Alsa related settings *) module SyncSource = Clock.MkSyncSource (struct type t = unit let to_string _ = "alsa" end) let sync_source = SyncSource.make () (** ALSA should be quiet *) let () = Alsa.no_stderr_report () (** Error translator *) let error_translator e = match e with | Alsa.Buffer_xrun | Alsa.Bad_state | Alsa.Suspended | Alsa.IO_error | Alsa.Device_busy | Alsa.Invalid_argument | Alsa.Device_removed | Alsa.Interrupted | Alsa.Unknown_error _ -> Some (Printf.sprintf "Alsa error: %s" (Alsa.string_of_error e)) | _ -> None let () = Printexc.register_printer error_translator let conf = Dtools.Conf.void ~p:(Configure.conf#plug "alsa") "ALSA configuration" let periods = Dtools.Conf.int ~p:(conf#plug "periods") ~d:0 "Number of periods" ~comments:["Set to 0 to disable this setting and use ALSA's default."] let alsa_buffer = Dtools.Conf.int ~p:(conf#plug "alsa_buffer") ~d:0 "Alsa internal buffer size" ~comments: [ "This setting is only used in buffered alsa I/O, and affects latency."; "Set to 0 to disable this setting and use ALSA's default."; ] liquidsoap-2.3.2/src/core/builtins/000077500000000000000000000000001477303350200172445ustar00rootroot00000000000000liquidsoap-2.3.2/src/core/builtins/builtins_callbacks.ml000066400000000000000000000041321477303350200234260ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let _ = Lang.add_builtin "on_shutdown" ~category:`System [("", Lang.fun_t [] Lang.unit_t, None, None)] Lang.unit_t ~descr:"Register a function to be called when Liquidsoap shuts down." (fun p -> let f = List.assoc "" p in Lifecycle.before_core_shutdown ~name:"on shutdown execution" (fun () -> ignore (Lang.apply f [])); Lang.unit) let _ = Lang.add_builtin "on_cleanup" ~category:`System [("", Lang.fun_t [] Lang.unit_t, None, None)] Lang.unit_t ~descr:"Register a function to be called for the final cleanup." (fun p -> let f = List.assoc "" p in Lifecycle.on_final_cleanup ~name:"on cleanup execution" (fun () -> ignore (Lang.apply f [])); Lang.unit) let _ = Lang.add_builtin "on_start" ~category:`System [("", Lang.fun_t [] Lang.unit_t, None, None)] Lang.unit_t ~descr:"Register a function to be called when Liquidsoap starts." (fun p -> let f = List.assoc "" p in let wrap_f () = ignore (Lang.apply f []) in Lifecycle.after_start ~name:"on start execution" wrap_f; Lang.unit) liquidsoap-2.3.2/src/core/builtins/builtins_clock.ml000066400000000000000000000063541477303350200226120ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let clock = Lang.add_builtin "clock" ~category:`Liquidsoap ~descr:"Decorate a clock with all its methods." [("", Lang_source.ClockValue.base_t, None, None)] Lang_source.ClockValue.t (fun p -> Lang_source.ClockValue.(to_value (of_value (List.assoc "" p)))) let _ = Lang.add_builtin ~base:clock "active" ~category:`Liquidsoap ~descr:"Return the list of clocks currently in use." [] (Lang.list_t Lang_source.ClockValue.t) (fun _ -> Lang.list (List.map Lang_source.ClockValue.to_value (Clock.clocks ()))) let _ = Lang.add_builtin ~base:clock "create" ~category:`Liquidsoap ~descr:"Create a new clock" [ ( "id", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Identifier for the new clock." ); ( "on_error", Lang.nullable_t (Lang.fun_t [(false, "", Lang.error_t)] Lang.unit_t), Some Lang.null, Some "Error callback executed when a streaming error occurs. When passed, \ all streaming errors are silenced. Intended mostly for debugging \ purposes." ); ( "sync", Lang.string_t, Some (Lang.string "auto"), Some "Clock sync mode. Should be one of: `\"auto\"`, `\"CPU\"`, \ `\"unsynced\"` or `\"passive\"`. Defaults to `\"auto\"`. Defaults \ to: \"auto\"" ); ] Lang_source.ClockValue.t (fun p -> let id = Lang.to_valued_option Lang.to_string (List.assoc "id" p) in let on_error = Lang.to_option (List.assoc "on_error" p) in let on_error = Option.map (fun on_error exn bt -> let error = Lang.runtime_error_of_exception ~bt ~kind:"output" exn in ignore (Lang.apply on_error [("", Lang.error error)])) on_error in let sync = List.assoc "sync" p in let sync = try Clock.active_sync_mode_of_string (Lang.to_string sync) with _ -> raise (Error.Invalid_value ( sync, "Invalid sync mode! Should be one of: `\"auto\"`, `\"CPU\"`, \ `\"unsynced\"` or `\"passive\"`" )) in Lang_source.ClockValue.to_value (Clock.create ~stack:(Lang.pos p) ?on_error ?id ~sync ())) liquidsoap-2.3.2/src/core/builtins/builtins_cry.ml000066400000000000000000000112211477303350200223010ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let icy = Lang.add_module "icy" let log = Log.make ["icy"; "update_metadata"] module Http = Liq_http let _ = let user_agent = Lang.product (Lang.string "User-Agent") (Lang.string Http.user_agent) in Lang.add_builtin ~base:icy "update_metadata" ~category:`Interaction ~descr:"Update metata on an icecast mountpoint using the ICY protocol." [ ("host", Lang.string_t, Some (Lang.string "localhost"), None); ("port", Lang.int_t, Some (Lang.int 8000), None); ("user", Lang.string_t, Some (Lang.string "source"), None); ( "transport", Lang.http_transport_base_t, Some (Lang.base_http_transport Http.unix_transport), Some "Http transport. Use `http.transport.ssl` or \ `http.transport.secure_transport`, when available, to enable HTTPS \ output" ); ("password", Lang.string_t, Some (Lang.string "hackme"), None); ( "mount", Lang.string_t, Some (Lang.string ""), Some "Source mount point. Mandatory when streaming to icecast." ); ( "icy_id", Lang.int_t, Some (Lang.int 1), Some "Shoutcast source ID. Only supported by Shoutcast v2." ); ( "protocol", Lang.string_t, Some (Lang.string "http"), Some "Protocol to use. One of: \"icy\", \"http\" or \"https\"" ); ( "encoding", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Encoding used to send metadata, default (UTF-8) if null." ); ( "headers", Lang.metadata_t, Some (Lang.list [user_agent]), Some "Additional headers." ); ("", Lang.metadata_t, None, None); ] Lang.unit_t (fun p -> let user = Lang.to_string (List.assoc "user" p) in let password = Lang.to_string (List.assoc "password" p) in let mount = Lang.to_string (List.assoc "mount" p) in let icy_id = Lang.to_int (List.assoc "icy_id" p) in let metas = Lang.to_metadata (Lang.assoc "" 1 p) in let out_enc = List.assoc "encoding" p |> Lang.to_valued_option Lang.to_string |> Option.map Charset.of_string in let metas = let ret = Hashtbl.create 0 in Frame.Metadata.iter (fun x y -> Hashtbl.replace ret x (Charset.convert ?target:out_enc y)) metas; ret in let host = Lang.to_string (List.assoc "host" p) in let port = Lang.to_int (List.assoc "port" p) in let transport = Lang.to_http_transport (List.assoc "transport" p) in let headers = List.map (fun v -> let f (x, y) = (Lang.to_string x, Lang.to_string y) in f (Lang.to_product v)) (Lang.to_list (List.assoc "headers" p)) in let headers = let h = Hashtbl.create 10 in List.iter (fun (x, y) -> Hashtbl.replace h x y) headers; h in let protocol = let v = List.assoc "protocol" p in match Lang.to_string v with | "icy" -> Cry.Icy | "http" -> Cry.Http Cry.Source (* Verb doesn't matter here. *) | _ -> raise (Error.Invalid_value (v, "protocol should be one of: 'icy', 'http' or 'https'.")) in let mount = match protocol with | Cry.Icy -> Cry.Icy_id icy_id | _ -> Cry.Icecast_mount mount in begin try let transport = (transport :> Cry.transport) in Cry.manual_update_metadata ~host ~port ~protocol ~user ~password ~mount ~headers ~transport metas with e -> log#severe "Manual metadata update failed: %s" (Printexc.to_string e) end; Lang.unit) liquidsoap-2.3.2/src/core/builtins/builtins_ffmpeg_base.ml000066400000000000000000000024261477303350200237510ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let ffmpeg = Lang.add_module "ffmpeg" let ffmpeg_filter = Lang.add_module ~base:ffmpeg "filter" let ffmpeg_raw = Lang.add_module ~base:ffmpeg "raw" let track_ffmpeg = Lang.add_module ~base:Modules.track "ffmpeg" let track_ffmpeg_raw = Lang.add_module ~base:track_ffmpeg "raw" liquidsoap-2.3.2/src/core/builtins/builtins_ffmpeg_bitstream_filters.ml000066400000000000000000000301601477303350200265550ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Builtins_ffmpeg_base let log = Log.make ["ffmpeg"; "filter"; "bitstream"] let ffmpeg_filter_bitstream = Lang.add_module ~base:ffmpeg_filter "bitstream" type 'a handler = { stream_idx : int64; filter : 'a Avcodec.BitstreamFilter.t; params : 'a Avcodec.params; time_base : Avutil.rational; duration_converter : 'a Avcodec.Packet.t Ffmpeg_utils.Duration.t; } type 'a get_params = Ffmpeg_copy_content.params_payload -> 'a Avcodec.params type 'a mk_params = 'a Avcodec.params -> Ffmpeg_copy_content.params_payload type 'a get_packet = Ffmpeg_copy_content.packet -> 'a Avcodec.Packet.t type 'a mk_packet = 'a Avcodec.Packet.t -> Ffmpeg_copy_content.packet let args_of_args args = let opts = Hashtbl.create 10 in let rec f = function | [] -> () | `Pair (lbl, v) :: args -> Hashtbl.replace opts lbl v; f args in f args; opts let modes name (codecs : Avcodec.id list) = let has_audio = List.exists (fun codec -> List.mem (codec :> Avcodec.id) codecs) Avcodec.Audio.codec_ids in let has_video = List.exists (fun codec -> List.mem (codec :> Avcodec.id) codecs) Avcodec.Video.codec_ids in match (codecs, has_audio, has_video) with | [], _, _ | _, true, true -> [`Audio; `Video] | _, true, false -> [`Audio_only] | _, false, true -> [`Video_only] | _, false, false -> log#important "No valid mode found for filter %s!" name; [] let process (type a) ~put_data ~(mk_params : a mk_params) ~(mk_packet : a mk_packet) ({ time_base; params; stream_idx } : a handler) ((length, packets) : int * (int * a Avcodec.Packet.t) list) = let data = List.map (fun (pos, packet) -> ( pos, { Ffmpeg_copy_content.packet = mk_packet packet; time_base; stream_idx; } )) packets in let data = { Ffmpeg_copy_content.params = Some (mk_params params); data; length } in let data = Ffmpeg_copy_content.lift_data data in put_data data let flush_filter ~put_data ~mk_params ~mk_packet handler = let rec f () = match Ffmpeg_utils.Duration.push handler.duration_converter (Avcodec.BitstreamFilter.receive_packet handler.filter) with | None -> f () | Some v -> process ~put_data ~mk_params ~mk_packet handler v; f () | (exception Avutil.Error `Eagain) | (exception Avutil.Error `Eof) -> () in f () let flush (type a) ~put_data ~(mk_params : a mk_params) ~(mk_packet : a mk_packet) (handler : a handler option) = match handler with | None -> () | Some h -> Avcodec.BitstreamFilter.send_eof h.filter; flush_filter ~put_data ~mk_params ~mk_packet h; process ~put_data ~mk_params ~mk_packet h (0, Ffmpeg_utils.Duration.flush h.duration_converter) let on_data (type a) ~(get_handler : put_data:(Content.data -> unit) -> stream_idx:int64 -> time_base:Avutil.rational -> a Avcodec.params -> a handler) ~put_data ~(get_params : a get_params) ~(get_packet : a get_packet) ~(mk_params : a mk_params) ~(mk_packet : a mk_packet) ({ Ffmpeg_copy_content.params; data } : Ffmpeg_copy_content.data) = List.iter (fun (_, { Ffmpeg_copy_content.stream_idx; time_base; packet }) -> let handler = get_handler ~put_data ~stream_idx ~time_base (get_params (Option.get params)) in Avcodec.BitstreamFilter.send_packet handler.filter (get_packet packet); flush_filter ~put_data ~mk_packet ~mk_params handler) data let mk_filter ~opts ~filter = Avcodec.BitstreamFilter.init ~opts filter let mk_handler (type a) ~stream_idx ~time_base ~(filter : a Avcodec.BitstreamFilter.t) (params : a Avcodec.params) = { stream_idx; filter; params; time_base; duration_converter = Ffmpeg_utils.Duration.init ~mode:`DTS ~src:time_base ~convert_ts:false ~get_ts:Avcodec.Packet.get_dts ~set_ts:Avcodec.Packet.set_dts (); } let handler_getters (type a) ~(mk_params : a mk_params) ~(mk_packet : a mk_packet) ~filter ~filter_opts = let handler : a handler option Atomic.t = Atomic.make None in let current_handler () : a handler option = Atomic.get handler in let clear_handler () = Atomic.set handler None in let get_handler ~put_data ~stream_idx ~time_base (params : a Avcodec.params) = match Atomic.get handler with | None -> let filter, params = mk_filter ~filter ~opts:filter_opts params in let h = mk_handler ~stream_idx ~time_base ~filter params in Atomic.set handler (Some h); (h : a handler) | Some h -> if h.stream_idx <> stream_idx then ( flush ~put_data ~mk_params ~mk_packet (Some h); let filter, params = mk_filter ~filter ~opts:filter_opts params in let h = mk_handler ~stream_idx ~time_base ~filter params in Atomic.set handler (Some h); (h : a handler)) else (h : a handler) in (current_handler, get_handler, clear_handler) let register_filters () = List.iter (fun ({ Avcodec.BitstreamFilter.name; codecs; options } as filter) -> let args, args_parser = Builtins_ffmpeg_filters.mk_options options in let modes = modes name codecs in let base, module_name = if List.length modes > 1 then (Lang.add_module ~base:ffmpeg_filter_bitstream name, [name]) else (ffmpeg_filter_bitstream, []) in List.iter (fun mode -> let typ = Type.make (Format_type.descr (`Format (Content.default_format Ffmpeg_copy_content.kind))) in let name, field, source_fields_t = match mode with | `Audio -> ("audio", Frame.Fields.audio, Frame.Fields.make ~audio:typ ()) | `Audio_only -> (name, Frame.Fields.audio, Frame.Fields.make ~audio:typ ()) | `Video -> ("video", Frame.Fields.video, Frame.Fields.make ~video:typ ()) | `Video_only -> (name, Frame.Fields.video, Frame.Fields.make ~video:typ ()) in let source_t = Lang.frame_t (Lang.univ_t ()) source_fields_t in let args_t = ("", Lang.source_t source_t, None, None) :: args in ignore (Lang.add_operator ~category:`FFmpegFilter name ~base ~descr: ("FFmpeg " ^ String.concat "." (module_name @ [name]) ^ " bitstream filter. See ffmpeg documentation for more \ details.") ~flags:[`Extra] args_t ~return_t:source_t (fun p -> let source = List.assoc "" p in let filter_opts = args_of_args (args_parser p []) in let flush, process = match mode with | `Audio | `Audio_only -> let mk_packet p = `Audio p in let get_packet = function | `Audio p -> p | _ -> assert false in let mk_params p = `Audio p in let get_params = function | `Audio p -> p | _ -> assert false in let current_handler, get_handler, clear_handler = handler_getters ~mk_packet ~mk_params ~filter ~filter_opts in let flush ~put_data = flush ~put_data ~mk_packet ~mk_params (current_handler ()); clear_handler () in ( flush, on_data ~get_handler ~get_params ~get_packet ~mk_packet ~mk_params ) | `Video | `Video_only -> let mk_packet p = `Video p in let get_packet = function | `Video p -> p | _ -> assert false in let mk_params params = `Video { Ffmpeg_copy_content.avg_frame_rate = None; params; } in let get_params = function | `Video { Ffmpeg_copy_content.params } -> params | _ -> assert false in let current_handler, get_handler, clear_handler = handler_getters ~mk_packet ~mk_params ~filter ~filter_opts in let flush ~put_data = flush ~put_data ~mk_packet ~mk_params (current_handler ()); clear_handler () in ( flush, on_data ~get_handler ~get_params ~get_packet ~mk_packet ~mk_params ) in let encode_frame generator = let put_data = Generator.put generator field in function | `Frame frame -> List.iter (fun (pos, m) -> Generator.add_metadata ~pos generator m) (Frame.get_all_metadata frame); List.iter (fun pos -> Generator.add_track_mark ~pos generator) (List.filter (fun x -> x < Lazy.force Frame.size) (Frame.track_marks frame)); Frame.Fields.iter (fun f data -> match f with | _ when f = Frame.Fields.metadata -> () | _ when f = Frame.Fields.track_marks -> () | _ when f = field -> process ~put_data (Ffmpeg_copy_content.get_data data) | _ -> Generator.put generator f data) frame | `Flush -> flush ~put_data in let consumer = new Producer_consumer.consumer ~write_frame:encode_frame ~name:(name ^ ".consumer") ~source () in let producer = new Producer_consumer.producer ~check_self_sync:false ~consumers:[consumer] ~name:(name ^ ".producer") () in Typing.(producer#frame_type <: consumer#frame_type); producer))) modes) Avcodec.BitstreamFilter.filters let () = Startup.time "FFmpeg bitstream filters registration" register_filters liquidsoap-2.3.2/src/core/builtins/builtins_ffmpeg_decoder.ml000066400000000000000000000431771477303350200244540ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm let ffmpeg_decode = Lang.add_module ~base:Builtins_ffmpeg_base.track_ffmpeg "decode" let ffmpeg_raw_decode = Lang.add_module ~base:Builtins_ffmpeg_base.track_ffmpeg_raw "decode" module InternalResampler = Swresample.Make (Swresample.Frame) (Swresample.PlanarFloatArray) module InternalScaler = Swscale.Make (Swscale.Frame) (Swscale.BigArray) let log = Log.make ["ffmpeg"; "internal"; "decoder"] let decode_audio_frame ~field ~mode generator = let internal_channel_layout = Avutil.Channel_layout.get_default (Lazy.force Frame.audio_channels) in let internal_samplerate = Lazy.force Frame.audio_rate in let mk_converter ~in_sample_format ~channel_layout ~samplerate = let converter = InternalResampler.create ~in_sample_format channel_layout samplerate internal_channel_layout internal_samplerate in fun data -> let data = match data with | `Frame data -> InternalResampler.convert converter data | `Flush -> InternalResampler.flush converter in let data = Content.Audio.lift_data data in Generator.put generator field data in let mk_copy_decoder () = let current_converter = ref None in let current_stream = ref None in let current_params = ref None in let mk_decoder ~time_base ~stream_idx params = let channels = Avcodec.Audio.get_nb_channels params in let channel_layout = Avutil.Channel_layout.get_default channels in let samplerate = Avcodec.Audio.get_sample_rate params in let codec_id = Avcodec.Audio.get_params_id params in let codec = Avcodec.Audio.find_decoder codec_id in let decoder = Avcodec.Audio.create_decoder ~params codec in let in_sample_format = Avcodec.Audio.sample_format decoder in let converter = mk_converter ~channel_layout ~samplerate ~in_sample_format in current_converter := Some (converter, decoder); current_stream := Some (stream_idx, time_base); current_params := Some params; (converter, decoder) in let get_converter ~time_base ~stream_idx params = match !current_converter with | None -> mk_decoder ~time_base ~stream_idx params | Some (converter, decoder) when !current_stream <> Some (stream_idx, time_base) -> Avcodec.flush_decoder decoder (fun frame -> converter (`Frame frame)); converter `Flush; mk_decoder ~time_base ~stream_idx params | Some c -> c in function | `Frame frame -> let data, params = match Ffmpeg_copy_content.get_data frame with | { Content.Video.data; params = Some (`Audio params) } -> (data, params) | _ -> assert false in let data = List.sort (fun (pos, _) (pos', _) -> compare pos pos') data in List.iter (function | ( _, { Ffmpeg_copy_content.packet = `Audio packet; stream_idx; time_base; } ) -> let converter, decoder = get_converter ~time_base ~stream_idx params in Avcodec.decode decoder (fun frame -> converter (`Frame frame)) packet | _ -> assert false) data | `Flush -> ignore (Option.map (fun (stream_idx, time_base) -> let converter, decoder = get_converter ~time_base ~stream_idx (Option.get !current_params) in Avcodec.flush_decoder decoder (fun frame -> converter (`Frame frame)); converter `Flush) !current_stream) in let mk_raw_decoder () = let current_converter = ref None in let mk_converter ~time_base ~stream_idx { Ffmpeg_raw_content.AudioSpecs.channel_layout; sample_rate; sample_format; } = let channel_layout = Option.get channel_layout in let samplerate = Option.get sample_rate in let in_sample_format = Option.get sample_format in let converter = mk_converter ~channel_layout ~samplerate ~in_sample_format in current_converter := Some (converter, time_base, stream_idx); converter in let get_converter ~time_base ~stream_idx params = match !current_converter with | None -> mk_converter ~time_base ~stream_idx params | Some (c, t, i) when (t, i) <> (time_base, stream_idx) -> c `Flush; mk_converter ~time_base ~stream_idx params | Some (c, _, _) -> c in function | `Frame frame -> let { Content.Video.data; params } = Ffmpeg_raw_content.Audio.get_data frame in let data = List.sort (fun (pos, _) (pos', _) -> compare pos pos') data in List.iter (fun (_, { Ffmpeg_raw_content.frame; time_base; stream_idx; _ }) -> (get_converter ~time_base ~stream_idx params) (`Frame frame)) data | `Flush -> ( match !current_converter with None -> () | Some (c, _, _) -> c `Flush) in let convert : 'a 'b. get_data:(Content.data -> ('a, 'b) Content_video.Base.content) -> decoder:([ `Frame of Content.data | `Flush ] -> unit) -> [ `Frame of Frame.t | `Flush ] -> unit = fun ~get_data ~decoder -> function | `Frame frame -> let frame = Frame.get frame field in let { Content.Video.data; _ } = get_data frame in if data = [] then () else decoder (`Frame frame) | `Flush -> decoder `Flush in match mode with | `Decode -> convert ~get_data:Ffmpeg_copy_content.get_data ~decoder:(mk_copy_decoder ()) | `Raw -> convert ~get_data:Ffmpeg_raw_content.Audio.get_data ~decoder:(mk_raw_decoder ()) let decode_video_frame ~field ~mode generator = let internal_width = Lazy.force Frame.video_width in let internal_height = Lazy.force Frame.video_height in let target_fps = Lazy.force Frame.video_rate in let mk_converter () = let converter = ref None in let current_format = ref None in let mk_converter ~width ~height ~pixel_format ~time_base ?pixel_aspect ~stream_idx () = current_format := Some (width, height, pixel_format, time_base, pixel_aspect, stream_idx); let scaler = InternalScaler.create [] width height pixel_format internal_width internal_height (Ffmpeg_utils.liq_frame_pixel_format ()) in let fps_converter = Ffmpeg_avfilter_utils.Fps.init ~width ~height ~pixel_format ~time_base ?pixel_aspect ~target_fps () in converter := Some (scaler, fps_converter); (scaler, fps_converter) in let get_converter ?pixel_aspect ~pixel_format ~time_base ~width ~height ~stream_idx () = match !converter with | None -> mk_converter ~width ~height ~pixel_format ~time_base ?pixel_aspect ~stream_idx () | Some _ when !current_format <> Some ( width, height, pixel_format, time_base, pixel_aspect, stream_idx ) -> log#info "Video frame format change detected.."; mk_converter ~width ~height ~pixel_format ~time_base ?pixel_aspect ~stream_idx () | Some v -> v in let put ~scaler data = let img = Ffmpeg_utils.unpack_image ~width:internal_width ~height:internal_height (InternalScaler.convert scaler data) in let data = Content.Video.lift_image (Video.Canvas.Image.make img) in Generator.put generator field data in fun ~time_base ~stream_idx -> function | `Frame frame -> let width = Avutil.Video.frame_get_width frame in let height = Avutil.Video.frame_get_height frame in let pixel_format = Avutil.Video.frame_get_pixel_format frame in let pixel_aspect = Avutil.Video.frame_get_pixel_aspect frame in let scaler, fps_converter = get_converter ?pixel_aspect ~pixel_format ~time_base ~width ~height ~stream_idx () in Ffmpeg_avfilter_utils.Fps.convert fps_converter frame (put ~scaler) | `Flush -> ignore (Option.map (fun (scaler, fps_converter) -> Ffmpeg_avfilter_utils.Fps.eof fps_converter (put ~scaler)) !converter) in let mk_copy_decoder () = let convert = mk_converter () in let current_stream_idx = ref None in let current_time_base = ref None in let current_params = ref None in let current_decoder = ref None in let mk_decoder ~params ~stream_idx ~time_base = let codec_id = Avcodec.Video.get_params_id params.Ffmpeg_copy_content.params in let codec = Avcodec.Video.find_decoder codec_id in let decoder = Avcodec.Video.create_decoder ~params:params.Ffmpeg_copy_content.params codec in current_decoder := Some decoder; current_stream_idx := Some stream_idx; current_params := Some params; current_time_base := Some time_base; decoder in let get_decoder ~params ~stream_idx ~time_base = match !current_decoder with | None -> mk_decoder ~params ~stream_idx ~time_base | Some decoder when !current_stream_idx <> Some stream_idx || !current_time_base <> Some time_base -> log#info "Video frame format change detected.."; ignore (Option.map (fun stream_idx -> Avcodec.flush_decoder decoder (fun frame -> convert ~time_base:(Option.get !current_time_base) ~stream_idx (`Frame frame))) !current_stream_idx); mk_decoder ~params ~stream_idx ~time_base | Some d -> d in function | `Frame frame -> let data, params = match Ffmpeg_copy_content.get_data frame with | { Content.Video.data; params = Some (`Video params) } -> (data, params) | _ -> assert false in let data = List.sort (fun (pos, _) (pos', _) -> compare pos pos') data in List.iter (function | ( _, { Ffmpeg_copy_content.packet = `Video packet; stream_idx; time_base; } ) -> let decoder = get_decoder ~params ~time_base ~stream_idx in Avcodec.decode decoder (fun frame -> convert ~time_base ~stream_idx (`Frame frame)) packet | _ -> assert false) data | `Flush -> ignore (Option.map (fun stream_idx -> let decoder = get_decoder ~params:(Option.get !current_params) ~time_base:(Option.get !current_time_base) ~stream_idx:(Option.get !current_stream_idx) in Avcodec.flush_decoder decoder (fun frame -> convert ~time_base:(Option.get !current_time_base) ~stream_idx (`Frame frame)); convert ~time_base:(Option.get !current_time_base) ~stream_idx `Flush) !current_stream_idx) in let mk_raw_decoder () = let convert = mk_converter () in let last_params = ref None in function | `Frame frame -> let { Content.Video.data; _ } = Ffmpeg_raw_content.Video.get_data frame in let data = List.sort (fun (pos, _) (pos', _) -> compare pos pos') data in List.iter (fun (_, { Ffmpeg_raw_content.frame; stream_idx; time_base }) -> last_params := Some (time_base, stream_idx); convert ~time_base ~stream_idx (`Frame frame)) data | `Flush -> ignore (Option.map (fun (time_base, stream_idx) -> convert ~time_base ~stream_idx `Flush) !last_params) in let convert : 'a 'b. get_data:(Content.data -> ('a, 'b) Content_video.Base.content) -> decoder:([ `Frame of Content.data | `Flush ] -> unit) -> [ `Frame of Frame.t | `Flush ] -> unit = fun ~get_data ~decoder -> function | `Frame frame -> let frame = Frame.get frame field in let { Content.Video.data; _ } = get_data frame in if data = [] then () else decoder (`Frame frame) | `Flush -> decoder `Flush in match mode with | `Decode -> convert ~get_data:Ffmpeg_copy_content.get_data ~decoder:(mk_copy_decoder ()) | `Raw -> convert ~get_data:Ffmpeg_raw_content.Video.get_data ~decoder:(mk_raw_decoder ()) let mk_decoder mode = let input_frame_t = match mode with | `Audio_encoded -> Type.make (Format_type.descr (`Kind Ffmpeg_copy_content.kind)) | `Audio_raw -> Type.make (Format_type.descr (`Kind Ffmpeg_raw_content.Audio.kind)) | `Video_encoded -> Type.make (Format_type.descr (`Kind Ffmpeg_copy_content.kind)) | `Video_raw -> Type.make (Format_type.descr (`Kind Ffmpeg_raw_content.Video.kind)) in let output_frame_t = match mode with | `Audio_encoded | `Audio_raw -> Format_type.audio () | `Video_encoded | `Video_raw -> Format_type.video () in let base, name, decode_mode = match mode with | `Audio_encoded -> (ffmpeg_decode, "audio", `Decode) | `Audio_raw -> (ffmpeg_raw_decode, "audio", `Raw) | `Video_encoded -> (ffmpeg_decode, "video", `Decode) | `Video_raw -> (ffmpeg_raw_decode, "video", `Raw) in let proto = [("", input_frame_t, None, None)] in ignore (Lang.add_track_operator name proto ~base ~return_t:output_frame_t ~category:`Conversion ~descr:"Decode a track content" (fun p -> let id = Lang.to_default_option ~default:name Lang.to_string (List.assoc "id" p) in let field, source = Lang.to_track (List.assoc "" p) in let mk_decode_frame generator = let decode_frame = match mode with | `Audio_encoded | `Audio_raw -> decode_audio_frame ~field ~mode:decode_mode generator | `Video_encoded | `Video_raw -> decode_video_frame ~field ~mode:decode_mode generator in let size = Lazy.force Frame.size in let decode_frame = function | `Frame frame -> List.iter (fun (pos, m) -> Generator.add_metadata ~pos generator m) (Frame.get_all_metadata frame); List.iter (fun pos -> Generator.add_track_mark ~pos generator) (List.filter (fun x -> x < size) (Frame.track_marks frame)); decode_frame (`Frame frame) | `Flush -> decode_frame `Flush in decode_frame in let decode_frame_ref = ref None in let get_decode_frame generator = match !decode_frame_ref with | None -> let fn = mk_decode_frame generator in decode_frame_ref := Some fn; fn | Some fn -> fn in let decode_frame generator frame = let decode_frame = get_decode_frame generator in match frame with | `Frame frame -> decode_frame (`Frame frame) | `Flush -> decode_frame `Flush; decode_frame_ref := None in let consumer = new Producer_consumer.consumer ~always_enabled:true ~write_frame:decode_frame ~name:(id ^ ".consumer") ~source:(Lang.source source) () in let stack = Liquidsoap_lang.Lang_core.pos p in consumer#set_stack stack; let input_frame_t = Type.fresh input_frame_t in Typing.( consumer#frame_type <: Lang.frame_t (Lang.univ_t ()) (Frame.Fields.add field input_frame_t Frame.Fields.empty)); ( field, new Producer_consumer.producer (* We are expecting real-rate with a couple of hickups.. *) ~stack ~check_self_sync:false ~consumers:[consumer] ~name:(id ^ ".producer") () ))) let () = List.iter mk_decoder [`Audio_encoded; `Audio_raw; `Video_encoded; `Video_raw] liquidsoap-2.3.2/src/core/builtins/builtins_ffmpeg_encoder.ml000066400000000000000000000513741477303350200244640ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm let ffmpeg_encode = Lang.add_module ~base:Builtins_ffmpeg_base.track_ffmpeg "encode" let ffmpeg_raw_encode = Lang.add_module ~base:Builtins_ffmpeg_base.track_ffmpeg_raw "encode" module InternalResampler = Swresample.Make (Swresample.PlanarFloatArray) (Swresample.Frame) module InternalScaler = Swscale.Make (Swscale.BigArray) (Swscale.Frame) type source_idx = { source : Source.source; idx : int64 } module SourceIdx = Weak.Make (struct type t = source_idx let equal x y = x.source == y.source let hash x = Obj.magic x.source end) let source_idx_map = SourceIdx.create 0 let encode_audio_frame ~source_idx ~type_t ~mode ~opts ?codec ~format ~content_type ~field generator = let internal_channel_layout = Avutil.Channel_layout.get_default (Content.Audio.channels_of_format (Frame.Fields.find field (content_type ()))) in let internal_samplerate = Lazy.force Frame.audio_rate in let target_channels = format.Ffmpeg_format.channels in let target_channel_layout = Avutil.Channel_layout.get_default target_channels in let target_samplerate = Lazy.force format.Ffmpeg_format.samplerate in let target_time_base = { Avutil.num = 1; den = target_samplerate } in let target_sample_format = match format.Ffmpeg_format.sample_format with | Some format -> Avutil.Sample_format.find format | None -> `Dbl in let target_sample_format, frame_size, encode_frame = match mode with | `Encoded -> ( let codec = Option.get codec in let target_sample_format = Avcodec.Audio.find_best_sample_format codec target_sample_format in let encoder = Avcodec.Audio.create_encoder ~opts ~channel_layout:target_channel_layout ~sample_format:target_sample_format ~sample_rate:target_samplerate ~time_base:target_time_base codec in let encoder_time_base = Avcodec.time_base encoder in let duration_converter = Ffmpeg_utils.Duration.init ~mode:`DTS ~src:encoder_time_base ~convert_ts:false ~get_ts:Avcodec.Packet.get_dts ~set_ts:Avcodec.Packet.set_dts () in let params = Some (`Audio (Avcodec.params encoder)) in let effective_t = Type.make (Format_type.descr (`Format (Ffmpeg_copy_content.lift_params params))) in Typing.(effective_t <: type_t); let write_packet packet = match Ffmpeg_utils.Duration.push duration_converter packet with | Some (length, packets) -> let data = List.map (fun (pos, packet) -> ( pos, { Ffmpeg_copy_content.packet = `Audio packet; time_base = encoder_time_base; stream_idx = source_idx.idx; } )) packets in let data = { Content.Video.params; data; length } in let data = Ffmpeg_copy_content.lift_data data in Generator.put generator field data | None -> () in ( target_sample_format, (if List.mem `Variable_frame_size (Avcodec.capabilities codec) then None else Some (Avcodec.Audio.frame_size encoder)), function | `Frame frame -> Avcodec.encode encoder write_packet frame | `Flush -> Avcodec.flush_encoder encoder write_packet )) | `Raw -> ( let params = { Ffmpeg_raw_content.AudioSpecs.channel_layout = Some target_channel_layout; sample_format = Some target_sample_format; sample_rate = Some target_samplerate; } in let effective_t = Type.make (Format_type.descr (`Format (Ffmpeg_raw_content.Audio.lift_params params))) in Typing.(effective_t <: type_t); let duration_converter = Ffmpeg_utils.Duration.init ~mode:`PTS ~src:target_time_base ~convert_ts:false ~get_ts:Avutil.Frame.pts ~set_ts:Avutil.Frame.set_pts () in ( target_sample_format, None, function | `Frame frame -> ( match Ffmpeg_utils.Duration.push duration_converter frame with | Some (length, frames) -> let data = List.map (fun (pos, frame) -> ( pos, { Ffmpeg_raw_content.time_base = target_time_base; stream_idx = source_idx.idx; frame; } )) frames in let data = { Content.Video.params; data; length } in let data = Ffmpeg_raw_content.Audio.lift_data data in Generator.put generator field data | None -> ()) | `Flush -> () )) in let resampler = InternalResampler.create ~out_sample_format:target_sample_format internal_channel_layout internal_samplerate target_channel_layout target_samplerate in let encode_ffmpeg_frame = Ffmpeg_internal_encoder.write_audio_frame ~time_base:target_time_base ~sample_rate:target_samplerate ~channel_layout:target_channel_layout ~sample_format:target_sample_format ~frame_size (fun frame -> encode_frame (`Frame frame)) in function | `Frame frame -> let frame = InternalResampler.convert ~length:(AFrame.position frame) resampler (AFrame.pcm frame) in encode_ffmpeg_frame frame | `Flush -> encode_frame `Flush let encode_video_frame ~source_idx ~type_t ~mode ~opts ?codec ~format ~field generator = let internal_fps = Lazy.force Frame.video_rate in let internal_time_base = { Avutil.num = 1; den = internal_fps } in let internal_width = Lazy.force Frame.video_width in let internal_height = Lazy.force Frame.video_height in let target_fps = Lazy.force format.Ffmpeg_format.framerate in let target_frame_rate = { Avutil.num = target_fps; den = 1 } in let target_width = Lazy.force format.Ffmpeg_format.width in let target_height = Lazy.force format.Ffmpeg_format.height in let target_pixel_aspect = { Avutil.num = 1; den = 1 } in let flag = match Ffmpeg_utils.conf_scaling_algorithm#get with | "fast_bilinear" -> Swscale.Fast_bilinear | "bilinear" -> Swscale.Bilinear | "bicubic" -> Swscale.Bicubic | _ -> failwith "Invalid value set for ffmpeg scaling algorithm!" in let scaler = ref None in let mk_scaler ~target_pixel_format = scaler := Some (InternalScaler.create [flag] internal_width internal_height (Ffmpeg_utils.liq_frame_pixel_format ()) target_width target_height target_pixel_format) in let fps_converter = ref None in let mk_fps_converter ~target_pixel_format = fps_converter := Some (Ffmpeg_avfilter_utils.Fps.init ~width:target_width ~height:target_height ~pixel_format:target_pixel_format ~time_base:internal_time_base ~pixel_aspect:target_pixel_aspect ~target_fps ()) in let encode_frame = match mode with | `Encoded -> ( let codec = Option.get codec in let target_pixel_format = Ffmpeg_utils.pixel_format codec format.Ffmpeg_format.pixel_format in mk_scaler ~target_pixel_format; mk_fps_converter ~target_pixel_format; let time_base = Ffmpeg_avfilter_utils.Fps.time_base (Option.get !fps_converter) in let hwaccel = format.Ffmpeg_format.hwaccel in let hwaccel_device = format.Ffmpeg_format.hwaccel_device in let hwaccel_pixel_format = Option.map Avutil.Pixel_format.of_string format.Ffmpeg_format.hwaccel_pixel_format in let hardware_context, stream_pixel_format = Ffmpeg_utils.mk_hardware_context ~hwaccel ~hwaccel_pixel_format ~hwaccel_device ~opts ~target_pixel_format ~target_width ~target_height codec in let encoder = Avcodec.Video.create_encoder ?hardware_context ~opts ~frame_rate:target_frame_rate ~pixel_format:stream_pixel_format ~width:target_width ~height:target_height ~time_base codec in let params = Some (`Video { Ffmpeg_copy_content.avg_frame_rate = Some target_frame_rate; params = Avcodec.params encoder; }) in let effective_t = Type.make (Format_type.descr (`Format (Ffmpeg_copy_content.lift_params params))) in Typing.(effective_t <: type_t); let encoder_time_base = Avcodec.time_base encoder in let duration_converter = Ffmpeg_utils.Duration.init ~mode:`DTS ~src:encoder_time_base ~convert_ts:false ~get_ts:Avcodec.Packet.get_dts ~set_ts:Avcodec.Packet.set_dts () in let write_packet packet = match Ffmpeg_utils.Duration.push duration_converter packet with | Some (length, packets) -> let data = List.map (fun (pos, packet) -> ( pos, { Ffmpeg_copy_content.packet = `Video packet; time_base = encoder_time_base; stream_idx = source_idx.idx; } )) packets in let data = { Content.Video.params; data; length } in let data = Ffmpeg_copy_content.lift_data data in Generator.put generator field data | None -> () in function | `Frame frame -> Avcodec.encode encoder write_packet frame | `Flush -> Avcodec.flush_encoder encoder write_packet) | `Raw -> ( let target_pixel_format = Ffmpeg_utils.liq_frame_pixel_format () in mk_scaler ~target_pixel_format; mk_fps_converter ~target_pixel_format; let time_base = Ffmpeg_avfilter_utils.Fps.time_base (Option.get !fps_converter) in let params = { Ffmpeg_raw_content.VideoSpecs.width = Some target_width; height = Some target_height; pixel_format = Some target_pixel_format; pixel_aspect = Some target_pixel_aspect; } in let effective_t = Type.make (Format_type.descr (`Format (Ffmpeg_raw_content.Video.lift_params params))) in Typing.(effective_t <: type_t); let duration_converter = Ffmpeg_utils.Duration.init ~mode:`PTS ~src:time_base ~convert_ts:false ~get_ts:Avutil.Frame.pts ~set_ts:Avutil.Frame.set_pts () in function | `Frame frame -> ( match Ffmpeg_utils.Duration.push duration_converter frame with | Some (length, frames) -> let data = List.map (fun (pos, frame) -> ( pos, { Ffmpeg_raw_content.time_base; stream_idx = source_idx.idx; frame; } )) frames in let data = { Content.Video.params; data; length } in let data = Ffmpeg_raw_content.Video.lift_data data in Generator.put generator field data | None -> ()) | `Flush -> ()) in (* We don't know packet duration in advance so we have to infer it from the next packet. *) let encode_ffmpeg_frame frame = Ffmpeg_avfilter_utils.Fps.convert (Option.get !fps_converter) frame (fun frame -> encode_frame (`Frame frame)) in let nb_frames = ref 0L in function | `Frame frame -> let vbuf = VFrame.data frame in List.iter (fun (_, img) -> let f = Video.Canvas.Image.render img in let vdata = Ffmpeg_utils.pack_image f in let frame = InternalScaler.convert (Option.get !scaler) vdata in Avutil.Frame.set_pts frame (Some !nb_frames); nb_frames := Int64.succ !nb_frames; encode_ffmpeg_frame frame) vbuf.Content.Video.data | `Flush -> encode_frame `Flush let mk_encoder mode = let format_field, input_frame_t = match mode with | `Audio_encoded | `Audio_raw -> (Frame.Fields.audio, Format_type.audio ()) | `Video_encoded | `Video_raw -> (Frame.Fields.video, Format_type.video ()) in let output_frame_t = match mode with | `Audio_encoded -> Type.make (Format_type.descr (`Kind Ffmpeg_copy_content.kind)) | `Audio_raw -> Type.make (Format_type.descr (`Kind Ffmpeg_raw_content.Audio.kind)) | `Video_encoded -> Type.make (Format_type.descr (`Kind Ffmpeg_copy_content.kind)) | `Video_raw -> Type.make (Format_type.descr (`Kind Ffmpeg_raw_content.Video.kind)) in let format_frame_t = match mode with | `Audio_encoded | `Video_encoded -> input_frame_t | `Audio_raw | `Video_raw -> output_frame_t in let proto = [ ( "", Lang.format_t (Lang.frame_t (Lang.univ_t ()) (Frame.Fields.add format_field format_frame_t Frame.Fields.empty)), None, Some "Encoding format." ); ("", input_frame_t, None, None); ] in let base, name = match mode with | `Audio_encoded -> (ffmpeg_encode, "audio") | `Audio_raw -> (ffmpeg_raw_encode, "audio") | `Video_encoded -> (ffmpeg_encode, "video") | `Video_raw -> (ffmpeg_raw_encode, "video") in ignore (Lang.add_track_operator name proto ~base ~return_t:output_frame_t ~category:`Conversion ~descr:"Convert a track's content" (fun p -> let id = Lang.to_default_option ~default:name Lang.to_string (List.assoc "id" p) in let format_val = Lang.assoc "" 1 p in let format = match Lang.to_format format_val with | Encoder.Ffmpeg ffmpeg -> ffmpeg | _ -> assert false in let field, source = Lang.to_track (Lang.assoc "" 2 p) in let content_type () = source#content_type in if Hashtbl.length format.Ffmpeg_format.opts > 0 then raise (Error.Invalid_value ( format_val, Printf.sprintf "Muxer options are not supported for inline encoders: %s" (Ffmpeg_format.string_of_options format.Ffmpeg_format.opts) )); if format.Ffmpeg_format.format <> None then raise (Error.Invalid_value (format_val, "Format option is not supported inline encoders")); let mk_encode_frame generator = let output_frame_t = Type.fresh output_frame_t in let stream = List.assoc format_field format.Ffmpeg_format.streams in let opts = match stream with | `Encode { Ffmpeg_format.opts } -> opts | _ -> assert false in let original_opts = Hashtbl.create 10 in let source_idx = SourceIdx.merge source_idx_map { source; idx = Ffmpeg_content_base.new_stream_idx () } in let encode_frame = match stream with | `Encode { mode = `Raw; options = `Audio format } -> encode_audio_frame ~source_idx ~type_t:output_frame_t ~mode:`Raw ~opts ~format ~content_type ~field generator | `Encode { mode = `Internal; codec = Some codec; options = `Audio format; } -> let codec = Avcodec.Audio.find_encoder_by_name codec in encode_audio_frame ~source_idx ~type_t:output_frame_t ~mode:`Encoded ~opts ~codec ~format ~content_type ~field generator | `Encode { mode = `Raw; options = `Video format } -> encode_video_frame ~source_idx ~type_t:output_frame_t ~mode:`Raw ~opts ~format ~field generator | `Encode { mode = `Internal; codec = Some codec; options = `Video format; } -> let codec = Avcodec.Video.find_encoder_by_name codec in encode_video_frame ~source_idx ~type_t:output_frame_t ~mode:`Encoded ~opts ~codec ~format ~field generator | _ -> assert false in let size = Lazy.force Frame.size in let encode_frame = function | `Frame frame -> List.iter (fun (pos, m) -> Generator.add_metadata ~pos generator m) (Frame.get_all_metadata frame); List.iter (fun pos -> Generator.add_track_mark ~pos generator) (List.filter (fun x -> x < size) (Frame.track_marks frame)); encode_frame (`Frame frame) | `Flush -> encode_frame `Flush in let left_over_opts = Hashtbl.create 10 in Hashtbl.filter_map_inplace (fun l v -> if Hashtbl.mem left_over_opts l then Some v else None) original_opts; if Hashtbl.length original_opts > 0 then raise (Error.Invalid_value ( format_val, Printf.sprintf "Unrecognized options: %s" (Ffmpeg_format.string_of_options original_opts) )); encode_frame in let encode_frame_ref = ref None in let get_encode_frame generator = match !encode_frame_ref with | None -> let fn = mk_encode_frame generator in encode_frame_ref := Some fn; fn | Some fn -> fn in let encode_frame generator frame = let encode_frame = get_encode_frame generator in match frame with | `Frame frame -> encode_frame (`Frame frame) | `Flush -> encode_frame `Flush; encode_frame_ref := None in let consumer = new Producer_consumer.consumer ~write_frame:encode_frame ~name:(id ^ ".consumer") ~source:(Lang.source source) () in let stack = Liquidsoap_lang.Lang_core.pos p in consumer#set_stack stack; let input_frame_t = Type.fresh input_frame_t in Typing.( consumer#frame_type <: Lang.frame_t (Lang.univ_t ()) (Frame.Fields.add field input_frame_t Frame.Fields.empty)); ( field, new Producer_consumer.producer (* We are expecting real-rate with a couple of hickups.. *) ~stack ~check_self_sync:false ~consumers:[consumer] ~name:(id ^ ".producer") () ))) let () = List.iter mk_encoder [`Audio_encoded; `Audio_raw; `Video_encoded; `Video_raw] liquidsoap-2.3.2/src/core/builtins/builtins_ffmpeg_filters.ml000066400000000000000000001042311477303350200245040ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Builtins_ffmpeg_base module Queue = Queues.Queue (** FFmpeg filter graphs initialization is pretty tricky. Things to consider: - FFmpeg filters are using a push paradigm, pushing from the sources down to the outputs - Liquidsoap uses a pull paradigm, pulling data from the outputs. - FFmpeg filters inputs need to know the exact content of the data sent to them before being initialized, which is only known in the worst case when receiving the first frame. Therefore, the intended implementation is to: -> Consider ffmpeg filter graphs as a single operator with N inputs and M outputs (audio/video) with inputs being any source converted to a ffmpeg graph input, even if not used, for simplification. -> The outputs are placed in their clock which controls a child clock containing the inputs. -> The graph initialization is suspended until its initialization conditions are met. -> When all the inputs have been initialized and are ready (call to `#is_ready`), the graph is considered ready and its output can start requesting content. This is captured by the call to `is_ready` below. -> When requesting content, the outputs can tick the input clock as many time as needed to generate output data. We expect more or less real time with perhaps an accordion pattern between input and output so we do not look at latency control like we do for crossfades. -> When receiving its first frame, the liquidsoap input will then initialize the corresponding ffmpeg graph input with full format info. -> When all inputs have been initialized, which is know by checking if all of the graph's lazy values for input pads have been forced (executed), the whole graph is initialized, outputs connected and data can start to flow! This is captured by the call to `initialized` below. This is contingent to the inputs being checked for `#is_ready` and the outputs only pulling when _all_ inputs are available, to avoid running into endless pulling loops. *) let ffmpeg_filter_audio = Lang.add_module ~base:ffmpeg_filter "audio" let ffmpeg_filter_video = Lang.add_module ~base:ffmpeg_filter "video" let log = Log.make ["ffmpeg"; "filter"] type 'a input = 'a Avfilter.input type 'a output = 'a Avfilter.output type 'a setter = 'a -> unit type 'a entries = (string, 'a setter) Hashtbl.t type inputs = ([ `Audio ] input entries, [ `Video ] input entries) Avfilter.av type outputs = ([ `Audio ] output entries, [ `Video ] output entries) Avfilter.av type graph = { mutable config : Avfilter.config option; mutable failed : bool; input_inits : (unit -> bool) Queue.t; graph_inputs : Source.source Queue.t; graph_outputs : Source.source Queue.t; init : unit Lazy.t Queue.t; entries : (inputs, outputs) Avfilter.io; } let init_graph graph = if Queue.fold graph.input_inits (fun v b -> b && v ()) true then ( try Queue.iter graph.init Lazy.force with exn -> let bt = Printexc.get_raw_backtrace () in graph.failed <- true; Printexc.raise_with_backtrace exn bt) let initialized graph = Queue.fold graph.init (fun q cur -> cur && Lazy.is_val q) true let is_ready graph = (match (initialized graph, Queue.peek_opt graph.graph_inputs) with | false, Some s -> Clock.tick s#clock | _ -> ()); (not graph.failed) && Queue.fold graph.graph_inputs (fun (s : Source.source) cur -> cur && s#is_ready) true let pull graph = match Queue.peek_opt graph.graph_inputs with | Some s -> Clock.tick s#clock | None -> () let self_sync graph source = (Clock_base.self_sync ~source (Queue.elements graph.graph_inputs)) () module Graph = Value.MkCustom (struct type content = graph let name = "ffmpeg.filter.graph" let to_string _ = name let to_json ~pos _ = Lang.raise_error ~message:"Ffmpeg filter graph cannot be represented as json" ~pos "json" let compare = Stdlib.compare end) module Audio = Value.MkCustom (struct type content = [ `Input of ([ `Attached ], [ `Audio ], [ `Input ]) Avfilter.pad | `Output of ([ `Attached ], [ `Audio ], [ `Output ]) Avfilter.pad Lazy.t ] let name = "ffmpeg.filter.audio" let to_string _ = name let to_json ~pos _ = Lang.raise_error ~pos ~message:"Ffmpeg filter audio input cannot be represented as json" "json" let compare = Stdlib.compare end) module Video = Value.MkCustom (struct type content = [ `Input of ([ `Attached ], [ `Video ], [ `Input ]) Avfilter.pad | `Output of ([ `Attached ], [ `Video ], [ `Output ]) Avfilter.pad Lazy.t ] let name = "ffmpeg.filter.video" let to_string _ = name let to_json ~pos _ = Lang.raise_error ~pos ~message:"Ffmpeg filter video input cannot be represented as json" "json" let compare = Stdlib.compare end) let uniq_name = let names = Hashtbl.create 10 in let name_idx name = match Hashtbl.find_opt names name with | Some x -> Hashtbl.replace names name (x + 1); x | None -> Hashtbl.replace names name 1; 0 in fun name -> Printf.sprintf "%s_%d" name (name_idx name) exception No_value_for_option let mk_options options = Avutil.Options.( let mk_opt ~t ~to_string ~from_value name help { default; min; max; values } = let desc = let string_of_value (name, value) = Printf.sprintf "%s (%s)" (to_string value) name in let string_of_values values = String.concat ", " (List.map string_of_value values) in match (help, default, values) with | Some help, None, [] -> Some help | Some help, _, _ -> let values = if values = [] then None else Some (Printf.sprintf "possible values: %s" (string_of_values values)) in let default = Option.map (fun default -> Printf.sprintf "default: %s" (to_string default)) default in let l = List.fold_left (fun l -> function Some v -> v :: l | None -> l) [] [values; default] in Some (Printf.sprintf "%s. (%s)" help (String.concat ", " l)) | None, None, _ :: _ -> Some (Printf.sprintf "Possible values: %s" (string_of_values values)) | None, Some v, [] -> Some (Printf.sprintf "Default: %s" (to_string v)) | None, Some v, _ :: _ -> Some (Printf.sprintf "Default: %s, possible values: %s" (to_string v) (string_of_values values)) | None, None, [] -> None in let opt = (name, Lang.nullable_t t, Some Lang.null, desc) in let getter p l = try let v = List.assoc name p in let v = match Lang.to_option v with | None -> raise No_value_for_option | Some v -> v in let x = try from_value v with _ -> raise (Error.Invalid_value (v, "Invalid value")) in (match min with | Some m when x < m -> raise (Error.Invalid_value ( v, Printf.sprintf "%s must be more than %s" name (to_string m) )) | _ -> ()); (match max with | Some m when m < x -> raise (Error.Invalid_value ( v, Printf.sprintf "%s must be less than %s" name (to_string m) )) | _ -> ()); (match values with | _ :: _ when List.find_opt (fun (_, v) -> v = x) values = None -> raise (Error.Invalid_value ( v, Printf.sprintf "%s should be one of: %s" name (String.concat ", " (List.map (fun (_, v) -> to_string v) values)) )) | _ -> ()); let x = match default with | Some v when to_string v = Int64.to_string Int64.max_int && to_string x = string_of_int max_int -> `Int64 Int64.max_int | Some v when to_string v = Int64.to_string Int64.min_int && to_string x = string_of_int min_int -> `Int64 Int64.min_int | _ -> `String (to_string x) in `Pair (name, x) :: l with No_value_for_option -> l in (opt, getter) in let mk_opt (p, getter) { name; help; spec } = let mk_opt ~t ~to_string ~from_value spec = let opt, get = mk_opt ~t ~to_string ~from_value name help spec in let getter p l = get p (getter p l) in (opt :: p, getter) in match spec with | `Int s -> mk_opt ~t:Lang.int_t ~to_string:string_of_int ~from_value:Lang.to_int s | `Flags s | `Int64 s | `UInt64 s | `Duration s -> mk_opt ~t:Lang.int_t ~to_string:Int64.to_string ~from_value:(fun v -> Int64.of_int (Lang.to_int v)) s | `Float s | `Double s -> mk_opt ~t:Lang.float_t ~to_string:string_of_float ~from_value:Lang.to_float s | `Rational s -> let to_string { Avutil.num; den } = Printf.sprintf "%i/%i" num den in let from_value v = let x = Lang.to_string v in match String.split_on_char '/' x with | [num; den] -> { Avutil.num = int_of_string num; den = int_of_string den } | _ -> assert false in mk_opt ~t:Lang.string_t ~to_string ~from_value s | `Bool s -> mk_opt ~t:Lang.bool_t ~to_string:string_of_bool ~from_value:Lang.to_bool s | `String s | `Binary s | `Dict s | `Image_size s | `Video_rate s | `Color s -> mk_opt ~t:Lang.string_t ~to_string:(fun x -> x) ~from_value:Lang.to_string s | `Pixel_fmt s -> mk_opt ~t:Lang.string_t ~to_string:(fun p -> match Avutil.Pixel_format.to_string p with | None -> "none" | Some p -> p) ~from_value:(fun v -> Avutil.Pixel_format.of_string (Lang.to_string v)) s | `Sample_fmt s -> mk_opt ~t:Lang.string_t ~to_string:(fun p -> match Avutil.Sample_format.get_name p with | None -> "none" | Some p -> p) ~from_value:(fun v -> Avutil.Sample_format.find (Lang.to_string v)) s | `Channel_layout s -> mk_opt ~t:Lang.string_t ~to_string:Avutil.Channel_layout.get_description ~from_value:(fun v -> Avutil.Channel_layout.find (Lang.to_string v)) s in List.fold_left mk_opt ([], fun _ x -> x) (Avutil.Options.opts options)) let get_config graph = let { config; _ } = Graph.of_value graph in match config with | Some config -> config | None -> raise (Error.Invalid_value ( graph, "Graph variables cannot be used outside of ffmpeg.filter.create!" )) let apply_filter ~args_parser ~filter ~sources_t p = Avfilter.( let graph_v = Lang.assoc "" 1 p in let config = get_config graph_v in let graph = Graph.of_value graph_v in let name = uniq_name filter.name in let flags = filter.flags in let filter = attach ~args:(args_parser p []) ~name filter config in let input_set = ref false in let meths = [ ( "process_command", Lang.val_fun [ ("fast", "fast", Some (Lang.bool false)); ("", "", None); ("", "", None); ] (fun p -> let fast = Lang.to_bool (List.assoc "fast" p) in let flags = if fast then [`Fast] else [] in let cmd = Lang.to_string (Lang.assoc "" 1 p) in let arg = Lang.to_string (Lang.assoc "" 2 p) in if initialized graph then ( try Lang.string (Avfilter.process_command ~flags ~cmd ~arg filter) with exn -> let bt = Printexc.get_raw_backtrace () in Lang.raise_as_runtime ~bt ~kind:"ffmpeg.filter" exn) else Lang.string "graph not started!") ); ( "output", let audio = List.map (fun p -> Audio.to_value (`Output (Lazy.from_val p))) filter.io.outputs.audio in let video = List.map (fun p -> Video.to_value (`Output (Lazy.from_val p))) filter.io.outputs.video in if List.mem `Dynamic_outputs flags then Lang.tuple [Lang.list audio; Lang.list video] else (match audio @ video with [x] -> x | l -> Lang.tuple l) ); ( "set_input", Lang.val_fun (List.map (fun (_, lbl, _) -> (lbl, lbl, None)) sources_t) (fun p -> if !input_set then ( let pos = match Value.pos (List.assoc "" p) with | (exception Not_found) | None -> [] | Some p -> [p] in Lang.raise_error ~pos ~message:"Filter input already set!" "ffmpeg.filter"); let audio_inputs_c = List.length filter.io.inputs.audio in let get_input ~mode ~ofs idx = if List.mem `Dynamic_inputs flags then ( let v = Lang.assoc "" (if mode = `Audio then 1 else 2) p in let inputs = Lang.to_list v in if List.length inputs <= idx then raise (Error.Invalid_value ( v, Printf.sprintf "Invalid number of input for filter %s" filter.name )); List.nth inputs idx) else Lang.assoc "" (idx + ofs + 1) p in Queue.push graph.init (Lazy.from_fun (fun () -> List.iteri (fun idx input -> let output = match Audio.of_value (get_input ~mode:`Audio ~ofs:0 idx) with | `Output output -> Lazy.force output | _ -> assert false in link output input) filter.io.inputs.audio; List.iteri (fun idx input -> let output = match Video.of_value (get_input ~mode:`Video ~ofs:audio_inputs_c idx) with | `Output output -> output | _ -> assert false in link (Lazy.force output) input) filter.io.inputs.video)); input_set := true; Lang.unit) ); ] in Lang.meth Lang.unit meths) let register_filters () = Avfilter.( let mk_av_t ~flags ~mode { audio; video } = match mode with | `Input when List.mem `Dynamic_inputs flags -> [Lang.list_t Audio.t; Lang.list_t Video.t] | `Output when List.mem `Dynamic_outputs flags -> [Lang.list_t Audio.t; Lang.list_t Video.t] | _ -> let audio = List.map (fun _ -> Audio.t) audio in let video = List.map (fun _ -> Video.t) video in audio @ video in List.iter (fun ({ name; description; io; flags } as filter) -> let args, args_parser = mk_options filter.options in let args_t = args @ [("", Graph.t, None, None)] in let sources_t = List.map (fun t -> (false, "", t)) (mk_av_t ~flags ~mode:`Input io.inputs) in let output_t = match mk_av_t ~flags ~mode:`Output io.outputs with | [x] -> x | l -> Lang.tuple_t l in let return_t = Lang.method_t Lang.unit_t [ ( "process_command", ( [], Lang.fun_t [ (true, "fast", Lang.bool_t); (false, "", Lang.string_t); (false, "", Lang.string_t); ] Lang.string_t ), "`process_command(?fast, \"command\", \"argument\")` sends the \ given command to this filter. Set `fast` to `true` to only \ execute the command when it is fast." ); ("output", ([], output_t), "Filter output(s)"); ( "set_input", ([], Lang.fun_t sources_t Lang.unit_t), "Set the filter's input(s)" ); ] in let explanation = String.concat " " ((if List.mem `Dynamic_inputs flags then [ "This filter has dynamic inputs: last two arguments are \ lists of audio and video inputs. Total number of inputs is \ determined at runtime."; ] else []) @ if List.mem `Dynamic_outputs flags then [ "This filter has dynamic outputs: returned value is a tuple of \ audio and video outputs. Total number of outputs is \ determined at runtime."; ] else []) in let descr = Printf.sprintf "Ffmpeg filter: %s%s" description (if explanation <> "" then " " ^ explanation else "") in let base_filter = Lang.add_builtin ~category:(`Source `FFmpegFilter) name ~base:ffmpeg_filter ~descr ~flags:[`Extra] (args_t @ List.map (fun (_, lbl, t) -> (lbl, t, None, None)) sources_t) output_t (fun p -> let named_args = List.filter (fun (lbl, _) -> lbl <> "") p in let unnamed_args = List.filter (fun (lbl, _) -> lbl = "") p in (* Unnamed args are ordered. The last [n] ones are from [sources_t] *) let n_sources = List.length sources_t in let n_args = List.length unnamed_args in let unnamed_args, inputs = List.fold_left (fun (args, inputs) el -> if List.length args < n_args - n_sources then (args @ [el], inputs) else (args, inputs @ [el])) ([], []) unnamed_args in let args = named_args @ unnamed_args in let filter = apply_filter ~args_parser ~filter ~sources_t args in ignore (Lang.apply ~pos:(Lang.pos p) (Value.invoke filter "set_input") inputs); Value.invoke filter "output") in ignore (Lang.add_builtin ~category:(`Source `FFmpegFilter) ~base:base_filter "create" ~descr: (Printf.sprintf "%s. Use this operator to initiate the filter independently \ of its inputs, to be able to send commands to the filter \ instance." descr) ~flags:[`Extra] args_t return_t (apply_filter ~args_parser ~filter ~sources_t))) filters) let () = Startup.time "FFmpeg filters registration" register_filters let abuffer_args frame = let sample_rate = Avutil.Audio.frame_get_sample_rate frame in let channel_layout = Avutil.Audio.frame_get_channel_layout frame in let channel_layout_params = match Avutil.Channel_layout.get_native_id channel_layout with | Some id -> ("channel_layout", `Int64 id) | None -> let channel_layout = Avutil.Channel_layout.get_default (Avutil.Channel_layout.get_nb_channels channel_layout) in ( "channel_layout", `Int64 (Option.get (Avutil.Channel_layout.get_native_id channel_layout)) ) in let sample_format = Avutil.Audio.frame_get_sample_format frame in [ `Pair ("sample_rate", `Int sample_rate); `Pair ("time_base", `Rational (Ffmpeg_utils.liq_main_ticks_time_base ())); `Pair channel_layout_params; `Pair ("sample_fmt", `Int (Avutil.Sample_format.get_id sample_format)); ] let buffer_args frame = let width = Avutil.Video.frame_get_width frame in let height = Avutil.Video.frame_get_height frame in let pixel_format = Avutil.Video.frame_get_pixel_format frame in [ `Pair ("time_base", `Rational (Ffmpeg_utils.liq_main_ticks_time_base ())); `Pair ("width", `Int width); `Pair ("height", `Int height); `Pair ("pix_fmt", `Int Avutil.Pixel_format.(get_id pixel_format)); ] let _ = let raw_audio_format = `Kind Ffmpeg_raw_content.Audio.kind in let raw_video_format = `Kind Ffmpeg_raw_content.Video.kind in let audio_frame_t = Type.make (Format_type.descr raw_audio_format) in let video_frame_t = Type.make (Format_type.descr raw_video_format) in ignore (Lang.add_builtin ~category:(`Source `FFmpegFilter) ~base:ffmpeg_filter_audio "input" ~descr:"Attach an audio track to a filter's input" [ ("id", Lang.nullable_t Lang.string_t, Some Lang.null, None); ( "pass_metadata", Lang.bool_t, Some (Lang.bool true), Some "Pass liquidsoap's metadata to this stream" ); ("", Graph.t, None, None); ("", audio_frame_t, None, None); ] Audio.t (fun p -> let id = Option.value ~default:"ffmpeg.filter.audio.input" (Lang.to_valued_option Lang.to_string (List.assoc "id" p)) in let pass_metadata = Lang.to_bool (List.assoc "pass_metadata" p) in let graph_v = Lang.assoc "" 1 p in let config = get_config graph_v in let graph = Graph.of_value graph_v in let track_val = Lang.assoc "" 2 p in let field, source = Lang.to_track track_val in let frame_t = Lang.frame_t Lang.unit_t (Frame.Fields.make (* We need to make sure that we are using a format here to ensure that its params are properly unified with the underlying source. *) ~audio: (Type.make (Format_type.descr (`Format Ffmpeg_raw_content.Audio.( lift_params (default_params `Raw))))) ()) in let name = uniq_name "abuffer" in let s = Ffmpeg_filter_io.( new audio_output ~pass_metadata ~name ~frame_t ~field source) in s#set_stack (Liquidsoap_lang.Lang_core.pos p); s#set_id id; Queue.push graph.graph_inputs (s :> Source.source); let args = ref None in let audio = Lazy.from_fun (fun () -> let _abuffer = Avfilter.attach ~args:(Option.get !args) ~name Avfilter.abuffer config in Avfilter.( Hashtbl.replace graph.entries.inputs.audio name s#set_input); List.hd Avfilter.(_abuffer.io.outputs.audio)) in Queue.push graph.input_inits (fun () -> Lazy.is_val audio); s#set_init (fun frame -> if !args = None then ( args := Some (abuffer_args frame); ignore (Lazy.force audio); init_graph graph)); Audio.to_value (`Output audio))); let return_t = Type.make (Format_type.descr raw_audio_format) in ignore (Lang.add_track_operator ~base:ffmpeg_filter_audio "output" ~category:`FFmpegFilter ~descr:"Return an audio track from a filter's output" ~return_t [ ( "pass_metadata", Lang.bool_t, Some (Lang.bool true), Some "Pass ffmpeg stream metadata to liquidsoap" ); ("", Graph.t, None, None); ("", Audio.t, None, None); ] (fun p -> let pass_metadata = Lang.to_bool (List.assoc "pass_metadata" p) in let graph_v = Lang.assoc "" 1 p in let config = get_config graph_v in let graph = Graph.of_value graph_v in let frame_t = Lang.frame_t Lang.unit_t (Frame.Fields.make (* We need to make sure that we are using a format here to ensure that its params are properly unified with the underlying source. *) ~audio: (Type.make (Format_type.descr (`Kind Ffmpeg_raw_content.Audio.kind))) ()) in let field = Frame.Fields.audio in let s = new Ffmpeg_filter_io.audio_input ~field ~pull:(fun () -> pull graph) ~is_ready:(fun () -> is_ready graph) ~self_sync:(self_sync graph) ~pass_metadata frame_t in Queue.push graph.graph_outputs (s :> Source.source); let pad = Audio.of_value (Lang.assoc "" 2 p) in Queue.push graph.init (Lazy.from_fun (fun () -> let pad = match pad with | `Output pad -> Lazy.force pad | _ -> assert false in let name = uniq_name "abuffersink" in let _abuffersink = Avfilter.attach ~name Avfilter.abuffersink config in Avfilter.(link pad (List.hd _abuffersink.io.inputs.audio)); Avfilter.( Hashtbl.replace graph.entries.outputs.audio name s#set_output))); (field, (s :> Source.source)))); ignore (Lang.add_builtin ~category:(`Source `FFmpegFilter) ~base:ffmpeg_filter_video "input" ~descr:"Attach a video track to a filter's input" [ ("id", Lang.nullable_t Lang.string_t, Some Lang.null, None); ( "pass_metadata", Lang.bool_t, Some (Lang.bool true), Some "Pass liquidsoap's metadata to this stream" ); ("", Graph.t, None, None); ("", video_frame_t, None, None); ] Video.t (fun p -> let id = Option.value ~default:"ffmpeg.filter.video.input" (Lang.to_valued_option Lang.to_string (List.assoc "id" p)) in let pass_metadata = Lang.to_bool (List.assoc "pass_metadata" p) in let graph_v = Lang.assoc "" 1 p in let config = get_config graph_v in let graph = Graph.of_value graph_v in let track_val = Lang.assoc "" 2 p in let field, source = Lang.to_track track_val in let frame_t = Lang.frame_t Lang.unit_t (Frame.Fields.make (* We need to make sure that we are using a format here to ensure that its params are properly unified with the underlying source. *) ~video: (Type.make (Format_type.descr (`Format Ffmpeg_raw_content.Video.( lift_params (default_params `Raw))))) ()) in let name = uniq_name "buffer" in let s = Ffmpeg_filter_io.( new video_output ~pass_metadata ~name ~frame_t ~field source) in s#set_stack (Liquidsoap_lang.Lang_core.pos p); s#set_id id; Queue.push graph.graph_inputs (s :> Source.source); let args = ref None in let video = Lazy.from_fun (fun () -> let _buffer = Avfilter.attach ~args:(Option.get !args) ~name Avfilter.buffer config in Avfilter.( Hashtbl.replace graph.entries.inputs.video name s#set_input); List.hd Avfilter.(_buffer.io.outputs.video)) in Queue.push graph.input_inits (fun () -> Lazy.is_val video); s#set_init (fun frame -> if !args = None then ( args := Some (buffer_args frame); ignore (Lazy.force video); init_graph graph)); Video.to_value (`Output video))); let return_t = Type.make (Format_type.descr raw_video_format) in Lang.add_track_operator ~base:ffmpeg_filter_video "output" ~category:`Video ~descr:"Return a video track from a filter's output" ~return_t [ ( "pass_metadata", Lang.bool_t, Some (Lang.bool true), Some "Pass ffmpeg stream metadata to liquidsoap" ); ("", Graph.t, None, None); ("", Video.t, None, None); ] (fun p -> let pass_metadata = Lang.to_bool (List.assoc "pass_metadata" p) in let graph_v = Lang.assoc "" 1 p in let config = get_config graph_v in let graph = Graph.of_value graph_v in let frame_t = Lang.frame_t Lang.unit_t (Frame.Fields.make ~video: (Type.make (Format_type.descr (`Kind Ffmpeg_raw_content.Video.kind))) ()) in let field = Frame.Fields.video in let s = new Ffmpeg_filter_io.video_input ~field ~pull:(fun () -> pull graph) ~is_ready:(fun () -> is_ready graph) ~self_sync:(self_sync graph) ~pass_metadata frame_t in Queue.push graph.graph_outputs (s :> Source.source); Queue.push graph.init (Lazy.from_fun (fun () -> let pad = match Video.of_value (Lang.assoc "" 2 p) with | `Output p -> Lazy.force p | _ -> assert false in let name = uniq_name "buffersink" in let _buffersink = Avfilter.attach ~name Avfilter.buffersink config in Avfilter.(link pad (List.hd _buffersink.io.inputs.video)); Avfilter.( Hashtbl.replace graph.entries.outputs.video name s#set_output))); (field, (s :> Source.source))) let unify_clocks ~clock sources = Queue.iter sources (fun s -> Clock.unify ~pos:s#pos clock s#clock) let _ = let univ_t = Lang.univ_t () in Lang.add_builtin ~base:ffmpeg_filter "create" ~category:(`Source `FFmpegFilter) ~descr:"Configure and launch a filter graph" [("", Lang.fun_t [(false, "", Graph.t)] univ_t, None, None)] univ_t (fun p -> let fn = List.assoc "" p in let config = Avfilter.init () in let graph = Avfilter. { config = Some config; failed = false; input_inits = Queue.create (); graph_inputs = Queue.create (); graph_outputs = Queue.create (); init = Queue.create (); entries = { inputs = { audio = Hashtbl.create 10; video = Hashtbl.create 10 }; outputs = { audio = Hashtbl.create 10; video = Hashtbl.create 10 }; }; } in let ret = Lang.apply ~pos:(Lang.pos p) fn [("", Graph.to_value graph)] in let id = "ffmpeg.filter" in let output_clock = Clock.create ~id () in let input_clock = Clock.create_sub_clock ~id:(id ^ ".input") output_clock in unify_clocks ~clock:input_clock graph.graph_inputs; unify_clocks ~clock:output_clock graph.graph_outputs; Queue.push graph.init (Lazy.from_fun (fun () -> log#info "Initializing graph"; let filter = Avfilter.launch config in Avfilter.( List.iter (fun (name, input) -> let set_input = Hashtbl.find graph.entries.inputs.audio name in set_input input) filter.inputs.audio); Avfilter.( List.iter (fun (name, input) -> let set_input = Hashtbl.find graph.entries.inputs.video name in set_input input) filter.inputs.video); Avfilter.( List.iter (fun (name, output) -> let set_output = Hashtbl.find graph.entries.outputs.audio name in set_output output) filter.outputs.audio); Avfilter.( List.iter (fun (name, output) -> let set_output = Hashtbl.find graph.entries.outputs.video name in set_output output) filter.outputs.video))); graph.config <- None; ret) liquidsoap-2.3.2/src/core/builtins/builtins_files.ml000066400000000000000000000547401477303350200226230ustar00rootroot00000000000000module Http = Liq_http module Filename = struct include Filename let rand_digits () = let rand = Random.State.(bits (make_self_init ()) land 0xFFFFFF) in Printf.sprintf "%06x" rand let mk_temp_dir ?(mode = 0o700) ?dir prefix suffix = let dir = match dir with Some d -> d | None -> get_temp_dir_name () in let raise_err msg = raise (Sys_error msg) in let rec loop count = if count < 0 then raise_err "mk_temp_dir: too many failing attempts" else ( let dir = Printf.sprintf "%s/%s%s%s" dir prefix (rand_digits ()) suffix in try Unix.mkdir dir mode; dir with | Unix.Unix_error (Unix.EEXIST, _, _) -> loop (count - 1) | Unix.Unix_error (Unix.EINTR, _, _) -> loop count | Unix.Unix_error (e, _, _) -> raise_err ("mk_temp_dir: " ^ Unix.error_message e)) in loop 1000 end (* From OCaml *) let file_extension_len ~dir_sep name = let rec check i0 i = if i < 0 || name.[i] = dir_sep then 0 else if name.[i] = '.' then check i0 (i - 1) else String.length name - i0 in let rec search_dot i = if i < 0 || name.[i] = dir_sep then 0 else if name.[i] = '.' then check i (i - 1) else search_dot (i - 1) in search_dot (String.length name - 1) let file_extension ?(leading_dot = true) ?(dir_sep = Filename.dir_sep) name = let dir_sep = dir_sep.[0] in let l = file_extension_len ~dir_sep name in let s = if l = 0 then "" else String.sub name (String.length name - l) l in try match (leading_dot, s.[0]) with | false, '.' -> String.sub s 1 (String.length s - 1) | _ -> s with Invalid_argument _ -> s let file = Modules.file let _ = Lang.add_builtin ~base:file "extension" ~category:`File ~descr:"Returns a file's extension." [ ( "dir_sep", Lang.string_t, Some (Lang.string Filename.dir_sep), Some "Directory separator." ); ( "leading_dot", Lang.bool_t, Some (Lang.bool true), Some "Return extension with a leading dot, e.g. `.foo`." ); ("", Lang.string_t, None, None); ] Lang.string_t (fun p -> let dir_sep = Lang.to_string (List.assoc "dir_sep" p) in let leading_dot = Lang.to_bool (List.assoc "leading_dot" p) in Lang.string (file_extension ~dir_sep ~leading_dot (Lang.to_string (List.assoc "" p)))) let _ = Lang.add_builtin ~base:file "remove" ~category:`File ~descr:"Remove a file." [("", Lang.string_t, None, None)] Lang.unit_t (fun p -> try Unix.unlink (Lang.to_string (List.assoc "" p)); Lang.unit with _ -> Lang.unit) let _ = Lang.add_builtin ~base:file "size" ~category:`File ~descr:"File size in bytes." [("", Lang.string_t, None, None)] Lang.int_t (fun p -> try let ic = open_in_bin (Lang.to_string (List.assoc "" p)) in let ret = in_channel_length ic in close_in ic; Lang.int ret with _ -> Lang.int 0) let _ = Lang.add_builtin ~base:file "mtime" ~category:`File ~descr:"Last modification time." [("", Lang.string_t, None, None)] Lang.float_t (fun p -> let fname = List.assoc "" p |> Lang.to_string in try Lang.float (Unix.stat fname).st_mtime with _ -> Lang.float 0.) let _ = Lang.add_builtin ~base:file "mkdir" ~category:`File ~descr:"Create a directory." [ ( "parents", Lang.bool_t, Some (Lang.bool false), Some "Also create parent directories if they do not exist." ); ( "perms", Lang.int_t, Some (Lang.octal_int 0o755), Some "Default file rights if created." ); ("", Lang.string_t, None, None); ] Lang.unit_t (fun p -> let parents = List.assoc "parents" p |> Lang.to_bool in let perms = List.assoc "perms" p |> Lang.to_int in let dir = List.assoc "" p |> Lang.to_string in try let rec recmkdir dir = if not (Sys.file_exists dir) then ( recmkdir (Filename.dirname dir); Unix.mkdir dir perms) in if parents then recmkdir dir else Unix.mkdir dir perms; Lang.unit with _ -> Lang.unit) let rm_dir dir = let rec finddepth f roots = Array.iter (fun root -> (match Unix.lstat root with | { Unix.st_kind = S_DIR } -> finddepth f (Array.map (Filename.concat root) (Sys.readdir root)) | _ -> ()); f root) roots in let zap path = match Unix.lstat path with | { st_kind = S_DIR } -> Unix.rmdir path | _ -> Unix.unlink path in finddepth zap [| dir |]; Unix.rmdir dir let _ = Lang.add_builtin ~base:file "rmdir" ~category:`File ~descr:"Remove a directory and its content." [("", Lang.string_t, None, None)] Lang.unit_t (fun p -> try rm_dir (Lang.to_string (List.assoc "" p)); Lang.unit with _ -> Lang.unit) let _ = Lang.add_builtin ~base:file "temp" ~category:`File ~descr: "Return a fresh temporary filename. The temporary file is created empty, \ with permissions 0o600 (readable and writable only by the file owner)." [ ( "directory", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Directory where to create the file." ); ("", Lang.string_t, None, Some "File prefix"); ("", Lang.string_t, None, Some "File suffix"); ] Lang.string_t (fun p -> let temp_dir = Lang.to_valued_option Lang.to_string (List.assoc "directory" p) in try Lang.string (Filename.temp_file ?temp_dir (Lang.to_string (Lang.assoc "" 1 p)) (Lang.to_string (Lang.assoc "" 2 p))) with exn -> let bt = Printexc.get_raw_backtrace () in Lang.raise_as_runtime ~bt ~kind:"file" exn) let _ = Lang.add_builtin ~base:file "temp_dir" ~category:`File ~descr: "Return a fresh temporary directory name. The temporary directory is \ created empty, in the default tmp directory, with permissions 0o700 \ (readable, writable and listable only by the file owner)." [ ("", Lang.string_t, None, Some "Directory name prefix."); ("", Lang.string_t, Some (Lang.string ""), Some "Directory name suffix."); ] Lang.string_t (fun p -> try let prefix = Lang.to_string (Lang.assoc "" 1 p) in let suffix = Lang.to_string (Lang.assoc "" 2 p) in Lang.string (Filename.mk_temp_dir prefix suffix) with exn -> let bt = Printexc.get_raw_backtrace () in Lang.raise_as_runtime ~bt ~kind:"file" exn) let _ = Lang.add_builtin ~base:file "exists" ~category:`File [("", Lang.string_t, None, None)] Lang.bool_t ~descr:"Returns true if the file or directory exists." (fun p -> let f = Lang.to_string (List.assoc "" p) in let f = Lang_string.home_unrelate f in Lang.bool (Sys.file_exists f)) let _ = Lang.add_builtin ~base:file "is_directory" ~category:`File [("", Lang.string_t, None, None)] Lang.bool_t ~descr:"Returns true if the file exists and is a directory." (fun p -> let f = Lang.to_string (List.assoc "" p) in let f = Lang_string.home_unrelate f in Lang.bool (try Sys.is_directory f with Sys_error _ -> false)) let _ = Lang.add_builtin ~base:file "ls" ~category:`File [ ( "absolute", Lang.bool_t, Some (Lang.bool false), Some "Whether to return absolute paths." ); ( "recursive", Lang.bool_t, Some (Lang.bool false), Some "Whether to look recursively in subdirectories." ); ( "pattern", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Pattern that the filenames should match (e.g. `\"*.mp3\"`)." ); ( "sorted", Lang.bool_t, Some (Lang.bool false), Some "Return results in a sorted order." ); ("", Lang.string_t, None, Some "Directory to look in."); ] (Lang.list_t Lang.string_t) ~descr:"List all the files in a directory." (fun p -> let absolute = Lang.to_bool (List.assoc "absolute" p) in let recursive = Lang.to_bool (List.assoc "recursive" p) in let pattern = List.assoc "pattern" p |> Lang.to_option |> Option.map Lang.to_string in let pattern = pattern |> Option.map (fun s -> String.concat "\\." (String.split_on_char '.' s)) |> Option.map (fun s -> String.concat ".*" (String.split_on_char '*' s)) |> Option.map (fun s -> "^" ^ s ^ "$") |> Option.value ~default:"" in let sorted = List.assoc "sorted" p |> Lang.to_bool in let rex = Re.Pcre.regexp pattern in let dir = Lang.to_string (List.assoc "" p) in let dir = Lang_string.home_unrelate dir in let readdir dir = Array.to_list (Sys.readdir dir) |> List.filter (fun s -> Re.Pcre.pmatch ~rex s) in let files = if not recursive then readdir dir else ( let rec aux subdir acc = function | f :: l -> let concat d f = if d = Filename.current_dir_name then f else Filename.concat d f in let df = concat subdir f in let df = Filename.concat dir df in if try Sys.is_directory df with _ -> false then ( let f = concat subdir f in let acc = if f <> Filename.current_dir_name then f :: acc else acc in let in_dir = (* Cope with permission problems. *) try readdir df with Sys_error _ -> [] in let acc = aux f acc in_dir in aux subdir acc l) else aux subdir (concat subdir f :: acc) l | [] -> acc in aux Filename.current_dir_name [] [Filename.current_dir_name]) in let files = if absolute then List.map (Filename.concat dir) files else files in let files = List.map Lang.string files in let files = if sorted then List.sort compare files else files in Lang.list files) (************** Paths ********************) let path = Lang.add_module "path" let path_home = Lang.add_module ~base:path "home" let _ = Lang.add_builtin ~base:path_home "unrelate" ~category:`File [("", Lang.string_t, None, None)] Lang.string_t ~descr:"Expand path that start with '~' with the current home directory." (fun p -> let f = Lang.to_string (List.assoc "" p) in Lang.string (Lang_string.home_unrelate f)) let _ = Lang.add_builtin ~base:path "basename" ~category:`File [("", Lang.string_t, None, None)] Lang.string_t ~descr: "Get the base name of a path, i.e. the name of the file without the full \ path. For instance `file.basename(\"/tmp/folder/bla.mp3\")` returns \ `\"bla.mp3\"`." (fun p -> let f = Lang.to_string (List.assoc "" p) in Lang.string (Filename.basename f)) let _ = Lang.add_builtin ~base:path "dirname" ~category:`File [("", Lang.string_t, None, None)] Lang.string_t ~descr:"Get the directory name of a path." (fun p -> let f = Lang.to_string (List.assoc "" p) in Lang.string (Filename.dirname f)) let _ = Lang.add_builtin ~base:path "concat" ~category:`File [("", Lang.string_t, None, None); ("", Lang.string_t, None, None)] Lang.string_t ~descr:"Concatenate two paths, using the appropriate directory separator." (fun p -> let f = Lang.to_string (Lang.assoc "" 1 p) in let s = Lang.to_string (Lang.assoc "" 2 p) in Lang.string (Filename.concat f s)) let _ = Lang.add_builtin ~base:path "remove_extension" ~category:`File [("", Lang.string_t, None, None)] Lang.string_t ~descr:"Remove the file extension from a path." (fun p -> let f = Lang.to_string (List.assoc "" p) in Lang.string (Filename.remove_extension f)) let _ = Lang.add_builtin ~base:file "digest" ~category:`File ~descr:"Return an MD5 digest for the given file." [("", Lang.string_t, None, None)] Lang.string_t (fun p -> let file = Lang.to_string (List.assoc "" p) in if Sys.file_exists file then Lang.string (Digest.to_hex (Digest.file file)) else ( let message = Printf.sprintf "The file %s does not exist." file in Lang.raise_error ~pos:(Lang.pos p) ~message "file")) let _ = Lang.add_builtin ~base:file "open" ~category:`File [ ( "write", Lang.bool_t, Some (Lang.bool false), Some "Open file for writing" ); ( "create", Lang.nullable_t Lang.bool_t, Some Lang.null, Some "Create if nonexistent. Default: `false` in read-only mode, `true` \ when writing." ); ( "append", Lang.bool_t, Some (Lang.bool false), Some "Append data if file exists." ); ( "non_blocking", Lang.bool_t, Some (Lang.bool false), Some "Open in non-blocking mode." ); ( "perms", Lang.int_t, Some (Lang.octal_int 0o644), Some "Default file rights if created." ); ("", Lang.string_t, None, None); ] Builtins_socket.Socket_value.t ~descr:"Open a file." (fun p -> let write = Lang.to_bool (List.assoc "write" p) in let access_flag = if write then Unix.O_RDWR else Unix.O_RDONLY in let create = Lang.to_valued_option Lang.to_bool (List.assoc "create" p) in let create_flags = Option.value ~default:(if write then [Unix.O_CREAT] else []) (Option.map (fun x -> if x then [Unix.O_CREAT] else []) create) in let data_flags = match (write, Lang.to_bool (List.assoc "append" p)) with | true, true -> [Unix.O_APPEND] | true, false -> [Unix.O_TRUNC] | false, _ -> [] in let non_blocking_flags = if Lang.to_bool (List.assoc "non_blocking" p) then [Unix.O_NONBLOCK] else [] in let flags = [access_flag] @ create_flags @ data_flags @ non_blocking_flags in let file_perms = Lang.to_int (List.assoc "perms" p) in let path = Lang_string.home_unrelate (Lang.to_string (List.assoc "" p)) in try Builtins_socket.Socket_value.( to_value (Http.unix_socket (Unix.openfile path flags file_perms))) with exn -> let bt = Printexc.get_raw_backtrace () in Lang.raise_as_runtime ~bt ~kind:"file" exn) let _ = Lang.add_builtin ~base:file "watch" ~category:`File [ ("", Lang.string_t, None, Some "File to watch."); ("", Lang.fun_t [] Lang.unit_t, None, Some "Handler function."); ] (Lang.method_t Lang.unit_t [ ( "unwatch", ([], Lang.fun_t [] Lang.unit_t), "Function to remove the watch on the file." ); ]) ~descr: "Call a function when a file is modified. Returns unwatch function in \ `unwatch` method." (fun p -> let fname = Lang.to_string (Extralib.List.assoc_nth "" 0 p) in let fname = Lang_string.home_unrelate fname in let f = Extralib.List.assoc_nth "" 1 p in let f () = try ignore (Lang.apply f []) with exn -> let bt = Printexc.get_raw_backtrace () in Lang.raise_as_runtime ~bt ~kind:"file" exn in let unwatch = File_watcher.watch ~pos:(Lang.pos p) [`Modify] fname f in Lang.meth Lang.unit [ ( "unwatch", Lang.val_fun [] (fun _ -> unwatch (); Lang.unit) ); ]) let file_metadata = Lang.add_builtin ~base:file "metadata" ~category:`File [ ( "", Lang.string_t, None, Some "File from which the metadata should be read." ); ( "exclude", Lang.list_t Lang.string_t, Some (Lang.list []), Some "Decoders to exclude" ); ] Lang.metadata_t ~descr:"Read metadata from a file." (fun p -> let uri = Lang.to_string (List.assoc "" p) in let excluded = List.map Lang.to_string (Lang.to_list (List.assoc "exclude" p)) in Lang.metadata (Request.resolve_metadata ~initial_metadata:Frame.Metadata.empty ~excluded uri)) let () = Lifecycle.on_load ~name:"metadata resolvers registration" (fun () -> Plug.iter Request.mresolvers (fun name decoder -> let name = String.lowercase_ascii name in ignore (Lang.add_builtin ~base:file_metadata name ~category:`File [ ( "", Lang.string_t, None, Some "File from which the metadata should be read." ); ] Lang.metadata_t ~descr: ("Read metadata from a file using the " ^ name ^ " decoder.") (fun p -> let uri = Lang.to_string (List.assoc "" p) in let extension = try Some (Utils.get_ext uri) with _ -> None in let mime = Magic_mime.lookup uri in let m = try decoder.Request.resolver ~metadata:Frame.Metadata.empty ~extension ~mime uri with _ -> [] in let m = List.map (fun (k, v) -> (String.lowercase_ascii k, v)) m in Lang.metadata (Frame.Metadata.from_list m))))) let _ = Lang.add_builtin ~base:file_metadata "native" ~category:`File [ ( "", Lang.string_t, None, Some "File from which the metadata should be read." ); ] Lang.metadata_t ~descr:"Read metadata from a file using the native decoder." (fun p -> let file = List.assoc "" p |> Lang.to_string in let m = try Metadata.parse_file file with _ -> [] in let m = List.map (fun (k, v) -> (String.lowercase_ascii k, v)) m in Lang.metadata (Frame.Metadata.from_list m)) let _ = Lang.add_builtin ~base:file "which" ~category:`File ~descr: "`file.which(\"progname\")` looks for an executable named \"progname\" \ using directories from the PATH environment variable and returns \"\" \ if it could not find one." [("", Lang.string_t, None, None)] (Lang.nullable_t Lang.string_t) (fun p -> let file = Lang.to_string (List.assoc "" p) in try Lang.string (Utils.which ~path:(Configure.path ()) file) with Not_found -> Lang.null) let _ = Lang.add_builtin ~base:file "copy" ~category:`File ~descr: "Copy a file. Arguments and implementation follows the POSIX `cp` \ command line specifications." [ ( "recursive", Lang.bool_t, Some (Lang.bool false), Some "Copy file hierarchies." ); ( "force", Lang.bool_t, Some (Lang.bool true), Some "If a file descriptor for a destination file cannot be obtained \ attempt to unlink the destination file and proceed." ); ( "preserve", Lang.bool_t, Some (Lang.bool false), Some "Duplicate source files attributes in the destination file." ); ("", Lang.string_t, None, Some "Source"); ("", Lang.string_t, None, Some "Destination"); ] Lang.unit_t (fun p -> let recurse = Lang.to_bool (List.assoc "recursive" p) in let force = if Lang.to_bool (List.assoc "force" p) then FileUtil.Force else FileUtil.Ask (fun _ -> false) in let preserve = Lang.to_bool (List.assoc "preserve" p) in let src = Lang.to_string (Lang.assoc "" 1 p) in let dst = Lang.to_string (Lang.assoc "" 2 p) in let error message _ = Runtime_error.raise ~pos:(Lang.pos p) ~message "file" in try FileUtil.cp ~recurse ~force ~preserve ~error [src] dst; Lang.unit with exn -> let bt = Printexc.get_raw_backtrace () in Lang.raise_as_runtime ~bt ~kind:"file" exn) let _ = Lang.add_builtin ~base:file "move" ~category:`File ~descr:"Move a file" [ ( "force", Lang.bool_t, Some (Lang.bool false), Some "Do not prompt for confirmation if the destination path exists." ); ( "atomic", Lang.bool_t, Some (Lang.bool false), Some "Move the file atomically. Implies `force` and raises \ `error.file.cross_device` if atomic move fails because the source \ and destination files are not on the same partition." ); ("", Lang.string_t, None, Some "Source"); ("", Lang.string_t, None, Some "Destination"); ] Lang.unit_t (fun p -> let force = if Lang.to_bool (List.assoc "force" p) then FileUtil.Force else FileUtil.Ask (fun _ -> false) in let atomic = Lang.to_bool (List.assoc "atomic" p) in let src = Lang.to_string (Lang.assoc "" 1 p) in let dst = Lang.to_string (Lang.assoc "" 2 p) in let error message _ = Runtime_error.raise ~pos:(Lang.pos p) ~message "file" in try if atomic then Unix.rename src dst else FileUtil.mv ~force ~error src dst; Lang.unit with | Unix.Unix_error (Unix.EXDEV, _, _) -> Runtime_error.raise ~pos:(Lang.pos p) ~message: "Rename failed! Directory for temporary files appears to be on \ a different filesystem" "file.cross_device" | exn -> let bt = Printexc.get_raw_backtrace () in Lang.raise_as_runtime ~bt ~kind:"file" exn) let () = if not Sys.win32 then ( let umask_m = Mutex.create () in let get_umask = Mutex_utils.mutexify umask_m (fun () -> let umask = Unix.umask 0 in ignore (Unix.umask umask); umask) in let set_umask = Mutex_utils.mutexify umask_m (fun umask -> ignore (Unix.umask umask)) in let umask = Lang.add_builtin ~base:file "umask" ~category:`File ~descr:"Get the process's file mode creation mask." [] Lang.int_t (fun _ -> Lang.int (get_umask ())) in ignore (Lang.add_builtin ~base:umask "set" ~category:`File ~descr:"Set process's file mode creation mask." [("", Lang.int_t, None, None)] Lang.unit_t (fun p -> set_umask (Lang.to_int (List.assoc "" p)); Lang.unit))) let _ = Lang.add_builtin ~base:Modules.file_mime "magic" ~category:`File ~descr:"Get the MIME type of a file." [("", Lang.string_t, None, None)] Lang.string_t (fun p -> let file = Lang.to_string (Lang.assoc "" 1 p) in Lang.string (Magic_mime.lookup file)) liquidsoap-2.3.2/src/core/builtins/builtins_harbor.ml000066400000000000000000000120061477303350200227630ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let resp_t = Lang.nullable_t (Lang.getter_t Lang.string_t) let harbor = Modules.harbor let harbor_http = Lang.add_module ~base:harbor "http" module Http = Liq_http let request_t = Lang.record_t [ ("http_version", Lang.string_t); ("method", Lang.string_t); ("data", Lang.fun_t [(true, "timeout", Lang.float_t)] Lang.string_t); ("headers", Lang.list_t (Lang.product_t Lang.string_t Lang.string_t)); ("query", Lang.list_t (Lang.product_t Lang.string_t Lang.string_t)); ("socket", Builtins_socket.Socket_value.t); ("path", Lang.string_t); ] let register_args = [ ("port", Lang.int_t, Some (Lang.int 8000), Some "Port to serve."); ( "transport", Lang.http_transport_t, Some (Lang.http_transport Http.unix_transport), Some "Http transport. Use `http.transport.ssl` or \ `http.transport.secure_transport`, when available, to enable HTTPS \ output" ); ( "method", Lang.string_t, Some (Lang.string "GET"), Some "Accepted method (\"GET\" / \"POST\" / \"PUT\" / \"DELETE\" / \"HEAD\" \ / \"OPTIONS\")." ); ("", Lang.regexp_t, None, Some "path to to serve."); ( "", Lang.fun_t [(false, "", request_t)] resp_t, None, Some "Handler function" ); ] let parse_register_args p = let port = Lang.to_int (List.assoc "port" p) in let transport = Lang.to_http_transport (List.assoc "transport" p) in let verb = Harbor.verb_of_string (Lang.to_string (List.assoc "method" p)) in let uri = Lang.to_regexp (Lang.assoc "" 1 p) in let f = Lang.assoc "" 2 p in let handler ~protocol ~meth ~data ~headers ~query ~socket path = let meth = Harbor.string_of_verb meth in let headers = List.map (fun (x, y) -> Lang.product (Lang.string x) (Lang.string y)) headers in let headers = Lang.list headers in let query = List.map (fun (x, y) -> Lang.product (Lang.string x) (Lang.string y)) query in let query = Lang.list query in let socket = Builtins_socket.Socket_value.to_value (socket :> Builtins_socket.Socket_value.content) in let request = Lang.record [ ("http_version", Lang.string protocol); ("method", Lang.string meth); ("headers", headers); ("query", query); ("socket", socket); ( "data", Lang.val_fun [("timeout", "timeout", Some (Lang.float 10.))] (fun p -> let timeout = Lang.to_float (List.assoc "timeout" p) in Lang.string (data timeout)) ); ("path", Lang.string path); ] in let resp = Lang.apply f [("", request)] in match Lang.to_option resp with | None -> Harbor.custom () | Some resp -> ( try Harbor.simple_reply (Lang.to_string resp) with _ -> Harbor.reply (Lang.to_string_getter resp)) in (uri, port, transport, verb, handler) let _ = Lang.add_builtin ~base:harbor_http "register" ~category:`Internet ~descr: "Low-level harbor handler registration. Overridden in standard library." register_args Lang.unit_t (fun p -> let uri, port, transport, verb, handler = parse_register_args p in Harbor.add_http_handler ~pos:(Lang.pos p) ~transport ~port ~verb ~uri handler; Lang.unit) let _ = Lang.add_builtin ~base:harbor "remove" ~category:`Internet ~descr:(Printf.sprintf "Remove a registered handler on the harbor.") [ ("port", Lang.int_t, Some (Lang.int 8000), Some "Port to serve."); ("method", Lang.string_t, Some (Lang.string "GET"), Some "Method served."); ("", Lang.regexp_t, None, Some "URI served."); ] Lang.unit_t (fun p -> let port = Lang.to_int (List.assoc "port" p) in let uri = Lang.to_regexp (Lang.assoc "" 1 p) in let verb = Harbor.verb_of_string (Lang.to_string (List.assoc "method" p)) in Harbor.remove_http_handler ~port ~verb ~uri (); Lang.unit) liquidsoap-2.3.2/src/core/builtins/builtins_harbor_ssl.ml000066400000000000000000000022441477303350200236470ustar00rootroot00000000000000(* -*- mode: tuareg; -*- *) (***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) module Ssl_harbor = struct include Harbor_ssl let name = "https" end module Ssl = Builtins_harbor.Make (Ssl_harbor) include Ssl liquidsoap-2.3.2/src/core/builtins/builtins_http.ml000066400000000000000000000235751477303350200225020ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) type request = Get | Post | Put | Head | Delete module Http = Liq_http let conf_http = Dtools.Conf.void ~p:(Configure.conf#plug "http") "Settings for HTTP requests" let conf_normalize_url = Dtools.Conf.bool ~d:true ~p:(conf_http#plug "normalize_url") "When `true`, HTTP urls are normalized by default, i.e. spaces are \ replaced with `%20` and etc." let string_of_request = function | Get -> "get" | Post -> "post" | Put -> "put" | Head -> "head" | Delete -> "delete" let request_with_body = [Get; Post; Put] let http = Modules.http let http_transport = Modules.http_transport let _ = Lang.add_builtin_value ~category:`Internet ~descr:"Http unencrypted transport" ~base:http_transport "unix" (Lang.http_transport Http.unix_transport) Lang.http_transport_t let add_http_request ~base ~stream_body ~descr ~request name = let header_t = Lang.product_t Lang.string_t Lang.string_t in let headers_t = Lang.list_t header_t in let has_body = List.mem request request_with_body in let log = Log.make ["http"; string_of_request request] in let request_return_t = Lang.method_t (if (not has_body) || stream_body then Lang.unit_t else Lang.string_t) [ ("http_version", ([], Lang.string_t), "Version of the HTTP protocol."); ("status_code", ([], Lang.int_t), "Status code."); ("status_message", ([], Lang.string_t), "Status message."); ("headers", ([], headers_t), "HTTP headers."); ] in let params = if List.mem request [Get; Head; Delete] then [] else [ ( "data", Lang.getter_t Lang.string_t, Some (Lang.string ""), Some "POST data. Use a `string` getter to stream data and return `\"\"` \ when all data has been passed." ); ] in let params = params @ [ ("headers", headers_t, Some (Lang.list []), Some "Additional headers."); ( "http_version", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Http request version." ); ( "redirect", Lang.bool_t, Some (Lang.bool true), Some "Perform redirections if needed." ); ( "timeout", Lang.nullable_t Lang.float_t, Some (Lang.float 10.), Some "Timeout for network operations in seconds." ); ( "normalize_url", Lang.nullable_t Lang.bool_t, Some Lang.null, Some "Normalize url, replacing spaces with `%20` and more. Defaults to \ `settings.http.normalize_url` when `null`." ); ( "", Lang.string_t, None, Some "Requested URL, e.g. `\"http://www.liquidsoap.info/\"`." ); ] @ if stream_body then [ ( "on_body_data", Lang.fun_t [(false, "", Lang.nullable_t Lang.string_t)] Lang.unit_t, None, Some "function called when receiving response body data. `null` or \ `\"\"` means that all the data has been passed." ); ] else [] in Lang.add_builtin name ~base ~category:`Internet ~descr params request_return_t (fun p -> let headers = List.assoc "headers" p in let headers = Lang.to_list headers in let headers = List.map Lang.to_product headers in let headers = List.map (fun (x, y) -> (Lang.to_string x, Lang.to_string y)) headers in let timeout = Lang.to_valued_option (fun v -> int_of_float (1000. *. Lang.to_float v)) (List.assoc "timeout" p) in let http_version = Option.map Lang.to_string (Lang.to_option (List.assoc "http_version" p)) in let original_url = Lang.to_string (List.assoc "" p) in let normalize_url = Option.value ~default:conf_normalize_url#get (Lang.to_valued_option Lang.to_bool (List.assoc "normalize_url" p)) in let url = if normalize_url then Uri.(to_string (of_string original_url)) else original_url in if Uri.pct_decode url <> original_url then log#important "Requested url %s is different from normalized url: %s. URL are \ normalized by default to ensure maximum compatibility with e.g. \ URLs with spaces in them. However, this can also cause issues so we \ recommend passing normalized URLs. Url normalization can be \ disabled on a case-by-case basis using the `normalize_url` \ parameter or globally using the `settings.http.normalize_url` \ setting." (Lang_string.quote_utf8_string original_url) (Lang_string.quote_utf8_string (Uri.pct_decode url)); let redirect = Lang.to_bool (List.assoc "redirect" p) in let on_body_data, get_body = if stream_body then ( let on_body_data = List.assoc "on_body_data" p in ( (fun s -> let v = match s with None -> Lang.null | Some s -> Lang.string s in ignore (Lang.apply on_body_data [("", v)])), fun _ -> assert false )) else ( let body = Buffer.create 1024 in ( (fun s -> ignore (Option.map (Buffer.add_string body) s)), fun () -> Buffer.contents body )) in let get_data () = let data = List.assoc "data" p in let buf = Buffer.create 10 in let len, refill = try let data = Lang.to_string data in Buffer.add_string buf data; (Some (Int64.of_int (String.length data)), fun () -> ()) with _ -> let fn = Lang.to_getter data in (None, fun () -> Buffer.add_string buf (Lang.to_string (fn ()))) in ( len, fun len -> if Buffer.length buf = 0 then refill (); let len = min (Buffer.length buf) len in let ret = Buffer.sub buf 0 len in Utils.buffer_drop buf len; ret ) in let http_version, status_code, status_message, headers = try let request = match request with | Get -> `Get | Post -> `Post (get_data ()) | Put -> `Put (get_data ()) | Head -> `Head | Delete -> `Delete in let ans = Liqcurl.http_request ~pos:(Lang.pos p) ~follow_redirect:redirect ~timeout ~headers ~url ~on_body_data:(fun s -> on_body_data (Some s)) ~request ?http_version () in on_body_data None; ans with | Curl.CurlException (Curl.CURLE_ABORTED_BY_CALLBACK, _, _) -> ("1.0", 520, "Operation aborted", []) | Curl.(CurlException (CURLE_OPERATION_TIMEOUTED, _, _)) -> ("1.0", 522, "Connection timed out", []) | Curl.(CurlException (CURLE_COULDNT_CONNECT, _, _)) | Curl.(CurlException (CURLE_COULDNT_RESOLVE_HOST, _, _)) -> ("1.0", 523, "Origin is unreachable", []) | Curl.(CurlException (CURLE_GOT_NOTHING, _, _)) -> ("1.0", 523, "Remote server did not return any data", []) | Curl.(CurlException (CURLE_SSL_CONNECT_ERROR, _, _)) -> ("1.0", 525, "SSL handshake failed", []) | Curl.(CurlException (CURLE_SSL_CACERT, _, _)) -> ("1.0", 526, "Invalid SSL certificate", []) | e -> let bt = Printexc.get_raw_backtrace () in Lang.log#severe "Could not perform http request: %s." (Printexc.to_string e); Lang.raise_as_runtime ~bt ~kind:"http" e in let http_version = Lang.string http_version in let status_code = Lang.int status_code in let status_message = Lang.string status_message in let headers = List.map (fun (x, y) -> Lang.product (Lang.string x) (Lang.string y)) headers in let headers = Lang.list headers in let ret = if (not has_body) || stream_body then Lang.unit else Lang.string (get_body ()) in Lang.meth ret [ ("http_version", http_version); ("status_code", status_code); ("status_message", status_message); ("headers", headers); ]) let () = List.iter (fun (request, name) -> let base = add_http_request ~base:http ~descr: ("Perform a full http " ^ String.uppercase_ascii name ^ " request.") ~request ~stream_body:false name in ignore (add_http_request ~base ~descr: ("Perform a full http " ^ String.uppercase_ascii name ^ " request.") ~request ~stream_body:true "stream")) [ (Get, "get"); (Post, "post"); (Put, "put"); (Head, "head"); (Delete, "delete"); ] let _ = Lang.add_builtin_base ~category:`Internet ~descr:"Default user-agent" ~base:http "user_agent" (`String Http.user_agent) Lang.string_t liquidsoap-2.3.2/src/core/builtins/builtins_irc.ml000066400000000000000000000077531477303350200223000ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (* Inspired of https://github.com/johnelse/ocaml-irc-client/blob/main/examples/example2_unix.ml *) module C = Irc_client_unix module M = Irc_message let log = Log.make ["irc"] module List = struct include List (** Take the n first elements. *) let rec heads n = function | _ when n <= 0 -> [] | [] -> [] | x :: l -> x :: heads (n - 1) l end let irc = Lang.add_module "irc" let _ = Lang.add_builtin ~base:irc "channel" ~category:`String ~descr:"Contents of an IRC channel." ~examples: [ {| # Display messages in the #liquidsoap-test room over a video s = single("test.mp4") s = video.add_text.native(irc.channel(channel="#liquidsoap-test"), s) |}; ] [ ( "server", Lang.string_t, Some (Lang.string "irc.libera.chat"), Some "IRC server." ); ("port", Lang.int_t, Some (Lang.int 6667), Some "Port for IRC server."); ( "channel", Lang.string_t, Some (Lang.string "#liquidsoap"), Some "IRC chan to join." ); ("nick", Lang.string_t, Some (Lang.string "liquidbot"), Some "Nickname."); ("limit", Lang.int_t, Some (Lang.int 10), Some "Limit to n last messages"); ] (Lang.fun_t [] Lang.string_t) (fun p -> let server = List.assoc "server" p |> Lang.to_string in let port = List.assoc "port" p |> Lang.to_int in let nick = List.assoc "nick" p |> Lang.to_string in let channel = List.assoc "channel" p |> Lang.to_string in let limit = List.assoc "limit" p |> Lang.to_int in let s = ref [] in let connect () = log#info "Connecting to %s:%d..." server port; C.connect_by_name ~server ~port ~nick () in let connected connection = log#info "Connected"; C.send_join ~connection ~channel; C.send_privmsg ~connection ~target:channel ~message:"Hi everybody!" in let callback _ result = match result with | Result.Ok { M.command = M.PRIVMSG (target, data); prefix } when target = channel -> let from = match prefix with | None -> "?" | Some prefix -> ( match String.index_opt prefix '!' with | Some n -> String.sub prefix 0 n | None -> "?") in let msg = Printf.sprintf "<%s> %s" from data in log#debug "Message from %s: %s" (Option.value ~default:"?" prefix) msg; s := List.heads limit (msg :: !s) | _ -> () in let _ = (* TODO: I feel that this is not entirely clean: we should wait for startup and clean the thread in the end... *) Tutils.create (fun () -> C.reconnect_loop ~reconnect:true ~after:30 ~connect ~f:connected ~callback ()) () "irc.channel" in let s () = String.concat "\n" (List.rev !s) in Lang.val_fun [] (fun _ -> Lang.string (s ()))) liquidsoap-2.3.2/src/core/builtins/builtins_jemalloc.ml000066400000000000000000000112141477303350200232740ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let jemalloc = Lang.add_module ~base:Modules.runtime "jemalloc" let mallctl = Lang.add_module ~base:jemalloc "mallctl" let _ = Lang.add_builtin ~base:jemalloc "epoch" ~category:`System ~descr:"Refresh the epoch counter for statistics" [] Lang.unit_t (fun _ -> Jemalloc.epoch (); Lang.unit) let () = let register (name, typ, cmd, from_val, to_val) = let get_t = Lang.method_t (Lang.fun_t [] typ) [ ( "set", ([], Lang.fun_t [(false, "", typ)] Lang.unit_t), "Set option value" ); ] in ignore (Lang.add_builtin ~base:mallctl name ~category:`System ~descr: ("Get an option of type " ^ name ^ " using mallctl. Returned value has the same type as a reference.") [("", Lang.string_t, None, Some "Option name")] get_t (fun p -> let name = Lang.to_string (List.assoc "" p) in let cmd value = try to_val (cmd name value) with Jemalloc.Invalid_property name -> Runtime_error.raise ~pos:(Lang.pos p) ~message: (if value = None then "Invalid property: " ^ name else "Failed to set value on property " ^ name) "invalid" in let get = Lang.val_fun [] (fun _ -> cmd None) in Lang.meth get [ ( "set", Lang.val_fun [("", "", None)] (fun p -> let value = from_val (List.assoc "" p) in ignore (cmd (Some value)); Lang.unit) ); ])) in register ("bool", Lang.bool_t, Jemalloc.mallctl_bool, Lang.to_bool, Lang.bool); register ("int", Lang.int_t, Jemalloc.mallctl_int, Lang.to_int, Lang.int); register ( "string", Lang.string_t, Jemalloc.mallctl_string, Lang.to_string, Lang.string ); ignore (Lang.add_builtin ~base:mallctl "flag" ~category:`System ~descr:"Set an option flag" [("", Lang.string_t, None, Some "Flag name")] Lang.unit_t (fun p -> let name = Lang.to_string (List.assoc "" p) in (try Jemalloc.mallctl_unit name with Jemalloc.Invalid_property name -> Runtime_error.raise ~pos:(Lang.pos p) ~message:("Invalid property: " ^ name) "invalid"); Lang.unit)) let _ = let stat_t = Lang.record_t [ ("active", Lang.int_t); ("resident", Lang.int_t); ("allocated", Lang.int_t); ("mapped", Lang.int_t); ] in let stat () = let { Jemalloc.active; resident; allocated; mapped } = Jemalloc.get_memory_stats () in Lang.record [ ("active", Lang.int active); ("resident", Lang.int resident); ("allocated", Lang.int allocated); ("mapped", Lang.int mapped); ] in Lang.add_builtin ~base:jemalloc "memory_stats" ~category:`System ~descr:"Return memory allocation stats from `jemalloc`." [] stat_t (fun _ -> stat ()) let _ = Lang.add_builtin ~base:jemalloc "version" ~category:`System ~descr:"Jemalloc version information" [] (Lang.method_t Lang.string_t [ ("major", ([], Lang.int_t), "Major version"); ("minor", ([], Lang.int_t), "Minor version"); ("git_version", ([], Lang.string_t), "Git version"); ]) (fun _ -> let version, major, minor, git = Jemalloc.version () in Lang.meth (Lang.string version) [ ("major", Lang.int major); ("minor", Lang.int minor); ("git", Lang.string git); ]) liquidsoap-2.3.2/src/core/builtins/builtins_lo.ml000066400000000000000000000151201477303350200221200ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Extralib module S = Lo.Server let osc = Modules.osc let conf_osc = Dtools.Conf.void ~p:(Configure.conf#plug "osc") "Interactions through the OSC protocol." let conf_port = Dtools.Conf.int ~p:(conf_osc#plug "port") ~d:7777 "Port for OSC server." (* (path,type),handler *) let handlers = ref [] let add_handler path t f = handlers := ((path, t), f) :: !handlers let handler path (data : Lo.Message.data array) = let typ = function | `Float _ | `Double _ -> `Float | `Int32 _ | `Int64 _ -> `Int | `True | `False -> `Bool | `String _ | `Symbol _ -> `String | _ -> failwith "Unhandled value." in let value = function | `Float x | `Double x -> Lang.float x | `Int32 x | `Int64 x -> Lang.int x | (`True | `False) as b -> Lang.bool (b = `True) | `String s | `Symbol s -> Lang.string s | _ -> failwith "Unhandled value." in try let t = Array.map typ data in let v = Array.map value data in let h = List.assoc_all (path, t) !handlers in List.iter (fun f -> f v) h with _ -> () (* We have to start the server _after_ daemonizing. See: savonet/liquidsoap#1365. There are two cases: - Server is requested before we daemonize (i.e. in a top-level call), start it after daemonization. - Server is requested after we daemonize (i.e. in a callback), start it immediately. *) let server = ref None let should_start = ref false let started = ref false let started_m = Mutex.create () let log = Log.make ["lo"] let start_server () = log#info "Starting OSC server"; let port = conf_port#get in let s = S.create port handler in server := Some s; ignore (Thread.create (fun () -> try while true do S.recv s done with | Lo.Server.Stopped -> () | exn -> let backtrace = Printexc.get_backtrace () in log#important "OSC server thread exited with exception: %s\n%s" (Printexc.to_string exn) backtrace) ()) let () = Lifecycle.on_start ~name:"lo initialization" (Mutex_utils.mutexify started_m (fun () -> if !should_start && !server = None then start_server () else started := true)) let () = Lifecycle.on_core_shutdown ~name:"lo shutdown" (Mutex_utils.mutexify started_m (fun () -> match !server with | Some s -> log#info "Stopping OSC server"; S.stop s; server := None | None -> ())) let start_server = Mutex_utils.mutexify started_m (fun () -> if !started && !server = None then start_server () else should_start := true) let register name osc_t liq_t = let val_array vv = match Array.length vv with | 1 -> vv.(0) | 2 -> Lang.product vv.(0) vv.(1) | _ -> assert false in ignore (Lang.add_builtin ~base:osc name ~category:`Interaction [ ("", Lang.string_t, None, Some "OSC path."); ("", liq_t, None, Some "Initial value."); ] (Lang.fun_t [] liq_t) ~descr:"Read from an OSC path." (fun p -> let path = Lang.to_string (Lang.assoc "" 1 p) in let v = Lang.assoc "" 2 p in let v = ref v in let handle vv = v := val_array vv in add_handler path osc_t handle; start_server (); Lang.val_fun [] (fun _ -> !v))); ignore (Lang.add_builtin ~base:osc ("on_" ^ name) ~category:`Interaction [ ("", Lang.string_t, None, Some "OSC path."); ( "", Lang.fun_t [(false, "", liq_t)] Lang.unit_t, None, Some "Callback function." ); ] Lang.unit_t ~descr:"Register a callback on OSC messages." (fun p -> let path = Lang.to_string (Lang.assoc "" 1 p) in let f = Lang.assoc "" 2 p in let handle v = let v = val_array v in ignore (Lang.apply f [("", v)]) in add_handler path osc_t handle; start_server (); Lang.unit)); ignore (Lang.add_builtin ~base:osc ("send_" ^ name) ~category:`Interaction [ ("host", Lang.string_t, None, Some "OSC client address."); ("port", Lang.int_t, None, Some "OSC client port."); ("", Lang.string_t, None, Some "OSC path."); ("", liq_t, None, Some "Value to send."); ] Lang.unit_t ~descr:"Send a value to an OSC client." (fun p -> let host = Lang.to_string (List.assoc "host" p) in let port = Lang.to_int (List.assoc "port" p) in let path = Lang.to_string (Lang.assoc "" 1 p) in let v = Lang.assoc "" 2 p in let address = Lo.Address.create host port in let osc_val v = match v with | Value.Bool { value = b } -> if b then [`True] else [`False] | Value.String { value = s } -> [`String s] | Value.Float { value = x } -> [`Float x] | _ -> failwith "Unhandled value." in (* There was a bug in early versions of lo bindings and anyway we don't really want errors to show up here... *) (try Lo.send address path (osc_val v) with _ -> ()); Lang.unit)) let () = register "float" [| `Float |] Lang.float_t; register "float_pair" [| `Float; `Float |] (Lang.product_t Lang.float_t Lang.float_t); register "int" [| `Int |] Lang.int_t; register "int_pair" [| `Int; `Int |] (Lang.product_t Lang.int_t Lang.int_t); register "bool" [| `Bool |] Lang.bool_t; register "string" [| `String |] Lang.string_t; register "string_pair" [| `String; `String |] (Lang.product_t Lang.string_t Lang.string_t) liquidsoap-2.3.2/src/core/builtins/builtins_lo.mli000066400000000000000000000000001477303350200222600ustar00rootroot00000000000000liquidsoap-2.3.2/src/core/builtins/builtins_mem_usage.ml000066400000000000000000000072711477303350200234600ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let _ = let mem_usage_t = Lang.record_t [ ("total_virtual_memory", Lang.int_t); ("total_physical_memory", Lang.int_t); ("total_used_virtual_memory", Lang.int_t); ("total_used_physical_memory", Lang.int_t); ("process_virtual_memory", Lang.int_t); ("process_physical_memory", Lang.int_t); ("process_private_memory", Lang.int_t); ("process_swapped_memory", Lang.int_t); ] in let mem_usage { Mem_usage.total_virtual_memory; total_physical_memory; total_used_virtual_memory; total_used_physical_memory; process_virtual_memory; process_physical_memory; process_private_memory; process_swapped_memory; } = Lang.record [ ("total_virtual_memory", Lang.int total_virtual_memory); ("total_physical_memory", Lang.int total_physical_memory); ("total_used_virtual_memory", Lang.int total_used_virtual_memory); ("total_used_physical_memory", Lang.int total_used_physical_memory); ("process_virtual_memory", Lang.int process_virtual_memory); ("process_physical_memory", Lang.int process_physical_memory); ("process_private_memory", Lang.int process_private_memory); ("process_swapped_memory", Lang.int process_swapped_memory); ] in let runtime_mem_usage = Lang.add_builtin ~base:Modules.runtime "memory" ~category:`System ~descr:"Returns information about the system and process' memory." [] mem_usage_t (fun _ -> mem_usage (Mem_usage.info ())) in Lang.add_builtin ~base:runtime_mem_usage "prettify_bytes" ~category:`String ~descr:"Returns a human-redable description of an amount of bytes." [ ( "float_printer", Lang.nullable_t (Lang.fun_t [(false, "", Lang.float_t)] Lang.string_t), Some Lang.null, None ); ("signed", Lang.nullable_t Lang.bool_t, Some Lang.null, None); ("bits", Lang.nullable_t Lang.bool_t, Some Lang.null, None); ("binary", Lang.nullable_t Lang.bool_t, Some Lang.null, None); ("", Lang.int_t, None, None); ] Lang.string_t (fun p -> let float_printer = Lang.to_valued_option (fun v f -> Lang.to_string (Lang.apply v [("", Lang.float f)])) (List.assoc "float_printer" p) in let signed = Lang.to_valued_option Lang.to_bool (List.assoc "signed" p) in let bits = Lang.to_valued_option Lang.to_bool (List.assoc "bits" p) in let binary = Lang.to_valued_option Lang.to_bool (List.assoc "binary" p) in let bytes = Lang.to_int (List.assoc "" p) in Lang.string (Mem_usage.prettify_bytes ?float_printer ?signed ?bits ?binary bytes)) liquidsoap-2.3.2/src/core/builtins/builtins_metadata.ml000066400000000000000000000041671477303350200232770ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let id3v2 = Lang.add_module ~base:Modules.metadata "id3v2" let _ = Lang.add_builtin ~base:id3v2 "render" ~category:`String ~descr:"Return a string representation of a id3v2 metadata tag" [ ("", Lang.metadata_t, None, None); ( "version", Lang.int_t, Some (Lang.int 3), Some "Tag version. One of: 3 or 4" ); ] Lang.string_t (fun p -> let m = Frame.Metadata.to_list (Lang.to_metadata (List.assoc "" p)) in let version = Lang.to_int (List.assoc "version" p) in Lang.string (Utils.id3v2_of_metadata ~version m)) let parse = Lang.add_module ~base:Modules.metadata "parse" let _ = Lang.add_builtin ~base:parse "amplify" ~category:`String ~descr: "Parse an amplify metadata. Parsing is the same as in the `amplify` \ operator. Metadata can be of the form: \" dB\" for a decibel-based \ value or \"\" for a linear-based value. Returns a decibel value." [("", Lang.string_t, None, None)] Lang.float_t (fun p -> Lang.float (Mm.Audio.dB_of_lin (Amplify.parse_db (Lang.to_string (List.assoc "" p))))) liquidsoap-2.3.2/src/core/builtins/builtins_optionals.ml000066400000000000000000000040211477303350200235140ustar00rootroot00000000000000let liquidsoap_build_config_optionals = Lang.add_module ~base:Liquidsoap_lang.Builtins_lang.liquidsoap_build_config "optionals" let () = List.iter (fun (name, value) -> ignore (Lang.add_builtin_base ~category:`Configuration ~descr:("Build-time configuration for " ^ name) ~base:liquidsoap_build_config_optionals name (`Bool value) Lang.bool_t)) [ ("alsa", Alsa_option.enabled); ("ao", Ao_option.enabled); ("bjack", Bjack_option.enabled); ("camlimages", Camlimages_option.enabled); ("dssi", Dssi_option.enabled); ("faad", Faad_option.enabled); ("fdkaac", Fdkaac_option.enabled); ("ffmpeg", Ffmpeg_option.enabled); ("flac", Flac_option.enabled); ("frei0r", Frei0r_option.enabled); ("gd", Gd_option.enabled); ("graphics", Graphics_option.enabled); ("inotify", Inotify_option.enabled); ("imagelib", Imagelib_option.enabled); ("irc", Irc_option.enabled); ("ladspa", Ladspa_option.enabled); ("lame", Lame_option.enabled); ("lilv", Lilv_option.enabled); ("lo", Lo_option.enabled); ("mad", Mad_option.enabled); ("memtrace", Memtrace_option.enabled); ("ogg", Ogg_option.enabled); ("opus", Opus_option.enabled); ("osc", Osc_option.enabled); ("oss", Oss_option.enabled); ("portaudio", Portaudio_option.enabled); ("posix_time2", Posix_time_option.enabled); ("prometheus", Prometheus_option.enabled); ("pulseaudio", Pulseaudio_option.enabled); ("samplerate", Samplerate_option.enabled); ("sdl", Sdl_option.enabled); ("shine", Shine_option.enabled); ("soundtouch", Soundtouch_option.enabled); ("speex", Speex_option.enabled); ("srt", Srt_option.enabled); ("ssl", Ssl_option.enabled); ("tls", Tls_option.enabled); ("theora", Theora_option.enabled); ("vorbis", Vorbis_option.enabled); ("winsvc", Winsvc_option.enabled); ("xmlplaylist", Xmlplaylist_option.enabled); ] liquidsoap-2.3.2/src/core/builtins/builtins_osc.ml000066400000000000000000000157511477303350200223040ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Extralib let conf_osc = Dtools.Conf.void ~p:(Configure.conf#plug "oscnative") "Interactions through the OSC protocol." let conf_port = Dtools.Conf.int ~p:(conf_osc#plug "port") ~d:7777 "Port for OSC server." (* (path,type),handler *) let handlers = ref [] let add_handler path t f = handlers := ((path, t), f) :: !handlers let handler path (data : Osc.Types.argument array) = let typ : Osc.Types.argument -> [ `Float | `Int | `String ] = function | Float32 _ -> `Float | Int32 _ -> `Int | String _ -> `String | _ -> failwith "Unhandled value." in let value : Osc.Types.argument -> Lang.value = function | Float32 x -> Lang.float x | Int32 x -> Lang.int (Int32.to_int x) | String s -> Lang.string s | _ -> failwith "Unhandled value." in try let t = Array.map typ data in let v = Array.map value data in let h = List.assoc_all (path, t) !handlers in List.iter (fun f -> f v) h with _ -> () (* We have to start the server _after_ daemonizing. See: savonet/liquidsoap#1365. There are two cases: - Server is requested before we daemonize (i.e. in a top-level call), start it after daemonization. - Server is requested after we daemonize (i.e. in a callback), start it immediately. *) let server = ref None let should_start = ref false let started = ref false let started_m = Mutex.create () let log = Log.make ["osc.native"] let start_server () = log#info "Starting OSC server"; let buflen = 1024 in let port = conf_port#get in let addr = Unix.ADDR_INET (Unix.inet_addr_any, port) in let s = Osc_unix.Udp.Server.create addr buflen in server := Some s; ignore (Thread.create (fun () -> try while !server <> None do match Osc_unix.Udp.Server.recv s with | Ok (Osc.Types.Message m, _) -> handler m.address (Array.of_list m.arguments) | _ -> () done with exn -> let backtrace = Printexc.get_backtrace () in log#important "OSC server thread exited with exception: %s\n%s" (Printexc.to_string exn) backtrace) ()) let () = Lifecycle.on_start ~name:"osc initialization" (Mutex_utils.mutexify started_m (fun () -> if !should_start && !server = None then start_server () else started := true)) let () = Lifecycle.on_core_shutdown ~name:"osc shutdown" (Mutex_utils.mutexify started_m (fun () -> match !server with | Some s -> log#info "Stopping OSC server"; server := None; Osc_unix.Udp.Server.destroy s | None -> ())) let start_server = Mutex_utils.mutexify started_m (fun () -> if !started && !server = None then start_server () else should_start := true) let client = let c = ref None in fun () -> match !c with | Some c -> c | None -> let c' = Osc_unix.Udp.Client.create () in c := Some c'; c' let osc_native = Lang.add_module ~base:Modules.osc "native" let register name osc_t liq_t = let val_array vv = match Array.length vv with | 1 -> vv.(0) | 2 -> Lang.product vv.(0) vv.(1) | _ -> assert false in ignore (Lang.add_builtin ~base:osc_native name ~category:`Interaction [ ("", Lang.string_t, None, Some "OSC path."); ("", liq_t, None, Some "Initial value."); ] (Lang.fun_t [] liq_t) ~descr:"Read from an OSC path." (fun p -> let path = Lang.to_string (Lang.assoc "" 1 p) in let v = Lang.assoc "" 2 p in let v = ref v in let handle vv = v := val_array vv in add_handler path osc_t handle; start_server (); Lang.val_fun [] (fun _ -> !v))); ignore (Lang.add_builtin ~base:osc_native ("on_" ^ name) ~category:`Interaction [ ("", Lang.string_t, None, Some "OSC path."); ( "", Lang.fun_t [(false, "", liq_t)] Lang.unit_t, None, Some "Callback function." ); ] Lang.unit_t ~descr:"Register a callback on OSC messages." (fun p -> let path = Lang.to_string (Lang.assoc "" 1 p) in let f = Lang.assoc "" 2 p in let handle v = let v = val_array v in ignore (Lang.apply f [("", v)]) in add_handler path osc_t handle; start_server (); Lang.unit)); ignore (Lang.add_builtin ~base:osc_native ("send_" ^ name) ~category:`Interaction [ ("host", Lang.string_t, None, Some "OSC client address."); ("port", Lang.int_t, None, Some "OSC client port."); ("", Lang.string_t, None, Some "OSC path."); ("", liq_t, None, Some "Value to send."); ] Lang.unit_t ~descr:"Send a value to an OSC client." (fun p -> let host = Lang.to_string (List.assoc "host" p) in let port = Lang.to_int (List.assoc "port" p) in let path = Lang.to_string (Lang.assoc "" 1 p) in let v = Lang.assoc "" 2 p in let address = Unix.ADDR_INET ((Unix.gethostbyname host).h_addr_list.(0), port) in let osc_val v = match v with | Value.String { value = s } -> [Osc.Types.String s] | Value.Float { value = x } -> [Osc.Types.Float32 x] | _ -> failwith "Unhandled value." in let packet = Osc.Types.Message { address = path; arguments = osc_val v } in Osc_unix.Udp.Client.send (client ()) address packet; Lang.unit)) let () = register "float" [| `Float |] Lang.float_t; register "float_pair" [| `Float; `Float |] (Lang.product_t Lang.float_t Lang.float_t); register "int" [| `Int |] Lang.int_t; register "int_pair" [| `Int; `Int |] (Lang.product_t Lang.int_t Lang.int_t); (* register "bool" [| `Bool |] Lang.bool_t; *) register "string" [| `String |] Lang.string_t; register "string_pair" [| `String; `String |] (Lang.product_t Lang.string_t Lang.string_t) liquidsoap-2.3.2/src/core/builtins/builtins_process.ml000066400000000000000000000316361477303350200231760ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let log = Log.make ["process"] let process = Modules.process module Http = Liq_http let _ = if Sys.os_type <> "Win32" then ignore (let ret_t = Lang.record_t [("user", Lang.float_t); ("system", Lang.float_t)] in Lang.add_builtin ~base:process "time" ~category:`System [] ret_t ~descr:"Get the execution time of the current liquidsoap process." (fun _ -> let { Unix.tms_utime = user; tms_stime = system } = Unix.times () in Lang.record [("user", Lang.float user); ("system", Lang.float system)])) let () = List.iter (fun (name, fd) -> ignore (Lang.add_builtin_value ~base:process name ~category:`System ~descr:("The process' " ^ name) (Builtins_socket.Socket_value.to_value (Http.unix_socket fd)) Builtins_socket.Socket_value.t)) [("stdin", Unix.stdin); ("stdout", Unix.stdout); ("stderr", Unix.stderr)] let _ = let ret_t = Lang.method_t Lang.unit_t [ ( "stdout", ([], Lang.string_t), "Messages written by process on standard output stream." ); ( "stderr", ([], Lang.string_t), "Messages written by process on standard error stream." ); ( "status", ( [], Lang.method_t Lang.string_t [ ( "code", ([], Lang.int_t), "Returned status code (or signal, in case the process was \ killed or stopped by a signal)." ); ( "description", ([], Lang.string_t), "Returned description (in case an exception was raised)." ); ] ), "Status when process ended, can be one of `\"exit\"` (the program \ exited, the `status` code is then relevant), `\"killed\"` (the \ program was killed by signal given in `status` code), `\"stopped\"` \ (the program was stopped by signal given in `status` code) or \ `\"exception\"` (the program raised and exception detailed in the \ `description`)." ); ] in let env_t = Lang.product_t Lang.string_t Lang.string_t in let path_t = Lang.list_t Lang.string_t in Lang.add_builtin ~base:process "run" ~category:`System ~descr: "Run a process in a shell environment. Returns the standard output, as \ well as standard error and status as methods. The status can be \ \"exit\" (the status code is set), \"killed\" or \"stopped\" (the \ status code is the signal), or \"exception\" (the description is set) \ or \"timeout\" (the description is the run time)." [ ("env", Lang.list_t env_t, Some (Lang.list []), Some "Process environment"); ( "inherit_env", Lang.bool_t, Some (Lang.bool true), Some "Inherit calling process's environment when `env` parameter is empty." ); ( "stdin", Lang.string_t, Some (Lang.string ""), Some "Data to write to the process' standard input." ); ( "rwdirs", path_t, Some (Lang.list [Lang.string "default"]), Some "Read/write directories for sandboxing. `\"default\"` expands to \ sandbox default." ); ( "rodirs", path_t, Some (Lang.list [Lang.string "default"]), Some "Read-only directories for sandboxing `\"default\"` expands to \ sandbox default." ); ( "network", Lang.nullable_t Lang.bool_t, Some Lang.null, Some "Enable or disable network inside sandboxed environment (sandbox \ default if not specified)." ); ( "timeout", Lang.nullable_t Lang.float_t, Some Lang.null, Some "Cancel process after `timeout` has elapsed. Ignored if negative." ); ("", Lang.string_t, None, Some "Command to run"); ] ret_t (fun p -> let env = Lang.to_list (List.assoc "env" p) in let env = List.map (fun e -> let k, v = Lang.to_product e in (Lang.to_string k, Lang.to_string v)) env in let stdin = Lang.to_string (List.assoc "stdin" p) in let sandbox_rw = List.map Lang.to_string (Lang.to_list (List.assoc "rwdirs" p)) in let sandbox_rw = List.fold_left (fun cur el -> if el = "default" then cur @ Sandbox.conf_rw#get else el :: cur) [] sandbox_rw in let sandbox_ro = List.map Lang.to_string (Lang.to_list (List.assoc "rodirs" p)) in let sandbox_ro = List.fold_left (fun cur el -> if el = "default" then cur @ Sandbox.conf_ro#get else el :: cur) [] sandbox_ro in let sandbox_network = match Lang.to_option (List.assoc "network" p) with | None -> Sandbox.conf_network#get | Some v -> Lang.to_bool v in let inherit_env = Lang.to_bool (List.assoc "inherit_env" p) in let env = if env = [] && inherit_env then Lang.environment () else env in let timeout = match Option.map Lang.to_float (Lang.to_option (List.assoc "timeout" p)) with | Some f -> f | None -> -1. in let env = List.map (fun (k, v) -> Printf.sprintf "%s=%s" k v) env in let env = Array.of_list env in let cmd_value = Lang.to_string (List.assoc "" p) in let cmd = Sandbox.cmd ~rw:sandbox_rw ~ro:sandbox_ro ~network:sandbox_network cmd_value in let buflen = Utils.pagesize in let out_buf = Buffer.create buflen in let err_buf = Buffer.create buflen in let on_done (timed_out, status) = let stdout = Buffer.contents out_buf in let stderr = Buffer.contents err_buf in let status, code, description = match (timed_out, status) with | f, _ when 0. <= f -> ("timeout", -1, string_of_float f) | _, Some (`Exception e) -> ("exception", -1, Printexc.to_string e) | _, Some (`Status s) -> ( match s with | Unix.WEXITED c -> ("exit", c, "") | Unix.WSIGNALED s -> ("killed", s, "") | Unix.WSTOPPED s -> ("stopped", s, "")) | _ -> assert false in Lang.record [ ("stdout", Lang.string stdout); ("stderr", Lang.string stderr); ( "status", Lang.meth (Lang.string status) [ ("code", Lang.int code); ("description", Lang.string description); ] ); ] in let synchronous () = let ((in_chan, out_ch, err_chan) as p) = Unix.open_process_full cmd env in if stdin <> "" then output_string out_ch stdin; close_out out_ch; let pull buf ch = let tmp = Bytes.create Utils.pagesize in let rec aux () = let n = input ch tmp 0 Utils.pagesize in if n = 0 then () else ( Buffer.add_subbytes buf tmp 0 n; aux ()) in aux () in pull out_buf in_chan; pull err_buf err_chan; (-1., Some (`Status (Unix.close_process_full p))) in let asynchronous () = let out_pipe, in_pipe = Unix.pipe ~cloexec:true () in Fun.protect ~finally:(fun () -> ignore (Unix.close in_pipe); ignore (Unix.close out_pipe)) (fun () -> let pull buf fn = let bytes = Bytes.create buflen in let ret = fn bytes 0 buflen in Buffer.add_subbytes buf bytes 0 ret; `Continue in let on_stdout = pull out_buf in let on_stderr = pull err_buf in let status = ref None in let on_stop s = status := Some s; begin try ignore (Unix.write in_pipe (Bytes.of_string " ") 0 1) with _ -> () end; false in let on_start _ = `Stop in let p = let log s = log#info "%s" s in Process_handler.run ~env ~on_start ~on_stop ~on_stdout ~on_stderr ~log cmd in let timed_out = try Tutils.wait_for (`Read out_pipe) timeout; -1. with Tutils.Timeout f -> (try Process_handler.kill p with exn -> log#important "Error while killing process: %s" (Printexc.to_string exn)); f in (timed_out, !status)) in let sync_run = not (Tutils.running ()) in if sync_run && 0. < timeout then log#important "Command %s cannot be executed with timeout %.02f because the \ internal scheduler is not running. Most likely, this call is made \ at the beginning of your script. We suggest that you wrap this call \ in an asynchronous task using `thread.run`. If you really need this \ value immediately as your script is starting, you should implement \ the timeout within the process call itself." cmd_value timeout; on_done (if sync_run then synchronous () else asynchronous ())) let process_quote = Lang.add_builtin ~base:process "quote" ~category:`System ~descr: "Return a quoted copy of the given string, suitable for use as one \ argument in a command line, escaping all meta-characters. Warning: \ under Windows, the output is only suitable for use with programs that \ follow the standard Windows quoting conventions." [("", Lang.string_t, None, Some "String to escape")] Lang.string_t (fun p -> Lang.string (Filename.quote (Lang.to_string (List.assoc "" p)))) let _ = Lang.add_builtin ~base:process_quote "command" ~category:`System ~descr: "Return a quoted command line, suitable for use as an argument to \ `process.run`.\n\n\ The optional arguments `stdin`, `stdout` and `stderr` are file names \ used to redirect the standard input, the standard output, or the \ standard error of the command.\n\n\ If `stdin=f` is given, a redirection `< f` is performed and the \ standard input of the command reads from file `f`.\n\n\ If `stdout=f` is given, a redirection `> f` is performed and the \ standard output of the command is written to file `f`.\n\n\ If `stderr=f` is given, a redirection `2> f` is performed and the \ standard error of the command is written to file `f`.\n\n\ If both `stdout=f` and `stderr=f` are given, with the exact same file \ name `f`, a `2>&1` redirection is performed so that the standard output \ and the standard error of the command are interleaved and redirected to \ the same file `f`." [ ( "stdin", Lang.nullable_t Lang.string_t, Some Lang.null, Some "command standard input" ); ( "stdout", Lang.nullable_t Lang.string_t, Some Lang.null, Some "command standard output" ); ( "stderr", Lang.nullable_t Lang.string_t, Some Lang.null, Some "command standard error" ); ( "args", Lang.list_t Lang.string_t, Some (Lang.list []), Some "command arguments" ); ("", Lang.string_t, None, Some "Command to execute"); ] Lang.string_t (fun p -> let stdin = Lang.to_valued_option Lang.to_string (List.assoc "stdin" p) in let stdout = Lang.to_valued_option Lang.to_string (List.assoc "stdout" p) in let stderr = Lang.to_valued_option Lang.to_string (List.assoc "stderr" p) in let args = List.map Lang.to_string (Lang.to_list (List.assoc "args" p)) in let cmd = Lang.to_string (List.assoc "" p) in Lang.string (Filename.quote_command cmd ?stdin ?stdout ?stderr args)) liquidsoap-2.3.2/src/core/builtins/builtins_prometheus.ml000066400000000000000000000206601477303350200237060ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Prometheus let prometheus = Lang.add_module "prometheus" let log = Log.make ["prometheus"] let metric_proto = [ ("help", Lang.string_t, None, Some "Help of the metric"); ( "namespace", Lang.string_t, Some (Lang.string ""), Some "namespace of the metric" ); ( "subsystem", Lang.string_t, Some (Lang.string ""), Some "subsystem of the metric" ); ("labels", Lang.list_t Lang.string_t, None, Some "labels for the metric"); ("", Lang.string_t, None, Some "Name of the metric"); ] let set_t = Lang.fun_t [(false, "", Lang.float_t)] Lang.unit_t let register_t = Lang.fun_t [(false, "label_values", Lang.list_t Lang.string_t)] set_t let add_metric metric_name create register set = ignore (Lang.add_builtin ~base:prometheus metric_name ~category:`Interaction ~descr:("Register a prometheus " ^ metric_name) metric_proto register_t (fun p -> let help = Lang.to_string (List.assoc "help" p) in let label_names = List.map Lang.to_string (Lang.to_list (List.assoc "labels" p)) in let opt_v n = match Lang.to_string (List.assoc n p) with | s when s = "" -> None | v -> Some v in let namespace = opt_v "namespace" in let subsystem = opt_v "subsystem" in let name = Lang.to_string (List.assoc "" p) in let m = create ~label_names ?registry:None ~help ?namespace ?subsystem name in Lang.val_fun [("label_values", "label_values", None)] (fun p -> let labels_v = List.assoc "label_values" p in let labels = List.map Lang.to_string (Lang.to_list labels_v) in if List.length labels <> List.length label_names then raise (Error.Invalid_value (labels_v, "Not enough labels provided!")); let m = register m labels in Lang.val_fun [("", "", None)] (fun p -> let v = Lang.to_float (List.assoc "" p) in set m v; Lang.unit)))) let () = add_metric "counter" Counter.v_labels Counter.labels Counter.inc; add_metric "gauge" Gauge.v_labels Gauge.labels Gauge.set; add_metric "summary" Summary.v_labels Summary.labels Summary.observe let latencies = Hashtbl.create 10 let get_latencies ~prefix ~label_names mode = let key = String.concat "" (mode :: label_names) in match Hashtbl.find_opt latencies key with | Some l -> l | None -> let latency = Gauge.v_labels ~label_names ~help:(Printf.sprintf "Mean %s latency over the chosen window" mode) (Printf.sprintf "%s%s_latency_seconds" prefix mode) in let peak_latency = Prometheus.Gauge.v_labels ~label_names ~help:(Printf.sprintf "Peak %s latency over the chosen window" mode) (Printf.sprintf "%s%s_peak_latency_seconds" prefix mode) in let max_latency = Prometheus.Gauge.v_labels ~label_names ~help:(Printf.sprintf "Max %s latency since start" mode) (Printf.sprintf "%s%s_max_latency_seconds" prefix mode) in Hashtbl.replace latencies key (latency, peak_latency, max_latency); (latency, peak_latency, max_latency) let last_data = ref None let get_last_data ~label_names = match !last_data with | Some m -> m | None -> let m = Gauge.v_labels ~label_names ~help:"Last time source produced some data." "liquidsoap_time_of_last_data_timestamp" in last_data := Some m; m let source_monitor ~prefix ~label_names ~labels ~window s = let mean l = let n = Hashtbl.length l in if n = 0 then 0. else ( let s = Hashtbl.fold (fun _ v cur -> cur +. v) l 0. in s /. float_of_int n) in let track_latency mode = let latency, peak_latency, max_latency = get_latencies ~prefix ~label_names mode in let latency = Prometheus.Gauge.labels latency labels in let peak_latency = Prometheus.Gauge.labels peak_latency labels in let max_latency = Prometheus.Gauge.labels max_latency labels in let latencies = Hashtbl.create 100 in let max = ref (-1.) in let add_latency l = let t = Unix.gettimeofday () in Hashtbl.replace latencies t l; Hashtbl.filter_map_inplace (fun old_t v -> if t -. window <= old_t then Some v else None) latencies; let peak = Hashtbl.fold (fun _ v cur -> if cur < v then v else cur) latencies 0. in if !max < peak then max := peak; Prometheus.Gauge.set latency (mean latencies); Prometheus.Gauge.set peak_latency peak; Prometheus.Gauge.set max_latency !max in add_latency in let frame_duration = Lazy.force Frame.duration in let add_input_latency = track_latency "input" in let add_output_latency = track_latency "output" in let add_overall_latency = track_latency "overall" in let last_start_time = ref 0. in let last_end_time = ref 0. in let last_data = Gauge.labels (get_last_data ~label_names) labels in let wake_up ~fallible:_ ~source_type:_ ~id:_ ~ctype:_ ~clock_id:_ = () in let sleep () = () in let generate_frame ~start_time ~end_time ~length ~has_track_mark:_ ~metadata:_ = last_start_time := start_time; last_end_time := end_time; Prometheus.Gauge.set last_data end_time; let encoded_time = Frame.seconds_of_main length in let latency = (end_time -. start_time) /. encoded_time in add_input_latency latency in let after_streaming_cycle () = let current_time = Unix.gettimeofday () in add_output_latency ((current_time -. !last_end_time) /. frame_duration); add_overall_latency ((current_time -. !last_start_time) /. frame_duration) in let watcher = { Source.wake_up; sleep; generate_frame; before_streaming_cycle = (fun _ -> ()); after_streaming_cycle; } in s#add_watcher watcher let _ = let source_monitor_register_t = Lang.fun_t [ (false, "label_values", Lang.list_t Lang.string_t); (false, "", Lang.source_t (Lang.univ_t ())); ] Lang.unit_t in Lang.add_builtin ~base:prometheus "latency" [ ( "window", Lang.float_t, Some (Lang.float 5.), Some "Window over which mean and peak metrics are reported." ); ( "prefix", Lang.string_t, Some (Lang.string "liquidsoap_"), Some "Prefix for the metric's name" ); ("labels", Lang.list_t Lang.string_t, None, Some "labels for the metric"); ] source_monitor_register_t ~category:`Liquidsoap ~descr:"Monitor a source's internal latencies on Prometheus" (fun p -> let window = Lang.to_float (List.assoc "window" p) in let prefix = Lang.to_string (List.assoc "prefix" p) in let label_names = List.map Lang.to_string (Lang.to_list (List.assoc "labels" p)) in Lang.val_fun [("label_values", "label_values", None); ("", "", None)] (fun p -> let s = Lang.to_source (List.assoc "" p) in let labels_v = List.assoc "label_values" p in let labels = List.map Lang.to_string (Lang.to_list labels_v) in if List.length labels <> List.length label_names then raise (Error.Invalid_value (labels_v, "Not enough labels provided!")); source_monitor ~label_names ~labels ~window ~prefix s; Lang.unit)) liquidsoap-2.3.2/src/core/builtins/builtins_request.ml000066400000000000000000000442561477303350200232120ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let request = Modules.request let should_stop = Atomic.make false let () = Lifecycle.before_core_shutdown ~name:"builtin source shutdown" (fun () -> Atomic.set should_stop true) let _ = Lang.add_builtin ~base:request "all" ~category:`Liquidsoap ~descr:"Return all the requests currently available." [] (Lang.list_t Request.Value.t) (fun _ -> Lang.list (List.map Request.Value.to_value (Request.all ()))) let _ = Lang.add_builtin ~base:request "is_static" ~category:`Liquidsoap ~descr:"`true` if the given URI is assumed to be static, e.g. a file." [("", Lang.string_t, None, None)] Lang.bool_t (fun p -> Lang.bool (Request.is_static (Lang.to_string (List.assoc "" p)))) let _ = Lang.add_builtin ~base:request "create" ~category:`Liquidsoap ~descr:"Create a request from an URI." [ ( "cue_in_metadata", Lang.nullable_t Lang.string_t, Some (Lang.string "liq_cue_in"), Some "Metadata for cue in points. Disabled if `null`." ); ( "cue_out_metadata", Lang.nullable_t Lang.string_t, Some (Lang.string "liq_cue_out"), Some "Metadata for cue out points. Disabled if `null`." ); ( "persistent", Lang.bool_t, Some (Lang.bool false), Some "Indicate that the request is persistent, i.e. that it may be used \ again once it has been played." ); ( "resolve_metadata", Lang.bool_t, Some (Lang.bool true), Some "Set to `false` to prevent metadata resolution on this request." ); ( "excluded_metadata_resolvers", Lang.list_t Lang.string_t, Some (Lang.list []), Some "List of metadata resolves to exclude when resolving metadata." ); ( "temporary", Lang.bool_t, Some (Lang.bool false), Some "Indicate that the request is a temporary file: it will be destroyed \ after being played." ); ("", Lang.string_t, None, None); ] Request.Value.t (fun p -> let persistent = Lang.to_bool (List.assoc "persistent" p) in let resolve_metadata = Lang.to_bool (List.assoc "resolve_metadata" p) in let excluded_metadata_resolvers = List.map Lang.to_string (Lang.to_list (List.assoc "excluded_metadata_resolvers" p)) in let cue_in_metadata = Lang.to_valued_option Lang.to_string (List.assoc "cue_in_metadata" p) in let cue_out_metadata = Lang.to_valued_option Lang.to_string (List.assoc "cue_out_metadata" p) in let initial = Lang.to_string (List.assoc "" p) in let l = String.length initial in let initial = (* Remove trailing newline *) if l > 0 && initial.[l - 1] = '\n' then String.sub initial 0 (l - 1) else initial in let temporary = List.assoc "temporary" p |> Lang.to_bool in Request.Value.to_value (Request.create ~resolve_metadata ~persistent ~excluded_metadata_resolvers ~cue_in_metadata ~cue_out_metadata ~temporary initial)) let _ = Lang.add_builtin ~base:request "resolve" ~category:`Liquidsoap [ ( "timeout", Lang.nullable_t Lang.float_t, Some Lang.null, Some "Limit in seconds to the duration of the request resolution. \ Defaults to `settings.request.timeout` when `null`." ); ( "content_type", Lang.nullable_t (Lang.source_t (Lang.univ_t ())), Some Lang.null, Some "Check that the request can decode content suitable for the given \ source." ); ("", Request.Value.t, None, None); ] Lang.bool_t ~descr: "Resolve a request, i.e. attempt to get a valid local file. The \ operation can take some time. Return true if the resolving was \ successful, false otherwise (timeout or invalid URI)." (fun p -> let timeout = Lang.to_valued_option Lang.to_float (List.assoc "timeout" p) in let source = Lang.to_valued_option Lang.to_source (List.assoc "content_type" p) in let r = Request.Value.of_value (List.assoc "" p) in Lang.bool (match (Request.resolve ?timeout r, source) with | `Resolved, Some s -> ( try Request.get_decoder ~ctype:s#content_type r <> None with _ -> false) | `Resolved, None -> true | _ | (exception _) -> false)) let _ = Lang.add_builtin ~base:request "metadata" ~category:`Liquidsoap [("", Request.Value.t, None, None)] Lang.metadata_t ~descr:"Get the metadata associated to a request." (fun p -> let r = Request.Value.of_value (List.assoc "" p) in Lang.metadata (Request.metadata r)) let _ = Lang.add_builtin ~base:request "log" ~category:`Liquidsoap [("", Request.Value.t, None, None)] Lang.string_t ~descr:"Get log data associated to a request." (fun p -> let r = Request.Value.of_value (List.assoc "" p) in Lang.string (Request.log r)) let _ = Lang.add_builtin ~base:request "resolved" ~category:`Liquidsoap ~descr: "Check if a request is resolved, i.e. is associated to a valid local \ file." [("", Request.Value.t, None, None)] Lang.bool_t (fun p -> let e = Request.Value.of_value (List.assoc "" p) in Lang.bool (Request.resolved e)) let _ = Lang.add_builtin ~base:request "uri" ~category:`Liquidsoap ~descr:"Initial URI of a request." [("", Request.Value.t, None, None)] Lang.string_t (fun p -> let r = Request.Value.of_value (List.assoc "" p) in Lang.string (Request.initial_uri r)) let _ = Lang.add_builtin ~base:request "filename" ~category:`Liquidsoap ~descr: "Return a valid local filename if the request is ready, and the empty \ string otherwise." [("", Request.Value.t, None, None)] Lang.string_t (fun p -> let r = Request.Value.of_value (List.assoc "" p) in Lang.string (match Request.get_filename r with Some f -> f | None -> "")) let _ = Lang.add_builtin ~base:request "destroy" ~category:`Liquidsoap ~descr: "Destroying a request causes any temporary associated file to be \ deleted, and releases its RID. Persistent requests resist to \ destroying, unless forced." [ ( "force", Lang.bool_t, Some (Lang.bool false), Some "Destroy the request even if it is persistent." ); ("", Request.Value.t, None, None); ] Lang.unit_t (fun p -> let force = Lang.to_bool (List.assoc "force" p) in let e = Request.Value.of_value (List.assoc "" p) in Request.destroy ~force e; Lang.unit) let _ = let add_duration_resolver ~base ~name ~resolver () = Lang.add_builtin ~base name ~category:`Liquidsoap ((if resolver = None then [ ( "resolvers", Lang.nullable_t (Lang.list_t Lang.string_t), Some Lang.null, Some "Set to a list of resolvers to only resolve duration using a \ specific decoder." ); ] else []) @ [ ( "resolve_metadata", Lang.bool_t, Some (Lang.bool true), Some "Set to `false` to prevent metadata resolution on this request." ); ( "metadata", Lang.metadata_t, Some (Lang.list []), Some "Optional metadata used to decode the file, e.g. \ `ffmpeg_options`." ); ( "timeout", Lang.nullable_t Lang.float_t, Some Lang.null, Some "Limit in seconds to the duration of request resolution. \ Defaults to `settings.request.timeout` when `null`." ); ("", Lang.string_t, None, None); ]) (Lang.nullable_t Lang.float_t) ~descr: (Printf.sprintf "Compute the duration in seconds of audio data contained in a \ request%s. The computation may be expensive. Returns `null` if \ computation failed, typically if the file was not recognized as \ valid audio." (match resolver with | Some r -> " using the " ^ r ^ " decoder" | None -> "")) (fun p -> let f = Lang.to_string (List.assoc "" p) in let resolve_metadata = Lang.to_bool (List.assoc "resolve_metadata" p) in let resolvers = match resolver with | None -> Option.map (List.map Lang.to_string) (Lang.to_valued_option Lang.to_list (List.assoc "resolvers" p)) | Some r -> Some [r] in let metadata = Lang.to_metadata (List.assoc "metadata" p) in let timeout = Lang.to_valued_option Lang.to_float (List.assoc "timeout" p) in let r = Request.create ~resolve_metadata ~metadata ~cue_in_metadata:None ~cue_out_metadata:None f in if Request.resolve ?timeout r = `Resolved then ( match Request.duration ?resolvers ~metadata:(Request.metadata r) (Option.get (Request.get_filename r)) with | Some f -> Lang.float f | None -> Lang.null | exception exn -> let bt = Printexc.get_raw_backtrace () in Lang.raise_as_runtime ~bt ~kind:"failure" exn) else Lang.null) in let base = add_duration_resolver ~base:request ~name:"duration" ~resolver:None () in List.iter (fun name -> ignore (add_duration_resolver ~base ~name ~resolver:(Some name) ())) Request.conf_dresolvers#get let _ = Lang.add_builtin ~base:request "id" ~category:`Liquidsoap ~descr:"Identifier of a request." [("", Request.Value.t, None, None)] Lang.int_t (fun p -> let r = Request.Value.of_value (List.assoc "" p) in Lang.int (Request.id r)) let _ = Lang.add_builtin ~base:request "status" ~category:`Liquidsoap ~descr: "Current status of a request. Can be idle, resolving, ready, playing or \ destroyed." [("", Request.Value.t, None, None)] Lang.string_t (fun p -> let r = Request.Value.of_value (List.assoc "" p) in let s = match Request.status r with | `Idle -> "idle" | `Resolving _ -> "resolving" | `Ready -> "ready" | `Destroyed -> "destroyed" | `Failed -> "failed" in Lang.string s) exception Process_failed class process ~name r = object (self) inherit Request_dynamic.dynamic ~name ~retry_delay:(fun _ -> 0.1) ~available:(fun _ -> true) ~prefetch:1 ~timeout:None ~synchronous:true (Lang.val_fun [] (fun _ -> Lang.null)) initializer self#on_wake_up (fun () -> match Request.get_decoder ~ctype:self#content_type r with | Some _ -> self#set_queue [r] | None | (exception _) -> raise Process_failed) end let process_request ~log ~name ~ratio ~timeout ~sleep_latency ~process r = let module Time = (val Clock.time_implementation () : Liq_time.T) in let open Time in let start_time = Time.time () in match Request.resolve ~timeout r with | `Failed | `Timeout -> () | `Resolved -> ( let timeout = Time.of_float timeout in let timeout_time = Time.(start_time |+| timeout) in try let s = new process ~name r in let s = (process (s :> Source.source) :> Source.source) in let clock = Clock.create ~id:name ~sync:`Passive ~on_error:(fun exn bt -> Utils.log_exception ~log ~bt:(Printexc.raw_backtrace_to_string bt) (Printf.sprintf "Error while processing source: %s" (Printexc.to_string exn)); raise Process_failed) () in Fun.protect ~finally:(fun () -> try Clock.stop clock with _ -> ()) (fun () -> let started = ref false in let stopped = ref false in let _ = new Output.dummy ~clock ~infallible:false ~register_telnet:false ~on_start:(fun () -> started := true) ~on_stop:(fun () -> stopped := true) ~autostart:true (Lang.source s) in Clock.start ~force:true clock; log#info "Start streaming loop (ratio: %.02fx)" ratio; let sleep_latency = Time.of_float sleep_latency in let target_time () = Time.( start_time |+| sleep_latency |+| of_float (Clock.time clock /. ratio)) in while (not (Atomic.get should_stop)) && not !stopped do if (not !started) && Time.(timeout_time |<=| Time.time ()) then ( log#important "Timeout while waiting for the source to be ready!"; raise Process_failed) else ( Clock.tick clock; let target_time = target_time () in if Time.(time () |<| (target_time |+| sleep_latency)) then sleep_until target_time) done; let processing_time = Time.(to_float (time () |-| start_time)) in let effective_ratio = Clock.time clock /. processing_time in log#info "Request processed. Total processing time: %.02fs, effective \ ratio: %.02fx" processing_time effective_ratio) with Process_failed | Clock.Has_stopped -> ()) let _ = let log = Log.make ["request"; "dump"] in let kind = Lang.univ_t () in Lang.add_builtin ~base:request "dump" ~category:(`Source `Liquidsoap) ~descr:"Immediately encode the whole contents of a request into a file." ~flags:[`Experimental] [ ("", Lang.format_t kind, None, Some "Encoding format."); ("", Lang.string_t, None, Some "Name of the file."); ("", Request.Value.t, None, Some "Request to encode."); ( "ratio", Lang.float_t, Some (Lang.float 50.), Some "Time ratio. A value of `50` means process data at `50x` real rate, \ when possible." ); ( "timeout", Lang.float_t, Some (Lang.float 1.), Some "Stop processing the source if it has not started after the given \ timeout." ); ( "sleep_latency", Lang.float_t, Some (Lang.float 0.1), Some "How much time ahead, in seconds, should we should be before pausing \ the processing." ); ] Lang.unit_t (fun p -> let proto = let p = Pipe_output.file_proto (Lang.univ_t ()) in List.filter_map (fun (l, _, v, _) -> Option.map (fun v -> (l, v)) v) p in let proto = ("fallible", Lang.bool true) :: proto in let format = Lang.assoc "" 1 p in let file = Lang.assoc "" 2 p in let r = Request.Value.of_value (Lang.assoc "" 3 p) in let process s = let p = ("id", Lang.string "request.drop") :: ("", format) :: ("", file) :: ("", Lang.source s) :: (p @ proto) in Pipe_output.new_file_output p in let ratio = Lang.to_float (List.assoc "ratio" p) in let timeout = Lang.to_float (List.assoc "timeout" p) in let sleep_latency = Lang.to_float (List.assoc "sleep_latency" p) in process_request ~log ~name:"request.dump" ~ratio ~timeout ~sleep_latency ~process r; log#info "Request dumped."; Lang.unit) let _ = let log = Log.make ["request"; "process"] in Lang.add_builtin ~base:request "process" ~category:(`Source `Liquidsoap) ~descr: "Given a request and an optional function to process this request, \ animate the source as fast as possible until the request is fully \ processed." [ ("", Request.Value.t, None, Some "Request to process"); ( "process", Lang.fun_t [(false, "", Lang.source_t (Lang.univ_t ()))] (Lang.source_t (Lang.univ_t ())), Some (Lang.val_fun [("", "", None)] (fun p -> List.assoc "" p)), Some "Callback to create the source to animate." ); ( "ratio", Lang.float_t, Some (Lang.float 50.), Some "Time ratio. A value of `50` means process data at `50x` real rate, \ when possible." ); ( "timeout", Lang.float_t, Some (Lang.float 1.), Some "Stop processing the source if it has not started after the given \ timeout." ); ( "sleep_latency", Lang.float_t, Some (Lang.float 0.1), Some "How much time ahead, in seconds, should we should be before pausing \ the processing." ); ] Lang.unit_t (fun p -> let r = Request.Value.of_value (List.assoc "" p) in let process = List.assoc "process" p in let process s = Lang.to_source (Lang.apply process [("", Lang.source s)]) in let ratio = Lang.to_float (List.assoc "ratio" p) in let timeout = Lang.to_float (List.assoc "timeout" p) in let sleep_latency = Lang.to_float (List.assoc "sleep_latency" p) in process_request ~log ~name:"request.process" ~ratio ~timeout ~sleep_latency ~process r; Lang.unit) liquidsoap-2.3.2/src/core/builtins/builtins_resolvers.ml000066400000000000000000000233441477303350200235410ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let decoder_metadata = Lang.add_module ~base:Modules.decoder "metadata" let reentrant_decoders = ref [] let _ = Lang.add_builtin ~base:decoder_metadata "reentrant" ~category:`Liquidsoap ~descr:"Return the list of reentrant decoders." [] (Lang.list_t Lang.string_t) (fun _ -> Lang.list (List.map Lang.string !reentrant_decoders)) let _ = let resolver_t = Lang.fun_t [(false, "metadata", Lang.metadata_t); (false, "", Lang.string_t)] Lang.metadata_t in Lang.add_builtin ~base:decoder_metadata "add" ~category:`Liquidsoap ~descr:"Register an external file metadata decoder." [ ( "priority", Lang.getter_t Lang.int_t, Some (Lang.int 1), Some "Resolver's priority." ); ( "mime_types", Lang.nullable_t (Lang.list_t Lang.string_t), Some Lang.null, Some "Decode files that match the mime types in this list. Accept any \ file if `null`." ); ( "file_extensions", Lang.nullable_t (Lang.list_t Lang.string_t), Some Lang.null, Some "Decode files that have the file extensions in this list. Accept any \ file if `null`." ); ( "reentrant", Lang.bool_t, Some (Lang.bool false), Some "Set to `true` to indicate that the decoder needs to resolve a \ request. Such decoders need to be mutually exclusive to avoid \ request resolution loops!" ); ("", Lang.string_t, None, Some "Format/resolver's name."); ( "", resolver_t, None, Some "Process to start. The function takes the format and filename as \ argument and returns a list of (name,value) fields." ); ] Lang.unit_t (fun p -> let format = Lang.to_string (Lang.assoc "" 1 p) in let f = Lang.assoc "" 2 p in let mimes = Lang.to_valued_option (fun v -> List.map Lang.to_string (Lang.to_list v)) (List.assoc "mime_types" p) in let extensions = Lang.to_valued_option (fun v -> List.map Lang.to_string (Lang.to_list v)) (List.assoc "file_extensions" p) in let log = Log.make ["decoder"; "metadata"] in let reentrant = Lang.to_bool (List.assoc "reentrant" p) in let priority = Lang.to_int_getter (List.assoc "priority" p) in let resolver ~metadata ~extension ~mime fname = if not (Decoder.test_file ~log ~extension ~mime ~mimes ~extensions fname) then raise Metadata.Invalid; let ret = Lang.apply f [("metadata", Lang.metadata metadata); ("", Lang.string fname)] in let ret = Lang.to_list ret in let ret = List.map Lang.to_product ret in let ret = List.map (fun (x, y) -> (Lang.to_string x, Lang.to_string y)) ret in ret in Plug.register Request.mresolvers format ~doc:"" { Request.priority; resolver }; if reentrant then reentrant_decoders := format :: !reentrant_decoders; Lang.unit) let _ = Lang.add_builtin ~base:Builtins_sys.playlist_parse "get_file" ~category:`Liquidsoap ~descr:"Resolve a uri relative to a given pwd." [ ( "pwd", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Current directory to use for relative file path." ); ("", Lang.string_t, None, Some "URI"); ] Lang.string_t (fun p -> let pwd = Lang.to_valued_option Lang.to_string (List.assoc "pwd" p) in let uri = Lang.to_string (List.assoc "" p) in Lang.string (Playlist_parser.get_file ?pwd uri)) let add_playlist_parser ~format name (parser : Playlist_parser.parser) = let return_t = Lang.list_t (Lang.product_t Lang.metadata_t Lang.string_t) in Lang.add_builtin ~base:Builtins_sys.playlist_parse name ~category:`Liquidsoap ~descr:(Printf.sprintf "Parse %s playlists" format) [ ("", Lang.string_t, None, Some "Playlist file"); ( "pwd", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Current directory to use for relative file path." ); ] return_t (fun p -> let uri = Lang.to_string (List.assoc "" p) in let pwd = Lang.to_valued_option Lang.to_string (List.assoc "pwd" p) in let entries = parser ?pwd uri in Lang.list (List.map (fun (metadata, uri) -> Lang.product (Lang.metadata_list metadata) (Lang.string uri)) entries)) let _ = let playlist_t = Lang.list_t (Lang.product_t Lang.metadata_t Lang.string_t) in let parser_t = Lang.fun_t [(true, "pwd", Lang.string_t); (false, "", Lang.string_t)] playlist_t in Lang.add_builtin ~base:Builtins_sys.playlist_parse "register" ~category:`Liquidsoap ~descr: "Register a new playlist parser. An empty playlist is considered as a \ failure to resolve." [ ( "format", Lang.string_t, None, Some "Playlist format. If possible, a mime-type." ); ( "strict", Lang.bool_t, None, Some "True if playlist format can be detected unambiguously." ); ("", parser_t, None, Some "Playlist parser"); ] Lang.unit_t (fun p -> let format = Lang.to_string (List.assoc "format" p) in let strict = Lang.to_bool (List.assoc "strict" p) in let fn = List.assoc "" p in let fn ?pwd uri = let args = [("", Lang.string uri)] in let args = match pwd with | Some pwd -> ("pwd", Lang.string pwd) :: args | None -> args in let ret = Lang.to_list (Lang.apply fn args) in if ret = [] then raise Not_found; List.map (fun el -> let m, s = Lang.to_product el in (Lang.to_metadata_list m, Lang.to_string s)) ret in Plug.register Playlist_parser.parsers format ~doc:"" { Playlist_parser.strict; Playlist_parser.parser = fn }; Lang.unit) let default_static = Lang.eval ~cache:false ~typecheck:false ~stdlib:`Disabled "fun (_) -> false" let _ = let log_p = [("", "", None)] in let log_t = Lang.fun_t [(false, "", Lang.string_t)] Lang.unit_t in let protocol_t = Lang.fun_t [ (false, "rlog", log_t); (false, "maxtime", Lang.float_t); (false, "", Lang.string_t); ] Lang.(nullable_t string_t) in Lang.add_builtin ~base:Modules.protocol "add" ~category:`Liquidsoap ~descr:"Register a new protocol." [ ( "temporary", Lang.bool_t, Some (Lang.bool false), Some "if true, file is removed when it is finished." ); ( "static", Lang.fun_t [(false, "", Lang.string_t)] Lang.bool_t, Some default_static, Some "When given an uri for the protocol, if it returns `true`, then \ requests can be resolved once and for all. Typically, static \ protocols can be used to create infallible sources." ); ( "syntax", Lang.string_t, Some (Lang.string "Undocumented"), Some "URI syntax." ); ( "doc", Lang.string_t, Some (Lang.string "Undocumented"), Some "Protocol documentation." ); ( "", Lang.string_t, None, Some "Protocol name. Resolver will be called on uris of the form: \ `:...`." ); ( "", protocol_t, None, Some "Protocol resolver. Receives a function to log protocol resolution, \ the `` in `:` and the max delay that \ resolution should take." ); ] Lang.unit_t (fun p -> let name = Lang.to_string (Lang.assoc "" 1 p) in let f = Lang.assoc "" 2 p in let temporary = Lang.to_bool (List.assoc "temporary" p) in let static = List.assoc "static" p in let static s = Lang.to_bool (Lang.apply static [("", Lang.string s)]) in let doc = Lang.to_string (List.assoc "doc" p) in let syntax = Lang.to_string (List.assoc "syntax" p) in Lang.add_protocol ~syntax ~doc ~static name (fun arg ~log timeout -> let log = Lang.val_fun log_p (fun p -> let v = List.assoc "" p in log (Lang.to_string v); Lang.unit) in let ret = Lang.apply f [ ("rlog", log); ("maxtime", Lang.float timeout); ("", Lang.string arg); ] in Option.map (fun s -> Request.indicator ~temporary (Lang.to_string s)) (Lang.to_option ret)); Lang.unit) let _ = Lang.add_builtin ~base:Modules.protocol "count" ~category:`Liquidsoap ~descr:"Number of registered protocols." [] Lang.int_t (fun _ -> Doc.Protocol.count () |> Lang.int) liquidsoap-2.3.2/src/core/builtins/builtins_runtime.ml000066400000000000000000000202521477303350200231730ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let runtime = Modules.runtime let runtime_gc = Lang.add_module ~base:runtime "gc" let _ = Lang.add_builtin ~base:runtime_gc "full_major" ~category:`Liquidsoap ~descr:"Trigger full major garbage collection." [] Lang.unit_t (fun _ -> Gc.full_major (); Lang.unit) let _ = Lang.add_builtin ~base:runtime_gc "minor" ~category:`Liquidsoap ~descr:"Trigger full minor garbage collection." [] Lang.unit_t (fun _ -> Gc.minor (); Lang.unit) let _ = Lang.add_builtin ~base:runtime_gc "major_slice" ~category:`Liquidsoap ~descr: "Do a minor collection and a slice of major collection. The optional \ argument `n` is the size of the slice: the GC will do enough work to \ free (on average) `n` words of memory. If `0` (its default), the GC \ will try to do enough work to ensure that the next automatic slice has \ no work to do." [("", Lang.int_t, Some (Lang.int 0), Some "Size of the slice")] Lang.unit_t (fun p -> ignore (Gc.major_slice (Lang.to_int (List.assoc "" p))); Lang.unit) let _ = Lang.add_builtin ~base:runtime_gc "major" ~category:`Liquidsoap ~descr: "Trigger a minor collection and finish the current major collection \ cycle.." [] Lang.unit_t (fun _ -> Gc.major (); Lang.unit) let _ = Lang.add_builtin ~base:runtime_gc "compact" ~category:`Liquidsoap ~descr: "Perform a full major collection and compact the heap. Note that heap \ compaction is a lengthy operation." [] Lang.unit_t (fun _ -> Gc.compact (); Lang.unit) let _ = let stat_t = Lang.record_t [ ("minor_words", Lang.float_t); ("promoted_words", Lang.float_t); ("major_words", Lang.float_t); ("minor_collections", Lang.int_t); ("major_collections", Lang.int_t); ("forced_major_collections", Lang.int_t); ("heap_words", Lang.int_t); ("heap_chunks", Lang.int_t); ("live_words", Lang.int_t); ("live_blocks", Lang.int_t); ("free_words", Lang.int_t); ("free_blocks", Lang.int_t); ("largest_free", Lang.int_t); ("fragments", Lang.int_t); ("compactions", Lang.int_t); ("top_heap_words", Lang.int_t); ("stack_size", Lang.int_t); ] in let stat { Gc.minor_words; promoted_words; major_words; minor_collections; major_collections; forced_major_collections; heap_words; heap_chunks; live_words; live_blocks; free_words; free_blocks; largest_free; fragments; compactions; top_heap_words; stack_size; } = Lang.record [ ("minor_words", Lang.float minor_words); ("promoted_words", Lang.float promoted_words); ("major_words", Lang.float major_words); ("minor_collections", Lang.int minor_collections); ("major_collections", Lang.int major_collections); ("forced_major_collections", Lang.int forced_major_collections); ("heap_words", Lang.int heap_words); ("heap_chunks", Lang.int heap_chunks); ("live_words", Lang.int live_words); ("live_blocks", Lang.int live_blocks); ("free_words", Lang.int free_words); ("free_blocks", Lang.int free_blocks); ("largest_free", Lang.int largest_free); ("fragments", Lang.int fragments); ("compactions", Lang.int compactions); ("top_heap_words", Lang.int top_heap_words); ("stack_size", Lang.int stack_size); ] in ignore (Lang.add_builtin ~base:runtime_gc "stat" ~category:`System ~descr: "Return the current values of the memory management counters. This \ function examines every heap block to get the statistics." [] stat_t (fun _ -> stat (Gc.stat ()))); Lang.add_builtin ~base:runtime_gc "quick_stat" ~category:`System ~descr: "Same as stat except that `live_words`, `live_blocks`, `free_words`, \ `free_blocks`, `largest_free`, and `fragments` are set to `0`. This \ function is much faster than `gc.stat` because it does not need to go \ through the heap." [] stat_t (fun _ -> stat (Gc.quick_stat ())) let _ = Lang.add_builtin ~base:runtime_gc "print_stat" ~category:`System ~descr: "Print the current values of the memory management counters in \ human-readable form." [] Lang.unit_t (fun _ -> Gc.print_stat stdout; flush stdout; Lang.unit) let _ = let control_t = Lang.record_t [ ("minor_heap_size", Lang.int_t); ("major_heap_increment", Lang.int_t); ("space_overhead", Lang.int_t); ("verbose", Lang.int_t); ("max_overhead", Lang.int_t); ("stack_limit", Lang.int_t); ("allocation_policy", Lang.int_t); ("window_size", Lang.int_t); ("custom_major_ratio", Lang.int_t); ("custom_minor_ratio", Lang.int_t); ("custom_minor_max_size", Lang.int_t); ] in let control { Gc.minor_heap_size; major_heap_increment; space_overhead; verbose; max_overhead; stack_limit; allocation_policy; window_size; custom_major_ratio; custom_minor_ratio; custom_minor_max_size; } = Lang.record [ ("minor_heap_size", Lang.int minor_heap_size); ("major_heap_increment", Lang.int major_heap_increment); ("space_overhead", Lang.int space_overhead); ("verbose", Lang.int verbose); ("max_overhead", Lang.int max_overhead); ("stack_limit", Lang.int stack_limit); ("allocation_policy", Lang.int allocation_policy); ("window_size", Lang.int window_size); ("custom_major_ratio", Lang.int custom_major_ratio); ("custom_minor_ratio", Lang.int custom_minor_ratio); ("custom_minor_max_size", Lang.int custom_minor_max_size); ] in let to_control v = let f n = Lang.to_int (Value.invoke v n) in { Gc.minor_heap_size = f "minor_heap_size"; major_heap_increment = f "major_heap_increment"; space_overhead = f "space_overhead"; verbose = f "verbose"; max_overhead = f "max_overhead"; stack_limit = f "stack_limit"; allocation_policy = f "allocation_policy"; window_size = f "window_size"; custom_major_ratio = f "custom_major_ratio"; custom_minor_ratio = f "custom_minor_ratio"; custom_minor_max_size = f "custom_minor_max_size"; } in ignore (Lang.add_builtin ~base:runtime_gc "get" ~category:`System ~descr:"Return the current values of the GC parameters" [] control_t (fun _ -> control (Gc.get ()))); Lang.add_builtin ~base:runtime_gc "set" ~category:`System ~descr:"Set the GC parameters." [("", control_t, None, None)] Lang.unit_t (fun p -> let c = to_control (List.assoc "" p) in Gc.set c; Lang.unit) let runtime_sys = Lang.add_module ~base:runtime "sys" let _ = Lang.add_builtin_base ~category:`System ~descr: "Size of one word on the machine currently executing the program, in \ bits. Either `32` or `64`." ~base:runtime_sys "word_size" (`Int Sys.word_size) Lang.int_t liquidsoap-2.3.2/src/core/builtins/builtins_server.ml000066400000000000000000000054551477303350200230260ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let _ = Lang.add_builtin ~base:Modules.server "register" ~category:`Interaction ~descr: "Register a command. You can then execute this function through the \ server, either telnet or socket." [ ( "namespace", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Used to group multiple commands for the same functionality. If \ specified, the command will be named `namespace.command`." ); ( "description", Lang.string_t, Some (Lang.string "No documentation available."), Some "A description of your command." ); ( "usage", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Description of how the command should be used." ); ("", Lang.string_t, None, Some "Name of the command."); ( "", Lang.fun_t [(false, "", Lang.string_t)] Lang.string_t, None, Some "Function called when the command is executed. It takes as argument \ the argument passed on the commandline and returns the message \ which will be printed on the commandline." ); ] Lang.unit_t (fun p -> let ns = match Lang.to_valued_option Lang.to_string (List.assoc "namespace" p) with | None -> [] | Some s -> String.split_on_char '.' s in let descr = Lang.to_string (List.assoc "description" p) in let command = Lang.to_string (Lang.assoc "" 1 p) in let usage = Option.value ~default:command (Option.map Lang.to_string (Lang.to_option (List.assoc "usage" p))) in let f = Lang.assoc "" 2 p in let f x = Lang.to_string (Lang.apply f [("", Lang.string x)]) in Server.add ~ns ~usage ~descr command f; Lang.unit) liquidsoap-2.3.2/src/core/builtins/builtins_settings.ml000066400000000000000000000254341477303350200233570ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) exception Found of (Lang.value * Lang.value option) let settings = ref Lang.null let dtools_constr = let open Liquidsoap_lang in let open Type in { constr_descr = "unit, bool, int, float, string or [string]"; univ_descr = None; satisfied = (fun ~subtype ~satisfies:_ b -> let b = demeth b in match b.descr with | Bool | Int | Float | String -> () | Tuple [] -> () | List { t = b } -> subtype b (make String) | _ -> raise Unsatisfied_constraint); } (* Return a lazy variable, to be executed when all dependent OCaml modules have been linked. *) let settings_module = lazy (let get_conf_type conf = let is_type fn = try ignore (fn conf); true with _ -> false in let has_default_value fn = try ignore (fn conf)#get; true with _ -> false in if is_type Dtools.Conf.as_unit then (Lang.unit_t, false) else if is_type Dtools.Conf.as_int then (Lang.int_t, has_default_value Dtools.Conf.as_int) else if is_type Dtools.Conf.as_float then (Lang.float_t, has_default_value Dtools.Conf.as_float) else if is_type Dtools.Conf.as_bool then (Lang.bool_t, has_default_value Dtools.Conf.as_bool) else if is_type Dtools.Conf.as_string then (Lang.string_t, has_default_value Dtools.Conf.as_string) else if is_type Dtools.Conf.as_list then (Lang.list_t Lang.string_t, has_default_value Dtools.Conf.as_list) else (Lang.unit_t, false) in let set_t ty = [ ("description", ([], Lang.string_t), "Description of the setting"); ( "comments", ([], Lang.string_t), "Additional comments about the setting" ); ] @ if ty = Lang.unit_t then [] else [ ( "set", ([], Lang.fun_t [(false, "", ty)] Lang.unit_t), "Set configuration value" ); ] in let get_t ~has_default_value ty = match (ty, has_default_value) with | ty, _ when ty = Lang.unit_t -> Lang.unit_t | ty, true -> Lang.fun_t [] ty | ty, false -> Lang.fun_t [] (Lang.nullable_t ty) in let rec get_type ?(sub = []) conf = let ty, has_default_value = get_conf_type conf in Lang.method_t (get_t ~has_default_value ty) (set_t ty @ leaf_types conf @ sub) and leaf_types conf = List.map (fun label -> let ty = get_type (conf#path [label]) in let label = Utils.normalize_parameter_string label in ( label, ([], ty), Printf.sprintf "Entry for configuration key %s" label )) conf#subs in let settings_t = get_type Configure.conf in let get_v fn conv_to conv_from conf = let get = Lang.val_fun [] (fun _ -> try conv_to (fn conf)#get with _ -> Lang.null) in let set = Lang.val_fun [("", "", None)] (fun p -> (fn conf)#set (conv_from (List.assoc "" p)); Lang.unit) in (get, Some set) in let rec get_value ?(sub = []) conf = let to_v fn conv_to conv_from = try ignore (fn conf); raise (Found (get_v fn conv_to conv_from conf)) with | Found v -> raise (Found v) | _ -> () in let get_v, set_v = try to_v Dtools.Conf.as_int Lang.int Lang.to_int; to_v Dtools.Conf.as_float Lang.float Lang.to_float; to_v Dtools.Conf.as_bool Lang.bool Lang.to_bool; to_v Dtools.Conf.as_string Lang.string Lang.to_string; to_v Dtools.Conf.as_list (fun l -> Lang.list (List.map Lang.string l)) (fun v -> List.map Lang.to_string (Lang.to_list v)); (Lang.unit, None) with Found v -> v in Lang.meth get_v ((if set_v <> None then [("set", Option.get set_v)] else []) @ [ ("description", Lang.string (String.trim conf#descr)); ( "comments", Lang.string (String.trim (String.concat "" conf#comments)) ); ] @ leaf_values conf @ sub) and leaf_values conf = List.map (fun label -> let v = get_value (conf#path [label]) in (Utils.normalize_parameter_string label, v)) conf#subs in settings := get_value Configure.conf; ignore (Lang.add_builtin_value ~category:`Settings "settings" ~descr:"All settings." ~flags:[`Hidden] !settings settings_t)) (** Hack to keep track of latest settings at runtime. *) let _ = Lang.add_builtin ~category:`Settings "set_settings_ref" ~descr:"Internal use only!" ~flags:[`Hidden] [("", Lang.univ_t (), None, None)] Lang.unit_t (fun p -> settings := List.assoc "" p; Lang.unit) type descr = { description : string; comments : string; children : (string * descr) list; value : Lang.value; } let filtered_settings = ["subordinate log level"] let print_settings () = let rec grab_descr v = { description = (try Lang.to_string (Value.Methods.find "description" (Value.methods v)) with _ -> ""); comments = (try Lang.to_string (Value.Methods.find "comments" (Value.methods v)) with _ -> ""); children = Value.Methods.fold (fun key meth children -> if key <> "comments" && key <> "description" && key <> "set" then (key, grab_descr meth) :: children else children) (Value.methods v) []; value = v; } in let descr = grab_descr !settings in let filter_children = List.filter (fun (_, { description }) -> not (List.mem description filtered_settings)) in let print_set ~path = function | Liquidsoap_lang.Value.Tuple { value = [] } -> [] | Liquidsoap_lang.Value.(Fun { fun_args = [] } | FFI { ffi_args = []; _ }) as value -> let value = Lang.apply value [] in [ Printf.sprintf {| ```liquidsoap %s := %s ``` |} path (if match value with Null _ -> true | _ -> false then "" else Value.to_string value); ] | value -> [ Printf.sprintf {| ```liquidsoap %s := %s ``` |} path (Value.to_string value); ] in let rec print_descr ~level ~path descr = Printf.sprintf {| %s %s %s|} (String.make level '#') (String.capitalize_ascii descr.description) (String.concat "" ((match descr.comments with "" -> [] | v -> ["\n"; v; "\n"]) @ print_set ~path descr.value @ List.map (fun (k, d) -> print_descr ~level:(level + 1) ~path:(path ^ "." ^ k) d) (filter_children descr.children))) in print_descr ~level:1 ~path:"settings" descr (* Deprecated backward-compatible get/set. *) let log = Lang.log let _ = let grab path value = let path = String.split_on_char '.' path in List.fold_left (fun cur link -> Value.Methods.find link (Value.methods cur)) value path in ignore (Lang.add_builtin ~category:`Settings "set" ~descr: "Change some setting. Use `liquidsoap --list-settings` on the \ command-line to get some information about available settings." ~flags:[`Deprecated; `Hidden] [ ("", Lang.string_t, None, None); ("", Lang.univ_t ~constraints:[dtools_constr] (), None, None); ] Lang.unit_t (fun p -> log#severe "WARNING: \"set\" is deprecated and will be removed in future \ version. Please use `settings.path.to.key := value`"; let path = Lang.to_string (Lang.assoc "" 1 p) in let value = Lang.assoc "" 2 p in (try let set = grab (path ^ ".set") !settings in try ignore (Lang.apply set [("", value)]) with _ -> log#severe "WARNING: Error while setting value %s for setting %S. Is that \ the right type for it?" (Value.to_string value) path with Not_found -> log#severe "WARNING: setting %S does not exist!" path); Lang.unit)); let univ = Lang.univ_t ~constraints:[dtools_constr] () in Lang.add_builtin "get" ~category:`Settings ~descr:"Get a setting's value." ~flags:[`Deprecated; `Hidden] [("default", univ, None, None); ("", Lang.string_t, None, None)] univ (fun p -> log#severe "WARNING: \"get\" is deprecated and will be removed in future version. \ Please use `settings.path.to.key()`"; let path = Lang.to_string (List.assoc "" p) in let default = List.assoc "default" p in try let get = grab path !settings in let v = Lang.apply ~pos:(Lang.pos p) get [] in let open Liquidsoap_lang.Value in match (default, v) with | Bool _, Bool _ | Int _, Int _ | Float _, Float _ | String _, String _ | List { value = [] }, List { value = [] } | List { value = Value.String _ :: _ }, List { value = [] } | List { value = [] }, List { value = Value.String _ :: _ } | ( List { value = Value.String _ :: _ }, List { value = Value.String _ :: _ } ) -> v | _ -> log#severe "WARNING: Invalid value/default pair (%s vs. %s) for setting \ %S!" (Value.to_string v) (Value.to_string default) path; default with | Not_found -> log#severe "WARNING: setting %S does not exist!" path; default | _ -> log#severe "WARNING: could not get setting %s value!" path; default) liquidsoap-2.3.2/src/core/builtins/builtins_socket.ml000066400000000000000000000477571477303350200230230ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) module Http = Liq_http module Socket_domain = struct include Value.MkCustom (struct type content = Unix.socket_domain let name = "socket_domain" let to_json ~pos _ = Lang.raise_error ~pos ~message:"Socket domain cannot be represented as json" "json" let to_string = function | Unix.PF_UNIX -> "socket.domain.unix" | Unix.PF_INET -> "socket.domain.inet" | Unix.PF_INET6 -> "socket.domain.inet6" let compare = Stdlib.compare end) let unix = to_value Unix.PF_UNIX let inet = to_value Unix.PF_INET let inet6 = to_value Unix.PF_INET6 end module Socket_type = struct include Value.MkCustom (struct type content = Unix.socket_type let name = "socket_type" let to_json ~pos _ = Lang.raise_error ~pos ~message:"Socket type cannot be represented as json" "json" let to_string = function | Unix.SOCK_STREAM -> "socket.type.stream" | Unix.SOCK_DGRAM -> "socket.type.dgram" | Unix.SOCK_RAW -> "socket.type.raw" (* From OCaml: SOCK_SEQPACKET is included for completeness, but is rarely supported by the OS, and needs system calls that are not available in this library. *) | Unix.SOCK_SEQPACKET -> assert false let compare = Stdlib.compare end) let stream = to_value Unix.SOCK_STREAM let dgram = to_value Unix.SOCK_DGRAM let raw = to_value Unix.SOCK_RAW end module Inet_addr = struct let to_string s = Printf.sprintf "socket.internet_address(%S)" (Unix.string_of_inet_addr s) include Value.MkCustom (struct type content = Unix.inet_addr let name = "internet_address" let to_json ~pos _ = Lang.raise_error ~pos ~message:"Internet address type cannot be represented as json" "json" let to_string = to_string let compare s s' = Stdlib.compare (Unix.string_of_inet_addr s) (Unix.string_of_inet_addr s') end) let base_t = t let t = Lang.method_t base_t [ ( "to_string", ([], Lang.fun_t [] Lang.string_t), "String representation of the internet address" ); ("is_ipv6", ([], Lang.bool_t), "Is the internet address a ipv6 address?"); ] let to_value v = Lang.meth (to_value v) [ ( "to_string", Lang.val_fun [] (fun _ -> Lang.string (Unix.string_of_inet_addr v)) ); ("is_ipv6", Lang.bool (Unix.is_inet6_addr v)); ] let any = to_value Unix.inet_addr_any let loopback = to_value Unix.inet_addr_loopback let ipv6_any = to_value Unix.inet6_addr_any let ipv6_loopback = to_value Unix.inet6_addr_loopback end module Socket_addr = struct include Value.MkCustom (struct type content = Unix.sockaddr let name = "socket_address" let to_json ~pos _ = Lang.raise_error ~pos ~message:"Socket address type cannot be represented as json" "json" let to_string = function | Unix.ADDR_UNIX s -> Printf.sprintf "socket.address.unix(%S)" s | Unix.ADDR_INET (inet_addr, port) -> Printf.sprintf "socket.address.inet(%s, %i)" (Inet_addr.to_string inet_addr) port let compare = Stdlib.compare end) let base_t = t let t = Lang.method_t base_t [("domain", ([], Socket_domain.t), "Socket domain")] let to_value v = Lang.meth (to_value v) [("domain", Socket_domain.to_value (Unix.domain_of_sockaddr v))] let unix_t = Lang.method_t t [("path", ([], Lang.string_t), "Unix socket path")] let inet_t = Lang.method_t t [ ("internet_address", ([], Inet_addr.t), "Internet address"); ("port", ([], Lang.int_t), "Port"); ] let to_unix_value = function | Unix.ADDR_UNIX path as v -> Lang.meth (to_value v) [("path", Lang.string path)] | _ -> assert false let to_inet_value = function | Unix.ADDR_INET (addr, port) as v -> Lang.meth (to_value v) [ ("internet_address", Inet_addr.to_value addr); ("port", Lang.int port); ] | _ -> assert false end module Socket_value = struct include Value.MkCustom (struct type content = Http.socket let name = "socket" let to_json ~pos _ = Lang.raise_error ~pos ~message:"Socket cannot be represented as json" "json" let to_string s = Printf.sprintf "<%s socket>" s#typ let compare = Stdlib.compare end) let meths = let string_of_mode = function `Read -> "read" | `Write -> "write" in let wait_t ~mode t = Lang.method_t t [ ( "wait", ( [], Lang.fun_t [ (true, "timeout", Lang.nullable_t Lang.float_t); (false, "", Lang.fun_t [] Lang.unit_t); ] Lang.unit_t ), "Execute the given callback when the socket is ready to " ^ string_of_mode mode ^ " some data" ); ] in let wait_meth ~mode socket v = Lang.meth v [ ( "wait", Lang.val_fun [("timeout", "timeout", Some (Lang.float 10.)); ("", "", None)] (fun p -> let timeout = Lang.to_valued_option Lang.to_float (List.assoc "timeout" p) in let event = match mode with | `Read -> `Read socket#file_descr | `Write -> `Write socket#file_descr in let events = match timeout with | None -> [event] | Some t -> [`Delay t; event] in let fn = List.assoc "" p in let fn events = if not (List.mem event events) then Lang.raise_error ~pos:(Lang.pos p) ~message:"Timeout while writing to the socket!" "socket"; ignore (Lang.apply fn []); [] in Duppy.Task.add Tutils.scheduler { Duppy.Task.priority = `Maybe_blocking; events; handler = fn; }; Lang.unit) ); ] in [ ( "type", ([], Lang.string_t), "Socket type", fun (socket : content) -> Lang.string socket#typ ); ( "non_blocking", ([], Lang.fun_t [(false, "", Lang.bool_t)] Lang.unit_t), "Set the non-blocking flag on the socket", fun socket -> Lang.val_fun [("", "", None)] (fun p -> if Lang.to_bool (List.assoc "" p) then Unix.set_nonblock socket#file_descr else Unix.clear_nonblock socket#file_descr; Lang.unit) ); ( "write", ( [], wait_t ~mode:`Write (Lang.fun_t [ (true, "timeout", Lang.nullable_t Lang.float_t); (false, "", Lang.string_t); ] Lang.unit_t) ), "Write data to a socket", fun socket -> wait_meth ~mode:`Write socket (Lang.val_fun [("timeout", "timeout", Some (Lang.float 10.)); ("", "", None)] (fun p -> let timeout = Lang.to_valued_option Lang.to_float (List.assoc "timeout" p) in let data = Lang.to_string (List.assoc "" p) in let data = Bytes.of_string data in let len = Bytes.length data in let start_time = Unix.gettimeofday () in let check_timeout () = match timeout with | None -> () | Some t -> ( let rem = start_time +. t -. Unix.gettimeofday () in try if rem <= 0. then failwith "timeout!"; socket#wait_for `Write rem with _ -> Lang.raise_error ~pos:(Lang.pos p) ~message:"Timeout while writing to the socket!" "socket") in try let rec f pos = check_timeout (); let n = socket#write data pos (len - pos) in if n < len then f (pos + n) in f 0; Lang.unit with exn -> let bt = Printexc.get_raw_backtrace () in Lang.raise_as_runtime ~bt ~kind:"socket" exn)) ); ( "read", ( [], wait_t ~mode:`Read (Lang.fun_t [(true, "timeout", Lang.nullable_t Lang.float_t)] Lang.string_t) ), "Read data from a socket. Reading is done when the function returns an \ empty string `\"\"`.", fun socket -> let buflen = Utils.pagesize in let buf = Bytes.create buflen in wait_meth ~mode:`Read socket (Lang.val_fun [("timeout", "timeout", Some (Lang.float 10.))] (fun p -> let timeout = Lang.to_valued_option Lang.to_float (List.assoc "timeout" p) in let start_time = Unix.gettimeofday () in let check_timeout () = match timeout with | None -> () | Some t -> ( let rem = start_time +. t -. Unix.gettimeofday () in try if rem <= 0. then failwith "timeout!"; socket#wait_for `Read rem with _ -> Lang.raise_error ~pos:(Lang.pos p) ~message:"Timeout while reading from the socket!" "socket") in try check_timeout (); let n = socket#read buf 0 buflen in Lang.string (Bytes.sub_string buf 0 n) with exn -> let bt = Printexc.get_raw_backtrace () in Lang.raise_as_runtime ~bt ~kind:"socket" exn)) ); ( "close", ([], Lang.fun_t [] Lang.unit_t), "Close the socket.", fun socket -> Lang.val_fun [] (fun _ -> try socket#close; Lang.unit with exn -> let bt = Printexc.get_raw_backtrace () in Lang.raise_as_runtime ~bt ~kind:"socket" exn) ); ] let t = Lang.method_t t (List.map (fun (lbl, t, descr, _) -> (lbl, t, descr)) meths) let to_value v = Lang.meth (to_value v) (List.map (fun (lbl, _, _, m) -> (lbl, m v)) meths) let server_t = Lang.method_t t [ ( "bind", ([], Lang.fun_t [(false, "", Socket_addr.base_t)] Lang.unit_t), "Bind a socket to an address." ); ( "listen", ([], Lang.fun_t [(false, "", Lang.int_t)] Lang.unit_t), "Set up a socket for receiving connection requests. The integer \ argument is the maximal number of pending requests." ); ] let to_server_value socket = Lang.meth (to_value socket) [ ( "bind", Lang.val_fun [("", "", None)] (fun p -> Unix.bind socket#file_descr (Socket_addr.of_value (List.assoc "" p)); Lang.unit) ); ( "listen", Lang.val_fun [("", "", None)] (fun p -> Unix.listen socket#file_descr (Lang.to_int (List.assoc "" p)); Lang.unit) ); ] let unix_t = Lang.method_t server_t [ ( "accept", ( [], Lang.fun_t [(true, "timeout", Lang.nullable_t Lang.float_t)] (Lang.product_t t Socket_addr.base_t) ), "Accept connections on the given socket. The returned socket is a \ socket connected to the client; the returned address is the address \ of the connecting client. Timeout defaults to harbor's \ accept_timeout if `null`." ); ( "connect", ([], Lang.fun_t [(false, "", Socket_addr.base_t)] Lang.unit_t), "Connect a socket to an address." ); ] let to_unix_value v = let socket = Http.unix_socket v in let server = socket#transport#server in Lang.meth (to_server_value socket) [ ( "accept", Lang.val_fun [ ( "timeout", "timeout", Some (Lang.float Harbor_base.conf_accept_timeout#get) ); ] (fun p -> let timeout = Lang.to_valued_option Lang.to_float (List.assoc "timeout" p) in let fd, sockaddr = server#accept ?timeout socket#file_descr in Lang.product (to_value fd) (Socket_addr.to_value sockaddr)) ); ( "connect", Lang.val_fun [("", "", None)] (fun p -> Unix.connect socket#file_descr (Socket_addr.of_value (List.assoc "" p)); Lang.unit) ); ] end let socket = Lang.add_module "socket" let _ = Lang.add_builtin ~base:socket "unix" ~category:`Internet [ ("domain", Socket_domain.t, Some Socket_domain.inet, Some "Socket domain."); ("type", Socket_type.t, Some Socket_type.stream, Some "Socket type"); ( "protocol", Lang.int_t, Some (Lang.int 0), Some "Protocol type. `0` selects the default protocol for that kind of \ sockets." ); ] Socket_value.unix_t ~descr:"Create a unix socket." (fun p -> let domain = Socket_domain.of_value (List.assoc "domain" p) in let typ = Socket_type.of_value (List.assoc "type" p) in let protocol = Lang.to_int (List.assoc "protocol" p) in Socket_value.to_unix_value (Unix.socket ~cloexec:true domain typ protocol)) let _ = Lang.add_builtin ~base:socket "pair" ~category:`Internet [ ("domain", Socket_domain.t, Some Socket_domain.inet, Some "Socket domain."); ("type", Socket_type.t, Some Socket_type.stream, Some "Socket type"); ( "protocol", Lang.int_t, Some (Lang.int 0), Some "Protocol type. `0` selects the default protocol for that kind of \ sockets." ); ] Socket_value.t ~descr:"Create a pair of sockets connected together." (fun p -> let domain = Socket_domain.of_value (List.assoc "domain" p) in let typ = Socket_type.of_value (List.assoc "type" p) in let protocol = Lang.to_int (List.assoc "protocol" p) in let s, s' = Unix.socketpair ~cloexec:true domain typ protocol in Lang.product (Socket_value.to_unix_value s) (Socket_value.to_unix_value s')) let socket_domain = Lang.add_module ~base:socket "domain" let socket_type = Lang.add_module ~base:socket "type" let add ~t ~name ~descr ~base value = ignore (Lang.add_builtin_value ~category:`Internet ~descr ~base name value t) let () = add ~t:Socket_domain.t ~base:socket_domain ~name:"unix" ~descr:"Unix socket domain" Socket_domain.unix; add ~t:Socket_domain.t ~base:socket_domain ~name:"inet" ~descr:"Inet socket domain" Socket_domain.inet; add ~t:Socket_domain.t ~base:socket_domain ~name:"inet6" ~descr:"Inet6 socket domain" Socket_domain.inet6; add ~t:Socket_type.t ~base:socket_type ~name:"stream" ~descr:"Stream socket type" Socket_type.stream; add ~t:Socket_type.t ~base:socket_type ~name:"dgram" ~descr:"Dgram socket type" Socket_type.dgram; add ~t:Socket_type.t ~base:socket_type ~name:"raw" ~descr:"Raw socket type" Socket_type.raw let socket_internet_address = Lang.add_builtin ~base:socket "internet_address" ~category:`Internet [("", Lang.string_t, None, Some "Socket internet address.")] Inet_addr.t ~descr:"Return an internet address from its string representation." (fun p -> Inet_addr.to_value (Unix.inet_addr_of_string (Lang.to_string (List.assoc "" p)))) let () = add ~t:Inet_addr.t ~base:socket_internet_address ~name:"any" ~descr: "A special IPv4 address, for use only with `socket.bind`, representing \ all the Internet addresses that the host machine possesses." Inet_addr.any; add ~t:Inet_addr.t ~base:socket_internet_address ~name:"loopback" ~descr:"A special IPv4 address representing the host machine (`127.0.0.1`)." Inet_addr.loopback let socket_internet_address_ipv6 = Lang.add_module ~base:socket_internet_address "ipv6" let () = add ~t:Inet_addr.t ~base:socket_internet_address_ipv6 ~name:"any" ~descr: "A special IPv6 address, for use only with `socket.bind`, representing \ all the Internet addresses that the host machine possesses." Inet_addr.ipv6_any; add ~t:Inet_addr.t ~base:socket_internet_address_ipv6 ~name:"loopback" ~descr:"A special IPv6 address representing the host machine (`::1`)." Inet_addr.ipv6_loopback let socket_address = Lang.add_module ~base:socket "address" let socket_address_unix = Lang.add_builtin ~base:socket_address "unix" ~category:`Internet [("", Lang.string_t, None, Some "Unix socket path")] Socket_addr.unix_t ~descr:"Create a socket address for a unix file socket." (fun p -> Socket_addr.to_unix_value (Unix.ADDR_UNIX (Lang.to_string (List.assoc "" p)))) let _ = Lang.add_builtin ~base:socket_address "internet_address" ~category:`Internet [ ("", Inet_addr.t, None, Some "Internet address."); ("", Lang.int_t, None, Some "port"); ] Socket_addr.inet_t ~descr:"Create a socket address for a internet address." (fun p -> let addr = Inet_addr.of_value (Lang.assoc "" 1 p) in let port = Lang.to_int (Lang.assoc "" 2 p) in Socket_addr.to_inet_value (Unix.ADDR_INET (addr, port))) let host_t = Lang.record_t [ ("name", Lang.string_t); ("aliases", Lang.list_t Lang.string_t); ("domain", Socket_domain.t); ("addresses", Lang.list_t Inet_addr.t); ] let to_host_value { Unix.h_name; h_aliases; h_addrtype; h_addr_list } = Lang.record [ ("name", Lang.string h_name); ("aliases", Lang.list (List.map Lang.string (Array.to_list h_aliases))); ("domain", Socket_domain.to_value h_addrtype); ( "addresses", Lang.list (List.map Inet_addr.to_value (Array.to_list h_addr_list)) ); ] let host = Lang.add_module "host" let _ = Lang.add_builtin ~base:host "of_name" ~category:`Internet ~descr:"Find a host by name" [("", Lang.string_t, None, Some "hostname")] (Lang.nullable_t host_t) (fun p -> let hostname = Lang.to_string (List.assoc "" p) in try to_host_value (Unix.gethostbyname hostname) with Not_found -> Lang.null) let _ = Lang.add_builtin ~base:host "of_internet_address" ~category:`Internet ~descr:"Find a host by internet address" [("", Inet_addr.base_t, None, None)] (Lang.nullable_t host_t) (fun p -> let inet_addr = Inet_addr.of_value (List.assoc "" p) in try to_host_value (Unix.gethostbyaddr inet_addr) with Not_found -> Lang.null) liquidsoap-2.3.2/src/core/builtins/builtins_source.ml000066400000000000000000000134011477303350200230060ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let source = Muxer.source let _ = Lang.add_builtin ~base:source "set_id" ~category:(`Source `Liquidsoap) ~descr:"Set the id of an operator." [ ("", Lang.source_t (Lang.univ_t ()), None, None); ("", Lang.string_t, None, None); ] Lang.unit_t (fun p -> let s = Lang.assoc "" 1 p |> Lang.to_source in let n = Lang.assoc "" 2 p |> Lang.to_string in s#set_id n; Lang.unit) let _ = Lang.add_builtin ~base:source "last_metadata" ~category:(`Source `Liquidsoap) ~descr:"Return the last metadata from the source." [("", Lang.source_t (Lang.univ_t ()), None, None)] (Lang.nullable_t Lang.metadata_t) (fun p -> let s = Lang.to_source (List.assoc "" p) in match s#last_metadata with None -> Lang.null | Some m -> Lang.metadata m) let _ = Lang.add_builtin ~base:source "skip" ~category:(`Source `Liquidsoap) ~descr:"Skip to the next track." [("", Lang.source_t (Lang.univ_t ()), None, None)] Lang.unit_t (fun p -> (Lang.to_source (List.assoc "" p))#abort_track; Lang.unit) let _ = Lang.add_builtin ~base:source "seek" ~category:(`Source `Liquidsoap) ~descr: "Seek forward, in seconds. Returns the amount of time effectively seeked." [ ("", Lang.source_t (Lang.univ_t ()), None, None); ("", Lang.float_t, None, None); ] Lang.float_t (fun p -> let s = Lang.to_source (Lang.assoc "" 1 p) in let time = Lang.to_float (Lang.assoc "" 2 p) in let len = Frame.main_of_seconds time in let ret = s#seek len in Lang.float (Frame.seconds_of_main ret)) let _ = Lang.add_builtin ~base:source "id" ~category:(`Source `Liquidsoap) ~descr:"Get the identifier of a source." [("", Lang.source_t (Lang.univ_t ()), None, None)] Lang.string_t (fun p -> Lang.string (Lang.to_source (List.assoc "" p))#id) let _ = Lang.add_builtin ~base:source "fallible" ~category:(`Source `Liquidsoap) ~descr:"Indicate if a source may fail, i.e. may not be ready to stream." [("", Lang.source_t (Lang.univ_t ()), None, None)] Lang.bool_t (fun p -> Lang.bool (Lang.to_source (List.assoc "" p))#fallible) let _ = Lang.add_builtin ~base:source "is_ready" ~category:(`Source `Liquidsoap) ~descr: "Indicate if a source is ready to stream (we also say that it is \ available), or currently streaming." [("", Lang.source_t (Lang.univ_t ()), None, None)] Lang.bool_t (fun p -> Lang.bool (Lang.to_source (List.assoc "" p))#is_ready) let _ = Lang.add_builtin ~base:source "is_up" ~category:`System [("", Lang.source_t (Lang.univ_t ()), None, None)] Lang.bool_t ~descr:"Check whether a source is up." (fun p -> Lang.bool (Lang.to_source (Lang.assoc "" 1 p))#is_up) let _ = Lang.add_builtin ~base:source "remaining" ~category:(`Source `Liquidsoap) ~descr:"Estimation of remaining time in the current track." [("", Lang.source_t (Lang.univ_t ()), None, None)] Lang.float_t (fun p -> let r = (Lang.to_source (List.assoc "" p))#remaining in let f = if r < 0 then infinity else Frame.seconds_of_main r in Lang.float f) let _ = Lang.add_builtin ~base:source "elapsed" ~category:(`Source `Liquidsoap) ~descr:"Elapsed time in the current track." [("", Lang.source_t (Lang.univ_t ()), None, None)] Lang.float_t (fun p -> let d = (Lang.to_source (List.assoc "" p))#elapsed in let f = if d < 0 then infinity else Frame.seconds_of_main d in Lang.float f) let _ = Lang.add_builtin ~base:source "duration" ~category:(`Source `Liquidsoap) ~descr:"Estimation of the duration in the current track." [("", Lang.source_t (Lang.univ_t ()), None, None)] Lang.float_t (fun p -> let d = (Lang.to_source (List.assoc "" p))#duration in let f = if d < 0 then infinity else Frame.seconds_of_main d in Lang.float f) let _ = Lang.add_builtin ~category:(`Source `Liquidsoap) ~base:source "time" ~descr:"Get a source's time, based on its assigned clock" [("", Lang.source_t (Lang.univ_t ()), None, None)] Lang.float_t (fun p -> let s = Lang.to_source (List.assoc "" p) in let ticks = Clock.ticks s#clock in let frame_position = Lazy.force Frame.duration *. float ticks in Lang.float frame_position) let _ = Lang.add_builtin ~base:source "on_shutdown" ~category:(`Source `Liquidsoap) [ ("", Lang.source_t (Lang.univ_t ()), None, None); ("", Lang.fun_t [] Lang.unit_t, None, None); ] Lang.unit_t ~descr: "Register a function to be called when source is not used anymore by \ another source." (fun p -> let s = Lang.to_source (Lang.assoc "" 1 p) in let f = Lang.assoc "" 2 p in let wrap_f () = ignore (Lang.apply f []) in s#on_sleep wrap_f; Lang.unit) liquidsoap-2.3.2/src/core/builtins/builtins_sqlite.ml000066400000000000000000000274341477303350200230220ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let error fmt = Printf.ksprintf (fun message -> Runtime_error.raise ~pos:[] ~message "sqlite") fmt let escape s = "'" ^ String.concat "''" (String.split_on_char '\'' s) ^ "'" let insert_value_constr = let open Type in { constr_descr = "int, float, string or null."; univ_descr = None; satisfied = (fun ~subtype:_ ~satisfies b -> let rec check typ = match (deref typ).descr with | Var _ -> satisfies typ | Nullable typ -> check typ | Float | Int | String -> () | _ -> raise Unsatisfied_constraint in check b); } let insert_record_constr = let open Type in { constr_descr = "a record with int, float, string or null methods."; univ_descr = None; satisfied = (fun ~subtype ~satisfies b -> let m, b = split_meths b in match b.descr with | Var _ -> satisfies b | Tuple [] when m = [] -> raise Unsatisfied_constraint | Tuple [] -> List.iter (fun { scheme = _, typ } -> subtype typ (var ~constraints:[insert_value_constr] ())) m | _ -> raise Unsatisfied_constraint); } type row = { row : Sqlite3.row; headers : Sqlite3.headers } module SqliteRow = struct include Value.MkCustom (struct type content = row let name = "sqlite.row" let to_json ~pos _ = Runtime_error.raise ~pos ~message:"Sqlite rows cannot be represented as json" "json" let to_string _ = "sqlite.row" let compare = Stdlib.compare end) let t = Lang.method_t t [ ( "to_list", ( [], Lang.fun_t [] (Lang.list_t (Lang.product_t Lang.string_t (Lang.nullable_t Lang.string_t))) ), "Return the row as an associative `[(label, value)]` list." ); ] let to_value v = let to_list { headers; row } = List.fold_left2 (fun l header row -> Lang.product (Lang.string header) (match row with None -> Lang.null | Some r -> Lang.string r) :: l) [] (Array.to_list headers) (Array.to_list row) in Lang.meth (to_value v) [("to_list", Lang.val_fun [] (fun _ -> Lang.list (to_list v)))] let of_value v = of_value (Lang.demeth v) end let header_types ty = let open Type in let headers, _ = split_meths ty in let rec to_type ~nullable ty = match (deref ty).descr with | Nullable typ -> let typ = to_type ~nullable:false typ in if nullable then `Nullable typ else typ | Float -> `Float | Int -> `Int | String -> `String | Var _ -> raise Not_found | _ -> assert false in let headers = List.fold_left (fun headers { Type.meth = lbl; scheme = _, ty } -> try (lbl, to_type ~nullable:true ty) :: headers with Not_found -> headers) [] headers in headers let rec string_of_typ = function | `Nullable typ -> Printf.sprintf "%s?" (string_of_typ typ) | `Int -> "int" | `Float -> "float" | `String -> "string" let check db ?sql ans = let sql = match sql with | Some sql -> Printf.sprintf " Statement: %s." sql | None -> "" in if not (Sqlite3.Rc.is_success ans) then error "Command failed (%s): %s.%s" (Sqlite3.Rc.to_string ans) (Sqlite3.errmsg db) sql let exec db ?cb sql = Sqlite3.exec db ?cb sql |> check db ~sql let rec parse_value ~pos ~header ~typ v = let open Sqlite3.Data in match (typ, v) with | `Nullable _, NULL -> Lang.null | `Nullable typ, v -> parse_value ~pos ~header ~typ v | `Int, INT i -> Lang.int (Int64.to_int i) | `Int, TEXT s -> Lang.int (int_of_string s) | `Int, BLOB s -> Lang.int (int_of_string s) | `Float, FLOAT f -> Lang.float f | `Float, TEXT s -> Lang.float (float_of_string s) | `Float, BLOB s -> Lang.float (float_of_string s) | `String, TEXT s -> Lang.string s | `String, BLOB s -> Lang.string s | _ -> Runtime_error.raise ~pos ~message: (Printf.sprintf "Parse error: sqlite query response for column %s with value %s \ cannot be parsed as type %s" header (match to_string v with None -> "NULL" | Some s -> s) (string_of_typ typ)) "sqlite" let parse_row ~pos ~header_types { row; headers } = let row = Array.to_list row in let headers = Array.to_list headers in List.fold_left2 (fun l header row -> match List.assoc_opt header header_types with | Some typ -> (header, parse_value ~pos ~header ~typ (Sqlite3.Data.opt_text row)) :: l | None -> l) [] headers row |> Lang.record let query_parser ~db query = let ans = ref [] in let cb row headers = ans := SqliteRow.to_value { row; headers } :: !ans in exec db ~cb query; let ans = List.rev !ans in Lang.list ans let _ = let return_t = Type.var ~constraints:[insert_record_constr] () in Lang.add_builtin "_sqlite_row_parser_" ~category:`String ~flags:[`Hidden] ~descr:"Internal sql row parser" [ ("type", Value.RuntimeType.t, None, Some "Runtime type"); ("", SqliteRow.t, None, None); ] return_t (fun p -> let row = SqliteRow.of_value (List.assoc "" p) in let ty = Value.RuntimeType.of_value (List.assoc "type" p) in let header_types = header_types ty in let pos = Lang.pos p in try parse_row ~pos ~header_types row with exn -> ( let bt = Printexc.get_raw_backtrace () in match exn with | Runtime_error.Runtime_error _ as exn -> Printexc.raise_with_backtrace exn bt | _ -> Runtime_error.raise ~bt ~pos ~message: (Printf.sprintf "Parse error: sqlite query response value cannot be \ parsed as type: %s" (Type.to_string ty)) "sqlite")) let sqlite = let meth = [ ( "exec", ([], Lang.fun_t [(false, "", Lang.string_t)] Lang.unit_t), "Execute an SQL operation.", fun db -> Lang.val_fun [("", "", None)] (fun p -> let sql = List.assoc "" p |> Lang.to_string in exec db sql; Lang.unit) ); ( "query", ([], Lang.fun_t [(false, "", Lang.string_t)] (Lang.list_t SqliteRow.t)), "Execute an SQL operation returning the result. Result can be parsed \ using `let sqlite.query = ...`.", fun db -> Lang.val_fun [("", "", None)] (fun p -> let query = List.assoc "" p |> Lang.to_string in query_parser ~db query) ); ( "iter", ( [], Lang.fun_t [ (false, "", Lang.fun_t [(false, "", SqliteRow.t)] Lang.unit_t); (false, "", Lang.string_t); ] Lang.unit_t ), "Iterate a function over all the results of a query. Result can be \ parsed using `let sqlite.row = ...`.", fun db -> Lang.val_fun [("", "", None); ("", "", None)] (fun p -> let f = Lang.assoc "" 1 p in let sql = Lang.assoc "" 2 p |> Lang.to_string in let cb row headers = let row = SqliteRow.to_value { row; headers } in ignore (Lang.apply f [("", row)]) in exec db ~cb sql; Lang.unit) ); ( "insert", ( [], Lang.fun_t [ (false, "table", Lang.string_t); (true, "replace", Lang.bool_t); (false, "", Type.var ~constraints:[insert_record_constr] ()); ] Lang.unit_t ), "Insert a value represented as a record into a table.", fun db -> Lang.val_fun [ ("table", "table", None); ("replace", "replace", Some (Lang.bool false)); ("", "", None); ] (fun p -> let table = List.assoc "table" p |> Lang.to_string in let replace = List.assoc "replace" p |> Lang.to_bool in let v = List.assoc "" p |> Liquidsoap_lang.Builtins_json.json_of_value in match v with | `Assoc l -> let l = List.map (fun (k, v) -> ( k, match v with | `String s -> Sqlite3.Data.opt_text (Some s) | `Int n -> Sqlite3.Data.opt_int (Some n) | `Float x -> Sqlite3.Data.opt_float (Some x) | `Null -> Sqlite3.Data.NULL | _ -> error "Unexpected content for field %s." k )) l in let sql = let replace = if replace then " OR REPLACE" else "" in let fields = l |> List.map fst |> String.concat ", " in let values = l |> List.map (fun _ -> "?") |> String.concat ", " in Printf.sprintf "INSERT%s INTO %s (%s) VALUES (%s)" replace table fields values in let insert = Sqlite3.prepare db sql in l |> List.map snd |> List.iteri (fun i v -> Sqlite3.bind insert (i + 1) v |> check db); Sqlite3.step insert |> check db ~sql; Sqlite3.finalize insert |> check db; Lang.unit | _ -> error "A record was expected.") ); ( "close", ([], Lang.fun_t [] Lang.unit_t), "Close the database. It should not be accessed afterward.", fun db -> Lang.val_fun [] (fun _p -> Sqlite3.db_close db |> ignore; Lang.unit) ); ] in let t = List.map (fun (name, typ, doc, _) -> (name, typ, doc)) meth |> Lang.method_t Lang.unit_t in Lang.add_builtin "sqlite" ~category:`Programming ~descr:"Manipulate an SQLITE database." [("", Lang.string_t, None, Some "File where the data base is stored")] t (fun p -> let fname = List.assoc "" p |> Lang.to_string in let db = Sqlite3.db_open fname in let meth = List.map (fun (name, _, _, f) -> (name, f db)) meth in Lang.meth Lang.unit meth) let _ = Lang.add_builtin "escape" ~base:sqlite ~category:`Programming ~descr:"Escape a string for use in a query." [("", Lang.string_t, None, Some "String to escape.")] Lang.string_t (fun p -> List.assoc "" p |> Lang.to_string |> escape |> Lang.string) liquidsoap-2.3.2/src/core/builtins/builtins_srt.ml000066400000000000000000000354701477303350200223300ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** SRT input *) open Unsigned exception Done exception Not_connected module Socket_value = struct let read_only_socket_options_specs = [("rcvdata", `Int Srt.rcvdata)] let write_only_socket_options_specs = [ ("messageapi", `Bool Srt.messageapi); ("payloadsize", `Int Srt.payloadsize); ("conntimeo", `Int Srt.conntimeo); ("passphrase", `String Srt.passphrase); ("enforced_encryption", `Bool Srt.enforced_encryption); ] let read_write_socket_options_specs = [ ("rcvsyn", `Bool Srt.rcvsyn); ("sndsyn", `Bool Srt.sndsyn); ("rcvtimeout", `Int Srt.rcvtimeo); ("sndtimeout", `Int Srt.sndtimeo); ("reuseaddr", `Bool Srt.reuseaddr); ("rcvbuf", `Int Srt.rcvbuf); ("sndbuf", `Int Srt.sndbuf); ("udp_rcvbuf", `Int Srt.udp_rcvbuf); ("udp_sndbuf", `Int Srt.udp_sndbuf); ("streamid", `String Srt.streamid); ("pbkeylen", `Int Srt.pbkeylen); ("ipv6only", `Bool Srt.ipv6only); ("rcvlatency", `Int Srt.rcvlatency); ("peerlatency", `Int Srt.peerlatency); ("latency", `Int Srt.latency); ] let mk_read_socket_option name socket_opt = let t = match socket_opt with | `Int _ -> Lang.int_t | `Bool _ -> Lang.bool_t | `String _ -> Lang.string_t in ( name, ([], Lang.fun_t [] t), "Get " ^ name ^ " option", fun s -> Lang.val_fun [] (fun _ -> try match socket_opt with | `Int socket_opt -> Lang.int (Srt.getsockflag s socket_opt) | `Bool socket_opt -> Lang.bool (Srt.getsockflag s socket_opt) | `String socket_opt -> Lang.string (Srt.getsockflag s socket_opt) with exn -> let bt = Printexc.get_raw_backtrace () in Lang.raise_as_runtime ~bt ~kind:"srt" exn) ) let mk_write_socket_option name socket_opt = let t = match socket_opt with | `Int _ -> Lang.int_t | `Bool _ -> Lang.bool_t | `String _ -> Lang.string_t in ( "set_" ^ name, ([], Lang.fun_t [(false, "", t)] Lang.unit_t), "Set " ^ name ^ " option", fun s -> Lang.val_fun [("", "", None)] (fun p -> let v = List.assoc "" p in try (match socket_opt with | `Int socket_opt -> Srt.setsockflag s socket_opt (Lang.to_int v) | `Bool socket_opt -> Srt.setsockflag s socket_opt (Lang.to_bool v) | `String socket_opt -> Srt.setsockflag s socket_opt (Lang.to_string v)); Lang.unit with exn -> let bt = Printexc.get_raw_backtrace () in Lang.raise_as_runtime ~bt ~kind:"srt" exn) ) let socket_options_meths = let read_meths = List.fold_left (fun cur (name, socket_opt) -> mk_read_socket_option name socket_opt :: cur) (List.fold_left (fun cur (name, socket_opt) -> mk_read_socket_option name socket_opt :: cur) [] read_only_socket_options_specs) read_write_socket_options_specs in List.fold_left (fun cur (name, socket_opt) -> mk_write_socket_option name socket_opt :: cur) (List.fold_left (fun cur (name, socket_opt) -> mk_write_socket_option name socket_opt :: cur) read_meths write_only_socket_options_specs) read_write_socket_options_specs let stats_specs = [ ( "msTimeStamp", Lang.int_t, fun v -> Lang.int (Int64.to_int v.Srt.Stats.msTimeStamp) ); ( "pktSentTotal", Lang.int_t, fun v -> Lang.int (Int64.to_int v.Srt.Stats.pktSentTotal) ); ( "pktRecvTotal", Lang.int_t, fun v -> Lang.int (Int64.to_int v.Srt.Stats.pktRecvTotal) ); ( "pktSndLossTotal", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktSndLossTotal ); ( "pktRcvLossTotal", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktRcvLossTotal ); ( "pktRetransTotal", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktRetransTotal ); ( "pktSentACKTotal", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktSentACKTotal ); ( "pktRecvACKTotal", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktRecvACKTotal ); ( "pktSentNAKTotal", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktSentNAKTotal ); ( "pktRecvNAKTotal", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktRecvNAKTotal ); ( "usSndDurationTotal", Lang.int_t, fun v -> Lang.int (Int64.to_int v.Srt.Stats.usSndDurationTotal) ); ( "pktSndDropTotal", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktSndDropTotal ); ( "pktRcvDropTotal", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktRcvDropTotal ); ( "pktRcvUndecryptTotal", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktRcvUndecryptTotal ); ( "byteSentTotal", Lang.int_t, fun v -> Lang.int (UInt64.to_int v.Srt.Stats.byteSentTotal) ); ( "byteRecvTotal", Lang.int_t, fun v -> Lang.int (UInt64.to_int v.Srt.Stats.byteRecvTotal) ); ( "byteRetransTotal", Lang.int_t, fun v -> Lang.int (UInt64.to_int v.Srt.Stats.byteRetransTotal) ); ( "byteSndDropTotal", Lang.int_t, fun v -> Lang.int (UInt64.to_int v.Srt.Stats.byteSndDropTotal) ); ( "byteRcvDropTotal", Lang.int_t, fun v -> Lang.int (UInt64.to_int v.Srt.Stats.byteRcvDropTotal) ); ( "byteRcvUndecryptTotal", Lang.int_t, fun v -> Lang.int (UInt64.to_int v.Srt.Stats.byteRcvUndecryptTotal) ); ( "pktSent", Lang.int_t, fun v -> Lang.int (Int64.to_int v.Srt.Stats.pktSent) ); ( "pktRecv", Lang.int_t, fun v -> Lang.int (Int64.to_int v.Srt.Stats.pktRecv) ); ("pktSndLoss", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktSndLoss); ("pktRcvLoss", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktRcvLoss); ("pktRetrans", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktRetrans); ("pktRcvRetrans", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktRcvRetrans); ("pktSentACK", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktSentACK); ("pktRecvACK", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktRecvACK); ("pktSentNAK", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktSentNAK); ("pktRecvNAK", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktRecvNAK); ( "mbpsSendRate", Lang.float_t, fun v -> Lang.float v.Srt.Stats.mbpsSendRate ); ( "mbpsRecvRate", Lang.float_t, fun v -> Lang.float v.Srt.Stats.mbpsRecvRate ); ( "usSndDuration", Lang.int_t, fun v -> Lang.int (Int64.to_int v.Srt.Stats.usSndDuration) ); ( "pktReorderDistance", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktReorderDistance ); ( "pktRcvAvgBelatedTime", Lang.float_t, fun v -> Lang.float v.Srt.Stats.pktRcvAvgBelatedTime ); ( "pktRcvBelated", Lang.int_t, fun v -> Lang.int (Int64.to_int v.Srt.Stats.pktRcvBelated) ); ("pktSndDrop", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktSndDrop); ("pktRcvDrop", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktRcvDrop); ( "pktRcvUndecrypt", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktRcvUndecrypt ); ( "byteSent", Lang.int_t, fun v -> Lang.int (UInt64.to_int v.Srt.Stats.byteSent) ); ( "byteRecv", Lang.int_t, fun v -> Lang.int (UInt64.to_int v.Srt.Stats.byteRecv) ); ( "byteRetrans", Lang.int_t, fun v -> Lang.int (UInt64.to_int v.Srt.Stats.byteRetrans) ); ( "byteSndDrop", Lang.int_t, fun v -> Lang.int (UInt64.to_int v.Srt.Stats.byteSndDrop) ); ( "byteRcvDrop", Lang.int_t, fun v -> Lang.int (UInt64.to_int v.Srt.Stats.byteRcvDrop) ); ( "byteRcvUndecrypt", Lang.int_t, fun v -> Lang.int (UInt64.to_int v.Srt.Stats.byteRcvUndecrypt) ); ( "usPktSndPeriod", Lang.float_t, fun v -> Lang.float v.Srt.Stats.usPktSndPeriod ); ("pktFlowWindow", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktFlowWindow); ( "pktCongestionWindow", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktCongestionWindow ); ("pktFlightSize", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktFlightSize); ("msRTT", Lang.float_t, fun v -> Lang.float v.Srt.Stats.msRTT); ( "mbpsBandwidth", Lang.float_t, fun v -> Lang.float v.Srt.Stats.mbpsBandwidth ); ( "byteAvailSndBuf", Lang.int_t, fun v -> Lang.int v.Srt.Stats.byteAvailSndBuf ); ( "byteAvailRcvBuf", Lang.int_t, fun v -> Lang.int v.Srt.Stats.byteAvailRcvBuf ); ("mbpsMaxBW", Lang.float_t, fun v -> Lang.float v.Srt.Stats.mbpsMaxBW); ("byteMSS", Lang.int_t, fun v -> Lang.int v.Srt.Stats.byteMSS); ("pktSndBuf", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktSndBuf); ("byteSndBuf", Lang.int_t, fun v -> Lang.int v.Srt.Stats.byteSndBuf); ("msSndBuf", Lang.int_t, fun v -> Lang.int v.Srt.Stats.msSndBuf); ( "msSndTsbPdDelay", Lang.int_t, fun v -> Lang.int v.Srt.Stats.msSndTsbPdDelay ); ("pktRcvBuf", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktRcvBuf); ("byteRcvBuf", Lang.int_t, fun v -> Lang.int v.Srt.Stats.byteRcvBuf); ("msRcvBuf", Lang.int_t, fun v -> Lang.int v.Srt.Stats.msRcvBuf); ( "msRcvTsbPdDelay", Lang.int_t, fun v -> Lang.int v.Srt.Stats.msRcvTsbPdDelay ); ( "pktSndFilterExtraTotal", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktSndFilterExtraTotal ); ( "pktRcvFilterExtraTotal", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktRcvFilterExtraTotal ); ( "pktRcvFilterSupplyTotal", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktRcvFilterSupplyTotal ); ( "pktRcvFilterLossTotal", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktRcvFilterLossTotal ); ( "pktSndFilterExtra", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktSndFilterExtra ); ( "pktRcvFilterExtra", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktRcvFilterExtra ); ( "pktRcvFilterSupply", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktRcvFilterSupply ); ( "pktRcvFilterLoss", Lang.int_t, fun v -> Lang.int v.Srt.Stats.pktRcvFilterLoss ); ] let stats_t = Lang.record_t (List.map (fun (name, t, _) -> (name, t)) stats_specs) include Value.MkCustom (struct type content = Srt.socket let name = "srt_socket" let to_json ~pos _ = Runtime_error.raise ~pos ~message:"SRT socket cannot be represented as json" "json" let to_string _ = "" let compare = Stdlib.compare end) let meths = socket_options_meths @ [ ( "id", ([], Lang.int_t), "Socket ID", fun s -> Lang.int (Srt.socket_id s) ); ( "status", ([], Lang.fun_t [] Lang.string_t), "Socket status", fun s -> Lang.val_fun [] (fun _ -> Lang.string (match Srt.getsockstate s with | `Init -> "initialized" | `Opened -> "opened" | `Listening -> "listening" | `Connecting -> "connecting" | `Connected -> "connected" | `Broken -> "broken" | `Closing -> "closing" | `Closed -> "closed" | `Nonexist -> "non_existant")) ); ( "close", ([], Lang.fun_t [] Lang.unit_t), "Close socket", fun s -> Lang.val_fun [] (fun _ -> Srt.close s; Lang.unit) ); ( "bstats", ([], Lang.fun_t [(true, "clear", Lang.nullable_t Lang.bool_t)] stats_t), "Socket bstats", fun s -> Lang.val_fun [("clear", "clear", Some Lang.null)] (fun p -> let clear = Lang.to_valued_option Lang.to_bool (List.assoc "clear" p) in let stats = Srt.Stats.bstats ?clear s in Lang.record (List.map (fun (n, _, fn) -> (n, fn stats)) stats_specs)) ); ( "bistats", ( [], Lang.fun_t [ (true, "clear", Lang.nullable_t Lang.bool_t); (true, "instantaneous", Lang.nullable_t Lang.bool_t); ] stats_t ), "Socket bstats", fun s -> Lang.val_fun [ ("clear", "clear", Some Lang.null); ("instantaneous", "instantaneous", Some Lang.null); ] (fun p -> let clear = Lang.to_valued_option Lang.to_bool (List.assoc "clear" p) in let instantaneous = Lang.to_valued_option Lang.to_bool (List.assoc "instantaneous" p) in let stats = Srt.Stats.bistats ?clear ?instantaneous s in Lang.record (List.map (fun (n, _, fn) -> (n, fn stats)) stats_specs)) ); ] let base_t = t let t = Lang.method_t t (List.map (fun (lbl, t, descr, _) -> (lbl, t, descr)) meths) let to_base_value = to_value let to_value s = Lang.meth (to_value s) (List.map (fun (lbl, _, _, m) -> (lbl, m s)) meths) end let srt = Lang.add_module "srt" let clock = Lang.add_builtin "socket" ~base:srt ~category:`Liquidsoap ~descr:"Decorate a srt socket with all its methods." [("", Socket_value.base_t, None, None)] Socket_value.t (fun p -> Socket_value.(to_value (of_value (List.assoc "" p)))) liquidsoap-2.3.2/src/core/builtins/builtins_ssl.ml000066400000000000000000000214121477303350200223100ustar00rootroot00000000000000(* -*- mode: tuareg; -*- *) (***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let log = Log.make ["ssl"] module Http = Liq_http let protocol_of_value protocol_val = match Lang.to_string protocol_val with | "ssl.3" -> Ssl.SSLv3 [@alert "-deprecated"] | "tls.1" -> Ssl.TLSv1 [@alert "-deprecated"] | "tls.1.1" -> Ssl.TLSv1_1 [@alert "-deprecated"] | "tls.1.2" -> Ssl.TLSv1_2 | "tls.1.3" -> Ssl.TLSv1_3 | _ -> raise (Error.Invalid_value (protocol_val, "Invalid SSL protocol")) let ssl_socket transport ssl = object method typ = "ssl" method transport = transport method file_descr = Ssl.file_descr_of_socket ssl method wait_for ?log event timeout = let event = match event with | `Read -> `Read (Ssl.file_descr_of_socket ssl) | `Write -> `Write (Ssl.file_descr_of_socket ssl) | `Both -> `Both (Ssl.file_descr_of_socket ssl) in Tutils.wait_for ?log event timeout method read = Ssl.read ssl method write = Ssl.write ssl method close = let fd = Ssl.file_descr_of_socket ssl in Fun.protect ~finally:(fun () -> Unix.close fd) (fun () -> ignore (Ssl.close_notify ssl)) end let server ~min_protocol ~max_protocol ~read_timeout ~write_timeout ~password ~certificate ~key transport = let context = Ssl.create_context (Ssl.SSLv23 [@alert "-deprecated"]) Ssl.Server_context in let () = ignore (Option.map (Ssl.set_min_protocol_version context) min_protocol) in let () = ignore (Option.map (Ssl.set_max_protocol_version context) max_protocol) in let () = ignore (Option.map (fun password -> Ssl.set_password_callback context (fun _ -> password)) password); Ssl.use_certificate context (certificate ()) (key ()) in object method transport = transport method accept ?timeout sock = let s, caller = Http.accept ?timeout sock in try (match timeout with | Some timeout -> Http.set_socket_default ~read_timeout:timeout ~write_timeout:timeout s | None -> ()); let ssl_s = Ssl.embed_socket s context in Ssl.accept ssl_s; Http.set_socket_default ~read_timeout ~write_timeout s; (ssl_socket transport ssl_s, caller) with exn -> let bt = Printexc.get_raw_backtrace () in Unix.close s; Printexc.raise_with_backtrace exn bt end let transport ~min_protocol ~max_protocol ~read_timeout ~write_timeout ~password ~certificate ~key () = object (self) method name = "ssl" method protocol = "https" method default_port = 443 method connect ?bind_address ?timeout ?prefer host port = try let ctx = Ssl.create_context (Ssl.SSLv23 [@alert "-deprecated"]) Ssl.Client_context in let () = ignore (Option.map (Ssl.set_min_protocol_version ctx) min_protocol) in let () = ignore (Option.map (Ssl.set_max_protocol_version ctx) max_protocol) in (* TODO: add option.. *) Ssl.set_verify ctx [] (Some Ssl.client_verify_callback); (* Add certificate from transport if passed. *) (try let cert = Utils.read_all (certificate ()) in Ssl.add_cert_to_store ctx cert with _ -> ()); Ssl.set_verify_depth ctx 3; ignore (Ssl.set_default_verify_paths ctx); let unix_socket = Http.connect ?bind_address ?timeout ?prefer host port in try let socket = Ssl.embed_socket unix_socket ctx in (try Ssl.set_client_SNI_hostname socket host with _ -> ()); Ssl.connect socket; let err = Ssl.get_verify_result socket in if err <> 0 then Runtime_error.raise ~pos:[] ~message: (Printf.sprintf "SSL verification error: %s" (Ssl.get_verify_error_string err)) "ssl"; ssl_socket self socket with exn -> let bt = Printexc.get_raw_backtrace () in Unix.close unix_socket; Printexc.raise_with_backtrace exn bt with exn -> let bt = Printexc.get_raw_backtrace () in Lang.raise_as_runtime ~bt ~kind:"ssl" exn method server = server ~min_protocol ~max_protocol ~read_timeout ~write_timeout ~password ~certificate ~key self end let _ = Lang.add_builtin ~base:Modules.http_transport "ssl" ~category:`Internet ~descr:"Https transport using libssl" [ ( "read_timeout", Lang.nullable_t Lang.float_t, Some Lang.null, Some "Read timeout" ); ( "write_timeout", Lang.nullable_t Lang.float_t, Some Lang.null, Some "Write timeout" ); ( "password", Lang.nullable_t Lang.string_t, Some Lang.null, Some "SSL certificate password" ); ( "min_protocol", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Minimal accepted SSL protocol. One of, from least recent to most \ recent: `\"ssl.3\"`, `\"tls.1\"`, `\"tls.1.1\"`, `\"tls.1.2\"` or \ `\"tls.1.3\"`. The most recent available protocol between client \ and server is negotiated when initiating communication between \ minimal and maximal protocol version. All protocols up to \ `\"tls.1.2\"` and above are now deprecated so you might want to set \ this value to one of those two. Default to lowest support protocol \ if not set." ); ( "max_protocol", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Maximal accepted SSL protocol. One of, from least recent to most \ recent: `\"ssl.3\"`, `\"tls.1\"`, `\"tls.1.1\"`, `\"tls.1.2\"` or \ `\"tls.1.3\"`. The most recent available protocol between client \ and server is negotiated when initiating communication between \ minimal and maximal protocol version. Defaults to highest protocol \ supported if not set." ); ( "certificate", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Path to certificate file. Required in server mode, e.g. \ `input.harbor`, etc. If passed in client mode, certificate is added \ to the list of valid certificates." ); ( "key", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Path to certificate private key. Required in server mode, e.g. \ `input.harbor`, etc." ); ] Lang.http_transport_t (fun p -> let read_timeout = Lang.to_valued_option Lang.to_float (List.assoc "read_timeout" p) in let read_timeout = Option.value ~default:Harbor_base.conf_timeout#get read_timeout in let write_timeout = Lang.to_valued_option Lang.to_float (List.assoc "write_timeout" p) in let write_timeout = Option.value ~default:Harbor_base.conf_timeout#get write_timeout in let password = Lang.to_valued_option Lang.to_string (List.assoc "password" p) in let min_protocol = Option.map protocol_of_value (Lang.to_option (List.assoc "min_protocol" p)) in let max_protocol = Option.map protocol_of_value (Lang.to_option (List.assoc "max_protocol" p)) in let find name () = match Lang.to_valued_option Lang.to_string (List.assoc name p) with | None -> Runtime_error.raise ~pos:(Lang.pos p) "Cannot find " ^ name ^ "file!" | Some path -> Utils.check_readable ~pos:(Lang.pos p) path in let certificate = find "certificate" in let key = find "key" in Lang.http_transport (transport ~min_protocol ~max_protocol ~read_timeout ~write_timeout ~password ~certificate ~key ())) liquidsoap-2.3.2/src/core/builtins/builtins_string_extra.ml000066400000000000000000000147051477303350200242270ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let log = Log.make ["lang"; "string"] let conf_string = Dtools.Conf.void ~p:(Configure.conf#plug "string") "String settings" let () = let conf_default_encoding = Dtools.Conf.string ~p:(conf_string#plug "default_encoding") ~d:"utf8" "Default encoding for `string.length`, `string.chars` and `string.sub`" in conf_default_encoding#on_change (fun v -> let enc = match v with | "ascii" -> `Ascii | "utf8" -> `Utf8 | _ -> log#important "Invalid value %s for `settings.string.default_encoding`! \ Should be one of: \"ascii\" or \"utf8\"." v; `Utf8 in Liquidsoap_lang.Builtins_string.default_encoding := enc) let string = Liquidsoap_lang.Builtins_string.string let string_annotate = Lang.add_module ~base:string "annotate" let _ = Lang.add_builtin ~base:string_annotate "parse" ~category:`String ~descr: "Parse a string of the form `=,...:` as given by the \ `annotate:` protocol" [("", Lang.string_t, None, None)] (Lang.product_t Lang.metadata_t Lang.string_t) (fun p -> let v = List.assoc "" p in try let metadata, uri = Annotate.parse (Lang.to_string v) in Lang.product (Lang.metadata (Frame.Metadata.from_list metadata)) (Lang.string uri) with Annotate.Error err -> Lang.raise_error ~message:err ~pos:(Lang.pos p) "string") let _ = Lang.add_builtin ~base:string "recode" ~category:`String ~descr:"Convert a string. Effective only if Camomile is enabled." [ ( "in_enc", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Input encoding. Autodetected if null." ); ( "out_enc", Lang.string_t, Some (Lang.string "UTF-8"), Some "Output encoding." ); ("", Lang.string_t, None, None); ] Lang.string_t (fun p -> try let in_enc = List.assoc "in_enc" p |> Lang.to_valued_option Lang.to_string |> Option.map Charset.of_string in let out_enc = List.assoc "out_enc" p |> Lang.to_string |> Charset.of_string in let string = Lang.to_string (List.assoc "" p) in Lang.string (Charset.convert ?source:in_enc ~target:out_enc string) with exn -> let bt = Printexc.get_raw_backtrace () in Lang.raise_as_runtime ~bt ~kind:"string" exn) let _ = Lang.add_builtin "%" ~category:`String ~descr: "`pattern % [...,(k,v),...]` changes in the pattern occurrences of:\n\n\ - `$(k)` into `v`\n\ - `$(if $(k2),\"a\",\"b\") into \"a\" if k2 is found in the list, \"b\" \ otherwise." [("", Lang.string_t, None, None); ("", Lang.metadata_t, None, None)] Lang.string_t (fun p -> let s = Lang.to_string (Lang.assoc "" 1 p) in let l = List.map (fun p -> let a, b = Lang.to_product p in (Lang.to_string a, Lang.to_string b)) (Lang.to_list (Lang.assoc "" 2 p)) in Lang.string (Utils.interpolate (fun k -> List.assoc k l) s)) let string_apic = Lang.add_module ~base:string "apic" let _ = let t = Lang.method_t Lang.string_t [ ("mime", ([], Lang.string_t), "Mime type"); ("picture_type", ([], Lang.int_t), "Picture type"); ("description", ([], Lang.string_t), "Description"); ] in Lang.add_builtin ~base:string_apic "parse" ~category:`Metadata [("", Lang.string_t, None, Some "APIC data.")] t ~descr: "Parse APIC ID3v2 tags (such as those obtained in the APIC tag from \ `file.metadata.id3v2`). The returned values are: mime, picture type, \ description, and picture data." (fun p -> let apic = Lang.to_string (List.assoc "" p) in let apic = Metadata.ID3v2.parse_apic apic in Lang.meth (Lang.string apic.Metadata.ID3v2.data) [ ("mime", Lang.string apic.Metadata.ID3v2.mime); ("picture_type", Lang.int apic.Metadata.ID3v2.picture_type); ("description", Lang.string apic.Metadata.ID3v2.description); ]) let string_pic = Lang.add_module ~base:string "pic" let _ = let t = Lang.method_t Lang.string_t [ ("format", ([], Lang.string_t), "Picture format"); ("picture_type", ([], Lang.int_t), "Picture type"); ("description", ([], Lang.string_t), "Description"); ] in Lang.add_builtin ~base:string_pic "parse" ~category:`Metadata [("", Lang.string_t, None, Some "PIC data.")] t ~descr: "Parse PIC ID3v2 tags (such as those obtained in the PIC tag from \ `file.metadata.id3v2`). The returned values are: format, picture type, \ description, and picture data." (fun p -> let pic = Lang.to_string (List.assoc "" p) in let pic = Metadata.ID3v2.parse_pic pic in Lang.meth (Lang.string pic.Metadata.ID3v2.pic_data) [ ("format", Lang.string pic.Metadata.ID3v2.pic_format); ("picture_type", Lang.int pic.Metadata.ID3v2.pic_type); ("description", Lang.string pic.Metadata.ID3v2.pic_description); ]) let _ = Lang.add_builtin ~base:string "id" ~category:`String ~descr:"Generate an identifier with given operator name." [("", Lang.string_t, None, Some "Operator name.")] Lang.string_t (fun p -> let name = List.assoc "" p |> Lang.to_string in Lang.string (Lang_string.generate_id name)) liquidsoap-2.3.2/src/core/builtins/builtins_sys.ml000066400000000000000000000424071477303350200223340ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Extralib let configure = Modules.configure let () = List.iter (fun (name, kind, str) -> ignore (Lang.add_builtin_base ~category:`Configuration ~descr:(Printf.sprintf "Liquidsoap's %s." kind) ~base:configure name (`String str) Lang.string_t)) [ ("libdir", "library directory", Configure.liq_libs_dir ()); ("bindir", "Internal script directory", Configure.bin_dir ()); ("rundir", "PID file directory", Configure.rundir ()); ("logdir", "logging directory", Configure.logdir ()); ] (** Liquidsoap stuff *) let log = Lang.log let encoder = Modules.encoder let conf_runtime = Dtools.Conf.void ~p:(Configure.conf#plug "runtime") "Runtime configuration." let conf_strip_types_types = Dtools.Conf.bool ~p:(conf_runtime#plug "strip_types") ~d:true "Strip runtime types whenever possible to optimize memory usage." let _ = let kind = Lang.univ_t () in Lang.add_builtin ~category:`Liquidsoap ~base:encoder "content_type" ~descr:"Return the content-type (mime) of an encoder, if known." [("", Lang.format_t kind, None, None)] Lang.string_t (fun p -> let f = Lang.to_format (List.assoc "" p) in try Lang.string (Encoder.mime f) with _ -> Lang.string "") let _ = let kind = Lang.univ_t () in Lang.add_builtin ~category:`Liquidsoap ~base:encoder "extension" ~descr:"Return the file extension of an encoder, if known." [("", Lang.format_t kind, None, None)] Lang.string_t (fun p -> let f = Lang.to_format (List.assoc "" p) in try Lang.string (Encoder.extension f) with _ -> Lang.string "") let decoder = Modules.decoder let decoder_oblivious = Lang.add_module ~base:decoder "oblivious" let _ = (* The type of the test function for external decoders. Return is one of: . 0: no audio . -1: audio with unknown number of channels. . x >= 1: audio with a fixed number (x) of channels. *) let test_file_t = Lang.fun_t [(false, "", Lang.string_t)] Lang.int_t in let test_arg = ( "test", test_file_t, None, Some "Function used to determine if a file should be decoded by the \ decoder. Returned values are: 0: no decodable audio, -1: decodable \ audio but number of audio channels unknown, x: fixed number of \ decodable audio channels." ) in let test_f f file = Lang.to_int (Lang.apply f [("", Lang.string file)]) in ignore (Lang.add_builtin ~base:decoder "add" ~category:`Liquidsoap ~descr: "Register an external decoder. The encoder should output in WAV \ format to his standard output (stdout) and read data from its \ standard input (stdin)." [ ("name", Lang.string_t, None, Some "Format/decoder's name."); ("description", Lang.string_t, None, Some "Description of the decoder."); ( "mimes", Lang.list_t Lang.string_t, Some (Lang.list []), Some "List of mime types supported by this decoder. Empty means any \ mime type should be accepted." ); ( "file_extensions", Lang.list_t Lang.string_t, Some (Lang.list []), Some "List of file extensions. Empty means any file extension should \ be accepted." ); ("priority", Lang.int_t, Some (Lang.int 1), Some "Decoder priority"); test_arg; ("", Lang.string_t, None, Some "Process to start."); ] Lang.unit_t (fun p -> let process = Lang.to_string (Lang.assoc "" 1 p) in let name = Lang.to_string (List.assoc "name" p) in let doc = Lang.to_string (List.assoc "description" p) in let mimes = List.map Lang.to_string (Lang.to_list (List.assoc "mimes" p)) in let mimes = if mimes = [] then None else Some mimes in let file_extensions = List.map Lang.to_string (Lang.to_list (List.assoc "file_extensions" p)) in let file_extensions = if file_extensions = [] then None else Some file_extensions in let priority = Lang.to_int (List.assoc "priority" p) in let test = List.assoc "test" p in External_decoder.register_stdin ~name ~doc ~priority ~mimes ~file_extensions ~test:(test_f test) process; Lang.unit)); let process_t = Lang.fun_t [(false, "", Lang.string_t)] Lang.string_t in Lang.add_builtin ~base:decoder_oblivious "add" ~category:`Liquidsoap ~descr: "Register an external file decoder. The encoder should output in WAV \ format to his standard output (stdout) and read data from the file it \ receives. The estimated remaining duration for this decoder will be \ unknown until the `buffer` last seconds of the file. If possible, it is \ recommended to decode from stdin and use `decoder.add`." [ ("name", Lang.string_t, None, Some "Format/decoder's name."); ("description", Lang.string_t, None, Some "Description of the decoder."); test_arg; ("priority", Lang.int_t, Some (Lang.int 1), Some "Decoder priority"); ( "mimes", Lang.list_t Lang.string_t, Some (Lang.list []), Some "List of mime types supported by this decoder. Empty means any mime \ type should be accepted." ); ( "file_extensions", Lang.list_t Lang.string_t, Some (Lang.list []), Some "List of file extensions. Empty means any file extension should be \ accepted." ); ("buffer", Lang.float_t, Some (Lang.float 5.), None); ( "", process_t, None, Some "Process to start. The function takes the filename as argument and \ returns the process to start." ); ] Lang.unit_t (fun p -> let f = Lang.assoc "" 1 p in let name = Lang.to_string (List.assoc "name" p) in let doc = Lang.to_string (List.assoc "description" p) in let prebuf = Lang.to_float (List.assoc "buffer" p) in let process file = Lang.to_string (Lang.apply f [("", Lang.string file)]) in let test = List.assoc "test" p in let priority = Lang.to_int (List.assoc "priority" p) in let mimes = List.map Lang.to_string (Lang.to_list (List.assoc "mimes" p)) in let mimes = if mimes = [] then None else Some mimes in let file_extensions = List.map Lang.to_string (Lang.to_list (List.assoc "file_extensions" p)) in let file_extensions = if file_extensions = [] then None else Some file_extensions in External_decoder.register_oblivious ~name ~doc ~priority ~mimes ~file_extensions ~test:(test_f test) ~process prebuf; Lang.unit) (** Misc control/system functions. *) let _ = let descr = "Execute a liquidsoap server command." in let category = `Liquidsoap in let params = [ ("", Lang.string_t, None, Some "Command to execute."); ( "", Lang.string_t, Some (Lang.string ""), Some "Argument for the command." ); ] in let return_t = Lang.list_t Lang.string_t in let execute p = let c = Lang.to_string (Lang.assoc "" 1 p) in let a = Lang.to_string (Lang.assoc "" 2 p) in let s = match a with "" -> c | _ -> c ^ " " ^ a in let r = try Server.exec s with Not_found -> "Command not found!" in Lang.list (List.map Lang.string (Re.Pcre.split ~rex:(Re.Pcre.regexp "\r?\n") r)) in Lang.add_builtin ~base:Modules.server "execute" ~category ~descr params return_t execute let locale = Lang.add_module ~base:Modules.runtime "locale" let _ = Lang.add_builtin ~base:locale "set" ~category:`System ~descr: "Set the system's locale. This sets `LANG` and `LC_ALL` environment \ variables to the given value and then calls `setlocale`. This is set to \ `\"C\"` on startup, which defaults to the system's default locale. Keep \ in mind that changing this can potentially impact some functions such a \ `float_of_string`." [("", Lang.string_t, None, None)] Lang.unit_t (fun p -> Utils.force_locale (Lang.to_string (List.assoc "" p)); Lang.unit) let _ = Lang.add_builtin "shutdown" ~category:`System ~descr:"Shutdown the application." [("code", Lang.int_t, Some (Lang.int 0), Some "Exit code. Default: `0`")] Lang.unit_t (fun p -> Configure.restart := false; let code = Lang.to_int (List.assoc "code" p) in Tutils.shutdown code; Lang.unit) let _ = Lang.add_builtin "restart" ~category:`System ~descr:"Restart the application." [] Lang.unit_t (fun _ -> Configure.restart := true; Tutils.shutdown 0; Lang.unit) let _ = Lang.add_builtin "exit" ~category:`System ~descr: "Immediately stop the application. This should only be used in extreme \ cases or to specify an exit value. The recommended way of stopping \ Liquidsoap is to use shutdown." [("", Lang.int_t, None, Some "Exit value.")] Lang.unit_t (fun p -> let n = Lang.to_int (List.assoc "" p) in flush_all (); exit n) let reopen = Lang.add_module "reopen" let () = let reopen name descr f = ignore (Lang.add_builtin ~base:reopen name ~category:`System ~descr [("", Lang.string_t, None, None)] Lang.unit_t (fun p -> let file = Lang.to_string (List.assoc "" p) in f file; Lang.unit)) in reopen "stdin" "Reopen standard input on the given file" (Utils.reopen_in stdin); reopen "stdout" "Reopen standard output on the given file" (Utils.reopen_out stdout); reopen "stderr" "Reopen standard error on the given file" (Utils.reopen_out stderr) let _ = Lang.add_builtin ~base:Modules.process "pid" ~category:`System [] Lang.int_t ~descr:"Get the process' pid." (fun _ -> Lang.int (Unix.getpid ())) let _ = Lang.add_builtin "log" ~category:`Liquidsoap ~descr:"Log a message." [ ("label", Lang.string_t, Some (Lang.string "lang"), None); ("level", Lang.int_t, Some (Lang.int 3), None); ("", Lang.string_t, None, None); ] Lang.unit_t (fun p -> let msg = Lang.to_string (List.assoc "" p) in let label = Lang.to_string (List.assoc "label" p) in let level = Lang.to_int (List.assoc "level" p) in (Log.make [label])#f level "%s" msg; Lang.unit) let _ = (* Cheap implementation of "getopt" which does not really deserve its name since it has little to do with the standards that getopt(3) implements. A complete rework of argv() and getopt() should eventually be done. *) let argv = Shebang.argv in let offset = (* Index of the last non-script parameter on the command-line. *) let rec find i = if i >= Array.length argv || argv.(i) = "--" then i else find (i + 1) in find 0 in let opts = ref (Array.to_list (Array.sub argv offset (Array.length argv - offset))) in ignore (Lang.add_builtin "getopt" ~category:`System [ ("default", Lang.string_t, Some (Lang.string ""), None); ("", Lang.string_t, None, None); ] Lang.string_t ~descr: "Parse command line options:\n\ `getopt(\"-o\")` returns \"1\" if \"-o\" was passed without any \ parameter, \"0\" otherwise.\n\ `getopt(default=\"X\",\"-o\")` returns \"Y\" if \"-o Y\" was passed, \ \"X\" otherwise.\n\ The result is removed from the list of arguments, affecting subsequent\n\ calls to `argv()` and `getopt()`." (fun p -> let default = Lang.to_string (List.assoc "default" p) in let name = Lang.to_string (List.assoc "" p) in let argv = !opts in if default = "" then ( try ignore (List.find (fun x -> x = name) argv); opts := List.filter (fun x -> x <> name) argv; Lang.string "1" with Not_found -> Lang.string "0") else ( let rec find l l' = match l with | [] -> (default, List.rev l') | e :: v :: l when e = name -> (v, List.rev_append l' l) | e :: l -> find l (e :: l') in let v, l = find argv [] in opts := l; Lang.string v))); Lang.add_builtin "argv" ~category:`System ~descr: "Get command-line parameters. The parameters are numbered starting from \ 1, the zeroth parameter being the script name." [ ("default", Lang.string_t, Some (Lang.string ""), None); ("", Lang.int_t, None, None); ] Lang.string_t (fun p -> let default = Lang.to_string (List.assoc "default" p) in let i = Lang.to_int (List.assoc "" p) in let opts = !opts in if i = 0 then ( (* Special case so that argv(0) returns the script name *) let i = offset - 1 in if 0 <= i && i < Array.length argv then Lang.string argv.(i) else Lang.string default) else if i < List.length opts then Lang.string (List.nth opts i) else Lang.string default) let playlist_parse = Lang.add_builtin ~base:Modules.playlist "parse" ~category:`Liquidsoap [ ( "path", Lang.string_t, Some (Lang.string ""), Some "Default path for files." ); ( "mime", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Mime type for the playlist" ); ("", Lang.string_t, None, None); ] (Lang.list_t (Lang.product_t Lang.metadata_t Lang.string_t)) ~descr: "Try to parse a local playlist. Return a list of (metadata,URI) items, \ where metadata is a list of (key,value) bindings." (fun p -> let f = Lang.to_string (List.assoc "" p) in let f = Lang_string.home_unrelate f in if not (Sys.file_exists f) then Lang.raise_error ~pos:(Lang.pos p) ~message: (Printf.sprintf "File %s does not exist!" (Lang_string.quote_string f)) "playlist"; if Sys.is_directory f then Lang.raise_error ~pos:(Lang.pos p) ~message: (Printf.sprintf "File %s is a directory! A regular file was expected." (Lang_string.quote_string f)) "playlist"; let content = Utils.read_all f in let pwd = let pwd = Lang.to_string (List.assoc "path" p) in if pwd = "" then Filename.dirname f else pwd in let mime = Lang.to_valued_option Lang.to_string (List.assoc "mime" p) in try let _, l = match mime with | None -> Playlist_parser.search_valid ~pwd content | Some mime -> ( match Plug.get Playlist_parser.parsers mime with | Some plugin -> (mime, plugin.Playlist_parser.parser ~pwd content) | None -> log#important "Unknown mime type, trying autodetection."; Playlist_parser.search_valid ~pwd content) in let process m = let f (n, v) = Lang.product (Lang.string n) (Lang.string v) in Lang.list (List.map f m) in let process (m, uri) = Lang.product (process m) (Lang.string uri) in Lang.list (List.map process l) with _ -> Lang.list []) (** Sound utils. *) let _ = Lang.add_builtin "seconds_of_main" ~category:`Liquidsoap ~descr:"Convert a number of main ticks in seconds." [("", Lang.int_t, None, None)] Lang.float_t (fun p -> Lang.float (Frame.seconds_of_main (Lang.to_int (List.assoc "" p)))) let environment = let ss = Lang.product_t Lang.string_t Lang.string_t in let ret_t = Lang.list_t ss in Lang.add_builtin "environment" ~category:`System ~descr:"Return the process environment." [] ret_t (fun _ -> let l = Lang.environment () in let l = List.map (fun (x, y) -> (Lang.string x, Lang.string y)) l in let l = List.map (fun (x, y) -> Lang.product x y) l in Lang.list l) let _ = Lang.add_builtin ~base:environment "set" ~category:`System ~descr:"Set the value associated to a variable in the process environment." [ ("", Lang.string_t, None, Some "Variable to be set."); ("", Lang.string_t, None, Some "Value to set."); ] Lang.unit_t (fun p -> let label = Lang.to_string (Lang.assoc "" 1 p) in let value = Lang.to_string (Lang.assoc "" 2 p) in Unix.putenv label value; Lang.unit) liquidsoap-2.3.2/src/core/builtins/builtins_thread.ml000066400000000000000000000136611477303350200227650ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let thread = Modules.thread let thread_run = Lang.add_module ~base:thread "run" let should_stop = Atomic.make false let () = Lifecycle.before_scheduler_shutdown ~name:"thread shutdown" (fun () -> Atomic.set should_stop true) let _ = Lang.add_builtin ~base:thread "delay" ~category:`Programming ~descr:"Delay the current thread by the given duration in seconds." [("", Lang.float_t, None, None)] Lang.unit_t (fun p -> Unix.sleepf (Lang.to_float (List.assoc "" p)); Lang.unit) let _ = Lang.add_builtin ~base:thread_run "recurrent" ~category:`Programming [ ( "fast", Lang.bool_t, Some (Lang.bool true), Some "Whether the thread is supposed to return quickly or not. Typically, \ blocking tasks (e.g. fetching data over the internet) should not be \ considered to be fast. When set to `false` its priority will be \ lowered below that of request resolutions and fast timeouts. This \ is only effective if you set a dedicated queue for fast tasks, see \ the \"scheduler\" settings for more details." ); ( "delay", Lang.float_t, Some (Lang.float 0.), Some "Delay (in sec.) after which the thread should be launched." ); ( "on_error", Lang.nullable_t (Lang.fun_t [(false, "", Lang.error_t)] Lang.float_t), Some Lang.null, Some "Error callback executed when an error occurred while running the \ given function. When passed, all raised errors are silenced unless \ re-raised by the callback." ); ( "", Lang.fun_t [] Lang.float_t, None, Some "Function to execute recurrently. The returned value is the delay \ (in sec.) in which the function should be run again (it won't be \ run if the value is strictly negative)." ); ] Lang.unit_t ~descr:"Run a recurrent function in a separate thread." (fun p -> let delay = Lang.to_float (List.assoc "delay" p) in let f = List.assoc "" p in let priority = if Lang.to_bool (List.assoc "fast" p) then `Maybe_blocking else `Blocking in let on_error = Lang.to_option (List.assoc "on_error" p) in let on_error = Option.map (fun on_error exn bt -> let error = Lang.runtime_error_of_exception ~bt ~kind:"output" exn in Lang.apply on_error [("", Lang.error error)]) on_error in let f () = try Lang.to_float (Lang.apply f []) with exn -> ( let bt = Printexc.get_raw_backtrace () in match on_error with | Some fn -> Lang.to_float (fn exn bt) | None -> Lang.raise_as_runtime ~bt ~kind:"eval" exn) in let rec task delay = { Duppy.Task.priority; events = [`Delay delay]; handler = (fun _ -> let delay = f () in if Atomic.get should_stop then [] else ( Clock.after_eval (); if delay >= 0. then [task delay] else [])); } in Lifecycle.after_start ~name:"thread start" (fun () -> Duppy.Task.add Tutils.scheduler (task delay)); Lang.unit) let _ = let fun_t = Lang.fun_t [(false, "backtrace", Lang.string_t); (false, "", Lang.error_t)] Lang.unit_t in Lang.add_builtin ~base:thread "on_error" ~category:`Programming ~descr: "Register the function to be called when an error of the given kind is \ raised in a thread. Catches all errors if first argument is `null`." [("", Lang.nullable_t Lang.error_t, None, None); ("", fun_t, None, None)] Lang.unit_t (fun p -> let on_err = Lang.to_valued_option Lang.to_error (Lang.assoc "" 1 p) in let fn = Lang.assoc "" 2 p in let handler ~bt err = match (err, on_err) with | Runtime_error.(Runtime_error error), None -> let error = Lang.error error in let bt = Lang.string bt in ignore (Lang.apply fn [("backtrace", bt); ("", error)]); true | Runtime_error.(Runtime_error error), Some err when error.Runtime_error.kind = err.Runtime_error.kind -> let error = Lang.error error in let bt = Lang.string bt in ignore (Lang.apply fn [("backtrace", bt); ("", error)]); true | _ -> false in Stack.push handler Tutils.error_handlers; Lang.unit) let _ = Lang.add_builtin ~base:thread "pause" ~category:`Programming ~descr: "Pause execution for a given amount of seconds. This puts the calling \ thread to sleep and should not be used in the main streaming loop." [("", Lang.float_t, None, Some "Number of seconds of pause.")] Lang.unit_t (fun p -> let t = Lang.to_float (List.assoc "" p) in Thread.delay t; Lang.unit) liquidsoap-2.3.2/src/core/builtins/builtins_time.ml000066400000000000000000000202301477303350200224420ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let _ = let t = ("", Lang.int_t, None, None) in Lang.add_builtin "time_in_mod" ~category:`Time ~flags:[`Hidden] ~descr: ("INTERNAL: time_in_mod(a,b,c) checks that the unix time T " ^ "satisfies a <= T mod c < b") [t; t; t] Lang.bool_t (fun p -> let g pos = Lang.to_int (Lang.assoc "" pos p) in let a, b, c = (g 1, g 2, g 3) in let t = Unix.localtime (Unix.time ()) in let t = t.Unix.tm_sec + (t.Unix.tm_min * 60) + (t.Unix.tm_hour * 60 * 60) + (t.Unix.tm_wday * 24 * 60 * 60) in let t = t mod c in if a <= b then Lang.bool (a <= t && t < b) else Lang.bool (not (b <= t && t < a))) let time = Lang.add_builtin ~category:`Time "time" ~descr: "Return the current time since 00:00:00 GMT, Jan. 1, 1970, in seconds." [] Lang.float_t (fun _ -> Lang.float (Unix.gettimeofday ())) let _ = Lang.add_builtin ~category:`Time ~base:time "up" ~descr:"Current time, in seconds, since the script has started." [] Lang.float_t (fun _ -> Lang.float (Utils.uptime ())) let _ = let time_t = Lang.method_t Lang.unit_t [ ("sec", ([], Lang.int_t), "Seconds."); ("min", ([], Lang.int_t), "Minutes."); ("hour", ([], Lang.int_t), "Hours."); ("day", ([], Lang.int_t), "Day of month."); ("month", ([], Lang.int_t), "Month of year."); ("year", ([], Lang.int_t), "Year."); ( "week_day", ([], Lang.int_t), "Day of week (Sunday is 0 or 7, Saturday is 6)." ); ("year_day", ([], Lang.int_t), "Day of year, between `1` and `366`."); ("dst", ([], Lang.bool_t), "Daylight time savings in effect."); ] in let return tm = Lang.record [ ("sec", Lang.int tm.Unix.tm_sec); ("min", Lang.int tm.Unix.tm_min); ("hour", Lang.int tm.Unix.tm_hour); ("day", Lang.int tm.Unix.tm_mday); ("month", Lang.int (1 + tm.Unix.tm_mon)); ("year", Lang.int (1900 + tm.Unix.tm_year)); ("week_day", Lang.int tm.Unix.tm_wday); ("year_day", Lang.int (1 + tm.Unix.tm_yday)); ("dst", Lang.bool tm.Unix.tm_isdst); ] in let nullable_time v = match Lang.to_option v with | Some v -> Lang.to_float v | None -> Unix.gettimeofday () in let descr tz = Printf.sprintf "Convert a time in seconds into a date in the %s time zone (current time \ is used if no argument is provided)." tz in ignore (Lang.add_builtin ~category:`Time ~base:time "local" ~descr:(descr "local") [("", Lang.nullable_t Lang.float_t, Some Lang.null, None)] time_t (fun p -> let t = nullable_time (List.assoc "" p) in return (Unix.localtime t))); ignore (Lang.add_builtin ~category:`Time ~base:time "utc" ~descr:(descr "UTC") [("", Lang.nullable_t Lang.float_t, Some Lang.null, None)] time_t (fun p -> let t = nullable_time (List.assoc "" p) in return (Unix.gmtime t))) let _ = let time_t = Lang.method_t Lang.unit_t [ ("sec", ([], Lang.int_t), "Seconds."); ("min", ([], Lang.int_t), "Minutes."); ("hour", ([], Lang.int_t), "Hours."); ("day", ([], Lang.int_t), "Day of month."); ("month", ([], Lang.int_t), "Month of year."); ("year", ([], Lang.int_t), "Year."); ( "dst", ([], Lang.nullable_t Lang.bool_t), "Daylight time savings in effect." ); ] in Lang.add_builtin ~category:`Time ~base:time "make" ~descr: "Convert a date and time in the local timezone into a time, in seconds, \ since 00:00:00 GMT, Jan. 1, 1970." [("", time_t, None, None)] Lang.float_t (fun p -> let tm = List.assoc "" p in let tm = { Utils.tm_sec = Lang.to_int (Value.invoke tm "sec"); tm_min = Lang.to_int (Value.invoke tm "min"); tm_hour = Lang.to_int (Value.invoke tm "hour"); tm_mday = Lang.to_int (Value.invoke tm "day"); tm_mon = Lang.to_int (Value.invoke tm "month") - 1; tm_year = Lang.to_int (Value.invoke tm "year") - 1900; tm_isdst = Lang.to_valued_option Lang.to_bool (Value.invoke tm "dst"); } in Lang.float (Utils.mktime tm)) let _ = let processor = MenhirLib.Convert.Simplified.traditional2revised Liquidsoap_lang.Parser.time_predicate in Lang.add_builtin ~category:`Time ~base:time "predicate" ~descr:"Parse a string as a time predicate" [("", Lang.string_t, None, None)] (Lang.fun_t [] Lang.bool_t) (fun p -> let v = List.assoc "" p in let predicate = Lang.to_string v in let lexbuf = Sedlexing.Utf8.from_string predicate in try let tokenizer = Liquidsoap_lang.Preprocessor.mk_tokenizer lexbuf in let predicate = Liquidsoap_lang.Term_reducer.to_term (processor tokenizer) in Lang.val_fun [] (fun _ -> Liquidsoap_lang.Evaluation.eval predicate) with _ -> Lang.raise_error ~message: (Printf.sprintf "Failed to parse %s as time predicate" predicate) ~pos:(Lang.pos p) "string") let _ = let tz_t = Lang.method_t Lang.string_t [ ("daylight", ([], Lang.string_t), "Daylight Savings Time"); ( "utc_diff", ([], Lang.int_t), "Difference in seconds between the current timezone and UTC." ); ] in Lang.add_builtin ~category:`Time ~base:time "zone" ~descr:"Returns a description of the time zone set for the running process." [] tz_t (fun _ -> let std, dst = Utils.timezone_by_name () in let tz = Utils.timezone () in Lang.meth (Lang.string std) [("daylight", Lang.string dst); ("utc_diff", Lang.int tz)]) let _ = Lang.add_builtin ~category:`Time ~base:time "string" ~descr: "Obtain a string representation of the current time. It takes a string \ as argument where special strings are replaced roughly following \ [strftime](https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes): \ %H is replaced by the current hour, %M minute, %S second, %A week day \ (%a week day abbreviated), %d month day, %B month name (%b month name \ abbreviated), %z timezone." ~examples: [ {|s = time.string("Current time is %H:%M.") print(s)|}; {|# Backup a source naming the file based on time output.file({time.string("/path/to/file%H%M%S.wav")}, ...)|}; ] [ ( "time", Lang.nullable_t Lang.float_t, Some Lang.null, Some "If specified convert the given time (in seconds since 00:00:00 GMT, \ Jan. 1, 1970) instead of the current time." ); ( "", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Description of the string to produce, e.g. `\"Current time is \ %H:%M`\"`." ); ] Lang.string_t (fun p -> let time = List.assoc "time" p |> Lang.to_option |> Option.map Lang.to_float in let s = List.assoc "" p |> Lang.to_option |> Option.map Lang.to_string |> Option.value ~default:"%A, %d %B %Y %H:%M:%S" in Lang.string (Utils.strftime ?time s)) liquidsoap-2.3.2/src/core/builtins/builtins_tls.ml000066400000000000000000000250631477303350200223170ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) module Http = Liq_http module Liq_tls = struct type t = { read_pending : Buffer.t; fd : Unix.file_descr; buf : bytes; mutable state : Tls.Engine.state; } let () = Mirage_crypto_rng_unix.use_default () let buf_len = 4096 let string_of_alert_level = function | Tls.Packet.WARNING -> "Warning" | Tls.Packet.FATAL -> "Fatal" let string_of_failure error = let level, typ = Tls.Engine.alert_of_failure error in Printf.sprintf "%s error: %s" (string_of_alert_level level) (Tls.Packet.alert_type_to_string typ) let write_all ~timeout fd data = Tutils.write_all ~timeout fd (Bytes.unsafe_of_string data) let read ~timeout h len = Tutils.wait_for (`Read h.fd) timeout; let n = Unix.read h.fd h.buf 0 (min len buf_len) in Bytes.sub_string h.buf 0 n let read_pending h = function | None -> () | Some data -> Buffer.add_string h.read_pending (Cstruct.to_string data) let write_response ~timeout h = function | None -> () | Some data -> write_all ~timeout h.fd data let handshake ~timeout h = let rec f () = if Tls.Engine.handshake_in_progress h.state then ( match Tls.Engine.handle_tls h.state (read ~timeout h buf_len) with | Ok (_, Some `Eof, _, _) -> Runtime_error.raise ~pos:[] ~message:"Connection closed while negotiating TLS handshake!" "tls" | Ok (state, None, `Response response, `Data data) -> h.state <- state; read_pending h (Option.map Cstruct.of_string data); write_response ~timeout h response; f () | Error (error, `Response response) -> write_all ~timeout h.fd response; Runtime_error.raise ~pos:[] ~message: (Printf.sprintf "TLS handshake error: %s" (string_of_failure error)) "tls") in f () let init_base ~timeout ~state fd = let buf = Bytes.create buf_len in let read_pending = Buffer.create 4096 in let h = { read_pending; fd; buf; state } in handshake ~timeout h; h let init_server ~timeout ~server fd = let state = Tls.Engine.server server in init_base ~timeout ~state fd let init_client ~timeout ~client fd = let state, hello = Tls.Engine.client client in write_all ~timeout fd hello; init_base ~timeout ~state fd let write ?timeout h b off len = let timeout = Option.value ~default:Harbor_base.conf_timeout#get timeout in match Tls.Engine.send_application_data h.state [Bytes.sub_string b off len] with | None -> len | Some (state, data) -> write_all ~timeout h.fd data; h.state <- state; len let read ?read_timeout ?write_timeout h b off len = let read_timeout = Option.value ~default:Harbor_base.conf_timeout#get read_timeout in let write_timeout = Option.value ~default:Harbor_base.conf_timeout#get write_timeout in let pending = Buffer.length h.read_pending in if 0 < pending then ( let n = min pending len in Buffer.blit h.read_pending 0 b off n; Utils.buffer_drop h.read_pending n; n) else ( let rec f () = match Tls.Engine.handle_tls h.state (read ~timeout:read_timeout h len) with | Ok (state, eof, `Response response, `Data data) -> ( (match response with | None -> () | Some r -> write_all ~timeout:write_timeout h.fd r); h.state <- state; match (eof, data) with | Some `Eof, None -> 0 | _, None -> f () | _, Some data -> let data_len = String.length data in let n = min data_len len in Bytes.blit_string data 0 b off n; if n < data_len then Buffer.add_substring h.read_pending data n (data_len - n); n) | Error (error, `Response response) -> write_all ~timeout:write_timeout h.fd response; Runtime_error.raise ~pos:[] ~message: (Printf.sprintf "TLS read error: %s" (string_of_failure error)) "tls" in f ()) let close ?(timeout = 1.) h = let state, data = Tls.Engine.send_close_notify h.state in write_all ~timeout h.fd data; h.state <- state; Unix.close h.fd end let tls_socket ~session transport = object method typ = "tls" method transport = transport method file_descr = session.Liq_tls.fd method wait_for ?log event timeout = let event = match event with | `Read -> `Read session.Liq_tls.fd | `Write -> `Write session.Liq_tls.fd | `Both -> `Both session.Liq_tls.fd in Tutils.wait_for ?log event timeout method read = Liq_tls.read session method write = Liq_tls.write session method close = Liq_tls.close session end let server ~read_timeout ~write_timeout ~certificate ~key transport = let server = try let certificate = Utils.read_all (certificate ()) in let certificates = Result.get_ok (X509.Certificate.decode_pem_multiple certificate) in let key = Result.get_ok (X509.Private_key.decode_pem (Utils.read_all (key ()))) in match Tls.Config.server ~certificates:(`Single (certificates, key)) () with | Ok server -> server | Error (`Msg message) -> Runtime_error.raise ~pos:[] ~message "tls" with exn -> let bt = Printexc.get_raw_backtrace () in Lang.raise_as_runtime ~bt ~kind:"tls" exn in object method transport = transport method accept ?(timeout = 1.) sock = let fd, caller = Http.accept ~timeout sock in try Http.set_socket_default ~read_timeout:timeout ~write_timeout:timeout fd; let session = Liq_tls.init_server ~timeout ~server fd in Http.set_socket_default ~read_timeout ~write_timeout fd; (tls_socket ~session transport, caller) with exn -> let bt = Printexc.get_raw_backtrace () in Unix.close fd; Printexc.raise_with_backtrace exn bt end let transport ~read_timeout ~write_timeout ~certificate ~key () = object (self) method name = "tls" method protocol = "https" method default_port = 443 method connect ?bind_address ?(timeout = 1.) ?prefer host port = let domain = Domain_name.host_exn (Domain_name.of_string_exn host) in let authenticator = Result.get_ok (Ca_certs.authenticator ()) in let certificate_authenticator = try let certificates = Result.get_ok (X509.Certificate.decode_pem_multiple (Utils.read_all (certificate ()))) in Some (X509.Authenticator.chain_of_trust ~time:(fun () -> Some (Ptime_clock.now ())) certificates) with _ -> None in let authenticator ?ip ~host certs = match certificate_authenticator with | None -> authenticator ?ip ~host certs | Some auth -> let r = auth ?ip ~host certs in if Result.is_ok r then r else authenticator ?ip ~host certs in let client = match Tls.Config.client ~authenticator ~peer_name:domain () with | Ok client -> client | Error (`Msg message) -> Runtime_error.raise ~pos:[] ~message "tls" in let fd = Http.connect ?bind_address ~timeout ?prefer host port in let session = Liq_tls.init_client ~timeout ~client fd in tls_socket ~session self method server = server ~read_timeout ~write_timeout ~certificate ~key self end let _ = Lang.add_builtin ~base:Modules.http_transport "tls" ~category:`Internet ~descr:"Https transport using libtls" [ ( "read_timeout", Lang.nullable_t Lang.float_t, Some Lang.null, Some "Read timeout. Defaults to harbor's timeout if `null`." ); ( "write_timeout", Lang.nullable_t Lang.float_t, Some Lang.null, Some "Write timeout. Defaults to harbor's timeout if `null`." ); ( "certificate", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Path to certificate file. Required in server mode, e.g. \ `input.harbor`, etc. If passed in client mode, certificate is added \ to the list of valid certificates." ); ( "key", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Path to certificate private key. Required in server mode, e.g. \ `input.harbor`, etc. Unused in client mode." ); ] Lang.http_transport_t (fun p -> let read_timeout = Lang.to_valued_option Lang.to_float (List.assoc "read_timeout" p) in let read_timeout = Option.value ~default:Harbor_base.conf_timeout#get read_timeout in let write_timeout = Lang.to_valued_option Lang.to_float (List.assoc "write_timeout" p) in let write_timeout = Option.value ~default:Harbor_base.conf_timeout#get write_timeout in let find name () = match Lang.to_valued_option Lang.to_string (List.assoc name p) with | None -> Runtime_error.raise ~pos:(Lang.pos p) "Cannot find " ^ name ^ "file!" | Some path -> Utils.check_readable ~pos:(Lang.pos p) path in let certificate = find "certificate" in let key = find "key" in Lang.http_transport (transport ~read_timeout ~write_timeout ~certificate ~key ())) liquidsoap-2.3.2/src/core/builtins/builtins_track.ml000066400000000000000000000026111477303350200226130ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let _ = let track_t = Lang.univ_t ~constraints:[Format_type.track] () in Lang.add_builtin ~base:Modules.track "clock" ~category:`Liquidsoap ~descr:"Return the clock associated with the given track." [("", track_t, None, None)] Lang_source.ClockValue.base_t (fun p -> let _, s = Lang.to_track (List.assoc "" p) in Lang_source.ClockValue.to_base_value s#clock) liquidsoap-2.3.2/src/core/builtins/builtins_yaml.ml000066400000000000000000000063601477303350200224560ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let () = Atomic.set Liquidsoap_lang.Builtins_yaml.yaml_parser Yaml.of_string_exn let yaml = Lang.add_module "yaml" let rec yaml_of_json = function | `Assoc l -> `O (List.map (fun (lbl, v) -> (lbl, yaml_of_json v)) l) | `Tuple l -> `A (List.map yaml_of_json l) | `String s -> `String s | `Bool b -> `Bool b | `Float f -> `Float f | `Int i -> `Float (float i) | `Null -> `Null let scalar_style pos = function | "any" -> `Any | "plain" -> `Plain | "single_quoted" -> `Single_quoted | "double_quoted" -> `Double_quoted | "literal" -> `Literal | "folded" -> `Folded | v -> Runtime_error.raise ~message:(Printf.sprintf "Invalid scalar style: %s" v) ~pos "yaml" let layout_style pos = function | "any" -> `Any | "block" -> `Block | "flow" -> `Flow | v -> Runtime_error.raise ~message:(Printf.sprintf "Invalid layout style: %s" v) ~pos "yaml" let _ = Lang.add_builtin ~base:yaml "stringify" ~category:`String ~descr: "Convert a value to YAML. If the value cannot be represented as YAML \ (for instance a function), a `error.yaml` exception is raised." [ ( "scalar_style", Lang.string_t, Some (Lang.string "any"), Some "Scalar style. One of: \"any\", \"plain\", \"single_quoted\", \ \"double_quoted\", \"literal\" or \"folded\"." ); ( "layout_style", Lang.string_t, Some (Lang.string "any"), Some "Layout style. One of: \"any\", \"block\" or \"flow\"." ); ("", Lang.univ_t (), None, None); ] Lang.string_t (fun p -> let pos = Lang.pos p in let v = List.assoc "" p in let scalar_style = scalar_style pos (Lang.to_string (List.assoc "scalar_style" p)) in let layout_style = layout_style pos (Lang.to_string (List.assoc "layout_style" p)) in try let json = Liquidsoap_lang.Builtins_json.json_of_value v in Lang.string (Yaml.to_string_exn ~encoding:`Utf8 ~scalar_style ~layout_style (yaml_of_json json)) with _ -> Runtime_error.raise ~message: (Printf.sprintf "Value %s cannot be represented as YAML" (Value.to_string v)) ~pos "yaml") liquidsoap-2.3.2/src/core/clock.ml000066400000000000000000000503431477303350200170450ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) include Clock_base type active_sync_mode = [ `Automatic | `CPU | `Unsynced | `Passive ] type sync_mode = [ active_sync_mode | `Stopping | `Stopped ] let string_of_sync_mode = function | `Stopped -> "stopped" | `Stopping -> "stopping" | `Automatic -> "auto" | `CPU -> "cpu" | `Unsynced -> "none" | `Passive -> "passive" let active_sync_mode_of_string = function | "auto" -> `Automatic | "cpu" -> `CPU | "none" -> `Unsynced | "passive" -> `Passive | _ -> raise Not_found let log = Log.make ["clock"] let conf_clock = Dtools.Conf.void ~p:(Configure.conf#plug "clock") "Clock settings" let conf_log_delay = Dtools.Conf.float ~p:(conf_clock#plug "log_delay") ~d:1. "How often (in seconds) we should notify latency issues." let conf_log_delay_threshold = Dtools.Conf.float ~p:(conf_clock#plug "log_delay_threshold") ~d:0.2 "Notify latency issues after delay exceeds this threshold." let conf_max_latency = Dtools.Conf.float ~p:(conf_clock#plug "max_latency") ~d:60. "Maximum latency in seconds" ~comments: [ "If the latency gets higher than this value, the outputs will be reset,"; "instead of trying to catch it up second by second."; "The reset is typically only useful to reconnect icecast mounts."; ] let conf_clock_preferred = Dtools.Conf.string ~d:"posix" ~p:(conf_clock#plug "preferred") "Preferred clock implementation. One if: \"posix\" or \"ocaml\"." let conf_clock_sleep_latency = Dtools.Conf.int ~p:(conf_clock#plug "sleep_latency") ~d:5 "How much time ahead (in frame duration) we should be until we let the \ streaming loop sleep." ~comments: [ "Once we have computed the given amount of time time in advance,"; "we wait until re-starting the streaming loop."; ] let time_implementation () = try Hashtbl.find Liq_time.implementations conf_clock_preferred#get with Not_found -> Liq_time.unix let () = Lifecycle.on_init ~name:"Clock initialization" (fun () -> let module Time = (val time_implementation () : Liq_time.T) in log#important "Using %s implementation for latency control" Time.implementation) module Pos = Liquidsoap_lang.Pos module Unifier = Liquidsoap_lang.Unifier type active_params = { sync : active_sync_mode; mutable is_self_sync : bool; log : Log.t; time_implementation : Liq_time.implementation; t0 : Liq_time.t; log_delay : Liq_time.t; log_delay_threshold : Liq_time.t; frame_duration : Liq_time.t; max_latency : Liq_time.t; last_catchup_log : Liq_time.t Atomic.t; outputs : source Queue.t; active_sources : source WeakQueue.t; passive_sources : source WeakQueue.t; on_tick : (unit -> unit) Queue.t; after_tick : (unit -> unit) Queue.t; ticks : int Atomic.t; } type state = [ `Stopping of active_params | `Started of active_params | `Stopped of active_sync_mode ] type clock = { id : string option Unifier.t; sub_ids : string list; stack : Pos.t list Atomic.t; state : state Atomic.t; pending_activations : source Queue.t; sub_clocks : t Queue.t; on_error : (exn -> Printexc.raw_backtrace -> unit) Queue.t; } and t = clock Unifier.t let _default_id { id; pending_activations } = match (Unifier.deref id, Queue.elements pending_activations) with | Some id, _ -> id | None, el :: _ -> el#id | None, _ -> "generic" let _id clock = _default_id clock ^ match clock.sub_ids with [] -> "" | l -> "." ^ String.concat "." l let id c = _id (Unifier.deref c) let set_id c id = Unifier.set (Unifier.deref c).id (Some (Lang_string.generate_id id)) let attach c s = let clock = Unifier.deref c in Queue.push clock.pending_activations s let _detach x s = Queue.filter_out x.pending_activations (fun s' -> s == s'); match Atomic.get x.state with | `Stopped _ -> () | `Stopping { outputs; active_sources; passive_sources } | `Started { outputs; active_sources; passive_sources } -> Queue.filter_out outputs (fun s' -> s == s'); WeakQueue.filter_out active_sources (fun s' -> s == s'); WeakQueue.filter_out passive_sources (fun s' -> s == s') let detach c s = _detach (Unifier.deref c) s let active_sources c = match Atomic.get (Unifier.deref c).state with | `Started { active_sources } | `Stopping { active_sources } -> WeakQueue.elements active_sources | _ -> [] let outputs c = match Atomic.get (Unifier.deref c).state with | `Started { outputs } | `Stopping { outputs } -> Queue.elements outputs | _ -> [] let passive_sources c = match Atomic.get (Unifier.deref c).state with | `Started { passive_sources } | `Stopping { passive_sources } -> WeakQueue.elements passive_sources | _ -> [] let pending_activations c = Queue.elements (Unifier.deref c).pending_activations let sources c = let clock = Unifier.deref c in Queue.elements clock.pending_activations @ match Atomic.get clock.state with | `Started { passive_sources; active_sources; outputs } | `Stopping { passive_sources; active_sources; outputs } -> WeakQueue.elements passive_sources @ WeakQueue.elements active_sources @ Queue.elements outputs | _ -> [] (* Return the clock effective sync. Stopped clocks can be unified with any active type clocks so [`Stopped _] returns [`Stopped]. *) let _sync ?(pending = false) x = match Atomic.get x.state with | `Stopped p when pending -> (p :> sync_mode) | `Stopped _ -> `Stopped | `Stopping _ -> `Stopping | `Started { sync } -> (sync :> sync_mode) let sync c = _sync (Unifier.deref c) let cleanup_source s = try s#force_sleep with _ -> () let pending_clocks = WeakQueue.create () let clocks = Queue.create () let rec _cleanup ~clock { outputs; passive_sources; active_sources } = Queue.iter outputs cleanup_source; WeakQueue.iter passive_sources cleanup_source; WeakQueue.iter active_sources cleanup_source; Queue.iter clock.sub_clocks stop; Queue.filter_out clocks (fun c -> Unifier.deref c == clock) and stop c = let clock = Unifier.deref c in match Atomic.get clock.state with | `Stopped _ | `Stopping _ -> () | `Started ({ sync = `Passive } as x) -> _cleanup ~clock x; x.log#debug "Clock stopped"; Atomic.set clock.state (`Stopped `Passive) | `Started x -> x.log#debug "Clock stopping"; Atomic.set clock.state (`Stopping x) let clocks_started = Atomic.make false let global_stop = Atomic.make false exception Has_stopped let[@inline] check_stopped () = if Atomic.get global_stop then raise Has_stopped let descr clock = let clock = Unifier.deref clock in Printf.sprintf "clock(id=%s,sync=%s%s)" (_id clock) (string_of_sync_mode (_sync clock)) (match Atomic.get clock.state with | `Stopped pending -> Printf.sprintf ",pending=%s" (string_of_sync_mode (pending :> sync_mode)) | _ -> "") let unify = let unify c c' = let clock = Unifier.deref c in let clock' = Unifier.deref c' in Queue.flush_iter clock.pending_activations (Queue.push clock'.pending_activations); Queue.flush_iter clock.sub_clocks (Queue.push clock'.sub_clocks); Queue.flush_iter clock.on_error (Queue.push clock'.on_error); (match (Unifier.deref clock.id, Unifier.deref clock'.id) with | None, None -> Unifier.(clock.id <-- clock'.id) | Some _, None -> Unifier.(clock'.id <-- clock.id) | None, Some _ -> Unifier.(clock.id <-- clock'.id) | Some _, Some id -> log#important "Clocks %s and %s both have id already set. Setting id to %s" (descr c) (descr c') id; Unifier.(clock.id <-- clock'.id)); Unifier.(c <-- c'); Queue.filter_out clocks (fun el -> el == c) in fun ~pos c c' -> let _c = Unifier.deref c in let _c' = Unifier.deref c' in match (_c == _c', Atomic.get _c.state, Atomic.get _c'.state) with | true, _, _ -> () | _, `Stopped s, `Stopped s' when s = s' -> unify c c' | _, `Stopped s, _ when s = `Automatic || (s :> sync_mode) = _sync ~pending:true _c' -> unify c c' | _, _, `Stopped s' when s' = `Automatic || _sync ~pending:true _c = (s' :> sync_mode) -> unify c' c | _ -> raise (Liquidsoap_lang.Error.Clock_conflict (pos, descr c, descr c')) let () = Lifecycle.before_core_shutdown ~name:"Clocks stop" (fun () -> Atomic.set global_stop true; Queue.iter clocks (fun c -> if sync c <> `Passive then stop c)) let _animated_sources { outputs; active_sources } = Queue.elements outputs @ WeakQueue.elements active_sources let _self_sync ~clock x = let self_sync_sources = List.fold_left (fun self_sync_sources s -> match s#self_sync with | _, None -> self_sync_sources | _, Some sync_source -> { sync_source; name = s#id; stack = s#stack } :: self_sync_sources) [] (_animated_sources x) in let self_sync_sources = List.sort_uniq (fun { sync_source = s } { sync_source = s' } -> Stdlib.compare s s') self_sync_sources in if List.length self_sync_sources > 1 then raise (Sync_error { name = Printf.sprintf "clock %s" (_id clock); stack = Atomic.get clock.stack; sync_sources = self_sync_sources; }); let is_self_sync = List.length self_sync_sources = 1 in if x.is_self_sync <> is_self_sync && x.sync = `Automatic then ( x.log#important "Switching to %sself-sync mode" (if is_self_sync then "" else "non "); x.is_self_sync <- is_self_sync); is_self_sync let ticks c = match Atomic.get (Unifier.deref c).state with | `Stopped _ -> 0 | `Stopping { ticks } | `Started { ticks } -> Atomic.get ticks let _time { time_implementation; frame_duration; ticks } = let module Time = (val time_implementation : Liq_time.T) in Time.(frame_duration |*| of_float (float_of_int (Atomic.get ticks))) let _target_time ({ time_implementation; t0 } as c) = let module Time = (val time_implementation : Liq_time.T) in Time.(t0 |+| _time c) let _set_time { time_implementation; t0; frame_duration; ticks } t = let module Time = (val time_implementation : Liq_time.T) in let delta = Time.(to_float (t |-| t0)) in Atomic.set ticks (int_of_float (delta /. Time.to_float frame_duration)) let _after_tick ~clock x = Queue.flush_iter x.after_tick (fun fn -> check_stopped (); fn ()); let module Time = (val x.time_implementation : Liq_time.T) in let end_time = Time.time () in let target_time = _target_time x in check_stopped (); match (x.sync, _self_sync ~clock x, Time.(end_time |<| target_time)) with | `Unsynced, _, _ | `Passive, _, _ | `Automatic, true, _ -> () | `Automatic, false, true | `CPU, _, true -> if Time.( of_float (float conf_clock_sleep_latency#get) |*| x.frame_duration |<=| (target_time |-| end_time)) then Time.sleep_until target_time | _ -> let latency = Time.(end_time |-| target_time) in if Time.(x.max_latency |<=| latency) then ( x.log#severe "Too much latency! Resetting active sources..."; _set_time x end_time; List.iter (fun s -> match s#source_type with | `Passive -> assert false | `Active s -> s#reset | `Output s -> s#reset) (_animated_sources x)) else if Time.( x.log_delay_threshold |<=| latency && x.log_delay |<=| (end_time |-| Atomic.get x.last_catchup_log)) then ( Atomic.set x.last_catchup_log end_time; x.log#severe "We must catchup %.2f seconds!" Time.(to_float (end_time |-| target_time))) let started c = match Atomic.get (Unifier.deref c).state with | `Stopping _ | `Started _ -> true | `Stopped _ -> false let wrap_errors clock fn s = try fn s with exn when exn <> Has_stopped -> let bt = Printexc.get_raw_backtrace () in Printf.printf "Error: %s\n%s\n%!" (Printexc.to_string exn) (Printexc.raw_backtrace_to_string bt); log#severe "Source %s failed while streaming: %s!\n%s" s#id (Printexc.to_string exn) (Printexc.raw_backtrace_to_string bt); _detach clock s; if Queue.length clock.on_error > 0 then Queue.iter clock.on_error (fun fn -> fn exn bt) else Printexc.raise_with_backtrace exn bt let rec active_params c = match Atomic.get (Unifier.deref c).state with | `Stopping s | `Started s -> s | _ when Atomic.get global_stop -> raise Has_stopped | _ -> raise Invalid_state and _activate_pending_sources ~clock x = Queue.flush_iter clock.pending_activations (wrap_errors clock (fun s -> check_stopped (); s#wake_up; match s#source_type with | `Active _ -> WeakQueue.push x.active_sources s | `Output _ -> Queue.push x.outputs s | `Passive -> WeakQueue.push x.passive_sources s)) and _tick ~clock x = _activate_pending_sources ~clock x; let sub_clocks = List.map (fun c -> (c, ticks c)) (Queue.elements clock.sub_clocks) in let sources = _animated_sources x in List.iter (wrap_errors clock (fun s -> check_stopped (); match s#source_type with | `Output s | `Active s -> s#output | _ -> assert false)) sources; Queue.flush_iter x.on_tick (fun fn -> check_stopped (); fn ()); List.iter (fun (c, old_ticks) -> if ticks c = old_ticks then _tick ~clock:(Unifier.deref c) (active_params c)) sub_clocks; Atomic.incr x.ticks; check_stopped (); _after_tick ~clock x; check_stopped () and _clock_thread ~clock x = let has_sources_to_process () = 0 < Queue.length clock.pending_activations || 0 < Queue.length x.outputs || 0 < WeakQueue.length x.active_sources in let on_stop () = x.log#info "Clock thread has stopped"; _cleanup ~clock x; Atomic.set clock.state (`Stopped x.sync) in let run () = try while (match Atomic.get clock.state with `Started _ -> true | _ -> false) && (not (Atomic.get global_stop)) && has_sources_to_process () do _tick ~clock x done; on_stop () with Has_stopped -> on_stop () in ignore (Tutils.create (fun () -> x.log#info "Clock thread is starting"; run ()) () ("Clock " ^ _id clock)) and _can_start ?(force = false) clock = let has_output = force || Queue.exists clock.pending_activations (fun s -> match s#source_type with `Output _ -> true | _ -> false) in let can_start = (not (Atomic.get global_stop)) && (force || Atomic.get clocks_started) in match (can_start, has_output, Atomic.get clock.state) with | true, _, `Stopped (`Passive as sync) | true, true, `Stopped sync -> `True sync | _ -> `False and _start ?force ~sync clock = Unifier.set clock.id (Some (Lang_string.generate_id (_default_id clock))); let id = _id clock in log#important "Starting clock %s with %d source(s) and sync: %s" id (Queue.length clock.pending_activations) (string_of_sync_mode sync); let time_implementation = time_implementation () in let module Time = (val time_implementation : Liq_time.T) in let frame_duration = Time.of_float (Lazy.force Frame.duration) in let max_latency = Time.of_float conf_max_latency#get in let log_delay = Time.of_float conf_log_delay#get in let log_delay_threshold = Time.of_float conf_log_delay_threshold#get in let t0 = Time.time () in let last_catchup_log = Atomic.make t0 in let x = { frame_duration; is_self_sync = false; log_delay; log_delay_threshold; max_latency; time_implementation; t0; log = Log.make (["clock"] @ String.split_on_char '.' id); last_catchup_log; sync; active_sources = WeakQueue.create (); passive_sources = WeakQueue.create (); on_tick = Queue.create (); after_tick = Queue.create (); outputs = Queue.create (); ticks = Atomic.make 0; } in Queue.iter clock.sub_clocks (fun c -> start ?force c); Atomic.set clock.state (`Started x); if sync <> `Passive then _clock_thread ~clock x and start ?force c = let clock = Unifier.deref c in match _can_start ?force clock with | `True sync -> _start ?force ~sync clock | `False -> () let add_pending_clock = (* Make sure that we're not collecting clocks between the time they have sources attached to them and before we get a chance to call [start_pending]. *) let finalise c = let clock = Unifier.deref c in match _can_start clock with | `True sync when sync <> `Passive -> _start ~sync clock; Queue.push clocks c | _ -> () in fun c -> Gc.finalise finalise c; WeakQueue.push pending_clocks c let create ?(stack = []) ?on_error ?id ?(sub_ids = []) ?(sync = `Automatic) () = let on_error_queue = Queue.create () in (match on_error with None -> () | Some fn -> Queue.push on_error_queue fn); let c = Unifier.make { id = Unifier.make (Option.map Lang_string.generate_id id); sub_ids; stack = Atomic.make stack; pending_activations = Queue.create (); sub_clocks = Queue.create (); state = Atomic.make (`Stopped sync); on_error = on_error_queue; } in if sync <> `Passive then add_pending_clock c; c let time c = let ({ time_implementation } as c) = active_params c in let module Time = (val time_implementation : Liq_time.T) in Time.to_float (_time c) let start_pending () = let c = WeakQueue.flush_elements pending_clocks in let c = List.map (fun c -> (c, Unifier.deref c)) c in let c = List.sort_uniq (fun (_, c) (_, c') -> Stdlib.compare c c') c in List.iter (fun (c, clock) -> match Atomic.get clock.state with | `Stopped _ -> ( match _can_start clock with | `True `Passive -> () | `True sync -> _start ~sync clock; Queue.push clocks c | `False -> WeakQueue.push pending_clocks c) | _ -> ()) c let () = Lifecycle.before_start ~name:"Clocks start" (fun () -> Atomic.set clocks_started true; start_pending ()) let on_tick c fn = let x = active_params c in Queue.push x.on_tick fn let after_tick c fn = let x = active_params c in Queue.push x.after_tick fn let after_eval () = if not (Atomic.get global_stop) then start_pending () let self_sync c = let clock = Unifier.deref c in match Atomic.get clock.state with | `Started params -> _self_sync ~clock params | _ -> false let activate_pending_sources clock = _activate_pending_sources ~clock:(Unifier.deref clock) (active_params clock) let tick clock = _tick ~clock:(Unifier.deref clock) (active_params clock) let set_stack c stack = ignore (Atomic.compare_and_set (Unifier.deref c).stack [] stack) let create_sub_clock ~id clock = let clock = Unifier.deref clock in let sub_clock = create ~stack:(Atomic.get clock.stack) ~id ~sub_ids:(clock.sub_ids @ ["child"]) ~sync:`Passive () in Queue.push clock.sub_clocks sub_clock; sub_clock let create ?stack ?on_error ?id ?sync () = create ?stack ?on_error ?id ?sync () let clocks () = List.sort_uniq (fun c c' -> Stdlib.compare (Unifier.deref c) (Unifier.deref c')) (WeakQueue.elements pending_clocks @ Queue.elements clocks) liquidsoap-2.3.2/src/core/clock.mli000066400000000000000000000064041477303350200172150ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) exception Invalid_state exception Has_stopped type t type active_source = < reset : unit ; output : unit > type source_type = [ `Passive | `Active of active_source | `Output of active_source ] type sync_source = Clock_base.sync_source type self_sync = [ `Static | `Dynamic ] * sync_source option val string_of_sync_source : sync_source -> string type sync_source_entry = { name : string; sync_source : sync_source; stack : Pos.t list; } type clock_sync_error = { name : string; stack : Pos.t list; sync_sources : sync_source_entry list; } exception Sync_error of clock_sync_error module type SyncSource = sig type t val to_string : t -> string end module MkSyncSource (S : SyncSource) : sig val make : S.t -> sync_source end type source = < id : string ; stack : Pos.t list ; self_sync : self_sync ; source_type : source_type ; active : bool ; wake_up : unit ; force_sleep : unit ; is_ready : bool ; get_frame : Frame.t > type active_sync_mode = [ `Automatic | `CPU | `Unsynced | `Passive ] type sync_mode = [ active_sync_mode | `Stopping | `Stopped ] val string_of_sync_mode : sync_mode -> string val active_sync_mode_of_string : string -> active_sync_mode val create : ?stack:Liquidsoap_lang.Pos.t list -> ?on_error:(exn -> Printexc.raw_backtrace -> unit) -> ?id:string -> ?sync:active_sync_mode -> unit -> t val active_sources : t -> source list val passive_sources : t -> source list val outputs : t -> source list val pending_activations : t -> source list val sources : t -> source list val clocks : unit -> t list val id : t -> string val set_id : t -> string -> unit val descr : t -> string val sync : t -> sync_mode val start : ?force:bool -> t -> unit val started : t -> bool val stop : t -> unit val set_stack : t -> Liquidsoap_lang.Pos.t list -> unit val self_sync : t -> bool val time : t -> float val unify : pos:Liquidsoap_lang.Pos.Option.t -> t -> t -> unit val create_sub_clock : id:string -> t -> t val attach : t -> source -> unit val detach : t -> source -> unit val activate_pending_sources : t -> unit val ticks : t -> int val on_tick : t -> (unit -> unit) -> unit val tick : t -> unit val after_tick : t -> (unit -> unit) -> unit val time_implementation : unit -> Liq_time.implementation val after_eval : unit -> unit liquidsoap-2.3.2/src/core/clock_base.ml000066400000000000000000000117601477303350200200370ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) exception Invalid_state module Evaluation = Liquidsoap_lang.Evaluation type active_source = < reset : unit ; output : unit > type source_type = [ `Passive | `Active of active_source | `Output of active_source ] type sync_source = .. type self_sync = [ `Static | `Dynamic ] * sync_source option module Queue = struct include Queues.Queue let push q v = if not (exists q (fun v' -> v == v')) then push q v end module WeakQueue = struct include Queues.WeakQueue let push q v = if not (exists q (fun v' -> v == v')) then push q v end exception Sync_source_name of string let sync_sources_handlers = Queue.create () let string_of_sync_source s = try Queue.iter sync_sources_handlers (fun fn -> fn s); assert false with Sync_source_name s -> s module type SyncSource = sig type t val to_string : t -> string end module MkSyncSource (S : SyncSource) = struct type sync_source += Sync_source of S.t let make v = Sync_source v let () = Queue.push sync_sources_handlers (function | Sync_source v -> raise (Sync_source_name (S.to_string v)) | _ -> ()) end type sync_source_entry = { name : string; sync_source : sync_source; stack : Pos.t list; } type clock_sync_error = { name : string; stack : Pos.t list; sync_sources : sync_source_entry list; } exception Sync_error of clock_sync_error let () = Printexc.register_printer (function | Sync_error { name; stack; sync_sources } -> let buf = Buffer.create 1024 in let formatter = Format.formatter_of_buffer buf in let open Liquidsoap_lang in let print_stack = function | [] -> " Unknown position" | l -> let l = List.init (min (List.length l) 3) (List.nth l) in String.concat "\n" (List.map (fun p -> " " ^ Liquidsoap_lang.Pos.to_string p) l) in ignore (print_stack []); let pos = match stack with [] -> None | p :: _ -> Some p in Runtime.error_header ~formatter 17 pos; Format.fprintf formatter "%s has multiple synchronization sources. Do you need to set \ self_sync=false?@." name; Format.fprintf formatter "\nSync sources:\n"; List.iter (fun { name; sync_source } -> Format.fprintf formatter " %s from source %s\n" (string_of_sync_source sync_source) name) sync_sources; Format.fprintf formatter "\nStack traces:\n"; Format.fprintf formatter "%s:\n%s\n\n" name (print_stack stack); List.iter (fun { name; stack; sync_source = _ } -> Format.fprintf formatter "%s:\n%s\n\n" name (print_stack stack)) sync_sources; Format.fprintf formatter "@]@."; Format.pp_print_flush formatter (); Some (Buffer.contents buf) | _ -> None) type source = < id : string ; stack : Pos.t list ; self_sync : self_sync ; source_type : source_type ; active : bool ; wake_up : unit ; force_sleep : unit ; is_ready : bool ; get_frame : Frame.t > let self_sync_type sources = Lazy.from_fun (fun () -> if List.exists (fun s -> fst s#self_sync = `Dynamic) sources then `Dynamic else `Static) let self_sync sources = let self_sync_type = self_sync_type sources in fun ~source () -> let sync_sources = List.fold_left (fun sync_sources s -> if s#is_ready then ( match s#self_sync with | _, Some sync_source -> { name = s#id; stack = s#stack; sync_source } :: sync_sources | _ -> sync_sources) else sync_sources) [] sources in match List.sort_uniq (fun { sync_source = s } { sync_source = s' } -> Stdlib.compare s s') sync_sources with | [] -> (Lazy.force self_sync_type, None) | [{ sync_source }] -> (Lazy.force self_sync_type, Some sync_source) | sync_sources -> raise (Sync_error { name = source#id; stack = source#stack; sync_sources }) liquidsoap-2.3.2/src/core/configure.ml000066400000000000000000000041311477303350200177250ustar00rootroot00000000000000open Liquidsoap_lang include Build_config include Liquidsoap_paths let git_snapshot = git_sha <> None let requests_max_id = 50 let requests_table_size = 50 let () = Liquidsoap_lang.Cache.user_dir_override := Liquidsoap_paths.user_cache_override; Liquidsoap_lang.Cache.system_dir_override := Liquidsoap_paths.system_cache_override (** General configuration *) let conf = Dtools.Conf.void "Liquidsoap configuration" let conf_default_font = Dtools.Conf.string ~d: (match (Sys.os_type, Build_config.system) with | _, "macosx" -> "/System/Library/Fonts/Times.ttc" | "Win32", _ -> {|C:\\Windows\WinSxS\calibri.ttf|} | _ -> "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf") ~p:(conf#plug "default_font") "Default font" let libs_versions () = Build_info.V1.Statically_linked_libraries.to_list () |> List.map (fun lib -> let name = Build_info.V1.Statically_linked_library.name lib in let version = Build_info.V1.Statically_linked_library.version lib |> Option.map Build_info.V1.Version.to_string |> Option.value ~default:"?" in (name, version)) |> List.sort compare |> List.map (fun (name, version) -> if version = "?" then name else name ^ "=" ^ version) |> String.concat " " let restart = ref false let vendor = Printf.sprintf "Liquidsoap/%s (%s; OCaml %s)" version Sys.os_type Sys.ocaml_version let path () = let s = try Sys.getenv "PATH" with Not_found -> "" in bin_dir () :: Str.split (Str.regexp_string ":") s let () = conf#plug "log" Dtools.Log.conf let conf_init = conf#plug "init" Dtools.Init.conf; Dtools.Init.conf let conf_debug = Dtools.Conf.bool ~p:(conf#plug "debug") ~d:!Term.conf_debug "Debug language features such as type inference and reduction." let conf_debug_errors = Dtools.Conf.bool ~p:(conf#plug "debug_errors") ~d:!Term.conf_debug_errors "Debug errors by showing stacktraces instead of printing messages." let () = conf_debug#on_change (fun v -> Term.conf_debug := v); conf_debug_errors#on_change (fun v -> Term.conf_debug_errors := v) liquidsoap-2.3.2/src/core/configure.mli000066400000000000000000000017511477303350200201030ustar00rootroot00000000000000(** Constants describing configuration options of liquidsoap. *) val conf : Dtools.Conf.ut val conf_init : Dtools.Conf.ut val conf_debug : bool Dtools.Conf.t val conf_debug_errors : bool Dtools.Conf.t val conf_default_font : string Dtools.Conf.t (** String describing the OS *) val host : string (** String describing the version. *) val version : string val restart : bool ref val git_snapshot : bool (** String describing the software. *) val vendor : string (** Where to look for standard .liq scripts to include *) val liq_libs_dir : unit -> string (** Where to look for private executables. *) val bin_dir : unit -> string (** Standard path. *) val path : unit -> string list (** Maximal id for a request. *) val requests_max_id : int val requests_table_size : int (** Configured directories. Typically /var/(run|log)/liquidsoap. *) val rundir : unit -> string val logdir : unit -> string (** String containing versions of all enabled bindings. *) val libs_versions : unit -> string liquidsoap-2.3.2/src/core/conversions/000077500000000000000000000000001477303350200177635ustar00rootroot00000000000000liquidsoap-2.3.2/src/core/conversions/conversion.ml000066400000000000000000000026071477303350200225070ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) class base ~converter (source : Source.source) = object method fallible = source#fallible method private can_generate_frame = source#is_ready method abort_track = source#abort_track method remaining = source#remaining method seek_source = source#seek_source method self_sync = source#self_sync method private generate_frame = converter source#get_frame end liquidsoap-2.3.2/src/core/conversions/mean.ml000066400000000000000000000047271477303350200212470ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Source open Mm class mean ~field ~normalize source = object inherit operator [source] ~name:"mean" inherit Conversion.base ~converter:(fun frame -> (* Compute the mean of audio channels *) let len = Frame.position frame in let alen = Frame.audio_of_main len in let src_content = Content.Audio.get_data (Frame.get frame field) in let dst_content = Audio.Mono.create alen in let amp = if normalize then 1. /. float (Array.length src_content) else 1. in for i = 0 to alen - 1 do dst_content.(i) <- Array.fold_left (fun m b -> m +. b.(i)) 0. src_content *. amp done; Frame.set_data frame field Content.Audio.lift_data [| dst_content |]) source end let _ = let in_t = Format_type.audio () in let out_t = Format_type.audio_mono () in Lang.add_track_operator ~base:Modules.track_audio "mean" [ ( "normalize", Lang.bool_t, Some (Lang.bool true), Some "Divide the output volume by the number of channels." ); ("", in_t, None, Some "Track whose mean should be computed."); ] ~return_t:out_t ~category:`Conversion ~descr:"Produce mono audio by taking the mean of all audio channels." (fun p -> let normalize = Lang.to_bool (List.assoc "normalize" p) in let field, s = Lang.to_track (Lang.assoc "" 1 p) in (field, new mean ~field ~normalize s)) liquidsoap-2.3.2/src/core/conversions/stereo.ml000066400000000000000000000051141477303350200216170ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (** These classes define conversion operators that accept a source streaming at least one audio channel, and no other kind of channel, and return a source that streams stereo audio. * We only have a basic implementation for now. In the future we may perform smart conversions from 5 channels audio. We may also want to detect stereo with one silent channel (often the right) and treat it as mono. *) (** Duplicate mono into stereo, drop channels when there are more than two. *) class basic ~field source = object inherit Source.operator [source] ~name:"stereo" inherit Conversion.base ~converter:(fun frame -> (* Set audio layer. *) let audio = match Content.Audio.get_data (Frame.get frame field) with | [||] -> let len = AFrame.size () in let buf = Audio.Mono.create len in Audio.Mono.clear buf 0 len; [| buf; buf |] | [| chan |] -> [| chan; chan |] | audio -> Array.sub audio 0 2 in Frame.set_data frame field Content.Audio.lift_data audio) source end let stereo = let input_type = Format_type.audio () in let output_type = Format_type.audio_stereo () in Lang.add_track_operator ~base:Modules.track_audio "stereo" ~category:`Conversion ~descr:"Convert any pcm audio track into a stereo track." ~return_t:output_type [("", input_type, None, None)] (fun p -> let field, s = Lang.to_track (List.assoc "" p) in (field, new basic ~field s)) liquidsoap-2.3.2/src/core/conversions/swap.ml000066400000000000000000000033151477303350200212710ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Source class swap ~field (source : source) = object inherit operator [source] ~name:"swap" inherit Conversion.base ~converter:(fun frame -> let buffer = Content.Audio.get_data (Frame.get frame field) in Frame.set_data frame field Content.Audio.lift_data [| buffer.(1); buffer.(0) |]) source end let _ = let track_t = Format_type.audio_stereo () in Lang.add_track_operator ~base:Modules.track_audio "swap" [("", track_t, None, None)] ~return_t:track_t ~category:`Conversion ~descr:"Swap two channels of a stereo track." (fun p -> let field, s = Lang.to_track (Lang.assoc "" 1 p) in (field, new swap ~field s)) liquidsoap-2.3.2/src/core/converters/000077500000000000000000000000001477303350200176055ustar00rootroot00000000000000liquidsoap-2.3.2/src/core/converters/audio/000077500000000000000000000000001477303350200207065ustar00rootroot00000000000000liquidsoap-2.3.2/src/core/converters/audio/ffmpeg_audio_converter.ml000066400000000000000000000034011477303350200257520ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm module Resampler = Swresample.Make (Swresample.PlanarFloatArray) (Swresample.PlanarFloatArray) let samplerate_converter channels = let chans = Avutil.Channel_layout.get_default channels in let in_freq = Lazy.force Frame.audio_rate in let rs = ref None in let rs_out_freq = ref 0 in fun x buf offset length -> let out_freq = int_of_float (float in_freq *. x) in if !rs = None || !rs_out_freq <> out_freq then ( rs := Some (Resampler.create chans in_freq chans out_freq); rs_out_freq := out_freq); let rs = Option.get !rs in let data = Resampler.convert ~offset ~length rs buf in (data, 0, Audio.length data) let () = Plug.register Audio_converter.Samplerate.converters "ffmpeg" ~doc:"" samplerate_converter liquidsoap-2.3.2/src/core/converters/audio/libsamplerate_converter.ml000066400000000000000000000053341477303350200261600ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (** Samplerate converter using libsamplerate *) let log = Log.make ["audio"; "converter"; "libsamplerate"] let samplerate_conf = Dtools.Conf.void ~p:(Audio_converter.Samplerate.samplerate_conf#plug "libsamplerate") "Libsamplerate conversion settings" ~comments:["Options related to libsamplerate conversion."] let quality_conf = Dtools.Conf.string ~p:(samplerate_conf#plug "quality") "Resampling quality" ~d:"fast" ~comments: [ "Resampling quality, one of: `\"best\"`, `\"medium\"`, `\"fast\"`, \ `\"zero_order\"` or `\"linear\"`. Refer to ocaml-samplerate for \ details."; ] let quality_of_string v = match v with | "best" -> Samplerate.Conv_sinc_best_quality | "medium" -> Samplerate.Conv_sinc_medium_quality | "fast" -> Samplerate.Conv_fastest | "zero_order" -> Samplerate.Conv_zero_order_hold | "linear" -> Samplerate.Conv_linear | _ -> raise (Error.Invalid_value ( Lang.string v, "libsamplerate quality must be one of: \"best\", \"medium\", \ \"fast\", \"zero_order\", \"linear\"." )) let samplerate_converter channels = let quality = quality_of_string quality_conf#get in let converters = Array.init channels (fun _ -> Samplerate.create quality 1) in let convert converter ratio b ofs len = Samplerate.process_alloc converter ratio b ofs len in fun ratio b ofs len -> let data = Array.mapi (fun i b -> convert converters.(i) ratio b ofs len) b in (data, 0, Audio.length data) let () = Plug.register Audio_converter.Samplerate.converters "libsamplerate" ~doc:"High-quality samplerate conversion using libsamblerate." samplerate_converter liquidsoap-2.3.2/src/core/converters/audio/native_audio_converter.ml000066400000000000000000000051271477303350200260030ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (** Native audio converters *) let samplerate_conf = Dtools.Conf.void ~p:(Audio_converter.Samplerate.samplerate_conf#plug "native") "Native samplerate conversion settings" ~comments:["Options related to native samplerate conversion."] let quality_conf = Dtools.Conf.string ~p:(samplerate_conf#plug "quality") "Resampling quality" ~d:"linear" ~comments:["Resampling quality: either \"nearest\" or \"linear\"."] let quality_of_string = function | "nearest" -> `Nearest | "linear" -> `Linear | s -> raise (Error.Invalid_value ( Lang.string s, "Native resampling quality must either be \"nearest\" or \ \"linear\"." )) let samplerate_converter _ = let mode = quality_of_string quality_conf#get in fun r data ofs len -> if r = 1. then (data, ofs, len) else ( let data = Audio.resample ~mode r data ofs len in (data, 0, Audio.length data)) let () = Plug.register Audio_converter.Samplerate.converters "native" ~doc: "Native samplerate converter. This is fast but bad quality: you should \ avoid using it for now if you are serious about sound." samplerate_converter let channel_layout_converter src dst = assert (src <> dst); match dst with | `Mono -> fun data -> [| Audio.to_mono data 0 (Audio.length data) |] | `Stereo -> fun data -> [| data.(0); data.(0) |] | _ -> raise Audio_converter.Channel_layout.Unsupported let () = Plug.register Audio_converter.Channel_layout.converters "native" ~doc:"Native channel layout converter." channel_layout_converter liquidsoap-2.3.2/src/core/converters/audio_converter.ml000066400000000000000000000123311477303350200233270ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** External audio samplerate conversion utilities. *) open Mm let log = Log.make ["audio"; "converter"] (* TODO: is it the right place for this ? *) let audio_conf = Dtools.Conf.void ~p:(Configure.conf#plug "audio") "Audio settings" ~comments:["Options related to audio."] let converter_conf = Dtools.Conf.void ~p:(audio_conf#plug "converter") "Conversion settings" ~comments:["Options related to audio conversion."] module Samplerate = struct exception Invalid_data (** A converter takes a conversion ratio (output samplerate / input samplerate), an audio buffer, and returns a resampled buffer. *) type converter = float -> Content_audio.data -> int -> int -> Content_audio.data * int * int type converter_plug = int -> converter type t = { channels : int; converter : converter } let samplerate_conf = Dtools.Conf.void ~p:(converter_conf#plug "samplerate") "Samplerate conversion settings" ~comments:["Options related to samplerate conversion."] let converters_conf = Dtools.Conf.list ~p:(samplerate_conf#plug "converters") "Preferred samplerate converters" ~d:["libsamplerate"; "ffmpeg"; "native"] ~comments: [ "Preferred samplerate converter. The native converter is always \ available."; ] let converters : converter_plug Plug.t = Plug.create "samplerate converters" ~doc:"Methods for converting samplerate." let create channels = let converter = let rec f = function | conv :: l -> ( match Plug.get converters conv with Some v -> v | None -> f l) | [] -> (* This should never come up since the native converter is always available. *) assert false in f converters_conf#get in { channels; converter = converter channels } let resample { channels; converter } ratio data ofs len = if channels <> Array.length data then raise Invalid_data; if ratio = 1. then (Audio.copy data ofs len, 0, len) else converter ratio data ofs len (** Log which converter is used at start. *) let () = Lifecycle.on_start ~name:"audio samplerate converter initialization" (fun () -> let rec f = function | conv :: _ when Plug.get converters conv <> None -> log#important "Using samplerate converter: %s." conv | _ :: l -> f l | [] -> assert false in f converters_conf#get) end module Channel_layout = struct exception Unsupported exception Invalid_data type layout = [ `Mono | `Stereo | `Five_point_one ] type converter = layout -> layout -> Content_audio.data -> Content_audio.data type t = { src : layout; converter : Content_audio.data -> Content_audio.data; } let channel_layout_conf = Dtools.Conf.void ~p:(converter_conf#plug "channel_layout") "Channel layout conversion settings" ~comments:["Options related to channel layout conversion."] let converters_conf = Dtools.Conf.list ~p:(channel_layout_conf#plug "converters") "Preferred samplerate converters" ~d:["native"] ~comments: [ "Preferred channel layout converter. The native converter is always \ available."; ] let converters : converter Plug.t = Plug.create "channel layout converters" ~doc:"Methods for converting channel layouts." let create src dst = let converter = if src = dst then fun _ _ x -> x else ( let rec f = function | conv :: l -> ( match Plug.get converters conv with Some v -> v | None -> f l) | [] -> (* This should never come up since the native converter is always available. *) assert false in f converters_conf#get) in { src; converter = converter src dst } let channels_of_layout = function | `Mono -> 1 | `Stereo -> 2 | `Five_point_one -> 6 let layout_of_channels = function | 1 -> `Mono | 2 -> `Stereo | 6 -> `Five_point_one | _ -> raise Unsupported let convert { src; converter } input = if Array.length input != channels_of_layout src then raise Invalid_data; converter input end liquidsoap-2.3.2/src/core/converters/audio_converter.mli000066400000000000000000000047741477303350200235140ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** External audio conversion utilities *) (* TODO: is it the right place for this ? *) val audio_conf : Dtools.Conf.ut val converter_conf : Dtools.Conf.ut module Samplerate : sig exception Invalid_data type converter = float -> Content_audio.data -> int -> int -> Content_audio.data * int * int type converter_plug = int -> converter type t val samplerate_conf : Dtools.Conf.ut val converters : converter_plug Plug.t (** [create chan_nb] creates a converter. *) val create : int -> t (** [resample converter ratio data]: converts input data at given ratio. Raises [Invalid_data] if number of channels do not match the number passed at [create]. *) val resample : t -> float -> Content_audio.data -> int -> int -> Content_audio.data * int * int end module Channel_layout : sig exception Unsupported exception Invalid_data type layout = [ `Mono | `Stereo | `Five_point_one ] type converter = layout -> layout -> Content_audio.data -> Content_audio.data type t val channels_of_layout : layout -> int val layout_of_channels : int -> layout val channel_layout_conf : Dtools.Conf.ut val converters : converter Plug.t (** [create src dst] creates a converter. *) val create : layout -> layout -> t (** [convert converter data]: converts input data to the destination layout. Raises [Invalid_data] if input layout does not match the layout passed as [create]. *) val convert : t -> Content_audio.data -> Content_audio.data end liquidsoap-2.3.2/src/core/converters/video/000077500000000000000000000000001477303350200207135ustar00rootroot00000000000000liquidsoap-2.3.2/src/core/converters/video/ffmpeg_video_converter.ml000066400000000000000000000073151477303350200257740ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Video_converter module Img = Image.Generic module P = Img.Pixel let formats = [ P.RGB P.RGB24; P.RGB P.BGR24; P.RGB P.RGB32; P.RGB P.BGR32; P.RGB P.RGBA32; P.YUV P.YUV422; P.YUV P.YUV444; P.YUV P.YUV411; P.YUV P.YUV410; P.YUV P.YUVJ420; P.YUV P.YUVJ422; P.YUV P.YUVJ444; ] let format_of frame = let fmt = Img.pixel_format frame in match fmt with | P.RGB fmt -> ( match fmt with | P.RGB24 -> `Rgb24 | P.BGR24 -> `Bgr24 | P.RGB32 -> `Rgba | P.BGR32 -> `Bgra | P.RGBA32 -> `Rgba) | P.YUV fmt -> ( match fmt with | P.YUV422 -> `Yuv422p | P.YUV444 -> `Yuv444p | P.YUV411 -> `Yuv411p | P.YUV410 -> `Yuv410p | P.YUVJ420 -> Ffmpeg_utils.liq_frame_pixel_format () | P.YUVJ422 -> `Yuvj422p | P.YUVJ444 -> `Yuvj444p) type fmt = Avutil.Pixel_format.t * int * int module HT = struct type t = (fmt * fmt) * Swscale.t option let equal (fmt, _) (fmt', _) = fmt = fmt' let hash (fmt, _) = Hashtbl.hash fmt end module WH = struct include Weak.Make (HT) (* Number of converters to always keep in memory. *) let n = 2 let keep = Array.make n None let add h fmt conv = let conv = (fmt, Some conv) in for i = 1 to n - 1 do keep.(i - 1) <- keep.(i) done; keep.(n - 1) <- Some conv; add h conv let assoc h fmt = Option.get (snd (find h (fmt, None))) end (* Weak hashtable containing converters already created. *) let converters = WH.create 5 let is_rgb = function P.RGB _ -> true | _ -> false let create () = let convert src dst = let src_f = format_of src in let dst_f = format_of dst in let src_w = Img.width src in let src_h = Img.height src in let dst_w = Img.width dst in let dst_h = Img.height dst in let conv = try WH.assoc converters ((src_f, src_w, src_h), (dst_f, dst_w, dst_h)) with Not_found -> let conv = Swscale.create [Swscale.Bilinear; Swscale.Print_info] src_w src_h src_f dst_w dst_h dst_f in WH.add converters ((src_f, src_w, src_h), (dst_f, dst_w, dst_h)) conv; conv in let data f = match Img.pixel_format f with | P.RGB _ -> let buf, stride = Img.rgb_data f in [| (buf, stride) |] | P.YUV _ -> let (y, sy), (u, v, s) = Img.yuv_data f in [| (y, sy); (u, s); (v, s) |] in let src_d = data src in let dst_d = data dst in Swscale.scale conv src_d 0 src_h dst_d 0 in convert let () = Plug.register video_converters "ffmpeg" ~doc:"FFmpeg image format converter." (formats, formats, create) liquidsoap-2.3.2/src/core/converters/video/native_video_converter.ml000066400000000000000000000025441477303350200260150ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Video_converter module Img = Image.Generic let formats = [Img.Pixel.RGB Img.Pixel.RGBA32; Img.Pixel.YUV Img.Pixel.YUVJ420] let convert src dst = Image.Generic.convert ~proportional:false src dst let create () = convert let () = Plug.register video_converters "native" ~doc:"Native image format converter." (formats, formats, create) liquidsoap-2.3.2/src/core/converters/video_converter.ml000066400000000000000000000073321477303350200233410ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (* Video format converters *) let log = Log.make ["video"; "converter"] (* TODO: is it the good place for this ? *) let video_conf = Dtools.Conf.void ~p:(Configure.conf#plug "video") "Video settings" ~comments:["Options related to video."] let video_converter_conf = Dtools.Conf.void ~p:(video_conf#plug "converter") "Video conversion" ~comments:["Options related to video conversion."] let preferred_converter_conf = Dtools.Conf.string ~p:(video_converter_conf#plug "preferred") ~d:"ffmpeg" "Preferred video converter" module Img = Image.Generic type converter = Img.t -> Img.t -> unit (* A converter plugin is a name, a list of input formats, * a list of output formats, * a function to create a converter. *) type converter_plug = Img.Pixel.format list * Img.Pixel.format list * (unit -> converter) let video_converters : converter_plug Plug.t = Plug.create ~doc:"Methods for converting video frames." "video converters" exception Exit of converter (** Only log preferred decoder availability once at start. *) let () = Lifecycle.on_start ~name:"video converter initialization" (fun () -> let preferred = preferred_converter_conf#get in match Plug.get video_converters preferred with | None -> log#important "Couldn't find preferred video converter: %s." preferred | _ -> log#important "Using preferred video converter: %s." preferred) let find_converter src dst = try begin let preferred = preferred_converter_conf#get in match Plug.get video_converters preferred with | None -> () | Some (sf, df, f) -> if List.mem src sf && List.mem dst df then raise (Exit (f ())) else log#important "Default video converter %s cannot do %s->%s." preferred (Img.Pixel.string_of_format src) (Img.Pixel.string_of_format dst) end; Plug.iter video_converters (fun name (sf, df, f) -> log#info "Trying %s video converter..." name; if List.mem src sf && List.mem dst df then raise (Exit (f ())) else ()); log#important "Couldn't find a video converter from format %s to format %s." (Img.Pixel.string_of_format src) (Img.Pixel.string_of_format dst); raise Not_found with Exit x -> x let scaler () = let f = find_converter (Image.Generic.Pixel.YUV Image.Generic.Pixel.YUVJ420) (Image.Generic.Pixel.YUV Image.Generic.Pixel.YUVJ420) in fun src dst -> (* TODO: optimized scalers don't handle α channels for now, defaulting to the native one. *) if Image.YUV420.has_alpha src then Image.YUV420.scale src dst else f (Image.Generic.of_YUV420 src) (Image.Generic.of_YUV420 dst) liquidsoap-2.3.2/src/core/converters/video_converter.mli000066400000000000000000000042071477303350200235100ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (* Video format converters *) (** Plugin to add video-related configuration keys. *) val video_converter_conf : Dtools.Conf.ut (** [f src dst] performs the conversion from frame [src] to frame [dst]. Raises [Not_found] if no conversion routine was found. *) type converter = Image.Generic.t -> Image.Generic.t -> unit (** A converter plugin is a name, a list of input formats, a list of output formats, a function to create a converter. *) type converter_plug = Image.Generic.Pixel.format list * Image.Generic.Pixel.format list * (unit -> converter) (** Plugin to register new converters. *) val video_converters : converter_plug Plug.t (** [find_converter source destination] tries to find a converter from source format to destination format. Proportional scale is implicitly set via global configuration key for now. Returns a conversion function: frame -> frame -> unit. *) val find_converter : Image.Generic.Pixel.format -> Image.Generic.Pixel.format -> Image.Generic.t -> Image.Generic.t -> unit (** Generate a function to scale images. *) val scaler : unit -> Video.Image.t -> Video.Image.t -> unit liquidsoap-2.3.2/src/core/decoder/000077500000000000000000000000001477303350200170205ustar00rootroot00000000000000liquidsoap-2.3.2/src/core/decoder/aac_decoder.ml000066400000000000000000000226201477303350200215650ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (** Decode and read metadatas of AAC files. *) let error_translator = function | Faad.Error x -> Some (Printf.sprintf "Faad error: %s" (Faad.error_message x)) | _ -> None let () = Printexc.register_printer error_translator exception End_of_stream (** Buffered input device. *) let buffered_input input = let buffer = Strings.Mutable.empty () in let pos = ref 0 in let drop len = pos := !pos + len; Strings.Mutable.drop buffer len in let tell = match input.Decoder.tell with | None -> None | Some f -> Some (fun () -> Strings.Mutable.length buffer + f ()) in let lseek = match input.Decoder.lseek with | None -> None | Some f -> let lseek len = ignore (Strings.Mutable.flush buffer); f len in Some lseek in (* Get at most [len] bytes from the buffer, which is refilled from [input] if needed. This does not remove data from the buffer. *) let tmplen = Utils.pagesize in let tmp = Bytes.create tmplen in let read buf ofs len = let size = Strings.Mutable.length buffer in let len = if size > len then len else ( let read = min tmplen len in let read = input.Decoder.read tmp 0 read in if read = 0 then raise End_of_stream; Strings.Mutable.add_subbytes buffer tmp 0 read; min len (size + read)) in Strings.Mutable.blit buffer 0 buf ofs len; len in ({ Decoder.read; tell; lseek; length = None }, drop, pos) let log = Log.make ["decoder"; "aac"] let create_decoder input = let dec = Faad.create () in let input, drop, pos = buffered_input input in let offset, samplerate, chans = let initbuflen = 1024 in let initbuf = Bytes.create initbuflen in let len = input.Decoder.read initbuf 0 initbuflen in Faad.init dec initbuf 0 len in drop offset; pos := 0; let processed = ref 0 in let aacbuflen = Faad.min_bytes_per_channel * chans in let aacbuf = Bytes.create aacbuflen in (* We approximate bitrate for seeking.. *) let seek ticks = if !processed == 0 || !pos == 0 || input.Decoder.lseek == None || input.Decoder.tell == None then 0 else ( let cur_time = float !processed /. float samplerate in let rate = float !pos /. cur_time in let offset = Frame.seconds_of_main ticks in let bytes = int_of_float (rate *. offset) in try ignore ((Option.get input.Decoder.lseek) ((Option.get input.Decoder.tell) () + bytes)); Faad.post_sync_reset dec; ticks with _ -> 0) in { Decoder.seek; eof = (fun _ -> ()); close = (fun _ -> ()); decode = (fun buffer -> let len = input.Decoder.read aacbuf 0 aacbuflen in if len = aacbuflen then ( let pos, data = Faad.decode dec aacbuf 0 len in begin try processed := !processed + Audio.length data with _ -> () end; drop pos; buffer.Decoder.put_pcm ~samplerate data)); } let aac_mime_types = Dtools.Conf.list ~p:(Decoder.conf_mime_types#plug "aac") "Mime-types used for guessing AAC format" ~d:["audio/aac"; "audio/aacp"; "audio/x-hx-aac-adts"] let aac_file_extensions = Dtools.Conf.list ~p:(Decoder.conf_file_extensions#plug "aac") "File extensions used for guessing AAC format" ~d:["aac"] let aac_priority = Dtools.Conf.int ~p:(Decoder.conf_priorities#plug "aac") "Priority for the AAC decoder" ~d:1 (* Get the number of channels of audio in an AAC file. *) let file_type ~metadata:_ ~ctype:_ filename = let fd = Decoder.openfile filename in Fun.protect ~finally:(fun () -> Unix.close fd) (fun () -> let dec = Faad.create () in let aacbuflen = Utils.pagesize in let aacbuf = Bytes.create aacbuflen in let _, rate, channels = let n = Unix.read fd aacbuf 0 aacbuflen in Faad.init dec aacbuf 0 n in log#important "Libfaad recognizes %s as AAC (%dHz,%d channels)." (Lang_string.quote_string filename) rate channels; Some (Frame.Fields.make ~audio:(Content.Audio.format_of_channels channels) ())) let file_decoder ~metadata:_ ~ctype filename = Decoder.opaque_file_decoder ~filename ~ctype create_decoder let () = Plug.register Decoder.decoders "aac" ~doc: "Use libfaad to decode AAC if MIME type or file extension is appropriate." { Decoder.priority = (fun () -> aac_priority#get); file_extensions = (fun () -> Some aac_file_extensions#get); mime_types = (fun () -> Some aac_mime_types#get); file_type; file_decoder = Some file_decoder; stream_decoder = Some (fun ~ctype:_ _ -> create_decoder); } (* Mp4 decoding. *) let log = Log.make ["decoder"; "mp4"] exception End_of_track let create_decoder input = let dec = Faad.create () in let read = input.Decoder.read in let mp4 = Faad.Mp4.openfile ?seek:input.Decoder.lseek read in let track = Faad.Mp4.find_aac_track mp4 in let samplerate, _ = Faad.Mp4.init mp4 dec track in let nb_samples = Faad.Mp4.samples mp4 track in let sample = ref 0 in let pos = ref 0 in let ended = ref false in let decode buffer = if !ended || !sample >= nb_samples || !sample < 0 then raise End_of_track; let data = Faad.Mp4.decode mp4 track !sample dec in incr sample; begin try pos := !pos + Audio.length data with _ -> () end; buffer.Decoder.put_pcm ~samplerate data in let seek ticks = try let time = Frame.seconds_of_main ticks in let audio_ticks = int_of_float (time *. float samplerate) in let offset = max (!pos + audio_ticks) 0 in let new_sample, _ = Faad.Mp4.seek mp4 track offset in sample := new_sample; let time = float (offset - !pos) /. float samplerate in pos := offset; Frame.main_of_seconds time with _ -> ended := true; 0 in { Decoder.decode; seek; eof = (fun _ -> ()); close = (fun _ -> ()) } (* Get the number of channels of audio in an MP4 file. *) let file_type ~metadata:_ ~ctype:_ filename = let dec = Faad.create () in let fd = Decoder.openfile filename in Fun.protect ~finally:(fun () -> Unix.close fd) (fun () -> let mp4 = Faad.Mp4.openfile_fd fd in let track = Faad.Mp4.find_aac_track mp4 in let rate, channels = Faad.Mp4.init mp4 dec track in log#important "Libfaad recognizes %s as MP4 (%dHz,%d channels)." (Lang_string.quote_string filename) rate channels; Some (Frame.Fields.make ~audio:(Content.Audio.format_of_channels channels) ())) let mp4_mime_types = Dtools.Conf.list ~p:(Decoder.conf_mime_types#plug "mp4") "Mime-types used for guessing MP4 format" ~d:["audio/mp4"; "application/mp4"] let mp4_file_extensions = Dtools.Conf.list ~p:(Decoder.conf_file_extensions#plug "mp4") "File extensions used for guessing MP4 format" ~d:["m4a"; "m4b"; "m4p"; "m4v"; "m4r"; "3gp"; "mp4"] let mp4_priority = Dtools.Conf.int ~p:(Decoder.conf_priorities#plug "mp4") "Priority for the MP4 decoder" ~d:1 let file_decoder ~metadata:_ ~ctype filename = Decoder.opaque_file_decoder ~filename ~ctype create_decoder let () = Plug.register Decoder.decoders "mp4" ~doc: "Use libfaad to decode MP4 if MIME type or file extension is appropriate." { Decoder.priority = (fun () -> mp4_priority#get); file_extensions = (fun () -> Some mp4_file_extensions#get); mime_types = (fun () -> Some mp4_mime_types#get); file_type; file_decoder = Some file_decoder; stream_decoder = Some (fun ~ctype:_ _ -> create_decoder); } let log = Log.make ["metadata"; "mp4"] let get_tags ~metadata:_ ~extension ~mime file = if not (Decoder.test_file ~log ~extension ~mime ~mimes:(Some mp4_mime_types#get) ~extensions:(Some mp4_file_extensions#get) file) then raise Not_found; let fd = Decoder.openfile file in Fun.protect ~finally:(fun () -> Unix.close fd) (fun () -> let mp4 = Faad.Mp4.openfile_fd fd in Array.to_list (Faad.Mp4.metadata mp4)) let mp4_metadata_decoder_priority = Dtools.Conf.int ~p:(Request.conf_metadata_decoder_priorities#plug "mp4") "Priority for the mp4 metadata decoder" ~d:1 let () = Plug.register Request.mresolvers "mp4" ~doc:"MP4 tag decoder." { Request.priority = (fun () -> mp4_metadata_decoder_priority#get); resolver = get_tags; } liquidsoap-2.3.2/src/core/decoder/decoder.ml000066400000000000000000000502161477303350200207630ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (** Media decoding infrastructure. We treat files and streams. We separate detection from the actual decoding. For files, the decoder detection function is passed a filename and an expected content kind. For streams, it is passed a MIME type and a content kind. In practice, most file decoders will be based on stream decoders, with a specific (more precise) detection function. Although we cannot force it at this point, we provide some infrastructure to help. In the short term, the plug infrastructure should provide a way to ban / prioritize plugins. For example: - choose ogg_demuxer when extension = ogg - choose mad when extension = mp3 - choose mad when mime-type = audio/mp3 *) let log = Log.make ["decoder"] (** A local file is simply identified by its filename. *) type file = string (** A stream is identified by a MIME type. *) type stream = string type fps = Decoder_utils.fps = { num : int; den : int } (* Buffer passed to decoder. This wraps around regular buffer, adding: - Implicit resampling - Implicit audio channel conversion - Implicit video resize - Implicit fps conversion - Implicit content drop *) type buffer = { generator : Generator.t; put_pcm : ?field:Frame.field -> samplerate:int -> Content.Audio.data -> unit; put_yuva420p : ?field:Frame.field -> fps:fps -> Video.Canvas.image -> unit; } type decoder = { decode : buffer -> unit; eof : buffer -> unit; (* [seek x]: Skip [x] main ticks. Returns the number of ticks actually skipped. *) seek : int -> int; close : unit -> unit; } type input = { read : bytes -> int -> int -> int; (* Seek to an absolute position in bytes. Returns the current position after seeking. *) lseek : (int -> int) option; tell : (unit -> int) option; length : (unit -> int) option; } type file_decoder_ops = { fread : int -> Frame.t; remaining : unit -> int; fseek : int -> int; fclose : unit -> unit; } type file_decoder = metadata:Frame.metadata -> ctype:Frame.content_type -> string -> file_decoder_ops (** A stream decoder does not "own" any file descriptor, and is generally assumed to not allocate resources (in the sense of things that should be explicitly managed, not just garbage collected). Hence it does not need a close function. *) type stream_decoder = input -> decoder type image_decoder = { image_decoder_priority : unit -> int; check_image : file -> bool; decode_image : file -> Video.Image.t; } (** Decoder description. *) type decoder_specs = { priority : unit -> int; file_extensions : unit -> string list option; mime_types : unit -> string list option; file_type : metadata:Frame.metadata -> ctype:Frame.content_type -> string -> Frame.content_type option; file_decoder : file_decoder option; stream_decoder : (ctype:Frame.content_type -> string -> stream_decoder) option; } (** Plugins might define various decoders. In order to be accessed, they should also register methods for choosing decoders. *) let conf_decoder = Dtools.Conf.void ~p:(Configure.conf#plug "decoder") "Decoder settings" let conf_decoders = Dtools.Conf.list ~p:(conf_decoder#plug "decoders") ~d:[] "Media decoders." let f c v = match c#get_d with | None -> c#set_d (Some [v]) | Some d -> c#set_d (Some (d @ [v])) let decoders : decoder_specs Plug.t = Plug.create ~register_hook:(fun name _ -> f conf_decoders name) ~doc:"Available decoders." "Available decoders" let get_decoders () = let f cur name = match Plug.get decoders name with | Some p -> (name, p) :: cur | None -> log#severe "Cannot find decoder %s" name; cur in let decoders = List.fold_left f [] conf_decoders#get in List.sort (fun (_, a) (_, b) -> compare (b.priority ()) (a.priority ())) decoders let conf_image_file_decoders = Dtools.Conf.list ~p:(conf_decoder#plug "image_file_decoders") ~d:[] "Decoders and order used to decode image files." let image_file_decoders : image_decoder Plug.t = Plug.create ~register_hook:(fun name _ -> f conf_image_file_decoders name) ~doc:"Image file decoding methods." "image file decoding" let get_image_file_decoders () = let f cur name = match Plug.get image_file_decoders name with | Some p -> (name, p) :: cur | None -> log#severe "Cannot find decoder %s" name; cur in List.sort (fun (_, { image_decoder_priority = p }) (_, { image_decoder_priority = p' }) -> Stdlib.compare (p' ()) (p ())) (List.fold_left f [] conf_image_file_decoders#get) let conf_debug = Dtools.Conf.bool ~p:(conf_decoder#plug "debug") ~d:false "Maximum debugging information (dev only)" ~comments: [ "WARNING: Do not enable unless a developer instructed you to do so!"; "The debugging mode makes it easier to understand why decoding fails,"; "but as a side effect it will crash liquidsoap at the end of every"; "track."; ] let conf_mime_types = Dtools.Conf.void ~p:(conf_decoder#plug "mime_types") "Mime-types used for choosing audio and video file decoders" ~comments: [ "When a mime-type is available (e.g. with input.http), it can be used "; "to guess which audio stream format is used."; "This section contains the listings used for that detection, which you "; "might want to tweak if you encounter a new mime-type."; "If you feel that new mime-types should be permanently added, please "; "contact the developers."; ] let conf_file_extensions = Dtools.Conf.void ~p:(conf_decoder#plug "file_extensions") "File extensions used for guessing audio formats" let conf_priorities = Dtools.Conf.void ~p:(conf_decoder#plug "priorities") "Priorities used for choosing audio and video file decoders" let conf_image_priorities = Dtools.Conf.void ~p:(conf_priorities#plug "image") "Priorities used for choosing image file decoders" let base_mime s = List.hd (String.split_on_char ';' s) let test_file ~(log : Log.t) ~extension ~mime ~mimes ~extensions fname = let ext_ok = match (extensions, extension) with | None, _ -> true | Some _, None -> false | Some extensions, Some extension -> let ret = List.mem extension extensions in if ret then log#info "Unsupported file extension for %s!" (Lang_string.quote_string fname); ret in let mime_ok = match (mimes, mime) with | None, _ -> true | Some mimes, mime -> let mimes = List.map base_mime mimes in let ret = List.mem (base_mime mime) mimes in if not ret then log#info "Unsupported MIME type for %s: %s!" (Lang_string.quote_string fname) mime; ret in ext_ok || mime_ok let channel_layout audio = Lazy.force Content.(Audio.(get_params audio).Content.Audio.channel_layout) let can_decode_type decoded_type target_type = let map_convertible cur (field, target_field) = let decoded_field = Frame.Fields.find_opt field decoded_type in match decoded_field with | None -> cur | Some decoded_field when Content.Audio.is_format decoded_field -> ( Audio_converter.Channel_layout.( try ignore (create (channel_layout decoded_field) (channel_layout target_field)); Frame.Fields.add field target_field cur with _ -> cur)) | Some decoded_field -> Frame.Fields.add field decoded_field cur in (* Map content that can be converted and drop content that isn't used *) let decoded_type = List.fold_left map_convertible Frame.Fields.empty (Frame.Fields.bindings target_type) in Frame.compatible decoded_type target_type exception Found of (string * Frame.content_type * decoder_specs) (** Get a valid decoder creator for [filename]. *) let get_file_decoder ~metadata ~ctype filename = if not (Sys.file_exists filename) then ( log#info "File %s does not exist!" (Lang_string.quote_string filename); None) else ( let extension = try Some (Utils.get_ext filename) with _ -> None in let mime = Magic_mime.lookup filename in let decoders = List.filter (fun (name, specs) -> let log = Log.make ["decoder"; String.lowercase_ascii name] in specs.file_decoder <> None && test_file ~log ~extension ~mime ~mimes:(specs.mime_types ()) ~extensions:(specs.file_extensions ()) filename) (get_decoders ()) in if decoders = [] then ( log#important "No decoder available for %s!" (Lang_string.quote_string filename); None) else ( log#info "Available decoders: %s" (String.concat ", " (List.map (fun (name, specs) -> Printf.sprintf "%s (priority: %d)" name (specs.priority ())) decoders)); try List.iter (fun (name, specs) -> log#info "Trying decoder %S" name; try match specs.file_type ~metadata ~ctype filename with | Some decoded_type -> if can_decode_type decoded_type ctype then raise (Found (name, decoded_type, specs)) else log#info "Cannot decode file %s with decoder %s as %s. Detected \ content: %s" (Lang_string.quote_string filename) name (Frame.string_of_content_type ctype) (Frame.string_of_content_type decoded_type) | None -> () with | Found v -> raise (Found v) | exn -> let bt = Printexc.get_backtrace () in Utils.log_exception ~log ~bt (Printf.sprintf "Error while checking file's content: %s" (Printexc.to_string exn))) decoders; log#important "Available decoders cannot decode %s as %s" (Lang_string.quote_string filename) (Frame.string_of_content_type ctype); None with Found (name, decoded_type, specs) -> log#info "Selected decoder %s for file %s with expected kind %s and detected \ content %s" name (Lang_string.quote_string filename) (Frame.string_of_content_type ctype) (Frame.string_of_content_type decoded_type); Some ( name, fun () -> (Option.get specs.file_decoder) ~metadata ~ctype filename ))) (** Check if decoder can decode image. *) let check_image_file_decoder filename = let decoders = get_image_file_decoders () in log#info "Available image decoders: %s" (String.concat ", " (List.map (fun (name, specs) -> Printf.sprintf "%s (priority: %d)" name (specs.image_decoder_priority ())) decoders)); List.exists (fun (name, { image_decoder_priority; check_image }) -> log#info "Trying image decoder %S (priority: %d)" name (image_decoder_priority ()); if check_image filename then ( log#important "Image decoder %S accepted %s" name (Lang_string.quote_string filename); true) else false) decoders (** Get a valid image decoder creator for [filename]. *) let get_image_file_decoder filename = match List.find_map (fun (name, { decode_image }) -> log#info "Trying method %S for %s..." name (Lang_string.quote_string filename); try Some (decode_image filename) with _ -> None) (get_image_file_decoders ()) with | None -> log#important "Unable to decode %s using image decoder(s)!" (Lang_string.quote_string filename); raise Not_found | Some d -> d let get_stream_decoder ~ctype mime = let decoders = List.filter (fun (_, specs) -> specs.stream_decoder <> None && match specs.mime_types () with | None -> false | Some mimes -> let mimes = List.map base_mime mimes in List.mem (base_mime mime) mimes) (get_decoders ()) in if decoders = [] then ( log#important "Unable to find a decoder for stream mime-type %s with expected content \ %s!" mime (Frame.string_of_content_type ctype); None) else ( log#info "Available stream decoders: %s" (String.concat ", " (List.map (fun (name, specs) -> Printf.sprintf "%s (priority: %d)" name (specs.priority ())) decoders)); let name, decoder = List.hd decoders in log#info "Selected decoder %s for mime-type %s with expected content %s" name mime (Frame.string_of_content_type ctype); Some ((Option.get decoder.stream_decoder ~ctype) mime)) (** {1 Helpers for defining decoders} *) let mk_buffer ~ctype generator = let audio_handlers = Hashtbl.create 0 in let video_handlers = Hashtbl.create 0 in let get_audio_handler ~field = try Hashtbl.find audio_handlers field with Not_found -> let handler = if Frame.Fields.mem field ctype then ( let resampler = Decoder_utils.samplerate_converter () in let current_channel_converter = ref None in let current_dst = ref None in let mk_channel_converter dst = let c = Decoder_utils.channels_converter dst in current_dst := Some dst; current_channel_converter := Some c; c in let get_channel_converter () = let dst = channel_layout (Frame.Fields.find field ctype) in match !current_channel_converter with | None -> mk_channel_converter dst | Some _ when !current_dst <> Some dst -> mk_channel_converter dst | Some c -> c in fun ~samplerate data -> let data, _, _ = resampler ~samplerate data 0 (Audio.length data) in let data = (get_channel_converter ()) data in let data = Content.Audio.lift_data data in Generator.put generator field data) else fun ~samplerate:_ _ -> () in Hashtbl.replace audio_handlers field handler; handler in let get_video_handler ~field = try Hashtbl.find video_handlers field with Not_found -> let handler = if Frame.Fields.mem field ctype then ( let video_resample = Decoder_utils.video_resample () in let video_scale = let width, height = try Content.Video.dimensions_of_format (Option.get (Frame.Fields.find_opt Frame.Fields.video ctype)) with Content.Invalid -> (* We might have encoded contents *) (Lazy.force Frame.video_width, Lazy.force Frame.video_height) in Decoder_utils.video_scale ~width ~height () in let out_freq = Decoder_utils.{ num = Lazy.force Frame.video_rate; den = 1 } in let params = { Content.Video.width = Some Frame.video_width; height = Some Frame.video_height; } in let interval = Frame.main_of_video 1 in fun ~fps img -> match video_resample ~in_freq:fps ~out_freq img with | [] -> () | data -> let data = List.mapi (fun i img -> (i * interval, video_scale img)) data in let length = List.length data * interval in let buf = Content.Video.lift_data { Content.Video.params; length; data } in Generator.put generator field buf) else fun ~fps:_ _ -> () in Hashtbl.replace video_handlers field handler; handler in let put_pcm ?(field = Frame.Fields.audio) ~samplerate data = get_audio_handler ~field ~samplerate data in let put_yuva420p ?(field = Frame.Fields.video) ~fps img = get_video_handler ~field ~fps img in { generator; put_pcm; put_yuva420p } let mk_decoder ~filename ~remaining ~buffer decoder = let decoding_done = ref false in let remaining () = try remaining () + Generator.length buffer.generator with e -> log#info "Error while getting decoder's remaining time: %s" (Printexc.to_string e); decoding_done := true; 0 in let fclose () = decoder.eof buffer; decoder.close () in let fread size = if not !decoding_done then ( try while Generator.length buffer.generator < size do decoder.decode buffer done with e -> let bt = Printexc.get_backtrace () in Utils.log_exception ~log ~bt (Printf.sprintf "Decoding %s ended: %s." (Lang_string.quote_string filename) (Printexc.to_string e)); decoding_done := true; if conf_debug#get then raise e); Generator.slice buffer.generator size in let fseek len = let gen_len = Generator.length buffer.generator in if len < 0 || len > gen_len then ( Generator.clear buffer.generator; gen_len + decoder.seek (len - gen_len)) else ( (* Seek within the pre-buffered data if possible *) Generator.truncate buffer.generator len; len) in { fread; remaining; fseek; fclose } let file_decoder ~filename ~remaining ~ctype decoder = let generator = Generator.create ~log ctype in let buffer = mk_buffer ~ctype generator in mk_decoder ~filename ~remaining ~buffer decoder let openfile filename = let extra_flags = if Sys.win32 then [Unix.O_SHARE_DELETE] else [] in Unix.openfile filename ([Unix.O_RDONLY; Unix.O_CLOEXEC] @ extra_flags) 0 let opaque_file_decoder ~filename ~ctype create_decoder = let fd = openfile filename in let file_size = (Unix.stat filename).Unix.st_size in let proc_bytes = ref 0 in let read buf ofs len = try let i = Unix.read fd buf ofs len in proc_bytes := !proc_bytes + i; i with _ -> 0 in let tell () = Unix.lseek fd 0 Unix.SEEK_CUR in let length () = (Unix.fstat fd).Unix.st_size in let lseek len = Unix.lseek fd len Unix.SEEK_SET in let input = { read; tell = Some tell; length = Some length; lseek = Some lseek } in let generator = Generator.create ~log ctype in let buffer = mk_buffer ~ctype generator in let decoder = create_decoder input in let out_ticks = ref 0 in let decode buffer = let start = Generator.length buffer.generator in decoder.decode buffer; let stop = Generator.length buffer.generator in out_ticks := !out_ticks + stop - start in let decoder = { decoder with decode; close = (fun () -> Unix.close fd) } in let remaining () = let in_bytes = tell () in (* Compute an estimated number of remaining ticks. *) if !proc_bytes = 0 then -1 else ( let compression = float !out_ticks /. float !proc_bytes in let remaining_ticks = float (file_size - in_bytes) *. compression in int_of_float remaining_ticks) in mk_decoder ~filename ~remaining ~buffer decoder liquidsoap-2.3.2/src/core/decoder/decoder.mli000066400000000000000000000115301477303350200211300ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm val log : Log.t type file = string type stream = string type input = { read : bytes -> int -> int -> int; (* Seek to an absolute position in bytes. Returns the current position after seeking or raises [No_seek] if no seek operation is available. *) lseek : (int -> int) option; tell : (unit -> int) option; length : (unit -> int) option; } type fps = { num : int; den : int } (* Buffer passed to decoder. This wraps around regular buffer, adding: - Implicit resampling - Implicit audio channel conversion - Implicit video resize - Implicit fps conversion - Implicit content drop *) type buffer = { generator : Generator.t; put_pcm : ?field:Frame.field -> samplerate:int -> Audio.t -> unit; put_yuva420p : ?field:Frame.field -> fps:fps -> Video.Canvas.image -> unit; } type decoder = { decode : buffer -> unit; eof : buffer -> unit; (* [seek x]: Skip [x] main ticks. Returns the number of ticks atcually skipped. *) seek : int -> int; close : unit -> unit; } type file_decoder_ops = { fread : int -> Frame.t; remaining : unit -> int; fseek : int -> int; fclose : unit -> unit; } type stream_decoder = input -> decoder type image_decoder = { image_decoder_priority : unit -> int; check_image : file -> bool; decode_image : file -> Video.Image.t; } type file_decoder = metadata:Frame.metadata -> ctype:Frame.content_type -> string -> file_decoder_ops type decoder_specs = { priority : unit -> int; (* None means accept all file extensions. *) file_extensions : unit -> string list option; (* Mime types are parsed up-to the first ; so a file with mime-type foo/bar; bla matches mime-type foo/bar. Furthermore, for streams, a stream with mime foo/bar matches mime-type foo/bar. None means accept all mime-types. *) mime_types : unit -> string list option; (* None means no decodable content for that file. *) file_type : metadata:Frame.metadata -> ctype:Frame.content_type -> string -> Frame.content_type option; file_decoder : file_decoder option; (* String argument is the full mime-type. *) stream_decoder : (ctype:Frame.content_type -> string -> stream_decoder) option; } val decoders : decoder_specs Plug.t val conf_decoder : Dtools.Conf.ut val conf_mime_types : Dtools.Conf.ut val conf_file_extensions : Dtools.Conf.ut val conf_priorities : Dtools.Conf.ut val conf_image_priorities : Dtools.Conf.ut (** Open file with readonly, cloexec and share delete on windows. *) val openfile : string -> Unix.file_descr (** Test file extension and mime if available *) val test_file : log:Log.t -> extension:string option -> mime:stream -> mimes:stream list option -> extensions:string list option -> stream -> bool (** Test if we can decode for a content_type. This include cases where we know how to convert channel layout. *) val can_decode_type : Frame.content_type -> Frame.content_type -> bool val get_file_decoder : metadata:Frame.metadata -> ctype:Frame.content_type -> string -> (string * (unit -> file_decoder_ops)) option val get_stream_decoder : ctype:Frame.content_type -> string -> stream_decoder option val image_file_decoders : image_decoder Plug.t val check_image_file_decoder : file -> bool val get_image_file_decoder : file -> Video.Image.t (* Initialize a decoding buffer *) val mk_buffer : ctype:Frame.content_type -> Generator.t -> buffer (* Create a file decoder when remaining time is known. *) val file_decoder : filename:string -> remaining:(unit -> int) -> ctype:Frame.content_type -> decoder -> file_decoder_ops (* Create a file decoder when remaining time is not know, in which case it is estimated from consumed bytes during the decoding process. *) val opaque_file_decoder : filename:string -> ctype:Frame.content_type -> (input -> decoder) -> file_decoder_ops liquidsoap-2.3.2/src/core/decoder/decoder_utils.ml000066400000000000000000000122001477303350200221720ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm type samplerate_converter = samplerate:int -> Content.Audio.data -> int -> int -> Content.Audio.data * int * int let samplerate_converter () = let state = ref None in fun ~samplerate audio_buf ofs len -> let _channels = Array.length audio_buf in let audio_dst_rate = float (Lazy.force Frame.audio_rate) in let ratio = audio_dst_rate /. float samplerate in match !state with | Some converter -> Audio_converter.Samplerate.resample converter ratio audio_buf ofs len | None -> let converter = Audio_converter.Samplerate.create _channels in state := Some converter; Audio_converter.Samplerate.resample converter ratio audio_buf 0 len type wav_converter = bytes -> int -> int -> Content.Audio.data let from_iff ~format ~channels ~samplesize = let sample_bytes = samplesize / 8 in let buf = Buffer.create Utils.pagesize in fun src ofs len -> Buffer.add_subbytes buf src ofs len; let src = Buffer.contents buf in let src_len = String.length src in let elem_len = sample_bytes * channels in let sample_len = src_len / elem_len in let sample_bytes_len = sample_len * elem_len in Buffer.reset buf; Buffer.add_substring buf src sample_bytes_len (src_len - sample_bytes_len); let dst = Audio.create channels sample_len in let to_audio = match samplesize with | 8 -> Audio.U8.to_audio | 16 when format = `Wav -> Audio.S16LE.to_audio | 16 when format = `Aiff -> Audio.S16BE.to_audio | 24 when format = `Wav -> Audio.S24LE.to_audio | 32 when format = `Wav -> Audio.S32LE.to_audio | _ -> failwith "unsupported sample size" in to_audio src 0 dst 0 sample_len; dst type channels_converter = Content.Audio.data -> Content.Audio.data let channels_converter dst = let converter = ref None in fun data -> let _src = Array.length data in match !converter with | Some (c, src) when src = _src -> Audio_converter.Channel_layout.convert c data | _ -> let src = Audio_converter.Channel_layout.layout_of_channels _src in let c = Audio_converter.Channel_layout.create src dst in converter := Some (c, _src); Audio_converter.Channel_layout.convert c data let video_scale ~width ~height () = let scaler = Video_converter.scaler () in Video.Canvas.Image.resize ~scaler width height type fps = { num : int; den : int } (** Stupid nearest neighbour resampling. For meaningful results, one should first partially apply the freq params, and reuse the resulting functions on consecutive chunks of a single input stream. *) let video_resample ~in_freq ~out_freq = (* We have something like this: * i i i i i i i i i i i i i i i i i i i ... o o o o o o ... * (1) We ensure that out_len/out_freq = in_len/in_freq asymptotically. For doing so, we must keep track of the full input length, modulo in_freq. (2) We do the simplest possible thing to choose which i becomes which o: nearest neighbour in the currently available buffer. This is not as good as nearest neighbour in the real stream. * Turns out the same code codes for when out_freq>in_freq works too. *) let in_pos = ref 0 in let in_freq = in_freq.num * out_freq.den and out_freq = out_freq.num * in_freq.num in let ratio = out_freq / in_freq in fun img -> let new_in_pos = !in_pos + 1 in let already_out_len = !in_pos * ratio in let needed_out_len = new_in_pos * ratio in let out_len = needed_out_len - already_out_len in in_pos := new_in_pos mod in_freq; List.init out_len (fun _ -> img) let video_resample () = let state = ref None in fun ~in_freq ~out_freq img -> if in_freq = out_freq then [img] else ( match !state with | Some (resampler, _in_freq, _out_freq) when in_freq = _in_freq && out_freq = _out_freq -> resampler img | _ -> let resampler = video_resample ~in_freq ~out_freq in state := Some (resampler, in_freq, out_freq); resampler img) liquidsoap-2.3.2/src/core/decoder/decoder_utils.mli000066400000000000000000000036241477303350200223550ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (** Resampling module for any Frame.content *) type samplerate_converter = samplerate:int -> Content.Audio.data -> int -> int -> Content.Audio.data * int * int val samplerate_converter : unit -> samplerate_converter type wav_converter = bytes -> int -> int -> Content.Audio.data (** samplesize is in bits. Formats: unsigned 8 bit (u8) or signed 16 bit little endian (s16le) *) val from_iff : format:Wav_aiff.format -> channels:int -> samplesize:int -> wav_converter type channels_converter = Content.Audio.data -> Content.Audio.data val channels_converter : Audio_converter.Channel_layout.layout -> channels_converter val video_scale : width:int -> height:int -> unit -> Video.Canvas.Image.t -> Video.Canvas.Image.t type fps = { num : int; den : int } val video_resample : unit -> in_freq:fps -> out_freq:fps -> Video.Canvas.image -> Video.Canvas.image list liquidsoap-2.3.2/src/core/decoder/external_decoder.ml000066400000000000000000000160121477303350200226610ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Decode files using an external decoder. *) (** First, an external decoder that receives on its stdin. *) let log = Log.make ["decoder"; "external"] let on_stderr = let buf = Bytes.create Utils.pagesize in fun puller -> let len = puller buf 0 Utils.pagesize in log#debug "stderr: %s" (Bytes.unsafe_to_string (Bytes.sub buf 0 len)); `Continue (** This function is used to wrap around the "real" input. It pipes its data to the external process and read the available output. *) let external_input process input = let buflen = Utils.pagesize in let buf = Bytes.create buflen in let on_stdin pusher = let read = input.Decoder.read buf 0 buflen in if read = 0 then `Stop else ( Process_handler.really_write (Bytes.sub buf 0 read) pusher; `Continue) in let log s = log#important "%s" s in (* reading from input is blocking.. *) let priority = `Blocking in let process = Process_handler.run ~priority ~on_stdin ~on_stderr ~log process in let read buf ofs len = try Process_handler.on_stdout process (fun reader -> reader buf ofs len) with Process_handler.Finished -> 0 in ( { Decoder.read; tell = None; length = None; lseek = None }, fun () -> try Process_handler.kill process with Process_handler.Finished -> () ) let duration process = let pull = Unix.open_process_in process in let w = Wav_aiff.in_chan_read_header pull in let ret = Wav_aiff.duration w in ignore (Unix.close_process_in pull); ret (** A function to wrap around the Wav_aiff_decoder *) let create process ctype filename = let close = ref (fun () -> ()) in let create input = let input, actual_close = external_input process input in close := actual_close; Wav_aiff_decoder.create ?header:None input in let dec = Decoder.opaque_file_decoder ~filename ~ctype create in { dec with Decoder.fclose = (fun () -> Fun.protect ~finally:(fun () -> dec.Decoder.fclose ()) !close); } let create_stream process input = let input, close = external_input process input in (* Put this here so that ret is not in its closure.. *) let close _ = close () in let ret = Wav_aiff_decoder.create input in Gc.finalise close ret; ret let audio_n = Frame_base.format_of_channels ~pcm_kind:Content.Audio.kind let test_ctype f filename = (* 0 = file rejected, n<0 = file accepted, unknown number of audio channels, n>0 = file accepted, known number of channels. *) let ret = f filename in if ret = 0 then None else Some (Frame.Fields.make ~audio: (if ret < 0 then audio_n (Lazy.force Frame.audio_channels) else audio_n ret) ()) let register_stdin ~name ~doc ~priority ~mimes ~file_extensions ~test process = Plug.register Decoder.decoders name ~doc { Decoder.priority = (fun () -> priority); file_extensions = (fun () -> file_extensions); mime_types = (fun () -> mimes); file_type = (fun ~metadata:_ ~ctype:_ filename -> test_ctype test filename); file_decoder = Some (fun ~metadata:_ ~ctype filename -> create process ctype filename); stream_decoder = Some (fun ~ctype:_ _ -> create_stream process); }; let dresolver ~metadata:_ filename = let process = Printf.sprintf "cat %s | %s" (Filename.quote filename) process in duration process in Plug.register Request.dresolvers name ~doc { dpriority = (fun () -> priority); file_extensions = (fun () -> Option.value ~default:[] file_extensions); dresolver; } (** Now an external decoder that directly operates on the file. The remaining time in this case can only be approximative. It is -1 while the file is being decoded and the length of the buffer when the external decoder has exited. *) let log = Log.make ["decoder"; "external"; "oblivious"] let external_input_oblivious process filename prebuf = let command = process filename in let process = Process_handler.run ~on_stderr ~log:(log#important "%s") command in let read buf ofs len = try Process_handler.on_stdout process (fun reader -> reader buf ofs len) with Process_handler.Finished -> 0 in let fclose () = try Process_handler.kill process with Process_handler.Finished -> () in let input = { Decoder.read; tell = None; length = None; lseek = None } in (* TODO: is this really what we want for audio channels? *) let ctype = Frame.Fields.make ~audio:(audio_n (Lazy.force Frame.audio_channels)) () in let gen = Generator.create ~log ctype in let buffer = Decoder.mk_buffer ~ctype gen in let prebuf = Frame.main_of_seconds prebuf in let decoder = Wav_aiff_decoder.create input in let fread len = begin try while Generator.length gen < max prebuf len && not (Process_handler.stopped process) do decoder.Decoder.decode buffer done with e -> log#info "Decoding %s ended: %s." command (Printexc.to_string e); fclose () end; Generator.slice gen len in let remaining () = (* We return -1 while the process is not yet * finished. *) if Process_handler.stopped process then Generator.length gen else -1 in { Decoder.fread; remaining; fseek = decoder.Decoder.seek; fclose } let register_oblivious ~name ~doc ~priority ~mimes ~file_extensions ~test ~process prebuf = Plug.register Decoder.decoders name ~doc { Decoder.priority = (fun () -> priority); file_extensions = (fun () -> file_extensions); mime_types = (fun () -> mimes); file_type = (fun ~metadata:_ ~ctype:_ filename -> test_ctype test filename); file_decoder = Some (fun ~metadata:_ ~ctype:_ filename -> external_input_oblivious process filename prebuf); stream_decoder = None; }; let dresolver ~metadata:_ filename = duration (process filename) in Plug.register Request.dresolvers name ~doc { dpriority = (fun () -> priority); file_extensions = (fun () -> Option.value ~default:[] file_extensions); dresolver; } liquidsoap-2.3.2/src/core/decoder/ffmpeg_copy_decoder.ml000066400000000000000000000063121477303350200233370ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Decode ffmpeg packets. *) open Avcodec let log = Log.make ["ffmpeg"; "decoder"; "copy"] exception Corrupt exception Empty let mk_decoder ~stream_idx ~stream_time_base ~mk_packet ~put_data params = let duration_converter = Ffmpeg_utils.Duration.init ~mode:`DTS ~src:stream_time_base ~convert_ts:false ~get_ts:Packet.get_dts ~set_ts:Packet.set_dts () in fun ~buffer packet -> try let flags = Packet.get_flags packet in if List.mem `Corrupt flags then ( log#important "Corrupted packet in stream!"; raise Corrupt); let packets = Ffmpeg_utils.Duration.push duration_converter packet in if packets = None then raise Empty; let length, packets = Option.get packets in let data = List.map (fun (pos, packet) -> ( pos, { Ffmpeg_copy_content.packet = mk_packet packet; time_base = stream_time_base; stream_idx; } )) packets in let data = { Content.Video.params = Some params; data; length } in let data = Ffmpeg_copy_content.lift_data data in put_data buffer.Decoder.generator data with Empty | Corrupt (* Might want to change that later. *) -> () let mk_audio_decoder ~stream_idx ~format ~field ~stream params = Ffmpeg_decoder_common.set_audio_stream_decoder stream; let params = `Audio params in ignore (Content.merge format (Ffmpeg_copy_content.lift_params (Some params))); let stream_time_base = Av.get_time_base stream in mk_decoder ~stream_idx ~mk_packet:(fun p -> `Audio p) ~stream_time_base ~put_data:(fun g c -> Generator.put g field c) params let mk_video_decoder ~stream_idx ~format ~stream ~field params = Ffmpeg_decoder_common.set_video_stream_decoder stream; let params = `Video { Ffmpeg_copy_content.avg_frame_rate = Av.get_avg_frame_rate stream; params; } in ignore (Content.merge format (Ffmpeg_copy_content.lift_params (Some params))); let stream_time_base = Av.get_time_base stream in mk_decoder ~stream_idx ~mk_packet:(fun p -> `Video p) ~stream_time_base ~put_data:(fun g c -> Generator.put g field c) params liquidsoap-2.3.2/src/core/decoder/ffmpeg_copy_decoder.mli000066400000000000000000000030501477303350200235040ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) val mk_audio_decoder : stream_idx:int64 -> format:Content_base.Contents.format -> field:int -> stream:(Avutil.input, Avutil.audio, [ `Packet ]) Av.stream -> Avutil.audio Avcodec.params -> buffer:Decoder.buffer -> Avutil.audio Avcodec.Packet.t -> unit val mk_video_decoder : stream_idx:int64 -> format:Content_base.Contents.format -> stream:(Avutil.input, Avutil.video, [ `Packet ]) Av.stream -> field:int -> Avutil.video Avcodec.params -> buffer:Decoder.buffer -> Avutil.video Avcodec.Packet.t -> unit liquidsoap-2.3.2/src/core/decoder/ffmpeg_decoder.ml000066400000000000000000001134721477303350200223130ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Decode and read metadata using ffmpeg. *) exception End_of_file exception No_stream exception Invalid_file let log = Log.make ["decoder"; "ffmpeg"] (* Workaround for https://trac.ffmpeg.org/ticket/9540. Should be fixed with the next FFMpeg release. *) let parse_timed_id3 content = if String.length content < 3 then failwith "Invalid content"; if String.sub content 0 3 = "ID3" then Metadata.Reader.with_string Metadata.ID3.parse content else ( try let metadata = Printf.sprintf "ID3\003\000%s" content in Metadata.Reader.with_string Metadata.ID3.parse metadata with _ -> let metadata = Printf.sprintf "ID3\004\000%s" content in Metadata.Reader.with_string Metadata.ID3.parse metadata) module Streams = Map.Make (struct type t = int let compare = Stdlib.compare end) (** Configuration keys for ffmpeg. *) let mime_types = Dtools.Conf.list ~p:(Decoder.conf_mime_types#plug "ffmpeg") "Mime-types used for decoding with ffmpeg" ~d: [ "application/f4v"; "application/ffmpeg"; "application/mp4"; "application/mxf"; "application/octet-stream"; "application/octet-stream"; "application/ogg"; "application/vnd.pg.format"; "application/vnd.rn-realmedia"; "application/vnd.smaf"; "application/x-mpegURL"; "application/x-ogg"; "application/x-pgs"; "application/x-shockwave-flash"; "application/x-subrip"; "application/xml"; "audio/G722"; "audio/MP4A-LATM"; "audio/MPA"; "audio/aac"; "audio/aacp"; "audio/aiff"; "audio/amr"; "audio/basic"; "audio/bit"; "audio/flac"; "audio/g723"; "audio/iLBC"; "audio/mp4"; "audio/mpeg"; "audio/ogg"; "audio/vnd.wave"; "audio/wav"; "audio/wave"; "audio/webm"; "audio/x-ac3"; "audio/x-adpcm"; "audio/x-caf"; "audio/x-dca"; "audio/x-eac3"; "audio/x-flac"; "audio/x-gsm"; "audio/x-hx-aac-adts"; "audio/x-ogg"; "audio/x-oma"; "audio/x-tta"; "audio/x-voc"; "audio/x-wav"; "audio/x-wavpack"; "multipart/x-mixed-replace;boundary=ffserver"; "text/vtt"; "text/x-ass"; "text/x-jacosub"; "text/x-microdvd"; "video/3gpp"; "video/3gpp2"; "video/MP2T"; "video/mp2t"; "video/mp4"; "video/mpeg"; "video/ogg"; "video/webm"; "video/x-flv"; "video/x-h261"; "video/x-h263"; "video/x-m4v"; "video/x-matroska"; "video/x-mjpeg"; "video/x-ms-asf"; "video/x-msvideo"; "video/x-nut"; ] let image_mime_types = Dtools.Conf.list ~p:(mime_types#plug "images") "Mime-types used for decoding images with ffmpeg" ~d: [ "image/gif"; "image/jpeg"; "image/png"; "image/vnd.microsoft.icon"; "image/webp"; ] let file_extensions = Dtools.Conf.list ~p:(Decoder.conf_file_extensions#plug "ffmpeg") "File extensions used for decoding media files (except images) with ffmpeg" ~d: [ "264"; "265"; "302"; "3g2"; "3gp"; "669"; "722"; "A64"; "a64"; "aa"; "aa3"; "aac"; "aax"; "ac3"; "acm"; "adf"; "adp"; "ads"; "adts"; "adx"; "aea"; "afc"; "aif"; "aifc"; "aiff"; "aix"; "amf"; "amr"; "ams"; "amv"; "ape"; "apl"; "apm"; "apng"; "aptx"; "aptxhd"; "aqt"; "asf"; "ass"; "ast"; "au"; "aud"; "avi"; "avr"; "avs"; "avs2"; "bcstm"; "bfstm"; "binka"; "bit"; "bmv"; "brstm"; "c2"; "calf"; "cavs"; "cdata"; "cdg"; "cdxl"; "cgi"; "chk"; "cif"; "cpk"; "cvg"; "dat"; "daud"; "dav"; "dbm"; "dif"; "digi"; "dmf"; "dnxhd"; "dnxhr"; "drc"; "dsm"; "dss"; "dtk"; "dtm"; "dts"; "dtshd"; "dv"; "dvd"; "eac3"; "f4v"; "fap"; "far"; "ffmeta"; "fits"; "flac"; "flm"; "flv"; "fsb"; "fwse"; "g722"; "g723_1"; "g729"; "gdm"; "genh"; "gif"; "gsm"; "gxf"; "h261"; "h263"; "h264"; "h265"; "hca"; "hevc"; "ice"; "ico"; "idf"; "idx"; "ifv"; "imf"; "imx"; "ipu"; "ircam"; "ism"; "isma"; "ismv"; "it"; "ivf"; "ivr"; "j2b"; "jss"; "kux"; "latm"; "lbc"; "loas"; "lrc"; "lvf"; "m15"; "m1v"; "m2a"; "m2t"; "m2ts"; "m2v"; "m3u8"; "m4a"; "m4b"; "m4v"; "mac"; "mca"; "mcc"; "mdl"; "med"; "mj2"; "mjpeg"; "mjpg"; "mk3d"; "mka"; "mks"; "mkv"; "mlp"; "mmcmp"; "mmf"; "mms"; "mo3"; "mod"; "mods"; "moflex"; "mov"; "mp2"; "mp3"; "mp4"; "mpa"; "mpc"; "mpd"; "mpeg"; "mpg"; "mpl2"; "mptm"; "msbc"; "msf"; "mt2"; "mtaf"; "mtm"; "mts"; "musx"; "mvi"; "mxf"; "mxg"; "nist"; "nsp"; "nst"; "nut"; "obu"; "oga"; "ogg"; "ogv"; "okt"; "oma"; "omg"; "opus"; "paf"; "pcm"; "pjs"; "plm"; "psm"; "psp"; "pt36"; "ptm"; "pvf"; "qcif"; "ra"; "rco"; "rcv"; "rgb"; "rm"; "roq"; "rsd"; "rso"; "rt"; "s3m"; "sami"; "sbc"; "sbg"; "scc"; "sdr2"; "sds"; "sdx"; "ser"; "sf"; "sfx"; "sfx2"; "sga"; "shn"; "sln"; "smi"; "son"; "sox"; "spdif"; "sph"; "spx"; "srt"; "ss2"; "ssa"; "st26"; "stk"; "stl"; "stm"; "stp"; "str"; "sub"; "sup"; "svag"; "svs"; "swf"; "tak"; "tco"; "thd"; "ts"; "tta"; "ttml"; "tun"; "txt"; "ty"; "ty+"; "ult"; "umx"; "v"; "v210"; "vag"; "vb"; "vc1"; "vc2"; "viv"; "vob"; "voc"; "vpk"; "vqe"; "vqf"; "vql"; "vtt"; "w64"; "wav"; "webm"; "wma"; "wmv"; "wow"; "wsd"; "wtv"; "wv"; "xl"; "xm"; "xml"; "xmv"; "xpk"; "xvag"; "y4m"; "yop"; "yuv"; ] let image_file_extensions = Dtools.Conf.list ~p:(file_extensions#plug "images") "File extensions used for decoding images with ffmpeg" ~d: [ "bmp"; "cri"; "dds"; "dng"; "dpx"; "exr"; "im1"; "im24"; "im32"; "im8"; "j2c"; "j2k"; "jls"; "jp2"; "jpc"; "jpeg"; "jpg"; "jps"; "ljpg"; "mng"; "mpg1-img"; "mpg2-img"; "mpg4-img"; "mpo"; "pam"; "pbm"; "pcd"; "pct"; "pcx"; "pfm"; "pgm"; "pgmyuv"; "pic"; "pict"; "pix"; "png"; "pnm"; "pns"; "ppm"; "ptx"; "ras"; "raw"; "rs"; "sgi"; "sun"; "sunras"; "svg"; "svgz"; "tga"; "tif"; "tiff"; "webp"; "xbm"; "xface"; "xpm"; "xwd"; "y"; "yuv10"; ] let priority = Dtools.Conf.int ~p:(Decoder.conf_priorities#plug "ffmpeg") "Priority for the ffmpeg decoder" ~d:10 let parse_encoder_params = let processor = MenhirLib.Convert.Simplified.traditional2revised Liquidsoap_lang.Parser.plain_encoder_params in fun s -> let lexbuf = Sedlexing.Utf8.from_string ("(" ^ s ^ ")") in let tokenizer = Liquidsoap_lang.Preprocessor.mk_tokenizer lexbuf in Liquidsoap_lang.Term_reducer.to_encoder_params (processor tokenizer) let parse_input_args args = try let args = parse_encoder_params args in List.fold_left (fun (args, format) -> function | `Labelled ("f", Term.{ term = `Var format; _ }) | `Labelled ("format", Term.{ term = `Var format; _ }) -> (args, Av.Format.find_input_format format) | `Labelled (k, Term.{ term = `Var v; _ }) -> ((k, `String v) :: args, format) | `Labelled (k, Term.{ term = `String s; _ }) -> ((k, `String s) :: args, format) | `Labelled (k, Term.{ term = `Int i; _ }) -> ((k, `Int i) :: args, format) | `Labelled (k, Term.{ term = `Float f; _ }) -> ((k, `Float f) :: args, format) | _ -> assert false) ([], None) args with _ -> Runtime_error.raise ~pos:[] ~message:"Invalid mime type arguments!" "ffmpeg_decoder" let parse_file_decoder_args metadata = match Frame.Metadata.find_opt "ffmpeg_options" metadata with | Some args -> parse_input_args args | None -> ([], None) let dresolver ~metadata file = let args, format = parse_file_decoder_args metadata in let opts = Hashtbl.create 10 in List.iter (fun (k, v) -> Hashtbl.replace opts k v) args; let container = Av.open_input ?format ~opts file in Fun.protect ~finally:(fun () -> Av.close container) (fun () -> let duration = Av.get_input_duration container ~format:`Millisecond in Option.map (fun d -> Int64.to_float d /. 1000.) duration) let () = Plug.register Request.dresolvers "ffmpeg" ~doc:"" { dpriority = (fun () -> priority#get); file_extensions = (fun () -> file_extensions#get); dresolver = (fun ~metadata fname -> match dresolver ~metadata fname with | None -> raise Not_found | Some d -> d); } let tags_substitutions = [("track", "tracknumber")] let get_tags ~metadata ~extension ~mime file = try if not (Decoder.test_file ~log ~extension ~mime ~mimes:(Some mime_types#get) ~extensions:(Some file_extensions#get) file) then raise Invalid_file; let args, format = parse_file_decoder_args metadata in let opts = Hashtbl.create 10 in List.iter (fun (k, v) -> Hashtbl.replace opts k v) args; let container = Av.open_input ?format ~opts file in Fun.protect ~finally:(fun () -> Av.close container) (fun () -> (* For now we only add the metadata from the best audio track *) let audio_tags = try let _, s, _ = Av.find_best_audio_stream container in Av.get_metadata s with _ -> [] in let tags = Av.get_input_metadata container in List.map (fun (lbl, v) -> try (List.assoc lbl tags_substitutions, v) with _ -> (lbl, v)) (audio_tags @ tags)) with | Invalid_file -> [] | e -> let bt = Printexc.get_backtrace () in Utils.log_exception ~log ~bt (Printf.sprintf "Error while decoding file tags: %s" (Printexc.to_string e)); raise Not_found let metadata_decoder_priority = Dtools.Conf.int ~p:(Request.conf_metadata_decoder_priorities#plug "ffmpeg") "Priority for the ffmpeg metadata decoder" ~d:1 let () = Plug.register Request.mresolvers "ffmpeg" ~doc:"" { Request.priority = (fun () -> metadata_decoder_priority#get); resolver = get_tags; } (* Get the type of an input container. *) let get_type ~ctype ~format ~url container = let uri = Lang_string.quote_string url in log#important "Requested content-type for %s%s: %s" (match format with | Some f -> Printf.sprintf "format: %s, uri: " (Lang_string.quote_string (Av.Format.get_input_name f)) | None -> "") uri (Frame.string_of_content_type ctype); let audio_streams, descriptions = List.fold_left (fun (audio_streams, descriptions) (_, _, params) -> try let field = Frame.Fields.audio_n (List.length audio_streams) in let channels = Avcodec.Audio.get_nb_channels params in let samplerate = Avcodec.Audio.get_sample_rate params in let codec_name = Avcodec.Audio.string_of_id (Avcodec.Audio.get_params_id params) in let description = Printf.sprintf "%s: {codec: %s, %dHz, %d channel(s)}" (Frame.Fields.string_of_field field) codec_name samplerate channels in ((field, params) :: audio_streams, description :: descriptions) with Avutil.Error _ as exn -> let bt = Printexc.get_raw_backtrace () in Utils.log_exception ~log ~bt:(Printexc.raw_backtrace_to_string bt) (Printf.sprintf "Failed to get an audio stream info: %s" (Printexc.to_string exn)); (audio_streams, descriptions)) ([], []) (Av.get_audio_streams container) in let video_streams, descriptions = List.fold_left (fun (video_streams, descriptions) (_, stream, params) -> try let field = Frame.Fields.video_n (List.length video_streams) in let width = Avcodec.Video.get_width params in let height = Avcodec.Video.get_height params in let pixel_format = match Avcodec.Video.get_pixel_format params with | None -> "unknown" | Some f -> ( match Avutil.Pixel_format.to_string f with | None -> "none" | Some s -> s) in let codec_name = Avcodec.Video.string_of_id (Avcodec.Video.get_params_id params) in let description = Printf.sprintf "%s: {codec: %s, %dx%d, %s}" (Frame.Fields.string_of_field field) codec_name width height pixel_format in ( video_streams @ [(field, Av.get_avg_frame_rate stream, params)], descriptions @ [description] ) with Avutil.Error _ as exn -> let bt = Printexc.get_raw_backtrace () in Utils.log_exception ~log ~bt:(Printexc.raw_backtrace_to_string bt) (Printf.sprintf "Failed to get video stream info: %s" (Printexc.to_string exn)); (video_streams, descriptions)) ([], descriptions) (Av.get_video_streams container) in let _, descriptions = List.fold_left (fun (n, descriptions) (_, _, params) -> try let field = Frame.Fields.data_n n in let codec_name = Avcodec.Unknown.string_of_id (Avcodec.Unknown.get_params_id params) in ( n + 1, descriptions @ [ Printf.sprintf "%s: {codec: %s}" (Frame.Fields.string_of_field field) codec_name; ] ) with Avutil.Error _ as exn -> let bt = Printexc.get_raw_backtrace () in Utils.log_exception ~log ~bt:(Printexc.raw_backtrace_to_string bt) (Printf.sprintf "Failed to get stream info: %s" (Printexc.to_string exn)); (n, descriptions)) (0, descriptions) (Av.get_data_streams container) in if audio_streams = [] && video_streams = [] then failwith "No valid stream found in container."; let content_type = List.fold_left (fun content_type (field, params) -> match (params, Frame.Fields.find_opt field ctype) with | p, Some format when Ffmpeg_copy_content.is_format format -> ignore (Content.merge format (Ffmpeg_copy_content.lift_params (Some (`Audio p)))); Frame.Fields.add field format content_type | p, Some format when Ffmpeg_raw_content.Audio.is_format format -> ignore (Content.merge format Ffmpeg_raw_content.( Audio.lift_params (AudioSpecs.mk_params p))); Frame.Fields.add field format content_type | p, Some format -> Frame.Fields.add field (Frame_base.format_of_channels ~pcm_kind:(Content.kind format) (Avcodec.Audio.get_nb_channels p)) content_type | _ -> content_type) Frame.Fields.empty audio_streams in let content_type = List.fold_left (fun content_type (field, avg_frame_rate, params) -> match (params, Frame.Fields.find_opt field ctype) with | params, Some format when Ffmpeg_copy_content.is_format format -> ignore (Content.merge format (Ffmpeg_copy_content.lift_params (Some (`Video { Ffmpeg_copy_content.avg_frame_rate; params })))); Frame.Fields.add field format content_type | p, Some format when Ffmpeg_raw_content.Video.is_format format -> ignore (Content.merge format Ffmpeg_raw_content.( Video.lift_params (VideoSpecs.mk_params p))); Frame.Fields.add field format content_type | _, Some _ -> Frame.Fields.add field Content.(default_format Video.kind) content_type | _ -> content_type) content_type video_streams in log#important "FFmpeg recognizes %s as %s" uri (String.concat ", " descriptions); log#important "Decoded content-type for %s: %s" uri (Frame.string_of_content_type content_type); content_type let seek ~target_position ~container ticks = let tpos = Frame.seconds_of_main ticks in log#debug "Setting target position to %f" tpos; target_position := Some tpos; let ts = Int64.of_float (tpos *. 1000.) in let frame_duration = Lazy.force Frame.duration in let min_ts = Int64.of_float ((tpos -. frame_duration) *. 1000.) in let max_ts = ts in Av.seek ~fmt:`Millisecond ~min_ts ~max_ts ~ts container; ticks let mk_eof streams buffer = Streams.iter (fun _ -> function | `Audio_frame (_, decoder) -> decoder ~buffer `Flush | `Video_frame (_, decoder) -> decoder ~buffer `Flush | _ -> ()) streams let mk_decoder ~streams ~target_position container = let streams_seen = Hashtbl.create 0 in let position ~pts stream = let { Avutil.num; den } = Av.get_time_base stream in Int64.to_float pts *. float num /. float den in let decodable = ref [] in let push (position, ts, decode) = decodable := (position, ts, decode) :: List.filter (fun (p, _, _) -> Float.abs (p -. position) <= Ffmpeg_decoder_common.conf_max_interleave_duration#get) !decodable in let flush position = let d = List.sort (fun (_, p, _) (_, p', _) -> Int64.compare p p') !decodable in let min_position = position -. Ffmpeg_decoder_common.conf_max_interleave_delta#get in List.iter (fun (p, _, decode) -> if min_position <= p then decode ()) d; decodable := [] in let check_pts ~decode ~ts stream pts = match (pts, !target_position) with | Some pts, Some target_position -> if target_position <= position ~pts stream then decode () | Some pts, None -> Hashtbl.replace streams_seen (Hashtbl.hash stream) true; let position = position ~pts stream in if Hashtbl.length streams_seen = Streams.cardinal streams then ( flush position; decode ()) else push (position, ts, decode) | None, _ -> log#important "Got packet or frame with no timestamp! Synchronization issues may \ happen."; decode () in let audio_frame = Streams.fold (fun _ v cur -> match v with `Audio_frame (s, _) -> s :: cur | _ -> cur) streams [] in let audio_packet = Streams.fold (fun _ v cur -> match v with `Audio_packet (s, _) -> s :: cur | _ -> cur) streams [] in let video_frame = Streams.fold (fun _ v cur -> match v with `Video_frame (s, _) -> s :: cur | _ -> cur) streams [] in let video_packet = Streams.fold (fun _ v cur -> match v with `Video_packet (s, _) -> s :: cur | _ -> cur) streams [] in let data_packet = Streams.fold (fun _ v cur -> match v with `Data_packet (s, _) -> s :: cur | _ -> cur) streams [] in fun buffer -> let rec f () = try let data = Av.read_input ~audio_frame ~audio_packet ~video_frame ~video_packet ~data_packet container in match data with | `Audio_frame (i, frame) -> ( match Streams.find_opt i streams with | Some (`Audio_frame (s, decode)) -> check_pts s ~ts:(Option.value ~default:0L (Avutil.Frame.pts frame)) ~decode:(fun () -> decode ~buffer (`Frame frame)) (Avutil.Frame.pts frame) | _ -> f ()) | `Audio_packet (i, packet) -> ( match Streams.find_opt i streams with | Some (`Audio_packet (s, decode)) -> check_pts ~ts: (Option.value ~default:0L (Avcodec.Packet.get_dts packet)) ~decode:(fun () -> decode ~buffer packet) s (Avcodec.Packet.get_pts packet) | _ -> f ()) | `Video_frame (i, frame) -> ( match Streams.find_opt i streams with | Some (`Video_frame (s, decode)) -> check_pts ~ts:(Option.value ~default:0L (Avutil.Frame.pts frame)) ~decode:(fun () -> decode ~buffer (`Frame frame)) s (Avutil.Frame.pts frame) | _ -> f ()) | `Video_packet (i, packet) -> ( match Streams.find_opt i streams with | Some (`Video_packet (s, decode)) -> check_pts ~ts: (Option.value ~default:0L (Avcodec.Packet.get_dts packet)) ~decode:(fun () -> decode ~buffer packet) s (Avcodec.Packet.get_pts packet) | _ -> f ()) | `Data_packet (i, packet) -> ( match Streams.find_opt i streams with | Some (`Data_packet (s, decode)) -> check_pts ~ts: (Option.value ~default:0L (Avcodec.Packet.get_dts packet)) ~decode:(fun () -> decode ~buffer packet) s (Avcodec.Packet.get_pts packet) | _ -> f ()) | _ -> () with | Avutil.Error `Eagain | Avutil.Error `Invalid_data -> f () | Avutil.Error `Eof -> Generator.add_track_mark buffer.Decoder.generator; raise End_of_file | exn -> let bt = Printexc.get_raw_backtrace () in Generator.add_track_mark buffer.Decoder.generator; Printexc.raise_with_backtrace exn bt in f () let mk_streams ~ctype ~decode_first_metadata container = let check_metadata stream fn = let is_first = ref true in let latest_metadata = ref None in fun ~buffer data -> let m = Av.get_metadata stream in if ((not !is_first) || decode_first_metadata) && Some m <> !latest_metadata then ( is_first := false; latest_metadata := Some m; Generator.add_metadata buffer.Decoder.generator (Frame.Metadata.from_list m)); fn ~buffer data in let stream_idx = Ffmpeg_content_base.new_stream_idx () in let streams, _ = List.fold_left (fun (streams, pos) (idx, stream, params) -> let field = Frame.Fields.audio_n pos in match Frame.Fields.find_opt field ctype with | Some format when Ffmpeg_copy_content.is_format format -> ( Streams.add idx (`Audio_packet ( stream, check_metadata stream (Ffmpeg_copy_decoder.mk_audio_decoder ~stream_idx ~format ~field ~stream params) )) streams, pos + 1 ) | _ -> (streams, pos + 1)) (Streams.empty, 0) (Av.get_audio_streams container) in let streams, _ = List.fold_left (fun (streams, pos) (idx, stream, params) -> let field = Frame.Fields.audio_n pos in match Frame.Fields.find_opt field ctype with | Some format when Ffmpeg_raw_content.Audio.is_format format -> ( Streams.add idx (`Audio_frame ( stream, check_metadata stream (Ffmpeg_raw_decoder.mk_audio_decoder ~stream_idx ~format ~stream ~field params) )) streams, pos + 1 ) | Some format when Content.Audio.is_format format -> let channels = Content.Audio.channels_of_format format in ( Streams.add idx (`Audio_frame ( stream, check_metadata stream (Ffmpeg_internal_decoder.mk_audio_decoder ~channels ~stream ~field ~pcm_kind:Content.Audio.kind params) )) streams, pos + 1 ) | Some format when Content_pcm_s16.is_format format -> let channels = Content_pcm_s16.channels_of_format format in ( Streams.add idx (`Audio_frame ( stream, check_metadata stream (Ffmpeg_internal_decoder.mk_audio_decoder ~channels ~stream ~field ~pcm_kind:Content_pcm_s16.kind params) )) streams, pos + 1 ) | Some format when Content_pcm_f32.is_format format -> let channels = Content_pcm_f32.channels_of_format format in ( Streams.add idx (`Audio_frame ( stream, check_metadata stream (Ffmpeg_internal_decoder.mk_audio_decoder ~channels ~stream ~field ~pcm_kind:Content_pcm_f32.kind params) )) streams, pos + 1 ) | _ -> (streams, pos + 1)) (streams, 0) (Av.get_audio_streams container) in let streams, _ = List.fold_left (fun (streams, pos) (idx, stream, params) -> let field = Frame.Fields.video_n pos in match Frame.Fields.find_opt field ctype with | Some format when Ffmpeg_copy_content.is_format format -> ( Streams.add idx (`Video_packet ( stream, check_metadata stream (Ffmpeg_copy_decoder.mk_video_decoder ~stream_idx ~format ~field ~stream params) )) streams, pos + 1 ) | _ -> (streams, pos + 1)) (streams, 0) (Av.get_video_streams container) in let streams, _ = List.fold_left (fun (streams, pos) (idx, stream, params) -> let field = Frame.Fields.video_n pos in match Frame.Fields.find_opt field ctype with | Some format when Ffmpeg_raw_content.Video.is_format format -> ( Streams.add idx (`Video_frame ( stream, check_metadata stream (Ffmpeg_raw_decoder.mk_video_decoder ~stream_idx ~format ~stream ~field params) )) streams, pos + 1 ) | Some format when Content.Video.is_format format -> let width, height = Content.Video.dimensions_of_format format in ( Streams.add idx (`Video_frame ( stream, check_metadata stream (Ffmpeg_internal_decoder.mk_video_decoder ~width ~height ~stream ~field params) )) streams, pos + 1 ) | _ -> (streams, pos + 1)) (streams, 0) (Av.get_video_streams container) in let streams, _ = List.fold_left (fun (streams, pos) (idx, stream, params) -> try if Avcodec.Unknown.get_params_id params = `Timed_id3 then ( Streams.add idx (`Data_packet ( stream, fun ~buffer p -> let metadata = try parse_timed_id3 (Avcodec.Packet.content p) with _ -> [] in if metadata <> [] then Generator.add_metadata buffer.Decoder.generator (Frame.Metadata.from_list metadata) )) streams, pos + 1 ) else (streams, pos + 1) with Avutil.Error _ as exn -> let bt = Printexc.get_raw_backtrace () in Utils.log_exception ~log ~bt:(Printexc.raw_backtrace_to_string bt) (Printf.sprintf "Failed to get stream info: %s" (Printexc.to_string exn)); (streams, pos + 1)) (streams, 0) (Av.get_data_streams container) in streams let create_decoder ~ctype ~metadata fname = let args, format = parse_file_decoder_args metadata in let file_duration = dresolver ~metadata fname in let remaining = Atomic.make file_duration in let set_remaining ~pts ~duration stream = let pts = Option.map (fun pts -> Int64.add pts (Option.value ~default:0L duration)) pts in match (file_duration, pts) with | None, _ | Some _, None -> () | Some d, Some pts -> ( let { Avutil.num; den } = Av.get_time_base stream in let position = Int64.to_float (Int64.mul (Int64.of_int num) pts) /. float den in match Atomic.get remaining with | None -> Atomic.set remaining (Some (d -. position)) | Some r -> Atomic.set remaining (Some (min (d -. position) r))) in let get_remaining () = match Atomic.get remaining with | None -> -1 | Some r -> Frame.main_of_seconds r in let opts = Hashtbl.create 10 in List.iter (fun (k, v) -> Hashtbl.replace opts k v) args; let ext = Filename.extension fname in if List.exists (fun s -> ext = "." ^ s) image_file_extensions#get then ( Hashtbl.replace opts "loop" (`Int 1); Hashtbl.replace opts "framerate" (`Int (Lazy.force Frame.video_rate))); let container = Av.open_input ?format ~opts fname in let streams = mk_streams ~ctype ~decode_first_metadata:false container in let streams = Streams.map (function | `Audio_packet (stream, decoder) -> let decoder ~buffer packet = set_remaining stream ~pts:(Avcodec.Packet.get_pts packet) ~duration:(Avcodec.Packet.get_duration packet); decoder ~buffer packet in `Audio_packet (stream, decoder) | `Audio_frame (stream, decoder) -> let decoder ~buffer frame = (match frame with | `Frame frame -> set_remaining stream ~pts:(Avutil.Frame.pts frame) ~duration:(Avutil.Frame.duration frame) | _ -> ()); decoder ~buffer frame in `Audio_frame (stream, decoder) | `Video_packet (stream, decoder) -> let decoder ~buffer packet = set_remaining stream ~pts:(Avcodec.Packet.get_pts packet) ~duration:(Avcodec.Packet.get_duration packet); decoder ~buffer packet in `Video_packet (stream, decoder) | `Video_frame (stream, decoder) -> let decoder ~buffer frame = (match frame with | `Frame frame -> set_remaining stream ~pts:(Avutil.Frame.pts frame) ~duration:(Avutil.Frame.duration frame) | _ -> ()); decoder ~buffer frame in `Video_frame (stream, decoder) | `Data_packet (stream, decoder) -> `Data_packet (stream, decoder)) streams in let close () = Av.close container in let target_position = ref None in ( { Decoder.seek = (fun ticks -> match file_duration with | None -> -1 | Some d -> ( let target = ticks + Frame.main_of_seconds d - get_remaining () in match seek ~target_position ~container target with | 0 -> 0 | _ -> ticks)); decode = mk_decoder ~streams ~target_position container; eof = mk_eof streams; close; }, get_remaining ) let create_file_decoder ~metadata ~ctype filename = let decoder, remaining = create_decoder ~ctype ~metadata filename in Decoder.file_decoder ~filename ~remaining ~ctype decoder let create_stream_decoder ~ctype mime input = let seek_input = match input.Decoder.lseek with | None -> None | Some fn -> Some (fun len _ -> fn len) in let mime, (args, format) = match String.split_on_char ';' mime with | "application/ffmpeg" :: args -> ("application/ffmpeg", parse_input_args (String.concat ";" args)) | _ -> (mime, ([], None)) in let opts = Hashtbl.create 10 in List.iter (fun (k, v) -> Hashtbl.replace opts k v) args; if List.exists (fun s -> mime = s) image_mime_types#get then ( Hashtbl.replace opts "loop" (`Int 1); Hashtbl.replace opts "framerate" (`Int (Lazy.force Frame.video_rate))); let container = Av.open_input_stream ?seek:seek_input ~opts ?format input.Decoder.read in if Hashtbl.length opts > 0 then Runtime_error.raise ~pos:[] ~message: (Printf.sprintf "Unrecognized options: %s" (Ffmpeg_format.string_of_options opts)) "ffmpeg_decoder"; let streams = mk_streams ~ctype ~decode_first_metadata:true container in let target_position = ref None in let close () = Av.close container in { Decoder.seek = seek ~target_position ~container; decode = mk_decoder ~streams ~target_position container; eof = mk_eof streams; close; } let get_file_type ~metadata ~ctype filename = (* If file is an image, leave internal decoding to the image decoder. *) match ( Utils.get_ext_opt filename, Frame.Fields.find_opt Frame.Fields.video ctype ) with | Some ext, Some format when List.mem ext image_file_extensions#get && Content.Video.is_format format -> Frame.Fields.make () | _ -> let args, format = parse_file_decoder_args metadata in let opts = Hashtbl.create 10 in List.iter (fun (k, v) -> Hashtbl.replace opts k v) args; let container = Av.open_input ?format ~opts filename in Fun.protect ~finally:(fun () -> Av.close container) (fun () -> get_type ~format ~ctype ~url:filename container) let () = Plug.register Decoder.decoders "ffmpeg" ~doc: "Use FFmpeg to decode any file or stream if its MIME type or file \ extension is appropriate." { Decoder.priority = (fun () -> priority#get); file_extensions = (fun () -> Some (file_extensions#get @ image_file_extensions#get)); mime_types = (fun () -> Some (mime_types#get @ image_mime_types#get)); file_type = (fun ~metadata ~ctype filename -> Some (get_file_type ~metadata ~ctype filename)); file_decoder = Some create_file_decoder; stream_decoder = Some create_stream_decoder; } liquidsoap-2.3.2/src/core/decoder/ffmpeg_decoder_common.ml000066400000000000000000000076431477303350200236650ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Decode and read metadata using ffmpeg. *) let log = Log.make ["decoder"; "ffmpeg"] let conf_ffmpeg_decoder = Dtools.Conf.unit ~p:(Decoder.conf_decoder#plug "ffmpeg") "FFmpeg decoder configuration" let conf_codecs = Dtools.Conf.unit ~p:(conf_ffmpeg_decoder#plug "codecs") "Codecs settings" let conf_max_interleave_duration = Dtools.Conf.float ~p:(conf_ffmpeg_decoder#plug "max_interleave_duration") ~d:5. "Maximum data buffered while waiting for all streams." let conf_max_interleave_delta = Dtools.Conf.float ~p:(conf_ffmpeg_decoder#plug "max_interleave_delta") ~d:0.04 "Maximum delay between interleaved streams." let conf_codecs = let codecs = Hashtbl.create 10 in List.iter (fun c -> let id = Avcodec.Audio.get_id c in let codec_name = Avcodec.Audio.string_of_id id in let name = Avcodec.Audio.get_name c in let c = match Hashtbl.find_opt codecs codec_name with | Some l -> name :: l | None -> [name] in Hashtbl.replace codecs codec_name c) Avcodec.Audio.decoders; List.iter (fun c -> let id = Avcodec.Video.get_id c in let codec_name = Avcodec.Video.string_of_id id in let name = Avcodec.Video.get_name c in let c = match Hashtbl.find_opt codecs codec_name with | Some l -> name :: l | None -> [name] in Hashtbl.replace codecs codec_name c) Avcodec.Video.decoders; Hashtbl.fold (fun name codecs l -> let conf = Dtools.Conf.string ~p:(conf_codecs#plug name) ("Preferred codec to decode " ^ name) in ignore (Dtools.Conf.list ~p:(conf#plug "available") ~d:codecs ("Available codecs to decode " ^ name)); (name, conf) :: l) codecs [] let set_stream_decoder ~get_name ~get_codec stream = let params = Av.get_codec_params stream in let name = get_name params in match List.assoc_opt name conf_codecs with | Some conf -> ( try let preferred = conf#get in log#info "Trying preferred decoder %s for codec %s" preferred name; try Av.set_decoder stream (get_codec preferred) with exn -> let bt = Printexc.get_backtrace () in Utils.log_exception ~log ~bt (Printf.sprintf "Failed to set decoder %s for codec %s: %s" preferred name (Printexc.to_string exn)) with _ -> ()) | None -> () let set_audio_stream_decoder (type a) (stream : (Avutil.input, Avutil.audio, a) Av.stream) = set_stream_decoder ~get_name:(fun p -> Avcodec.Audio.(string_of_id (get_params_id p))) ~get_codec:Avcodec.Audio.find_decoder_by_name stream let set_video_stream_decoder (type a) (stream : (Avutil.input, Avutil.video, a) Av.stream) = set_stream_decoder ~get_name:(fun p -> Avcodec.Video.(string_of_id (get_params_id p))) ~get_codec:Avcodec.Video.find_decoder_by_name stream liquidsoap-2.3.2/src/core/decoder/ffmpeg_internal_decoder.ml000066400000000000000000000146451477303350200242110ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (** Decode media using ffmpeg. *) let log = Log.make ["decoder"; "ffmpeg"; "internal"] module type Converter_type = sig type t module Content : sig type data val lift_data : ?offset:int -> ?length:int -> data -> Content_base.data end val create : ?options:Swresample.options list -> Avutil.Channel_layout.t -> ?in_sample_format:Avutil.Sample_format.t -> int -> Avutil.Channel_layout.t -> ?out_sample_format:Avutil.Sample_format.t -> int -> t val convert : ?offset:int -> ?length:int -> t -> Swresample.Frame.t -> Content.data end module ConverterInput = Swresample.Make (Swresample.Frame) module Converter = struct module Content = Content_audio include ConverterInput (Swresample.PlanarFloatArray) end module Converter_pcm_s16 = struct module Content = Content_pcm_s16 include ConverterInput (Swresample.S16PlanarBigArray) end module Converter_pcm_f32 = struct module Content = Content_pcm_f32 include ConverterInput (Swresample.FltPlanarBigArray) end module Scaler = Swscale.Make (Swscale.Frame) (Swscale.BigArray) let mk_audio_decoder ~channels ~stream ~field ~pcm_kind codec = let converter = match pcm_kind with | _ when Content_audio.is_kind pcm_kind -> (module Converter : Converter_type) | _ when Content_pcm_s16.is_kind pcm_kind -> (module Converter_pcm_s16 : Converter_type) | _ when Content_pcm_f32.is_kind pcm_kind -> (module Converter_pcm_f32 : Converter_type) | _ -> raise Content_base.Invalid in let module Converter = (val converter : Converter_type) in Ffmpeg_decoder_common.set_audio_stream_decoder stream; let in_sample_rate = ref (Avcodec.Audio.get_sample_rate codec) in let in_channel_layout = ref (Avcodec.Audio.get_channel_layout codec) in let in_sample_format = ref (Avcodec.Audio.get_sample_format codec) in let target_sample_rate = Lazy.force Frame.audio_rate in let target_channel_layout = Avutil.Channel_layout.get_default channels in let mk_converter () = Converter.create !in_channel_layout ~in_sample_format:!in_sample_format !in_sample_rate target_channel_layout target_sample_rate in let converter = ref (mk_converter ()) in fun ~buffer -> function | `Flush -> () | `Frame frame -> let frame_in_sample_rate = Avutil.Audio.frame_get_sample_rate frame in let frame_in_channel_layout = Avutil.Channel_layout.get_default (Avutil.Audio.frame_get_channels frame) in let frame_in_sample_format = Avutil.Audio.frame_get_sample_format frame in if !in_sample_rate <> frame_in_sample_rate || (not (Avutil.Channel_layout.compare !in_channel_layout frame_in_channel_layout)) || !in_sample_format <> frame_in_sample_format then ( log#important "Frame format change detected!"; in_sample_rate := frame_in_sample_rate; in_channel_layout := frame_in_channel_layout; in_sample_format := frame_in_sample_format; converter := mk_converter ()); let content = Converter.convert !converter frame in Generator.put buffer.Decoder.generator field (Converter.Content.lift_data content); let metadata = Avutil.Frame.metadata frame in if metadata <> [] then Generator.add_metadata buffer.Decoder.generator (Frame.Metadata.from_list metadata) let mk_video_decoder ~width ~height ~stream ~field codec = Ffmpeg_decoder_common.set_video_stream_decoder stream; let pixel_format = match Avcodec.Video.get_pixel_format codec with | None -> failwith "Pixel format unknown!" | Some f -> f in let target_width = width in let target_height = height in let width = Avcodec.Video.get_width codec in let height = Avcodec.Video.get_height codec in let target_fps = Lazy.force Frame.video_rate in let scale = let scale_proportional (sw, sh) (tw, th) = if th * sw < tw * sh then (sw * th / sh, th) else (tw, sh * tw / sw) in (* Actual proportional width an height. *) let aw, ah = scale_proportional (width, height) (target_width, target_height) in let scaler = Scaler.create [] width height pixel_format aw ah (Ffmpeg_utils.liq_frame_pixel_format ()) in fun frame : Video.Canvas.Image.t -> let img = Scaler.convert scaler frame |> Ffmpeg_utils.unpack_image ~width:aw ~height:ah in let x = (target_width - aw) / 2 in let y = (target_height - ah) / 2 in Video.Canvas.Image.make img |> Video.Canvas.Image.translate x y |> Video.Canvas.Image.viewport target_width target_height in let time_base = Av.get_time_base stream in let pixel_aspect = Av.get_pixel_aspect stream in let cb ~buffer frame = let img = scale frame in buffer.Decoder.put_yuva420p ~field ~fps:{ Decoder.num = target_fps; den = 1 } img; let metadata = Avutil.Frame.metadata frame in if metadata <> [] then Generator.add_metadata buffer.Decoder.generator (Frame.Metadata.from_list metadata) in let converter = Ffmpeg_avfilter_utils.Fps.init ~width ~height ~pixel_format ~time_base ?pixel_aspect ~target_fps () in fun ~buffer -> function | `Frame frame -> Ffmpeg_avfilter_utils.Fps.convert converter frame (cb ~buffer) | `Flush -> Ffmpeg_avfilter_utils.Fps.eof converter (cb ~buffer) liquidsoap-2.3.2/src/core/decoder/ffmpeg_internal_decoder.mli000066400000000000000000000030561477303350200243540ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) val mk_audio_decoder : channels:int -> stream:(Avutil.input, Avutil.audio, [ `Frame ]) Av.stream -> field:Frame.field -> pcm_kind:Content.kind -> Avutil.audio Avcodec.params -> buffer:Decoder.buffer -> [ `Frame of Avutil.audio Avutil.Frame.t | `Flush ] -> unit val mk_video_decoder : width:int -> height:int -> stream:(Avutil.input, Avutil.video, [ `Frame ]) Av.stream -> field:Frame.field -> Avutil.video Avcodec.params -> buffer:Decoder.buffer -> [ `Frame of Avutil.video Avutil.Frame.t | `Flush ] -> unit liquidsoap-2.3.2/src/core/decoder/ffmpeg_raw_decoder.ml000066400000000000000000000061641477303350200231630ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Decode raw ffmpeg frames. *) let mk_decoder ~stream_idx ~stream_time_base ~mk_params ~lift_data ~put_data params = let duration_converter = Ffmpeg_utils.Duration.init ~mode:`PTS ~src:stream_time_base ~convert_ts:false ~get_ts:Avutil.Frame.pts ~set_ts:Avutil.Frame.set_pts () in fun ~buffer -> function | `Flush -> () | `Frame frame -> ( match Ffmpeg_utils.Duration.push duration_converter frame with | Some (length, frames) -> let data = List.map (fun (pos, frame) -> ( pos, { Ffmpeg_raw_content.time_base = stream_time_base; stream_idx; frame; } )) frames in let data = { Content.Video.params = mk_params params; data; length } in let data = lift_data data in put_data buffer.Decoder.generator data | None -> ()) let mk_audio_decoder ~stream_idx ~format ~stream ~field params = Ffmpeg_decoder_common.set_audio_stream_decoder stream; ignore (Content.merge format Ffmpeg_raw_content.(Audio.lift_params (AudioSpecs.mk_params params))); let stream_time_base = Av.get_time_base stream in let lift_data data = Ffmpeg_raw_content.Audio.lift_data data in let mk_params = Ffmpeg_raw_content.AudioSpecs.mk_params in mk_decoder ~stream_idx ~lift_data ~mk_params ~stream_time_base ~put_data:(fun g c -> Generator.put g field c) params let mk_video_decoder ~stream_idx ~format ~stream ~field params = Ffmpeg_decoder_common.set_video_stream_decoder stream; ignore (Content.merge format Ffmpeg_raw_content.(Video.lift_params (VideoSpecs.mk_params params))); let stream_time_base = Av.get_time_base stream in let lift_data data = Ffmpeg_raw_content.Video.lift_data data in let mk_params = Ffmpeg_raw_content.VideoSpecs.mk_params in mk_decoder ~stream_idx ~mk_params ~lift_data ~stream_time_base ~put_data:(fun g c -> Generator.put g field c) params liquidsoap-2.3.2/src/core/decoder/ffmpeg_raw_decoder.mli000066400000000000000000000031401477303350200233230ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) val mk_audio_decoder : stream_idx:int64 -> format:Content_base.Contents.format -> stream:(Avutil.input, Avutil.audio, [ `Frame ]) Av.stream -> field:Frame.field -> Avutil.audio Avcodec.params -> buffer:Decoder.buffer -> [ `Frame of Avutil.audio Avutil.Frame.t | `Flush ] -> unit val mk_video_decoder : stream_idx:int64 -> format:Content_base.Contents.format -> stream:(Avutil.input, Avutil.video, [ `Frame ]) Av.stream -> field:Frame.field -> Avutil.video Avcodec.params -> buffer:Decoder.buffer -> [ `Frame of Avutil.video Avutil.Frame.t | `Flush ] -> unit liquidsoap-2.3.2/src/core/decoder/flac_metadata_plug.ml000066400000000000000000000055501477303350200231530ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let log = Log.make ["decoder"; "flac"; "metadata"] let mime_types = Dtools.Conf.list ~p:(Decoder.conf_mime_types#plug "flac_metadata") "Mime-types used for decoding metadata using native FLAC metadata parser." ~d:["audio/flac"] let conf_flac = Dtools.Conf.void ~p:(Decoder.conf_decoder#plug "flac_metadata") "Native FLAC metadata parser settings." let conf_separator = Dtools.Conf.string ~d:", " ~p:(conf_flac#plug "separator") "Separator used to join metadata field with several entries." let file_extensions = Dtools.Conf.list ~p:(Decoder.conf_file_extensions#plug "flac_metadata") "File extensions used for decoding metadata using native FLAC parser." ~d:["flac"] let priority = Dtools.Conf.int ~p:(Request.conf_metadata_decoder_priorities#plug "flac_native") "Priority for the flac native decoder" ~d:1 let get_tags ~metadata:_ ~extension ~mime parse fname = try if not (Decoder.test_file ~log ~extension ~mime ~mimes:(Some mime_types#get) ~extensions:(Some file_extensions#get) fname) then raise Metadata.Invalid; let m = parse fname in let sep = conf_separator#get in List.fold_left (fun m (key, new_entry) -> try let old_entry = List.assoc key m in (key, old_entry ^ sep ^ new_entry) :: List.remove_assoc key m with Not_found -> (key, new_entry) :: m) [] m with | Metadata.Invalid -> [] | e -> let bt = Printexc.get_backtrace () in Utils.log_exception ~log ~bt (Printf.sprintf "Error while decoding file tags: %s" (Printexc.to_string e)); raise Not_found let () = Plug.register Request.mresolvers "flac_native" ~doc:"Native FLAC metadata resolver." { Request.priority = (fun () -> priority#get); resolver = get_tags Metadata.FLAC.parse_file; } liquidsoap-2.3.2/src/core/decoder/id3_plug.ml000066400000000000000000000071051477303350200210630ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) module Metadata = Metadata.Make (struct let to_string = function | `UTF_8 -> Charset.utf8 | `UTF_16 -> Charset.utf16 | `UTF_16BE -> Charset.utf16be | `UTF_16LE -> Charset.utf16le | `ISO_8859_1 -> Charset.latin1 let convert ?source ?target s = Charset.convert ?source:(Option.map (fun source -> to_string source) source) ?target:(Option.map (fun target -> to_string target) target) s end) let log = Log.make ["decoder"; "id3"] (** Configuration keys for id3. *) let mime_types = Dtools.Conf.list ~p:(Decoder.conf_mime_types#plug "id3") "Mime-types used for decoding metadata using native ID3v1 and ID3v2 parser" ~d:["audio/mpeg"; "audio/x-wav"] let conf_id3 = Dtools.Conf.void ~p:(Decoder.conf_decoder#plug "id3") "Native ID3 parser settings" let file_extensions = Dtools.Conf.list ~p:(Decoder.conf_file_extensions#plug "id3") "File extensions used for decoding metadata using native ID3v1 and ID3v2 \ parser" ~d:["mp3"; "wav"] let priority = Dtools.Conf.int ~p:(Request.conf_metadata_decoder_priorities#plug "id3") "Priority for the native ID3 metadata decoder" ~d:1 let priority_v1 = Dtools.Conf.int ~p:(Request.conf_metadata_decoder_priorities#plug "id3v1") "Priority for the native ID3v1 metadata decoder" ~d:1 let priority_v2 = Dtools.Conf.int ~p:(Request.conf_metadata_decoder_priorities#plug "id3v2") "Priority for the native ID3v2 metadata decoder" ~d:1 let get_tags ~metadata:_ ~extension ~mime parse fname = try if not (Decoder.test_file ~log ~extension ~mime ~mimes:(Some mime_types#get) ~extensions:(Some file_extensions#get) fname) then raise Metadata.Invalid; parse fname with | Metadata.Invalid -> [] | e -> let bt = Printexc.get_backtrace () in Utils.log_exception ~log ~bt (Printf.sprintf "Error while decoding file tags: %s" (Printexc.to_string e)); raise Not_found let () = Plug.register Request.mresolvers "ID3" ~doc:"Native decoder for ID3 tags." { Request.priority = (fun () -> priority#get); resolver = get_tags Metadata.ID3.parse_file; } let () = Plug.register Request.mresolvers "ID3v1" ~doc:"Native decoder for ID3v1 tags." { Request.priority = (fun () -> priority_v1#get); resolver = get_tags Metadata.ID3v1.parse_file; } let () = Plug.register Request.mresolvers "ID3v2" ~doc:"Native decode for ID3v2 tags." { Request.priority = (fun () -> priority_v2#get); resolver = get_tags Metadata.ID3v2.parse_file; } liquidsoap-2.3.2/src/core/decoder/image/000077500000000000000000000000001477303350200201025ustar00rootroot00000000000000liquidsoap-2.3.2/src/core/decoder/image/camlimages_decoder.ml000066400000000000000000000051111477303350200242210ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm module Img = Image.RGBA32 module P = Image.Generic.Pixel let log = Log.make ["decoder"; "camlimages"] let priority = Dtools.Conf.int ~p:(Decoder.conf_image_priorities#plug "camlimages") "Priority for the camlimages image decoder" ~d:1 (* TODO: find something more efficient? *) let decode_image filename = let img = OImages.load filename [] in let width, height = (img#width, img#height) in let p = let rgba32_p img i j = let p = img#get i j in let c = p.Color.color in (c.Color.r, c.Color.g, c.Color.b, p.Color.alpha) in match OImages.tag img with | OImages.Rgba32 img -> rgba32_p img | OImages.Rgb24 img -> fun i j -> let p = img#get i j in (p.Color.r, p.Color.g, p.Color.b, 0xff) | OImages.Index8 img -> rgba32_p img#to_rgba32 | OImages.Index16 img -> rgba32_p img#to_rgba32 | OImages.Cmyk32 _ -> failwith "CMYK32 images are not supported for now." in let img = Video.Image.create width height in for j = 0 to height - 1 do for i = 0 to width - 1 do Video.Image.set_pixel_rgba img i j (p i j) done done; img let check_image filename = try ignore (decode_image filename); true with exn -> log#info "Failed to decode %s: %s" (Lang_string.quote_string filename) (Printexc.to_string exn); false let () = Plug.register Decoder.image_file_decoders "camlimages" ~doc:"Use camlimages library to decode images." { Decoder.image_decoder_priority = (fun () -> priority#get); check_image; decode_image; } liquidsoap-2.3.2/src/core/decoder/image/ffmpeg_image_decoder.ml000066400000000000000000000060501477303350200245300ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) module Scaler = Swscale.Make (Swscale.Frame) (Swscale.BigArray) let log = Log.make ["decoder"; "ffmpeg"; "image"] let priority = Dtools.Conf.int ~p:(Decoder.conf_image_priorities#plug "ffmpeg") "Priority for the ffmpeg image decoder" ~d:10 let check_container fname = let container = Av.open_input fname in try Fun.protect ~finally:(fun () -> Av.close container) (fun () -> let _, stream, _ = Av.find_best_video_stream container in let _ = Av.read_input ~video_frame:[stream] container in true) with exn -> log#info "Failed to decode %s: %s" (Lang_string.quote_string fname) (Printexc.to_string exn); false let check_image filename = let ext = Filename.extension filename in List.exists (fun s -> ext = "." ^ s) Ffmpeg_decoder.image_file_extensions#get && check_container filename let decode_image fname = let container = Av.open_input fname in Fun.protect ~finally:(fun () -> Av.close container) (fun () -> let _, stream, codec = Av.find_best_video_stream container in let pixel_format = match Avcodec.Video.get_pixel_format codec with | None -> failwith "Pixel format unknown!" | Some f -> f in let width = Avcodec.Video.get_width codec in let height = Avcodec.Video.get_height codec in (* Hardcoding this instead of using Ffmpeg_utils.liq_frame_pixel_format () in order to have alpha channel. *) let out_pixel_format = `Yuva420p in let scaler = Scaler.create [] width height pixel_format width height out_pixel_format in match Av.read_input ~video_frame:[stream] container with | `Video_frame (_, frame) -> let frame = Scaler.convert scaler frame in Ffmpeg_utils.unpack_image ~width ~height frame | _ -> raise Not_found) let () = Plug.register Decoder.image_file_decoders "ffmpeg" ~doc:"Decode images using Ffmpeg." { Decoder.image_decoder_priority = (fun () -> priority#get); check_image; decode_image; } liquidsoap-2.3.2/src/core/decoder/image/imagelib_decoder.ml000066400000000000000000000040521477303350200236730ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (* open Mm *) (* module Img = Image.RGBA32 *) (* module P = Image.Generic.Pixel *) let log = Log.make ["decoder"; "imagelib"] let priority = Dtools.Conf.int ~p:(Decoder.conf_image_priorities#plug "imagelib") "Priority for the imagelib image decoder" ~d:1 let decode_image fname = let f = ImageLib_unix.openfile fname in let width = f.width in let height = f.height in let img = Mm.Video.Image.create width height in for j = 0 to height - 1 do for i = 0 to width - 1 do Image.read_rgba f i j (fun r g b a -> Mm.Video.Image.set_pixel_rgba img i j (r, g, b, a)) done done; img let check_image filename = try ignore (decode_image filename); true with exn -> log#info "Failed to decode %s: %s" (Lang_string.quote_string filename) (Printexc.to_string exn); false let () = Plug.register Decoder.image_file_decoders "imagelib" ~doc:"Use ImageLib to decode images." { Decoder.image_decoder_priority = (fun () -> priority#get); check_image; decode_image; } liquidsoap-2.3.2/src/core/decoder/image/ppm_decoder.ml000066400000000000000000000032461477303350200227220ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm let log = Log.make ["decoder"; "ppm"] let priority = Dtools.Conf.int ~p:(Decoder.conf_image_priorities#plug "ppm") "Priority for the ppm image decoder" ~d:1 let decode_image fname = let ic = open_in_bin fname in let len = in_channel_length ic in let data = Bytes.create len in really_input ic data 0 len; close_in ic; Image.YUV420.of_PPM (Bytes.unsafe_to_string data) let () = Plug.register Decoder.image_file_decoders "ppm" ~doc:"Native decoding of PPM images." { Decoder.image_decoder_priority = (fun () -> priority#get); check_image = (fun filename -> Filename.extension filename = ".ppm"); decode_image; } liquidsoap-2.3.2/src/core/decoder/image/sdlimage_decoder.ml000066400000000000000000000035361477303350200237150ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Tsdl module P = Image.Generic.Pixel let log = Log.make ["decoder"; "sdlimage"] let priority = Dtools.Conf.int ~p:(Decoder.conf_image_priorities#plug "sdl") "Priority for the sdl image decoder" ~d:5 let decode_image filename = let surface = Sdl_utils.check Tsdl_image.Image.load filename in Fun.protect (fun () -> Sdl_utils.Surface.to_img surface) ~finally:(fun () -> Sdl.free_surface surface) let check_image filename = try ignore (decode_image filename); true with exn -> log#info "Failed to decode %s: %s" (Lang_string.quote_string filename) (Printexc.to_string exn); false let () = Plug.register Decoder.image_file_decoders "sdl" ~doc:"Use SDL to decode images." { Decoder.image_decoder_priority = (fun () -> priority#get); check_image; decode_image; } liquidsoap-2.3.2/src/core/decoder/image_decoder.ml000066400000000000000000000156471477303350200221360ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm let log = Log.make ["image"; "decoder"] (* TODO: put this in some library as it can be used in many other places... *) (** Function to retrieve width an height from parameters. *) let wh iw ih w h = let frame_w = Lazy.force Frame.video_width in let frame_h = Lazy.force Frame.video_height in match (w, h) with | None, None -> (* By default resize anamorphically to the maximum size wrt the frame *) let w = frame_w in let h = ih * frame_w / iw in let w, h = if h <= frame_h then (w, h) else ( let h = frame_h in let w = iw * frame_h / ih in (w, h)) in (w, h) | Some w, None -> (w, ih * w / iw) | None, Some h -> (iw * h / ih, h) | Some w, Some h -> (w, h) let wh_string iw ih w h = let frame_w = Lazy.force Frame.video_width in let frame_h = Lazy.force Frame.video_height in let f d i l = if l = "" then None else if l.[String.length l - 1] = '%' then ( let a = float_of_string (String.sub l 0 (String.length l - 1)) /. 100. in let d = float_of_int d in let l = int_of_float ((a *. d) +. 0.5) in Some l) else ( let l = int_of_string l in let l = if l < 0 then i else l in Some l) in wh iw ih (f frame_w iw w) (f frame_h ih h) (* TODO: negative used to mean from right but I don't think it's a good idea anymore (for instance to have a scrolling image). *) let off_string iw ih ox oy = let frame_w = Lazy.force Frame.video_width in let frame_h = Lazy.force Frame.video_height in let f d frame l = if l = "" then d else if l.[String.length l - 1] = '%' then ( let a = float_of_string (String.sub l 0 (String.length l - 1)) /. 100. in let frame = float_of_int frame in let o = int_of_float ((frame *. a) +. 0.5) in o) else int_of_string l in let ox = f ((frame_w - iw) / 2) frame_w ox in let oy = f ((frame_h - ih) / 2) frame_h oy in (ox, oy) let create_decoder ~ctype ~width ~height ~metadata img = let frame_width = width in let frame_height = height in (* Dimensions. *) let img_w = Image.YUV420.width img in let img_h = Image.YUV420.height img in let width = try Frame.Metadata.find "width" metadata with Not_found -> "" in let height = try Frame.Metadata.find "height" metadata with Not_found -> "" in let width, height = wh_string img_w img_h width height in (* Offset. *) let off_x = try Frame.Metadata.find "x" metadata with Not_found -> "" in let off_y = try Frame.Metadata.find "y" metadata with Not_found -> "" in let off_x, off_y = off_string width height off_x off_y in log#debug "Decoding to %dx%d at %dx%d" width height off_x off_y; (* We are likely to have no α channel, optimize it. *) Image.YUV420.optimize_alpha img; let img = Video.Canvas.Image.make ~width:frame_width ~height:frame_height img in let scaler = Video_converter.scaler () in let img = Video.Canvas.Image.scale ~scaler (width, img_w) (height, img_h) img in let img = Video.Canvas.Image.translate off_x off_y img in let duration = try let seconds = float_of_string (Frame.Metadata.find "duration" metadata) in if seconds < 0. then -1 else Frame.main_of_seconds seconds with Not_found -> -1 in let duration = Atomic.make duration in let fclose () = () in let remaining () = Atomic.get duration in let generator = Content.Video.make_generator (Content.Video.get_params (Frame.Fields.find Frame.Fields.video ctype)) in let fread length = let length = match Atomic.get duration with | -1 -> length | 0 -> 0 | d -> let length = min d length in Atomic.set duration (d - length); length in let frame = Frame.create ~length ctype in match length with | 0 -> frame | length -> ( let video = Content.Video.generate ~create:(fun ~pos:_ ~width:_ ~height:_ () -> img) generator length in let frame = Frame.set_data frame Frame.Fields.video Content.Video.lift_data video in match Frame.Fields.find_opt Frame.Fields.audio ctype with | None -> frame | Some format -> let pcm = Content.Audio.get_data (Content.make ~length format) in Audio.clear pcm 0 (Frame.audio_of_main length); Frame.set_data frame Frame.Fields.audio Content.Audio.lift_data pcm) in { Decoder.fread; remaining; fseek = (fun len -> len); fclose } let is_audio_compatible ctype = match Frame.Fields.find_opt Frame.Fields.audio ctype with | None -> true | Some f -> Content.Audio.is_format f let is_video_compatible ctype = match Frame.Fields.find_opt Frame.Fields.video ctype with | None -> false | Some f -> Content.Video.is_format f let () = Plug.register Decoder.decoders "image" ~doc:"Decoder for static images." { Decoder.priority = (fun () -> 1); file_extensions = (fun () -> None); mime_types = (fun () -> None); file_type = (fun ~metadata:_ ~ctype filename -> if Decoder.check_image_file_decoder filename && is_audio_compatible ctype && is_video_compatible ctype then Some (Frame.Fields.make ?audio:(Frame.Fields.find_opt Frame.Fields.audio ctype) ~video:Content.(default_format Video.kind) ()) else None); file_decoder = Some (fun ~metadata ~ctype filename -> let img = Decoder.get_image_file_decoder filename in let width, height = Content.Video.dimensions_of_format (Option.get (Frame.Fields.find_opt Frame.Fields.video ctype)) in create_decoder ~ctype ~width ~height ~metadata img); stream_decoder = None; } liquidsoap-2.3.2/src/core/decoder/image_plug.ml000066400000000000000000000046771477303350200215010ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let log = Log.make ["decoder"; "image"; "metadata"] (** Configuration keys. *) let mime_types = Dtools.Conf.list ~p:(Decoder.conf_mime_types#plug "image_metadata") "Mime-types used for decoding metadata using native parser." ~d:["image/png"; "image/jpeg"] let conf_image = Dtools.Conf.void ~p:(Decoder.conf_decoder#plug "image_metadata") "Native image metadata parser settings." let file_extensions = Dtools.Conf.list ~p:(Decoder.conf_file_extensions#plug "image_metadata") "File extensions used for decoding metadata using native parser." ~d:["png"; "jpg"; "jpeg"] let priority = Dtools.Conf.int ~p:(Request.conf_metadata_decoder_priorities#plug "image_metadata") "Priority for the image metadata decoder" ~d:1 let get_tags ~metadata:_ ~extension ~mime fname = try if not (Decoder.test_file ~log ~extension ~mime ~mimes:(Some mime_types#get) ~extensions:(Some file_extensions#get) fname) then raise Metadata.Invalid; Metadata.Image.parse_file fname with | Metadata.Invalid -> [] | e -> let bt = Printexc.get_backtrace () in Utils.log_exception ~log ~bt (Printf.sprintf "Error while decoding file tags: %s" (Printexc.to_string e)); raise Not_found let () = Plug.register Request.mresolvers "image" ~doc:"Native decoder for image metadata." { Request.priority = (fun () -> priority#get); resolver = get_tags } liquidsoap-2.3.2/src/core/decoder/liq_flac_decoder.ml000066400000000000000000000151431477303350200226150ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (** Decode and read metadata from flac files. *) let log = Log.make ["decoder"; "flac"] exception End_of_stream let create_decoder input = let read = input.Decoder.read in let seek = match input.Decoder.lseek with | Some f -> Some (fun len -> ignore (f (Int64.to_int len))) | None -> None in let tell = match input.Decoder.tell with | Some f -> Some (fun () -> Int64.of_int (f ())) | None -> None in let length = match input.Decoder.length with | Some f -> Some (fun () -> Int64.of_int (f ())) | None -> None in let write_ref = ref (fun _ -> ()) in let write v = let fn = !write_ref in fn v in let decoder, info, _ = Flac.Decoder.create ?seek ?tell ?length ~read ~write () in let samplerate, _ = (info.Flac.Decoder.sample_rate, info.Flac.Decoder.channels) in let processed = ref Int64.zero in { Decoder.seek = (fun ticks -> let duration = Frame.seconds_of_main ticks in let samples = Int64.of_float (duration *. float samplerate) in let pos = Int64.add !processed samples in let ret = Flac.Decoder.seek decoder pos in if ret = true then ( processed := pos; ticks) else ( match Flac.Decoder.state decoder with | `Seek_error -> if Flac.Decoder.flush decoder then 0 else (* Flushing failed, we are in an unknown state.. *) raise End_of_stream | _ -> 0)); decode = (fun buffer -> (write_ref := fun data -> let len = try Audio.length data with _ -> 0 in processed := Int64.add !processed (Int64.of_int len); buffer.Decoder.put_pcm ~samplerate data); match Flac.Decoder.state decoder with | `Search_for_metadata | `Read_metadata | `Search_for_frame_sync | `Read_frame -> Flac.Decoder.process decoder | _ -> raise End_of_stream); eof = (fun _ -> ()); close = (fun _ -> ()); } (** Configuration keys for flac. *) let mime_types = Dtools.Conf.list ~p:(Decoder.conf_mime_types#plug "flac") "Mime-types used for guessing FLAC format" ~d:["audio/flac"; "audio/x-flac"] let file_extensions = Dtools.Conf.list ~p:(Decoder.conf_file_extensions#plug "flac") "File extensions used for guessing FLAC format" ~d:["flac"] let priority = Dtools.Conf.int ~p:(Decoder.conf_priorities#plug "flac") "Priority for the flac decoder" ~d:1 (* Get the number of channels of audio in a flac file. This is done by decoding a first chunk of data, thus checking that libmad can actually open the file -- which doesn't mean much. *) let file_type filename = let fd = Decoder.openfile filename in Fun.protect ~finally:(fun () -> Unix.close fd) (fun () -> let write _ = () in let h = Flac.Decoder.File.create_from_fd ~write fd in let info = h.Flac.Decoder.File.info in let rate, channels = (info.Flac.Decoder.sample_rate, info.Flac.Decoder.channels) in log#important "Libflac recognizes %s as FLAC (%dHz,%d channels)." (Lang_string.quote_string filename) rate channels; Some (Frame.Fields.make ~audio:(Content.Audio.format_of_channels channels) ())) let file_decoder ~metadata:_ ~ctype filename = Decoder.opaque_file_decoder ~filename ~ctype create_decoder let () = Plug.register Decoder.decoders "flac" ~doc: "Use libflac to decode any file or stream if its MIME type or file \ extension is appropriate." { Decoder.priority = (fun () -> priority#get); file_extensions = (fun () -> Some file_extensions#get); mime_types = (fun () -> Some mime_types#get); file_type = (fun ~metadata:_ ~ctype:_ filename -> file_type filename); file_decoder = Some file_decoder; stream_decoder = Some (fun ~ctype:_ _ -> create_decoder); } let log = Log.make ["metadata"; "flac"] let get_tags ~metadata:_ ~extension ~mime file = if not (Decoder.test_file ~log ~extension ~mime ~mimes:(Some mime_types#get) ~extensions:(Some file_extensions#get) file) then raise Not_found; let fd = Decoder.openfile file in Fun.protect ~finally:(fun () -> Unix.close fd) (fun () -> let write _ = () in let h = Flac.Decoder.File.create_from_fd ~write fd in match h.Flac.Decoder.File.comments with Some (_, m) -> m | None -> []) let metadata_decoder_priority = Dtools.Conf.int ~p:(Request.conf_metadata_decoder_priorities#plug "flac") "Priority for the flac metadata decoder" ~d:1 let () = Plug.register Request.mresolvers "flac" ~doc:"" { Request.priority = (fun () -> metadata_decoder_priority#get); resolver = get_tags; } let check filename = List.mem (Magic_mime.lookup filename) mime_types#get || try ignore (file_type filename); true with _ -> false let dresolver ~metadata:_ file = if not (check file) then raise Not_found; let fd = Decoder.openfile file in Fun.protect ~finally:(fun () -> Unix.close fd) (fun () -> let write _ = () in let h = Flac.Decoder.File.create_from_fd ~write fd in let info = h.Flac.Decoder.File.info in match info.Flac.Decoder.total_samples with | x when x = Int64.zero -> raise Not_found | x -> Int64.to_float x /. float info.Flac.Decoder.sample_rate) let () = Plug.register Request.dresolvers "flac" ~doc:"Compute duration of flac files." { dpriority = (fun () -> priority#get); file_extensions = (fun () -> file_extensions#get); dresolver; } liquidsoap-2.3.2/src/core/decoder/liq_ogg_decoder.ml000066400000000000000000000267631477303350200224760ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (** Decode and read ogg files. *) module P = Image.Generic.Pixel let log = Log.make ["decoder"; "ogg"] (** Generic decoder *) exception Channels of int let converter () = let current_format = ref None in fun format -> let format = match format with | Ogg_decoder.Yuvj_422 -> P.YUVJ422 | Ogg_decoder.Yuvj_420 -> P.YUVJ420 | Ogg_decoder.Yuvj_444 -> P.YUVJ444 in match !current_format with | Some x when fst x = format -> snd x | _ -> let converter = Video_converter.find_converter (P.YUV format) (P.YUV P.YUVJ420) in current_format := Some (format, converter); converter (** Convert a video frame to YUV *) let video_convert scale = let converter = converter () in fun buf -> let width = Lazy.force Frame.video_width in let height = Lazy.force Frame.video_height in if buf.Ogg_decoder.format <> Ogg_decoder.Yuvj_420 then ( let img = Image.YUV420.make buf.Ogg_decoder.frame_width buf.Ogg_decoder.frame_height buf.Ogg_decoder.y buf.Ogg_decoder.y_stride buf.Ogg_decoder.u buf.Ogg_decoder.v buf.Ogg_decoder.uv_stride in let img2 = Video.Image.create width height in scale img img2; img2) else ( let converter = converter buf.Ogg_decoder.format in let yuv = Video.Image.create width height in let frame = Image.Generic.of_YUV420 yuv in let sframe = Image.YUV420.make buf.Ogg_decoder.frame_width buf.Ogg_decoder.frame_height buf.Ogg_decoder.y buf.Ogg_decoder.y_stride buf.Ogg_decoder.u buf.Ogg_decoder.v buf.Ogg_decoder.uv_stride in converter (Image.Generic.of_YUV420 sframe) frame; yuv) let demuxer_log x = log#debug "%s" x let create_decoder ?(merge_tracks = false) source input = let decoder = let callbacks = { Ogg_decoder.read = input.Decoder.read; seek = input.Decoder.lseek; tell = input.Decoder.tell; } in Ogg_decoder.init ~log:demuxer_log callbacks in let video_scale = Video_converter.scaler () in let started = ref false in let tracks = Ogg_decoder.get_standard_tracks decoder in let first_meta = ref true in let mode buffer = let content_type = Generator.content_type buffer.Decoder.generator in match ( Frame.Fields.mem Frame.Fields.audio content_type, Frame.Fields.mem Frame.Fields.video content_type ) with | true, false -> `Audio | false, true -> `Video | true, true -> `Both | _ -> assert false in let init ~reset buffer = if reset then ( Ogg_decoder.reset decoder; Ogg_decoder.update_standard_tracks decoder tracks; (* We enforce that all contents end together, otherwise there will * be a lag between different content types in the next track. *) if not merge_tracks then Generator.add_track_mark buffer.Decoder.generator); let add_meta f t = (* Initial metadata in files is handled separately. *) if source = `Stream || (merge_tracks && not !first_meta) then ( let _, (v, m) = f decoder t in let metas = Frame.Metadata.from_list (("vendor", v) :: List.map (fun (k, v) -> (String.lowercase_ascii k, v)) m) in Generator.add_metadata buffer.Decoder.generator metas); first_meta := false in let drop_track d t = match t with None -> () | Some t -> Ogg_decoder.drop_track d t in (* Make sure the stream has what we need *) (* TODO this should be done based on the kind, not the mode, * which should be (re)set accordingly *) match ( tracks.Ogg_decoder.audio_track, tracks.Ogg_decoder.video_track, mode buffer ) with | Some audio, Some video, `Both -> add_meta Ogg_decoder.audio_info audio; add_meta Ogg_decoder.video_info video | Some audio, video, `Audio -> drop_track decoder video; add_meta Ogg_decoder.audio_info audio | audio, Some video, `Video -> drop_track decoder audio; add_meta Ogg_decoder.video_info video | _ -> failwith "Ogg stream does not contain required data" in let decode buffer = let decode_audio, decode_video = match mode buffer with | `Both -> (true, true) | `Audio -> (true, false) | `Video -> (false, true) | _ -> assert false in try if not !started then ( init ~reset:false buffer; started := true); if Ogg_decoder.eos decoder then if merge_tracks || source = `Stream then init ~reset:true buffer else raise Ogg_decoder.End_of_stream; let audio_feed track buf = let info, _ = Ogg_decoder.audio_info decoder track in buffer.Decoder.put_pcm ~samplerate:info.Ogg_decoder.sample_rate buf in let video_feed track buf = let info, _ = Ogg_decoder.video_info decoder track in let img = video_convert video_scale buf in let fps = { Decoder.num = info.Ogg_decoder.fps_numerator; den = info.Ogg_decoder.fps_denominator; } in buffer.Decoder.put_yuva420p ~fps (Video.Canvas.Image.make img) in let decode_audio, decode_video = if decode_audio && decode_video then (* Only decode the one which is late, so that we don't have memory problems. *) if Generator.field_length buffer.Decoder.generator Frame.Fields.audio < Generator.field_length buffer.Decoder.generator Frame.Fields.video then (true, false) else (false, true) else (decode_audio, decode_video) in if decode_audio then ( let track = Option.get tracks.Ogg_decoder.audio_track in Ogg_decoder.decode_audio decoder track (audio_feed track)); if decode_video then ( let track = Option.get tracks.Ogg_decoder.video_track in Ogg_decoder.decode_video decoder track (video_feed track)) with (* We catch [Ogg_decoder.End_of_stream] only if asked to * to merge logical tracks or with a stream source. * In this case, we try to reset the decoder to see if * there could be another sequentialized logical stream * starting. Actual reset is handled in the * decoding function since we need the actual * buffer to add metadata etc. *) | Ogg_decoder.End_of_stream when merge_tracks || source = `Stream -> () (* We catch Ogg.Out_of_sync only in * stream mode. Ogg/theora streams, for instance, * in icecast contain the header (packet 0) and * then current stream, with packet 1543 for instance.. * Note: we only catch during audio/video decoding * which implies that the stream has already been * parsed as ogg. Indeed, Ogg.Out_of_sync when * parsing ogg means that the stream is not ogg... *) | Ogg.Out_of_sync when source = `Stream -> () in let seek offset = try let time_offset = Frame.seconds_of_main offset in let new_time = Ogg_decoder.seek ~relative:true decoder time_offset in Frame.main_of_seconds new_time with Ogg_decoder.End_of_stream | Ogg.End_of_stream -> log#info "End of track reached while seeking!"; 0 in { Decoder.decode; seek; eof = (fun _ -> ()); close = (fun _ -> ()) } (** File decoder *) let file_type ~metadata:_ ~ctype:_ filename = let decoder, fd = Ogg_decoder.init_from_file ~log:demuxer_log filename in let tracks = Ogg_decoder.get_standard_tracks decoder in Fun.protect ~finally:(fun () -> Unix.close fd) (fun () -> let audio = match tracks.Ogg_decoder.audio_track with | None -> 0 | Some t -> let info, _ = Ogg_decoder.audio_info decoder t in info.Ogg_decoder.channels in let video = if tracks.Ogg_decoder.video_track <> None then 1 else 0 in log#info "File %s recognized as audio=%d video=%d." (Lang_string.quote_string filename) audio video; let audio = if audio = 0 then None else Some (Frame_base.format_of_channels ~pcm_kind:Content.Audio.kind audio) in let video = if video = 0 then None else Some Content.(default_format Video.kind) in Some (Frame.Fields.make ?audio ?video ())) let mime_types = Dtools.Conf.list ~p:(Decoder.conf_mime_types#plug "ogg") ~d: [ "application/ogg"; "application/x-ogg"; "audio/x-ogg"; "audio/ogg"; "video/ogg"; ] "Mime-types used for guessing OGG format." let file_extensions = Dtools.Conf.list ~p:(Decoder.conf_file_extensions#plug "ogg") "File extensions used for guessing OGG format" ~d:["ogv"; "oga"; "ogx"; "ogg"; "opus"] let priority = Dtools.Conf.int ~p:(Decoder.conf_priorities#plug "ogg") "Priority for the OGG decoder" ~d:15 let create_file_decoder ~metadata:_ ~ctype filename = Decoder.opaque_file_decoder ~filename ~ctype (create_decoder ~merge_tracks:true `File) let () = Plug.register Decoder.decoders "ogg" ~doc:"Decode a file as OGG provided that libogg accepts it." { Decoder.priority = (fun () -> priority#get); file_extensions = (fun () -> Some file_extensions#get); mime_types = (fun () -> Some mime_types#get); file_type; file_decoder = Some create_file_decoder; stream_decoder = Some (fun ~ctype:_ _ -> create_decoder `Stream); } (** Metadata *) let get_tags ~metadata:_ ~extension ~mime file = if not (Decoder.test_file ~log ~extension ~mime ~mimes:(Some mime_types#get) ~extensions:(Some file_extensions#get) file) then raise Not_found; let decoder, fd = Ogg_decoder.init_from_file ~log:demuxer_log file in let tracks = Ogg_decoder.get_standard_tracks decoder in Fun.protect ~finally:(fun () -> Unix.close fd) (fun () -> let get f t = match t with | Some t -> let _, (_, m) = f decoder t in m | _ -> [] in get Ogg_decoder.audio_info tracks.Ogg_decoder.audio_track @ get Ogg_decoder.video_info tracks.Ogg_decoder.video_track) let metadata_decoder_priority = Dtools.Conf.int ~p:(Request.conf_metadata_decoder_priorities#plug "ogg") "Priority for the ogg metadata decoder" ~d:1 let () = Plug.register Request.mresolvers "ogg" ~doc:"" { Request.priority = (fun () -> metadata_decoder_priority#get); resolver = get_tags; } liquidsoap-2.3.2/src/core/decoder/mad_decoder.ml000066400000000000000000000150411477303350200216010ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Decode mpeg audio files using libmad. *) let log = Log.make ["decoder"; "mad"] let init input = let index = Hashtbl.create 10 in let time_offset = ref 0 in let dec = ref (Mad.openstream input.Decoder.read) in (* Skip id3 tags if possible. *) begin match (input.Decoder.lseek, input.Decoder.tell) with | Some seek, Some tell -> Mad.skip_id3tags ~read:input.Decoder.read ~seek ~tell | _, _ -> () end; let get_index time = Hashtbl.find index time in let update_index () = match input.Decoder.tell with | None -> () | Some f -> let time = !time_offset + Mad.get_current_time !dec Mad.Seconds in if not (Hashtbl.mem index time) then Hashtbl.replace index time (f ()) in (* Add an initial index. *) update_index (); let get_data () = let data = Mad.decode_frame_float !dec in update_index (); data in let get_time () = float !time_offset +. (float (Mad.get_current_time !dec Mad.Centiseconds) /. 100.) in let seek ticks = if ticks < 0 && input.Decoder.lseek = None then 0 else ( let time = Frame.seconds_of_main ticks in let cur_time = get_time () in let seek_time = cur_time +. time in let seek_time = if seek_time < 0. then 0. else seek_time in if time < 0. then ( try let seek_time = int_of_float (floor seek_time) in let seek_pos = if seek_time > 0 then get_index seek_time else 0 in ignore ((Option.get input.Decoder.lseek) seek_pos); dec := Mad.openstream input.Decoder.read; (* Decode one frame to set the decoder to a good reading position * on next read. *) ignore (Mad.decode_frame_float !dec); (* We have to assume here that new_pos = seek_pos.. *) time_offset := seek_time with _ -> ()); let rec f pos = if pos < seek_time then if try Mad.skip_frame !dec; true with Mad.End_of_stream -> false then ( update_index (); f (get_time ())) in f (get_time ()); let new_time = get_time () in Frame.main_of_seconds (new_time -. cur_time)) in let get_info () = Mad.get_frame_format !dec in (get_info, get_data, seek) let create_decoder input = let get_info, get_data, seek = init input in { Decoder.seek; decode = (fun buffer -> let data = get_data () in let { Mad.samplerate } = get_info () in buffer.Decoder.put_pcm ~samplerate data); eof = (fun _ -> ()); close = (fun _ -> ()); } (** Configuration keys for mad. *) let mime_types = Dtools.Conf.list ~p:(Decoder.conf_mime_types#plug "mad") "Mime-types used for guessing mpeg audio format" ~d:["audio/mpeg"; "audio/MPA"] let file_extensions = Dtools.Conf.list ~p:(Decoder.conf_file_extensions#plug "mad") "File extensions used for guessing mpeg audio format" ~d:["mp3"; "mp2"; "mp1"] let priority = Dtools.Conf.int ~p:(Decoder.conf_priorities#plug "mad") "Priority for the mpeg audio decoder" ~d:1 (* Backward-compatibility keys.. *) let () = ignore (mime_types#alias ~descr: "Mime-types used for guessing MP3 format (DEPRECATED, use *.mad \ configuration keys!)" (Decoder.conf_mime_types#plug "mp3")); ignore (file_extensions#alias ~descr: "File extensions used for guessing MP3 format (DEPRECATED, use *.mad \ configuration keys!)" (Decoder.conf_file_extensions#plug "mp3")) (* Get the number of channels of audio in a mpeg audio file. This is done by decoding a first chunk of data, thus checking that libmad can actually open the file -- which doesn't mean much. *) let file_type filename = let fd = Mad.openfile filename in Fun.protect ~finally:(fun () -> Mad.close fd) (fun () -> ignore (Mad.decode_frame_float fd); let f = Mad.get_frame_format fd in let layer = match f.Mad.layer with | Mad.Layer_I -> "I" | Mad.Layer_II -> "II" | Mad.Layer_III -> "III" in log#important "Libmad recognizes %S as mpeg audio (layer %s, %ikbps, %dHz, %d \ channels)." filename layer (f.Mad.bitrate / 1000) f.Mad.samplerate f.Mad.channels; Some (Frame.Fields.make ~audio:(Content.Audio.format_of_channels f.Mad.channels) ())) let create_file_decoder ~metadata:_ ~ctype filename = Decoder.opaque_file_decoder ~filename ~ctype create_decoder let () = Plug.register Decoder.decoders "mad" ~doc: "Use libmad to decode any file if its MIME type or file extension is \ appropriate." { Decoder.priority = (fun () -> priority#get); file_extensions = (fun () -> Some file_extensions#get); mime_types = (fun () -> Some mime_types#get); file_type = (fun ~metadata:_ ~ctype:_ f -> file_type f); file_decoder = Some create_file_decoder; stream_decoder = Some (fun ~ctype:_ _ -> create_decoder); } let check filename = List.mem (Magic_mime.lookup filename) mime_types#get || try ignore (file_type filename); true with _ -> false let dresolver ~metadata:_ file = if not (check file) then raise Not_found; let ans = Mad.duration file in match ans with 0. -> raise Not_found | _ -> ans let () = Plug.register Request.dresolvers "mad" ~doc:"Compute duration of mp3 files using MAD library." { dpriority = (fun () -> priority#get); file_extensions = (fun () -> file_extensions#get); dresolver; } liquidsoap-2.3.2/src/core/decoder/midi_decoder.ml000066400000000000000000000047761477303350200217770ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (** Read MIDI files. The metadata support is TODO. *) exception Invalid_header exception Invalid_data let log = Log.make ["decoder"; "midi"] let decoder ~ctype file = log#info "Decoding %s..." (Lang_string.quote_string file); let fd = new MIDI.IO.Reader.of_file file in let closed = ref false in let fclose () = assert (not !closed); closed := true; fd#close in let close_on_err f x = try f x with e -> log#info "Closing on error: %s." (Printexc.to_string e); fclose (); raise e in let fread length = let frame = Frame.create ~length ctype in let m = Content.Midi.get_data (Frame.get frame Frame.Fields.midi) in let r = close_on_err (fun () -> fd#read (Lazy.force Frame.midi_rate) m 0 length) () in Frame.set_data (Frame.slice frame r) Frame.Fields.midi Content.Midi.lift_data m in { Decoder.fread; remaining = (fun _ -> 0); fseek = (fun _ -> 0); fclose } let () = Plug.register Decoder.decoders "midi" ~doc:"Decode midi files." { Decoder.priority = (fun () -> 1); file_extensions = (fun () -> Some ["mid"]); mime_types = (fun () -> Some ["audio/midi"]); file_type = (fun ~metadata:_ ~ctype:_ _ -> Some (Frame.Fields.make ~midi:Content.(Midi.lift_params { Content.Midi.channels = 16 }) ())); file_decoder = Some (fun ~metadata:_ ~ctype filename -> decoder ~ctype filename); stream_decoder = None; } liquidsoap-2.3.2/src/core/decoder/ogg_flac_duration.ml000066400000000000000000000051311477303350200230200ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Read duration of ogg/flac files. *) let dresolver ~metadata:_ file = let sync, fd = Ogg.Sync.create_from_file file in Fun.protect ~finally:(fun () -> Unix.close fd) (fun _ -> let test_flac () = (* Get First page *) let page = Ogg.Sync.read sync in (* Check whether this is a b_o_s *) if not (Ogg.Page.bos page) then raise Flac.Decoder.Not_flac; (* Create a stream with this ID *) let serial = Ogg.Page.serialno page in let os = Ogg.Stream.create ~serial () in Ogg.Stream.put_page os page; let packet = Ogg.Stream.peek_packet os in (* Test header. Do not catch anything, first page should be sufficient *) if not (Flac_ogg.Decoder.check_packet packet) then raise Not_found; let fill () = let page = Ogg.Sync.read sync in if Ogg.Page.serialno page = serial then Ogg.Stream.put_page os page in Flac_ogg.Decoder.create ~fill ~write:(fun _ -> ()) os in (* Now find a flac stream *) let rec init () = try test_flac () with Not_found -> init () in let _, info, _ = init () in let samples = Int64.to_float info.Flac.Decoder.total_samples in (* If we have no sample, we play it safe and raise * Not_found *) if samples <= 0. then raise Not_found; samples /. float info.Flac.Decoder.sample_rate) let () = Plug.register Request.dresolvers "ogg_flac" ~doc:"" { dpriority = (fun () -> Liq_ogg_decoder.priority#get); file_extensions = (fun () -> Liq_ogg_decoder.file_extensions#get); dresolver; } liquidsoap-2.3.2/src/core/decoder/ogg_metadata_plug.ml000066400000000000000000000055431477303350200230240ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let log = Log.make ["decoder"; "ogg"; "metadata"] let mime_types = Dtools.Conf.list ~p:(Decoder.conf_mime_types#plug "ogg_metadata") "Mime-types used for decoding metadata using native ogg metadata parser." ~d:["audio/ogg"] let conf_ogg = Dtools.Conf.void ~p:(Decoder.conf_decoder#plug "ogg_metadata") "Native ogg metadata parser settings." let conf_separator = Dtools.Conf.string ~d:", " ~p:(conf_ogg#plug "separator") "Separator used to join metadata field with several entries." let file_extensions = Dtools.Conf.list ~p:(Decoder.conf_file_extensions#plug "ogg_metadata") "File extensions used for decoding metadata using native ogg parser." ~d:["ogg"] let priority = Dtools.Conf.int ~p:(Request.conf_metadata_decoder_priorities#plug "ogg_metadata") "Priority for the native ogg metadata decoder" ~d:1 let get_tags ~metadata:_ ~extension ~mime parse fname = try if not (Decoder.test_file ~log ~extension ~mime ~mimes:(Some mime_types#get) ~extensions:(Some file_extensions#get) fname) then raise Metadata.Invalid; let m = parse fname in let sep = conf_separator#get in List.fold_left (fun m (key, new_entry) -> try let old_entry = List.assoc key m in (key, old_entry ^ sep ^ new_entry) :: List.remove_assoc key m with Not_found -> (key, new_entry) :: m) [] m with | Metadata.Invalid -> [] | e -> let bt = Printexc.get_backtrace () in Utils.log_exception ~log ~bt (Printf.sprintf "Error while decoding file tags: %s" (Printexc.to_string e)); raise Not_found let () = Plug.register Request.mresolvers "ogg_native" ~doc:"Native ogg metadata resolver." { Request.priority = (fun () -> priority#get); resolver = get_tags Metadata.OGG.parse_file; } liquidsoap-2.3.2/src/core/decoder/raw_audio_decoder.ml000066400000000000000000000124641477303350200230200ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (** Decode raw data *) open Extralib let log = Log.make ["decoder"; "raw"] (** {1 Generic decoder} *) exception End_of_stream (* TODO: some code should be shared with wav decoder, and possibly others. *) type format = { format : [ `S8 | `S16LE | `F32LE ]; channels : int; interleaved : bool; samplerate : int; } (** Bytes per sample. *) let sample_size fmt = match fmt.format with `S8 -> 1 | `S16LE -> 2 | `F32LE -> 4 let create ~format input = (* TODO: can we handle interleaved with generators? I don't think so... *) assert (format.interleaved = true); let sample_size = sample_size format in let channels = format.channels in let bytes_to_get = sample_size * channels * Utils.pagesize in let converter src = let len = String.length src / (sample_size * channels) in let dst = Audio.create channels len in let sample = let pos = ref 0 in match format.format with | `S8 -> fun () -> let ans = int_of_char src.[!pos] in let ans = if ans > 128 then ans - 256 else ans in incr pos; float ans /. 128. | `S16LE -> fun () -> let ans = int_of_char src.[!pos] + (int_of_char src.[!pos + 1] lsl 8) in let ans = if ans > 32768 then ans - 65536 else ans in pos := !pos + 2; float ans /. 32768. | `F32LE -> (* TODO: handle endianness *) fun () -> let ans = ref Int32.zero in for i = 3 downto 0 do ans := Int32.shift_left !ans 8; ans := Int32.add !ans (Int32.of_int (int_of_char src.[!pos + i])) done; pos := !pos + sample_size; Int32.float_of_bits !ans in for i = 0 to len - 1 do for c = 0 to channels - 1 do dst.(c).(i) <- sample () done done; dst in let buf = Bytes.create bytes_to_get in let decoder buffer = let bytes = input.Decoder.read buf 0 bytes_to_get in if bytes = 0 then raise End_of_stream; let content = converter (Bytes.sub_string buf 0 bytes) in buffer.Decoder.put_pcm ~samplerate:format.samplerate content in (* TODO *) let seek _ = 0 in { Decoder.decode = decoder; seek; eof = (fun _ -> ()); close = (fun _ -> ()) } (* The mime types are inspired of GStreamer's convention. See http://gstreamer.freedesktop.org/data/doc/gstreamer/head/pwg/html/section-types-definitions.html For instance: audio/x-raw,format=F32LE,channels=2,layout=interleaved,rate=44100 *) (* TODO: proper parser? *) let parse_mime m = let ans = ref { format = `F32LE; channels = 2; interleaved = true; samplerate = 44100 } in try let m = String.split_char ',' m in if m = [] || List.hd m <> "audio/x-raw" then raise Exit; let m = List.tl m in let m = List.map (fun lv -> let lv = String.split_char '=' lv in match lv with [l; v] -> (l, v) | _ -> raise Exit) m in List.iter (fun (l, v) -> match l with | "format" -> let format = List.assoc v [("S8", `S8); ("S16LE", `S16LE); ("F32LE", `F32LE)] in ans := { !ans with format } | "channels" -> let channels = int_of_string v in ans := { !ans with channels } | "layout" -> let interleaved = if v = "interleaved" then true else if v = "non-interleaved" then false else raise Exit in ans := { !ans with interleaved } | "rate" | "samplerate" -> let samplerate = int_of_string v in ans := { !ans with samplerate } | _ -> failwith ("Unknown property: " ^ l)) m; Some !ans with _ -> None let () = Plug.register Decoder.decoders "raw audio" ~doc:"Decode audio/x-raw." { Decoder.priority = (fun () -> 1); file_extensions = (fun () -> None); mime_types = (fun () -> Some ["audio/x-raw"]); file_type = (fun ~metadata:_ ~ctype:_ _ -> None); file_decoder = None; stream_decoder = Some (fun ~ctype:_ mime -> let format = Option.get (parse_mime mime) in create ~format); } liquidsoap-2.3.2/src/core/decoder/srt_decoder.ml000066400000000000000000000072451477303350200216570ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Decode SRT files. *) let log = Log.make ["decoder"; "srt"] let srt_priorities = Dtools.Conf.int ~p:(Decoder.conf_priorities#plug "srt") "Priority for the SRT decoder" ~d:1 let srt_mime_types = Dtools.Conf.list ~p:(Decoder.conf_mime_types#plug "srt") "Mime-types used for guessing SRT format" ~d:["application/x-subrip"] let srt_file_extensions = Dtools.Conf.list ~p:(Decoder.conf_file_extensions#plug "srt") "File extensions used for guessing SRT format" ~d:["srt"] let () = Plug.register Decoder.decoders "srt" ~doc:"Decode srt files." { Decoder.priority = (fun () -> srt_priorities#get); file_extensions = (fun () -> Some srt_file_extensions#get); mime_types = (fun () -> Some srt_mime_types#get); file_type = (fun ~metadata:_ ~ctype fname -> if Srt_parser.check_file fname then Some (Frame.Fields.make ?audio:(Frame.Fields.find_opt Frame.Fields.audio ctype) ?video:(Frame.Fields.find_opt Frame.Fields.video ctype) ()) else None); file_decoder = Some (fun ~metadata:_ ~ctype fname -> let srt = Srt_parser.parse_file fname in let srt = List.map (fun ((t1, t2), s) -> [ (Srt_parser.seconds_of_time t1, s); (Srt_parser.seconds_of_time t2, ""); ]) srt |> List.flatten in let srt = List.map (fun (t, s) -> (Frame.main_of_seconds t, s)) srt in let srt = List.to_seq srt |> Queue.of_seq in let t = ref 0 in let remaining _ = -1 in let fread length = let rec fill frame = if Queue.is_empty srt then frame else ( let sub_t, sub = Queue.peek srt in let r = sub_t - !t in assert (r >= 0); if r < length then ( ignore (Queue.take srt); let frame = Frame.add_metadata frame r (Frame.Metadata.from_list [("subtitle", sub)]) in fill frame) else frame) in let frame = fill (Frame.create ~length ctype) in t := !t + length; frame in Decoder. { fread; remaining; fseek = (fun _ -> 0); fclose = (fun () -> ()); }); stream_decoder = None; } liquidsoap-2.3.2/src/core/decoder/text/000077500000000000000000000000001477303350200200045ustar00rootroot00000000000000liquidsoap-2.3.2/src/core/decoder/text/video_text_camlimages.ml000066400000000000000000000045601477303350200246770ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (* Based on https://gitlab.com/camlspotter/camlimages/-/blob/master/examples/ttfimg/ttfimg.ml *) let log = Log.make ["video"; "add_text"; "camlimages"] let init () = () let render_text ~font ~size text = if text = "" then (0, 0, fun _ _ -> 0) else ( let csize = float size in let face = new OFreetype.face font 0 in face#set_char_size csize csize 72 72; List.iter (fun cmap -> log#debug "available charmap: platform %d / encoding %d" cmap.Freetype.platform_id cmap.Freetype.encoding_id) face#charmaps; (try face#set_charmap { Freetype.platform_id = 3; encoding_id = 1 } with _ -> ( try face#set_charmap { Freetype.platform_id = 3; encoding_id = 0 } with _ -> face#set_charmap (List.hd face#charmaps))); let encoded = Fttext.unicode_of_latin text in let x1, y1, x2, y2 = face#size encoded in let plus = 8 in let w = truncate (x2 -. x1) + plus in let h = truncate (ceil y2) - truncate y1 + 1 + plus in let img = Array.init h (fun _ -> Array.make w 0) in OFreetype.draw_gen Freetype.Render_Normal Freetype.render_char 0. (fun x y l -> try img.(y).(x) <- l with _ -> ()) face ((plus / 2) - truncate x1) (truncate y2 + (plus / 2)) encoded; let get_pixel x y = img.(y).(x) in (w, h, get_pixel)) let () = Video_text.register "camlimages" init render_text liquidsoap-2.3.2/src/core/decoder/text/video_text_gd.ml000066400000000000000000000043261477303350200231670ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let init () = () let render_text ~font ~size text = if text = "" then (0, 0, fun _ _ -> 0) else ( let h = size in (* Scale because gd is using 96 DPI instead of the default 72 DPI. *) let size = float size *. 72. /. 96. in let fname = font in let angle = 0. in let bounds = Gd.ft_bbox ~fname ~size ~angle ~x:0 ~y:0 text in let lowline_reference = Gd.ft_bbox ~fname ~size ~angle ~x:0 ~y:0 "j" in let y = h - lowline_reference.(1) in let w = bounds.(2) - bounds.(0) in (* Anti-aliasing. *) let h = h * 2 in let size = size *. 2. in let y = y * 2 in let w = w * 2 in let img = Gd.create ~x:w ~y:h in let ca = img#colors in img#filled_rectangle ~x1:0 ~y1:0 ~x2:w ~y2:h ca#black; ignore (img#string_ft ~fname ~size ~angle:0. ~x:0 ~y ~fg:ca#white text); let get_pixel x y = let c = img#get_pixel ~x ~y in if c = ca#white then 0xff else 0 in (* Anti-aliasing. *) let get_pixel x y = (get_pixel (2 * x) (2 * y) + get_pixel ((2 * x) + 1) (2 * y) + get_pixel (2 * x) ((2 * y) + 1) + get_pixel ((2 * x) + 1) ((2 * y) + 1)) / 4 in (w, h, get_pixel)) let () = Video_text.register "gd" init render_text liquidsoap-2.3.2/src/core/decoder/text/video_text_native.ml000066400000000000000000000035641477303350200240660ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Extralib let log = Log.make ["video"; "text"; "native"] let render_text ~font ~size text = if font <> Configure.conf_default_font#get then Fun.once (fun () -> log#important "video.text.native does not support custom fonts yet!"); let () = ignore font in let font = Image.Bitmap.Font.native in let bmp = Image.Bitmap.Font.render text in let w = Image.Bitmap.width bmp in let h = Image.Bitmap.height bmp in let char_height = Image.Bitmap.Font.height font in let get_pixel x y = let x = x * char_height / size in let y = y * char_height / size in if 0 <= y && y < h && 0 <= x && x < w then if Mm.Image.Bitmap.get_pixel bmp x y then 0xff else 0x00 else 0x00 in let h = h * size / char_height in let w = w * size / char_height in (w, h, get_pixel) let () = Video_text.register "native" (fun () -> ()) render_text liquidsoap-2.3.2/src/core/decoder/text/video_text_sdl.ml000066400000000000000000000043031477303350200233520ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Tsdl open Tsdl_ttf let init () = Sdl_utils.init []; Sdl_utils.check Tsdl_ttf.Ttf.init () let get_font font size = let font = if font = "" then Configure.conf_default_font#get else font in try Sdl_utils.check (Ttf.open_font font) size with e -> raise (Error.Invalid_value (Lang.string font, Printexc.to_string e ^ "(font: " ^ font ^ ")")) let render_text ~font ~size text = let text = if text = "" then " " else text in let font = get_font font size in let ts = Fun.protect (fun () -> let white = Sdl.Color.create ~r:0xff ~g:0xff ~b:0xff ~a:0xff in Sdl_utils.check (fun () -> Ttf.render_utf8_blended_wrapped font text white Int32.zero) ()) ~finally:(fun () -> Ttf.close_font font) in let img = Fun.protect (fun () -> Sdl_utils.Surface.to_img ts) ~finally:(fun () -> Sdl.free_surface ts) in let w = Video.Image.width img in let h = Video.Image.height img in (* TODO: improve performance *) let get_pixel x y = assert (0 <= x && x < w); assert (0 <= y && y < h); Image.YUV420.get_pixel_a img x y in (w, h, get_pixel) let () = Video_text.register "sdl" init render_text liquidsoap-2.3.2/src/core/decoder/video_plug.ml000066400000000000000000000047151477303350200215160ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let log = Log.make ["decoder"; "video"; "metadata"] (** Configuration keys. *) let mime_types = Dtools.Conf.list ~p:(Decoder.conf_mime_types#plug "video_metadata") "Mime-types used for decoding metadata using native parser." ~d:["video/x-msvideo"; "video/mp4"] let conf_video = Dtools.Conf.void ~p:(Decoder.conf_decoder#plug "video_metadata") "Native video metadata parser settings." let file_extensions = Dtools.Conf.list ~p:(Decoder.conf_file_extensions#plug "video_metadata") "File extensions used for decoding metadata using native parser." ~d:["avi"; "mp4"] let priority = Dtools.Conf.int ~p:(Request.conf_metadata_decoder_priorities#plug "video_metadata") "Priority for the native video metadata decoder" ~d:1 let get_tags ~metadata:_ ~extension ~mime fname = try if not (Decoder.test_file ~log ~mime ~extension ~mimes:(Some mime_types#get) ~extensions:(Some file_extensions#get) fname) then raise Metadata.Invalid; Metadata.Video.parse_file fname with | Metadata.Invalid -> [] | e -> let bt = Printexc.get_backtrace () in Utils.log_exception ~log ~bt (Printf.sprintf "Error while decoding file tags: %s" (Printexc.to_string e)); raise Not_found let () = Plug.register Request.mresolvers "video-metadata" ~doc:"Native metadata decoder for videos." { Request.priority = (fun () -> priority#get); resolver = get_tags } liquidsoap-2.3.2/src/core/decoder/vorbisduration.ml000066400000000000000000000027151477303350200224310ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Read duration of ogg/vorbis files. *) let dresolver ~metadata:_ file = let dec, fd = Vorbis.File.Decoder.openfile file in Fun.protect ~finally:(fun () -> Unix.close fd) (fun _ -> Vorbis.File.Decoder.duration dec (-1)) let () = Plug.register Request.dresolvers "vorbis" ~doc:"" { dpriority = (fun () -> Liq_ogg_decoder.priority#get); file_extensions = (fun () -> Liq_ogg_decoder.file_extensions#get); dresolver; } liquidsoap-2.3.2/src/core/decoder/wav_aiff_decoder.ml000066400000000000000000000213721477303350200226260ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Decode WAV files. *) let log = Log.make ["decoder"; "wav/aiff"] (** {1 Generic decoder} *) exception End_of_stream let really_input input buf ofs len = let rec f pos = if pos < len then ( let i = input buf (ofs + pos) (len - pos) in if i = 0 then raise End_of_stream else f (pos + i)) in f ofs let input fn = fn let input_byte input = let buf = Bytes.create 1 in let i = input buf 0 1 in if i = 0 then raise End_of_stream; int_of_char (Bytes.get buf 0) let seek input len = let s = Bytes.create len in ignore (really_input input s 0 len) let input_ops = { Wav_aiff.really_input; input_byte; input; seek; close = (fun _ -> ()) } (* It might be more efficient to write our code for an input channel and use directly the one we have when decoding files or external processes, if we could wrap the input function used for decoding stream (in http and harbor) as an in_channel. *) let create ?header input = let decoder = ref (fun ~buffer:_ -> assert false) in let header = ref header in let main_decoder ~samplerate ~buffer remaining = let remaining = ref remaining in let bytes_to_get = Utils.pagesize * 64 in let buf = Bytes.create bytes_to_get in fun converter -> let bytes_to_get = if !remaining = -1 then bytes_to_get else min !remaining bytes_to_get in let bytes = input.Decoder.read buf 0 bytes_to_get in if !remaining <> -1 then remaining := !remaining - bytes; if bytes = 0 then raise End_of_stream; let content = converter buf 0 bytes in buffer.Decoder.put_pcm ~samplerate content in let read_header () = let iff_header = Wav_aiff.read_header input_ops input.Decoder.read in let samplesize = Wav_aiff.sample_size iff_header in let channels = Wav_aiff.channels iff_header in let samplerate = Wav_aiff.sample_rate iff_header in let datalen = Wav_aiff.data_length iff_header in let datalen = if datalen <= 0 then -1 else datalen in let format = Wav_aiff.format_of_handler iff_header in let converter = Decoder_utils.from_iff ~samplesize ~channels ~format in let format_descr = match format with `Wav -> "WAV" | `Aiff -> "AIFF" in log#info "%s header read (%d Hz, %d bits, %d bytes), starting decoding..." format_descr samplerate samplesize datalen; header := Some (format, samplesize, channels, samplerate, datalen); decoder := main_decoder ~samplerate datalen converter in begin match !header with | None -> decoder := fun ~buffer:_ -> read_header () | Some (format, samplesize, channels, samplerate, datalen) -> let converter = Decoder_utils.from_iff ~format ~samplesize ~channels in decoder := main_decoder ~samplerate datalen converter end; let seek ticks = match (input.Decoder.lseek, input.Decoder.tell, !header) with | Some seek, Some tell, Some (_, samplesize, channels, samplerate, _) -> ( (* seek is in absolute position *) let duration = Frame.seconds_of_main ticks in let samples = int_of_float (duration *. float samplerate) in let bytes = samples * samplesize * channels / 8 in try let pos = tell () in let ret = seek (pos + bytes) in let samples = 8 * (ret - pos) / (samplesize * channels) in let duration = float samples /. float samplerate in Frame.main_of_seconds duration with _ -> 0) | _, _, _ -> 0 in { Decoder.decode = (fun buffer -> !decoder ~buffer); seek; eof = (fun _ -> ()); close = (fun _ -> ()); } (* File decoding *) let file_type ~metadata:_ ~ctype:_ filename = let header = Wav_aiff.fopen filename in Fun.protect ~finally:(fun () -> Wav_aiff.close header) (fun () -> let channels = let channels = Wav_aiff.channels header in let sample_rate = Wav_aiff.sample_rate header in let ok_message s = log#info "%s recognized as WAV file (%s,%dHz,%d channels)." (Lang_string.quote_string filename) s sample_rate channels in match Wav_aiff.sample_size header with | 8 -> ok_message "u8"; channels | 16 -> ok_message "s16le"; channels | 24 -> ok_message "s24le"; channels | 32 -> ok_message "s32le"; channels | _ -> log#info "Only 8, 16, 24 and 32 bit WAV files are supported at the \ moment.."; 0 in Some (Frame.Fields.make ~audio: (Frame_base.format_of_channels ~pcm_kind:Content.Audio.kind channels) ())) let wav_mime_types = Dtools.Conf.list ~p:(Decoder.conf_mime_types#plug "wav") "Mime-types used for guessing WAV format" ~d:["audio/vnd.wave"; "audio/wav"; "audio/wave"; "audio/x-wav"] let wav_file_extensions = Dtools.Conf.list ~p:(Decoder.conf_file_extensions#plug "wav") "File extensions used for guessing WAV format" ~d:["wav"; "wave"] let wav_priority = Dtools.Conf.int ~p:(Decoder.conf_priorities#plug "wav") "Priority for the WAV decoder" ~d:1 let create_file_decoder ~metadata:_ ~ctype filename = Decoder.opaque_file_decoder ~filename ~ctype (create ?header:None) let () = Plug.register Decoder.decoders "wav" ~doc:"Decode file or streams as WAV." { Decoder.priority = (fun () -> wav_priority#get); file_extensions = (fun () -> Some wav_file_extensions#get); mime_types = (fun () -> Some wav_mime_types#get); file_type; file_decoder = Some create_file_decoder; stream_decoder = Some (fun ~ctype:_ _ -> create ?header:None); } let aiff_mime_types = Dtools.Conf.list ~p:(Decoder.conf_mime_types#plug "aiff") "Mime-types used for guessing AIFF format" ~d:["audio/x-aiff"; "audio/aiff"] let aiff_file_extensions = Dtools.Conf.list ~p:(Decoder.conf_file_extensions#plug "aiff") "File extensions used for guessing AIFF format" ~d:["aiff"; "aif"; "aifc"] let aiff_priorities = Dtools.Conf.int ~p:(Decoder.conf_priorities#plug "aiff") "Priority for the AIFF decoder" ~d:1 let () = Plug.register Decoder.decoders "aiff" ~doc:"Decode as AIFF any file with a correct header." { Decoder.priority = (fun () -> aiff_priorities#get); file_extensions = (fun () -> Some aiff_file_extensions#get); mime_types = (fun () -> Some aiff_mime_types#get); file_type; file_decoder = Some create_file_decoder; stream_decoder = Some (fun ~ctype:_ _ -> create ?header:None); } let () = let dresolver ~metadata:_ file = let w = Wav_aiff.fopen file in let ret = Wav_aiff.duration w in Wav_aiff.close w; ret in Plug.register Request.dresolvers "wav/aiff" ~doc:"Native computation of wav and aiff files duration." { dpriority = (fun () -> aiff_priorities#get); file_extensions = (fun () -> aiff_file_extensions#get); dresolver; } let basic_mime_types = Dtools.Conf.list ~p:(Decoder.conf_mime_types#plug "basic") "Mime-types used for guessing PCM/BASIC format" ~d:["audio/basic"] let basic_priorities = Dtools.Conf.int ~p:(Decoder.conf_priorities#plug "basic") "Priority for the PCM/BASIC decoder" ~d:1 let () = Plug.register Decoder.decoders "pcm/basic" ~doc:"Decode audio/basic as headerless stereo U8 PCM at 8kHz." { Decoder.priority = (fun () -> basic_priorities#get); file_extensions = (fun () -> None); mime_types = (fun () -> Some basic_mime_types#get); file_type = (fun ~metadata:_ ~ctype:_ _ -> None); file_decoder = None; stream_decoder = Some (fun ~ctype:_ _ -> create ~header:(`Wav, 8, 2, 8000, -1)); } liquidsoap-2.3.2/src/core/doc.ml000066400000000000000000000010331477303350200165070ustar00rootroot00000000000000include Liquidsoap_lang.Doc (** Documentation for protocols. *) module Protocol = struct type t = { name : string; description : string; syntax : string } let db = ref [] let add ~name ~doc ~syntax = let p = { name; description = doc; syntax } in db := p :: !db let db () = List.sort compare !db let count () = db () |> List.length let print_md print = List.iter (fun p -> Printf.ksprintf print "### %s\n\n%s\n\nThe syntax is `%s`.\n\n" p.name p.description p.syntax) (db ()) end liquidsoap-2.3.2/src/core/dune000066400000000000000000000452241477303350200163000ustar00rootroot00000000000000(env (release (ocamlopt_flags (:standard -w -9 -alert --deprecated -O2))) (_ (flags (:standard -w -9 -alert --deprecated)))) (include_subdirs unqualified) (rule (target liquidsoap_paths.ml) (action (copy liquidsoap_paths.%{env:LIQUIDSOAP_BUILD_TARGET=default}.ml %{target}))) (library (name liquidsoap_core) (preprocess (pps ppx_string)) (libraries mm dtools dune-build-info duppy fileutils liquidsoap-lang liquidsoap-lang.console menhirLib camomile.lib curl cry re uri metadata mem_usage magic-mime (select file_watcher.ml from (inotify -> file_watcher.inotify.ml) (-> file_watcher.mtime.ml))) (foreign_stubs (language c) (names unix_c defer_c content_pcm_c lufs_c)) (wrapped false) (library_flags -linkall) (modules aFrame accelerate add amplify annotate audio_converter audio_gen available avi avi_encoder avi_format biquad_filter blank charset_base charset child_support chord clip clock clock_base comb compand compress compress_exp content content_audio content_pcm_base content_pcm_f32 content_pcm_s16 content_base content_midi content_timed content_video conversion configure cross debug_sources decoder decoder_utils defer delay delay_line doc dtmf dyn_op echo encoder encoder_formats encoder_utils error external_decoder external_encoder external_encoder_format external_input external_input_audio external_input_video extralib extra_args fdkaac_format ffmpeg_format file_watcher filter filter_rc fir_filter flac_format flac_metadata_plug flanger frame frame_base frame_settings frame_type format_type gate generated generator harbor harbor_base harbor_input harbor_output hooks_implementations hls_output icecast_utils icecast2 id3_plug iir_filter image_decoder image_plug insert_metadata json keyboard lang lang_avi lang_encoder lang_external_encoder lang_fdkaac lang_flac lang_mp3 lang_ndi lang_ogg lang_opus lang_source lang_shine lang_speex lang_string lang_theora lang_vorbis lang_wav latest_metadata liqcurl lifecycle liq_http liq_time liquidsoap_paths log lufs mFrame map_metadata map_op max_duration mean merge_metadata metadata_base midi_decoder midi_routing midimeter modules mp3_format mpd ms_stereo muxer mutex_utils native_audio_converter native_video_converter ndi_format ndi_encoder noblank noise normalize ogg_format ogg_metadata_plug on_end on_frame on_metadata on_offset on_track opus_format output pan pipe pipe_output pitch playlist_parser plug pool pos ppm_decoder process_handler producer_consumer queues raw_audio_decoder replaygain_op request request_dynamic resample rms_smooth runtime_error sandbox sequence server server_builtins sha1 shebang shine_format source speex_format srt_decoder srt_parser start_stop startup stereo still_frame stringView strings swap switch synth_op synthesized term theora_format time_warp track source_tracks track_map tutils type typing unifier udp_io utils value vFrame video_board video_converter video_effects video_fade video_plug video_testsrc video_text video_text_native video_volume vorbis_format wav_aiff wav_aiff_decoder wav_encoder wav_format websocket window_op)) (library (name liquidsoap_builtins) (library_flags -linkall) (preprocess (pps ppx_string)) (wrapped false) (libraries liquidsoap_core) (modules builtins_callbacks builtins_clock builtins_cry builtins_files builtins_harbor builtins_http builtins_metadata builtins_mem_usage builtins_process builtins_request builtins_resolvers playlist_basic builtins_runtime builtins_server builtins_settings builtins_socket builtins_source builtins_string_extra builtins_sys builtins_thread builtins_time builtins_track)) (library (name liquidsoap_alsa) (libraries alsa liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules alsa_io alsa_settings)) (library (name liquidsoap_ao) (libraries ao liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules ao_out)) (library (name liquidsoap_bjack) (libraries bjack liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules bjack_in bjack_out)) (library (name liquidsoap_camlimages) (libraries camlimages.all_formats liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules camlimages_decoder)) (library (name liquidsoap_dssi) (libraries dssi liquidsoap_ladspa liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules dssi_op)) (library (name liquidsoap_faad) (libraries faad liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules aac_decoder)) (library (name liquidsoap_fdkaac) (libraries fdkaac liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules fdkaac_encoder)) (library (name liquidsoap_ffmpeg) (libraries ffmpeg-avutil ffmpeg-avcodec ffmpeg-avfilter ffmpeg-avdevice ffmpeg-swscale ffmpeg-swresample ffmpeg-av liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules builtins_ffmpeg_base builtins_ffmpeg_bitstream_filters builtins_ffmpeg_decoder builtins_ffmpeg_filters builtins_ffmpeg_encoder ffmpeg_avfilter_utils ffmpeg_audio_converter ffmpeg_content_base ffmpeg_copy_content ffmpeg_copy_decoder ffmpeg_copy_encoder ffmpeg_decoder ffmpeg_decoder_common ffmpeg_image_decoder ffmpeg_encoder ffmpeg_encoder_common ffmpeg_filter_io ffmpeg_internal_decoder ffmpeg_internal_encoder ffmpeg_io ffmpeg_raw_content ffmpeg_raw_decoder ffmpeg_utils ffmpeg_video_converter lang_ffmpeg)) (library (name liquidsoap_flac) (libraries flac liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules liq_flac_decoder flac_encoder)) (library (name liquidsoap_ogg_flac) (libraries flac.ogg flac.decoder liquidsoap_core liquidsoap_ogg) (library_flags -linkall) (wrapped false) (optional) (foreign_stubs (language c) (names ogg_flac_encoder_stubs)) (modules liq_flac_ogg_decoder ogg_flac_encoder ogg_flac_duration)) (library (name liquidsoap_frei0r) (libraries frei0r liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules frei0r_op)) (library (name liquidsoap_gd) (libraries gd liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules video_text_gd)) (library (name liquidsoap_graphics) (libraries graphics liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules graphics_out)) (library (name liquidsoap_irc) (libraries irc-client-unix liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules builtins_irc)) (library (name liquidsoap_jemalloc) (libraries jemalloc liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules builtins_jemalloc)) (library (name liquidsoap_ladspa) (libraries ladspa liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules ladspa_op)) (library (name liquidsoap_lame) (libraries lame liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules lame_encoder)) (library (name liquidsoap_lilv) (libraries lilv liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules lilv_op)) (library (name liquidsoap_lo) (libraries lo liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules builtins_lo)) (library (name liquidsoap_mad) (libraries mad liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules mad_decoder)) (library (name liquidsoap_ogg) (libraries ogg ogg.decoder liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules ogg_muxer ogg_encoder liq_ogg_decoder)) (library (name liquidsoap_opus) (libraries opus opus.decoder liquidsoap_core liquidsoap_ogg) (library_flags -linkall) (wrapped false) (optional) (modules liq_opus_decoder opus_encoder)) (library (name liquidsoap_osc) (libraries osc-unix liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules builtins_osc)) (library (name liquidsoap_oss) (libraries liquidsoap_core) (enabled_if (= "linux" %{system})) (library_flags -linkall) (wrapped false) (optional) (foreign_stubs (language c) (names oss_io_c)) (modules oss_io)) (library (name liquidsoap_portaudio) (libraries portaudio liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules portaudio_io)) (library (name liquidsoap_posix_time) (libraries posix-time2 liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules liq_posix_time)) (library (name liquidsoap_prometheus) (libraries cohttp-lwt-unix prometheus-app liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules liq_prometheus builtins_prometheus)) (library (name liquidsoap_pulseaudio) (libraries pulseaudio liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules pulseaudio_io)) (library (name liquidsoap_samplerate) (libraries samplerate liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules libsamplerate_converter)) (library (name liquidsoap_sdl_log_level) (libraries tsdl) (library_flags -linkall) (wrapped false) (optional) (modules sdl_log_level)) (library (name liquidsoap_sdl) (libraries liquidsoap_sdl_log_level tsdl-image tsdl-ttf liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules sdlimage_decoder video_text_sdl sdl_out keyboard_sdl sdl_utils)) (library (name liquidsoap_imagelib) (libraries imagelib imagelib.unix liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules imagelib_decoder)) (library (name liquidsoap_shine) (libraries shine liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules shine_encoder)) (library (name liquidsoap_soundtouch) (libraries soundtouch liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules soundtouch_op st_bpm)) (library (name liquidsoap_speex) (libraries speex speex.decoder liquidsoap_core liquidsoap_ogg) (library_flags -linkall) (wrapped false) (optional) (modules liq_speex_decoder speex_encoder)) (library (name liquidsoap_srt) (libraries srt liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules builtins_srt srt_io)) (library (name liquidsoap_ssl) (libraries ssl liquidsoap_core liquidsoap_builtins) (library_flags -linkall) (wrapped false) (optional) (modules ssl_base builtins_ssl)) (library (name liquidsoap_stereotool) (libraries stereotool liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules stereotool_op)) (library (name liquidsoap_ndi) (libraries ndi liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules ndi_out)) (library (name liquidsoap_theora) (libraries theora theora.decoder liquidsoap_core liquidsoap_ogg) (library_flags -linkall) (wrapped false) (optional) (modules liq_theora_decoder theora_encoder)) (library (name liquidsoap_tls) (libraries tls ca-certs mirage-crypto-rng.unix cstruct liquidsoap_core liquidsoap_builtins) (library_flags -linkall) (wrapped false) (optional) (modules builtins_tls)) (library (name liquidsoap_vorbis) (libraries vorbis vorbis.decoder liquidsoap_core liquidsoap_ogg) (library_flags -linkall) (wrapped false) (optional) (modules liq_vorbis_decoder vorbis_encoder vorbisduration)) (library (name liquidsoap_sqlite) (libraries sqlite3 liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules builtins_sqlite)) (library (name liquidsoap_yaml) (libraries yaml liquidsoap_core) (library_flags -linkall) (wrapped false) (optional) (modules builtins_yaml)) (library (name liquidsoap_xmlplaylist) (libraries xmlplaylist liquidsoap_core liquidsoap_builtins) (library_flags -linkall) (wrapped false) (optional) (modules playlist_xml)) (copy_files ../config/*.ml) (library (name liquidsoap_optionals) (library_flags -linkall) (preprocess (pps ppx_string)) (wrapped false) (modules alsa_option ao_option bjack_option builtins_optionals camlimages_option dssi_option faad_option fdkaac_option ffmpeg_option flac_option ogg_flac_option frei0r_option gd_option graphics_option imagelib_option inotify_option irc_option jemalloc_option ladspa_option lame_option lilv_option lo_option mad_option memtrace_option ndi_option ogg_option opus_option osc_option oss_option portaudio_option posix_time_option prometheus_option pulseaudio_option samplerate_option sdl_option shine_option soundtouch_option speex_option srt_option ssl_option stereotool_option sqlite3_option theora_option tls_option vorbis_option winsvc_option yaml_option xmlplaylist_option) (libraries liquidsoap_core (select alsa_option.ml from (liquidsoap_alsa -> alsa_option.enabled.ml) (-> alsa_option.disabled.ml)) (select ao_option.ml from (liquidsoap_ao -> ao_option.enabled.ml) (-> ao_option.disabled.ml)) (select bjack_option.ml from (liquidsoap_bjack -> bjack_option.enabled.ml) (-> bjack_option.disabled.ml)) (select camlimages_option.ml from (liquidsoap_camlimages -> camlimages_option.enabled.ml) (-> camlimages_option.disabled.ml)) (select dssi_option.ml from (liquidsoap_dssi -> dssi_option.enabled.ml) (-> dssi_option.disabled.ml)) (select faad_option.ml from (liquidsoap_faad -> faad_option.enabled.ml) (-> faad_option.disabled.ml)) (select fdkaac_option.ml from (liquidsoap_fdkaac -> fdkaac_option.enabled.ml) (-> fdkaac_option.disabled.ml)) (select ffmpeg_option.ml from (liquidsoap_ffmpeg -> ffmpeg_option.enabled.ml) (-> ffmpeg_option.disabled.ml)) (select flac_option.ml from (liquidsoap_flac -> flac_option.enabled.ml) (-> flac_option.disabled.ml)) (select ogg_flac_option.ml from (liquidsoap_ogg_flac -> ogg_flac_option.enabled.ml) (-> ogg_flac_option.disabled.ml)) (select frei0r_option.ml from (liquidsoap_frei0r -> frei0r_option.enabled.ml) (-> frei0r_option.disabled.ml)) (select gd_option.ml from (liquidsoap_gd -> gd_option.enabled.ml) (-> gd_option.disabled.ml)) (select graphics_option.ml from (liquidsoap_graphics -> graphics_option.enabled.ml) (-> graphics_option.disabled.ml)) (select inotify_option.ml from (inotify -> inotify_option.enabled.ml) (-> inotify_option.disabled.ml)) (select irc_option.ml from (liquidsoap_irc -> irc_option.enabled.ml) (-> irc_option.disabled.ml)) (select jemalloc_option.ml from (liquidsoap_jemalloc -> jemalloc_option.enabled.ml) (-> jemalloc_option.disabled.ml)) (select ladspa_option.ml from (liquidsoap_ladspa -> ladspa_option.enabled.ml) (-> ladspa_option.disabled.ml)) (select lame_option.ml from (liquidsoap_lame -> lame_option.enabled.ml) (-> lame_option.disabled.ml)) (select lilv_option.ml from (liquidsoap_lilv -> lilv_option.enabled.ml) (-> lilv_option.disabled.ml)) (select lo_option.ml from (liquidsoap_lo -> lo_option.enabled.ml) (-> lo_option.disabled.ml)) (select mad_option.ml from (liquidsoap_mad -> mad_option.enabled.ml) (-> mad_option.disabled.ml)) (select memtrace_option.ml from (memtrace -> memtrace_option.enabled.ml) (-> memtrace_option.disabled.ml)) (select ogg_option.ml from (liquidsoap_ogg -> ogg_option.enabled.ml) (-> ogg_option.disabled.ml)) (select opus_option.ml from (liquidsoap_opus -> opus_option.enabled.ml) (-> opus_option.disabled.ml)) (select osc_option.ml from (liquidsoap_osc -> osc_option.enabled.ml) (-> osc_option.disabled.ml)) (select oss_option.ml from (liquidsoap_oss -> oss_option.enabled.ml) (-> oss_option.disabled.ml)) (select portaudio_option.ml from (liquidsoap_portaudio -> portaudio_option.enabled.ml) (-> portaudio_option.disabled.ml)) (select ndi_option.ml from (liquidsoap_ndi -> ndi_option.enabled.ml) (-> ndi_option.disabled.ml)) (select posix_time_option.ml from (liquidsoap_posix_time -> posix_time_option.enabled.ml) (-> posix_time_option.disabled.ml)) (select prometheus_option.ml from (liquidsoap_prometheus -> prometheus_option.enabled.ml) (-> prometheus_option.disabled.ml)) (select pulseaudio_option.ml from (liquidsoap_pulseaudio -> pulseaudio_option.enabled.ml) (-> pulseaudio_option.disabled.ml)) (select samplerate_option.ml from (liquidsoap_samplerate -> samplerate_option.enabled.ml) (-> samplerate_option.disabled.ml)) (select sdl_option.ml from (liquidsoap_sdl -> sdl_option.enabled.ml) (-> sdl_option.disabled.ml)) (select imagelib_option.ml from (liquidsoap_imagelib -> imagelib_option.enabled.ml) (-> imagelib_option.disabled.ml)) (select shine_option.ml from (liquidsoap_shine -> shine_option.enabled.ml) (-> shine_option.disabled.ml)) (select soundtouch_option.ml from (liquidsoap_soundtouch -> soundtouch_option.enabled.ml) (-> soundtouch_option.disabled.ml)) (select speex_option.ml from (liquidsoap_speex -> speex_option.enabled.ml) (-> speex_option.disabled.ml)) (select srt_option.ml from (liquidsoap_srt -> srt_option.enabled.ml) (-> srt_option.disabled.ml)) (select ssl_option.ml from (liquidsoap_ssl -> ssl_option.enabled.ml) (-> ssl_option.disabled.ml)) (select stereotool_option.ml from (liquidsoap_stereotool -> stereotool_option.enabled.ml) (-> stereotool_option.disabled.ml)) (select sqlite3_option.ml from (liquidsoap_sqlite -> sqlite3_option.enabled.ml) (-> sqlite3_option.disabled.ml)) (select theora_option.ml from (liquidsoap_theora -> theora_option.enabled.ml) (-> theora_option.disabled.ml)) (select tls_option.ml from (liquidsoap_tls -> tls_option.enabled.ml) (-> tls_option.disabled.ml)) (select vorbis_option.ml from (liquidsoap_vorbis -> vorbis_option.enabled.ml) (-> vorbis_option.disabled.ml)) (select winsvc_option.ml from (winsvc -> winsvc_option.enabled.ml) (-> winsvc_option.disabled.ml)) (select yaml_option.ml from (liquidsoap_yaml -> yaml_option.enabled.ml) (-> yaml_option.disabled.ml)) (select xmlplaylist_option.ml from (liquidsoap_xmlplaylist -> xmlplaylist_option.enabled.ml) (-> xmlplaylist_option.disabled.ml)))) liquidsoap-2.3.2/src/core/encoder/000077500000000000000000000000001477303350200170325ustar00rootroot00000000000000liquidsoap-2.3.2/src/core/encoder/encoder.ml000066400000000000000000000345151477303350200210130ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Common infrastructure for encoding streams *) type format = | WAV of Wav_format.t | AVI of Avi_format.t | NDI of Ndi_format.t | Ogg of Ogg_format.t | MP3 of Mp3_format.t | Shine of Shine_format.t | Flac of Flac_format.t | Ffmpeg of Ffmpeg_format.t | FdkAacEnc of Fdkaac_format.t | External of External_encoder_format.t let audio_type ~pcm_kind n = Frame.Fields.make ~audio: (Type.make (Format_type.descr (`Format (Frame_base.format_of_channels ~pcm_kind n)))) () let video_format () = Content.(default_format Video.kind) let audio_video_type ~pcm_kind n = Frame.Fields.add Frame.Fields.video (Type.make (Format_type.descr (`Format (video_format ())))) (audio_type ~pcm_kind n) let video_type () = Frame.Fields.make ~video:(Type.make (Format_type.descr (`Format (video_format ())))) () let type_of_format f = let audio_type = audio_type ~pcm_kind:Content_audio.kind in let audio_video_type = audio_video_type ~pcm_kind:Content_audio.kind in match f with | WAV w -> audio_type w.Wav_format.channels | AVI a -> audio_video_type a.Avi_format.channels | MP3 m -> audio_type (if m.Mp3_format.stereo then 2 else 1) | Shine m -> audio_type m.Shine_format.channels | NDI { audio = false; video = false } -> assert false | NDI { audio = true; video = false } -> audio_type (Lazy.force Frame.audio_channels) | NDI { audio = true; video = true } -> audio_video_type (Lazy.force Frame.audio_channels) | NDI { audio = false; video = true } -> video_type () | Flac m -> audio_type m.Flac_format.channels | Ffmpeg m -> List.fold_left (fun ctype (field, c) -> Frame.Fields.add field (match c with | `Drop -> Type.var ~constraints:[Format_type.track] () | `Copy _ -> Type.make (Format_type.descr (`Format Content.( default_format (kind_of_string "ffmpeg.copy")))) | `Encode { Ffmpeg_format.mode = `Raw; options = `Audio _ } -> Type.make (Format_type.descr (`Format Content.( default_format (kind_of_string "ffmpeg.audio.raw")))) | `Encode { Ffmpeg_format.mode = `Raw; options = `Video _ } -> Type.make (Format_type.descr (`Format Content.( default_format (kind_of_string "ffmpeg.video.raw")))) | `Encode Ffmpeg_format. { mode = `Internal; options = `Audio { pcm_kind; channels }; } -> assert (channels > 0); let params = { Content.Audio.channel_layout = Lazy.from_val (Audio_converter.Channel_layout.layout_of_channels channels); } in Type.make (Format_type.descr (`Format (Frame_base.audio_format ~pcm_kind params))) | `Encode { Ffmpeg_format.mode = `Internal; options = `Video _ } -> Type.make (Format_type.descr (`Format Content.(default_format Video.kind)))) ctype) Frame.Fields.empty m.streams | FdkAacEnc m -> audio_type m.Fdkaac_format.channels | Ogg { Ogg_format.audio; video } -> ( let channels = match audio with | Some (Ogg_format.Vorbis { Vorbis_format.channels = n; _ }) | Some (Ogg_format.Opus { Opus_format.channels = n; _ }) | Some (Ogg_format.Flac { Flac_format.channels = n; _ }) -> n | Some (Ogg_format.Speex { Speex_format.stereo; _ }) -> if stereo then 2 else 1 | None -> 0 in match (channels, video) with | 0, Some _ -> video_type () | n, Some _ -> audio_video_type n | n, None -> audio_type n) | External e -> let channels = e.External_encoder_format.channels in if e.External_encoder_format.video <> None then audio_video_type channels else audio_type channels let string_of_format = function | WAV w -> Wav_format.to_string w | AVI w -> Avi_format.to_string w | Ogg w -> Ogg_format.to_string w | MP3 w -> Mp3_format.to_string w | NDI w -> Ndi_format.to_string w | Shine w -> Shine_format.to_string w | Flac w -> Flac_format.to_string w | Ffmpeg w -> Ffmpeg_format.to_string w | FdkAacEnc w -> Fdkaac_format.to_string w | External w -> External_encoder_format.to_string w let video_size = function | Ogg { Ogg_format.video = Some { Theora_format.width; height } } -> Some (Lazy.force width, Lazy.force height) | Ffmpeg m -> ( match List.fold_left (fun cur (_, stream) -> match stream with | `Encode Ffmpeg_format.{ options = `Video { width; height } } -> (width, height) :: cur | _ -> cur) [] m.Ffmpeg_format.streams with | (width, height) :: [] -> Some (Lazy.force width, Lazy.force height) | _ -> None) | _ -> None (** ISO Base Media File Format, see RFC 6381 section 3.3. *) let iso_base_file_media_file_format = function | MP3 _ | Shine _ -> "mp4a.40.34" (* I have also seen "mp4a.69" and "mp3" *) | FdkAacEnc m -> ( match m.Fdkaac_format.aot with | `Mpeg_4 `AAC_LC -> "mp4a.40.2" | `Mpeg_4 `HE_AAC -> "mp4a.40.5" | `Mpeg_4 `HE_AAC_v2 -> "mp4a.40.29" | `Mpeg_4 `AAC_LD -> "mp4a.40.23" | `Mpeg_4 `AAC_ELD -> "mp4a.40.39" | `Mpeg_2 `AAC_LC -> "mp4a.67" | `Mpeg_2 `HE_AAC -> "mp4a.67" (* TODO: check this *) | `Mpeg_2 `HE_AAC_v2 -> "mp4a.67" (* TODO: check this *)) | Ffmpeg { Ffmpeg_format.format = Some "libmp3lame" } -> "mp4a.40.34" | Ffmpeg { Ffmpeg_format.format = Some "aac" } -> "mp4a.40.2" | _ -> raise Not_found (** Proposed extension for files. *) let extension = function | WAV _ -> "wav" | AVI _ -> "avi" | Ogg _ -> "ogg" | MP3 _ -> "mp3" | Shine _ -> "mp3" | Flac _ -> "flac" | FdkAacEnc _ -> "aac" | Ffmpeg { Ffmpeg_format.format = Some "ogg" } -> "ogg" | Ffmpeg { Ffmpeg_format.format = Some "opus" } -> "opus" | Ffmpeg { Ffmpeg_format.format = Some "mp3" } -> "mp3" | Ffmpeg { Ffmpeg_format.format = Some "matroska" } -> "mkv" | Ffmpeg { Ffmpeg_format.format = Some "mpegts" } -> "ts" | Ffmpeg { Ffmpeg_format.format = Some "ac3" } -> "ac3" | Ffmpeg { Ffmpeg_format.format = Some "eac3" } -> "eac3" | Ffmpeg { Ffmpeg_format.format = Some "adts"; streams = [(_, `Encode { Ffmpeg_format.codec = Some "aac" })]; } -> "aac" | Ffmpeg { Ffmpeg_format.format = Some "adts" } -> "adts" | Ffmpeg { Ffmpeg_format.format = Some "mp4" } -> "mp4" | Ffmpeg { Ffmpeg_format.format = Some "wav" } -> "wav" | _ -> raise Not_found (** Mime types *) let mime = function | WAV _ -> "audio/wav" | AVI _ -> "video/avi" | Ogg _ -> "application/ogg" | MP3 _ -> "audio/mpeg" | Shine _ -> "audio/mpeg" | Flac _ -> "audio/flex" | FdkAacEnc _ -> "audio/aac" | Ffmpeg { Ffmpeg_format.format = Some "ogg" } -> "application/ogg" | Ffmpeg { Ffmpeg_format.format = Some "opus" } -> "application/ogg" | Ffmpeg { Ffmpeg_format.format = Some "mp3" } -> "audio/mpeg" | Ffmpeg { Ffmpeg_format.format = Some "matroska" } -> "video/x-matroska" | Ffmpeg { Ffmpeg_format.format = Some "ac3" } -> "audio/ac3" | Ffmpeg { Ffmpeg_format.format = Some "eac3" } -> "audio/eac3" | Ffmpeg { Ffmpeg_format.format = Some "adts"; streams = [(_, `Encode { Ffmpeg_format.codec = Some "aac" })]; } -> "audio/aac" | Ffmpeg { Ffmpeg_format.format = Some "mp4" } -> "video/mp4" | Ffmpeg { Ffmpeg_format.format = Some "wav" } -> "audio/wav" | _ -> "application/octet-stream" (** Bitrate estimation in kbits per second. *) let bitrate = function | MP3 w -> Mp3_format.bitrate w | Shine w -> Shine_format.bitrate w | FdkAacEnc w -> Fdkaac_format.bitrate w (* For Ffmpeg we rely on hls.bitrate *) | _ -> raise Not_found (** Encoders that can output to a file. *) let file_output = function Ffmpeg _ -> true | _ -> false let with_file_output ?(append = false) encoder file = match encoder with | Ffmpeg params -> let opts = Hashtbl.copy params.Ffmpeg_format.opts in Hashtbl.replace opts "truncate" (`Int (if append then 0 else 1)); Ffmpeg { params with Ffmpeg_format.output = `Url (Printf.sprintf "file:%s" file); opts; } | _ -> failwith "No file output!" (** Encoders that can output to a arbitrary url. *) let url_output = function Ffmpeg _ -> true | _ -> false let with_url_output encoder file = match encoder with | Ffmpeg opts -> Ffmpeg { opts with Ffmpeg_format.output = `Url file } | _ -> failwith "No file output!" (** An encoder, once initialized, is something that consumes frames, insert metadata and that you eventually close (triggers flushing). Insert metadata is really meant for inline metadata, i.e. in most cases, stream sources. Otherwise, metadata are passed when creating the encoder. For instance, the mp3 encoder may accept metadata initially and write them as id3 tags but does not support inline metadata. Also, the ogg encoder supports inline metadata but restarts its stream. This is ok, though, because the ogg container/streams is meant to be sequentialized but not the mp3 format. header contains data that should be sent first to streaming client. *) type split_result = [ (* Returns (flushed, first_bytes_for_next_segment) *) `Ok of Strings.t * Strings.t | `Nope of Strings.t ] (* Raised by [init_encode] if more data is needed. *) exception Not_enough_data type hls = { (* Returns true if id3 is enabled. *) init : ?id3_enabled:bool -> ?id3_version:int -> unit -> bool; (* Returns (init_segment, first_bytes) *) init_encode : Frame.t -> Strings.t option * Strings.t; split_encode : Frame.t -> split_result; codec_attrs : unit -> string option; insert_id3 : frame_position:int -> sample_position:int -> (string * string) list -> string option; bitrate : unit -> int option; (* width x height *) video_size : unit -> (int * int) option; } let dummy_hls encode = { init = (fun ?id3_enabled:_ ?id3_version:_ _ -> false); init_encode = (fun f -> (None, encode f)); split_encode = (fun f -> `Ok (Strings.empty, encode f)); codec_attrs = (fun () -> None); insert_id3 = (fun ~frame_position:_ ~sample_position:_ _ -> None); bitrate = (fun () -> None); video_size = (fun () -> None); } type encoder = { insert_metadata : Frame.Metadata.Export.t -> unit; header : unit -> Strings.t; hls : hls; encode : Frame.t -> Strings.t; stop : unit -> Strings.t; } type factory = ?hls:bool -> pos:Pos.t option -> string -> Frame.Metadata.Export.t -> encoder (** A plugin might or might not accept a given format. If it accepts it, it gives a function creating suitable encoders. *) type plugin = format -> factory option let plug : plugin Plug.t = Plug.create ~doc:"Methods to encode streams." "stream encoding formats" exception Found of factory (** Return the first available encoder factory for that format. *) let get_factory fmt = try Plug.iter plug (fun _ f -> match f fmt with Some factory -> raise (Found factory) | None -> ()); raise Not_found with Found factory -> fun ?hls ~pos name m -> let { insert_metadata; hls; encode; stop; header } = factory ?hls ~pos name m in (* Protect all functions with a mutex. *) let m = Mutex.create () in let insert_metadata = Mutex_utils.mutexify m insert_metadata in let header = Mutex_utils.mutexify m header in let { init; init_encode; split_encode; codec_attrs; insert_id3; bitrate; video_size; } = hls in let init ?id3_enabled ?id3_version () = Mutex_utils.mutexify m (fun () -> init ?id3_enabled ?id3_version ()) () in let init_encode frame = Mutex_utils.mutexify m (fun () -> init_encode frame) () in let split_encode frame = Mutex_utils.mutexify m (fun () -> split_encode frame) () in let codec_attrs = Mutex_utils.mutexify m codec_attrs in let insert_id3 ~frame_position ~sample_position meta = Mutex_utils.mutexify m (fun () -> insert_id3 ~frame_position ~sample_position meta) () in let bitrate = Mutex_utils.mutexify m bitrate in let video_size = Mutex_utils.mutexify m video_size in let hls = { init; init_encode; split_encode; codec_attrs; insert_id3; bitrate; video_size; } in let encode frame = Mutex_utils.mutexify m (fun () -> encode frame) () in let stop = Mutex_utils.mutexify m stop in { insert_metadata; hls; encode; stop; header } liquidsoap-2.3.2/src/core/encoder/encoder.mli000066400000000000000000000105001477303350200211500ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Common infrastructure for encoding streams *) type format = | WAV of Wav_format.t | AVI of Avi_format.t | NDI of Ndi_format.t | Ogg of Ogg_format.t | MP3 of Mp3_format.t | Shine of Shine_format.t | Flac of Flac_format.t | Ffmpeg of Ffmpeg_format.t | FdkAacEnc of Fdkaac_format.t | External of External_encoder_format.t val audio_type : pcm_kind:Content.kind -> int -> Type.t Frame.Fields.t val video_format : unit -> Content.format val video_type : unit -> Type.t Frame.Fields.t val audio_video_type : pcm_kind:Content.kind -> int -> Type.t Frame.Fields.t val type_of_format : format -> Type.t Frame.Fields.t val string_of_format : format -> string (** ISO Base Media File Format, see RFC 6381 section 3.3. *) val iso_base_file_media_file_format : format -> string (** Proposed extension for files. *) val extension : format -> string (** Mime types *) val mime : format -> string (** Video size when available. *) val video_size : format -> (int * int) option (** Bitrate estimation in bits per second. *) val bitrate : format -> int (** Encoders that can output to a file. *) val file_output : format -> bool val with_file_output : ?append:bool -> format -> string -> format (** Encoders that can output to a arbitrary url. *) val url_output : format -> bool val with_url_output : format -> string -> format (** An encoder, once initialized, is something that consumes frames, insert metadata and that you eventually close (triggers flushing). Insert metadata is really meant for inline metadata, i.e. in most cases, stream sources. Otherwise, metadata are passed when creating the encoder. For instance, the mp3 encoder may accept metadata initially and write them as id3 tags but does not support inline metadata. Also, the ogg encoder supports inline metadata but restarts its stream. This is ok, though, because the ogg container/streams is meant to be sequentialized but not the mp3 format. header contains data that should be sent first to streaming client. *) type split_result = [ (* Returns (flushed, first_bytes_for_next_segment) *) `Ok of Strings.t * Strings.t | `Nope of Strings.t ] (* Raised by [init_encode] if more data is needed. *) exception Not_enough_data type hls = { (* Returns true if id3 is enabled. *) init : ?id3_enabled:bool -> ?id3_version:int -> unit -> bool; (* Returns (init_segment, first_bytes) *) init_encode : Frame.t -> Strings.t option * Strings.t; split_encode : Frame.t -> split_result; codec_attrs : unit -> string option; insert_id3 : frame_position:int -> sample_position:int -> (string * string) list -> string option; bitrate : unit -> int option; (* width x height *) video_size : unit -> (int * int) option; } val dummy_hls : (Frame.t -> Strings.t) -> hls type encoder = { insert_metadata : Frame.Metadata.Export.t -> unit; header : unit -> Strings.t; hls : hls; encode : Frame.t -> Strings.t; stop : unit -> Strings.t; } type factory = ?hls:bool -> pos:Pos.t option -> string -> Frame.Metadata.Export.t -> encoder (** A plugin might or might not accept a given format. If it accepts it, it gives a function creating suitable encoders. *) type plugin = format -> factory option val plug : plugin Plug.t (** Return the first available encoder factory for that format. *) val get_factory : format -> factory liquidsoap-2.3.2/src/core/encoder/encoder_formats.ml000066400000000000000000000040371477303350200225420ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let conf = Dtools.Conf.void ~p:(Configure.conf#plug "encoder") "Encoder settings" ~comments:["Settings for the encoder"] let conf_meta = Dtools.Conf.void ~p:(conf#plug "metadata") "Metadata settings" ~comments:["Settings for the encoded metadata."] let conf_meta_cover = Dtools.Conf.list ~p:(conf_meta#plug "cover") "Metadata labels that represent coverart" ~d:["pic"; "apic"; "metadata_block_picture"; "cover"] (** The list of metadata fields that should be exported when encoding. *) let conf_export_metadata = Dtools.Conf.list ~p:(conf_meta#plug "export") "Exported metadata" ~d: [ "artist"; "title"; "album"; "genre"; "date"; "tracknumber"; "comment"; "track"; "year"; "dj"; "next"; "apic"; "pic"; "metadata_url"; "metadata_block_picture"; "coverart"; ] ~comments:["The list of labels of exported metadata."] let string_of_stereo s = if s then "stereo" else "mono" liquidsoap-2.3.2/src/core/encoder/encoder_utils.ml000066400000000000000000000050721477303350200222270ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (* See: https://datatracker.ietf.org/doc/html/rfc8216#section-3.4 *) let render_mpeg2_timestamp = let mpeg2_timestamp_unit = 90000. in let frame_len = Lazy.from_fun (fun () -> Int64.of_float (Frame.seconds_of_main (Lazy.force Frame.size) *. mpeg2_timestamp_unit)) in fun ~frame_position ~sample_position () -> let buf = Buffer.create 10 in let frame_position = Int64.mul (Lazy.force frame_len) (Int64.of_int frame_position) in let sample_position = Int64.of_float (Frame.seconds_of_main sample_position *. mpeg2_timestamp_unit) in let position = Int64.add frame_position sample_position in let position = Int64.unsigned_rem position 0x1ffffffffL in Buffer.add_int64_be buf position; Buffer.contents buf let mk_hls_id3 ?(id3_version = 3) ~frame_position ~sample_position m = let timestamp = Printf.sprintf "com.apple.streaming.transportStreamTimestamp\000%s" (render_mpeg2_timestamp ~frame_position ~sample_position ()) in let m = ("PRIV", timestamp) :: m in Utils.id3v2_of_metadata ~version:id3_version m let mk_id3_hls ~pos encoder = let id3_version_ref = ref None in let init ?id3_enabled ?id3_version () = id3_version_ref := id3_version; if id3_enabled = Some false then Lang_encoder.raise_error ~pos "Format requires ID3 metadata!"; true in let insert_id3 ~frame_position ~sample_position m = Some (mk_hls_id3 ?id3_version:!id3_version_ref ~frame_position ~sample_position m) in Encoder.{ (dummy_hls encoder) with init; insert_id3 } liquidsoap-2.3.2/src/core/encoder/encoders/000077500000000000000000000000001477303350200206345ustar00rootroot00000000000000liquidsoap-2.3.2/src/core/encoder/encoders/avi_encoder.ml000066400000000000000000000105231477303350200234450ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (** AVI encoder *) open Avi_format let log = Log.make ["avi"; "encoder"] let encode_frame ~channels ~samplerate ~width ~height ~converter frame = let target_width = width in let target_height = height in let ratio = float samplerate /. float (Lazy.force Frame.audio_rate) in let audio = let alen = AFrame.position frame in let pcm = AFrame.pcm frame in let pcm, astart, alen = Audio_converter.Samplerate.resample converter ratio pcm 0 alen in let data = Bytes.create (2 * channels * alen) in Audio.S16LE.of_audio pcm astart data 0 alen; Avi.audio_chunk (Bytes.unsafe_to_string data) in let video = let vbuf = VFrame.data frame in let data = Strings.Mutable.empty () in let scaler = Video_converter.scaler () in List.iter (fun (_, img) -> let img = img |> Video.Canvas.Image.resize ~scaler ~proportional:true target_width target_height |> Video.Canvas.Image.render ~transparent:false in let width = Image.YUV420.width img in let height = Image.YUV420.height img in if width <> target_width || height <> target_height then failwith (Printf.sprintf "Resizing is not yet supported by AVI encoder got %dx%d instead \ of %dx%d" width height target_width target_height); let y, u, v = Image.YUV420.data img in let y = Image.Data.to_string y in let u = Image.Data.to_string u in let v = Image.Data.to_string v in let y_stride = Image.YUV420.y_stride img in let uv_stride = Image.YUV420.uv_stride img in if y_stride = width then Strings.Mutable.add data y else for j = 0 to height - 1 do Strings.Mutable.add_substring data y (j * y_stride) width done; if uv_stride = width / 2 then ( Strings.Mutable.add data u; Strings.Mutable.add data v) else ( for j = 0 to (height / 2) - 1 do Strings.Mutable.add_substring data u (j * uv_stride) (width / 2) done; for j = 0 to (height / 2) - 1 do Strings.Mutable.add_substring data v (j * uv_stride) (width / 2) done)) vbuf.Content.Video.data; Avi.video_chunk_strings data in Strings.add video audio let encoder avi = let channels = avi.channels in let samplerate = Lazy.force avi.samplerate in let converter = Audio_converter.Samplerate.create channels in let width = Lazy.force avi.width in let height = Lazy.force avi.height in log#info "Encoding at %dx%d, %d channels, %d Hz.%!" width height channels samplerate; (* TODO: use duration *) let header = Avi.header ~width ~height ~channels ~samplerate () in let need_header = ref true in let encode frame = let ans = encode_frame ~channels ~samplerate ~width ~height ~converter frame in if !need_header then ( need_header := false; Strings.dda header ans) else ans in { Encoder.insert_metadata = (fun _ -> ()); hls = Encoder.dummy_hls encode; encode; header = (fun () -> Strings.of_string header); stop = (fun () -> Strings.empty); } let () = Plug.register Encoder.plug "avi" ~doc:"Native avi encoder." (function | Encoder.AVI avi -> Some (fun ?hls:_ ~pos:_ _ _ -> encoder avi) | _ -> None) liquidsoap-2.3.2/src/core/encoder/encoders/external_encoder.ml000066400000000000000000000133231477303350200245110ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (** External encoder *) open External_encoder_format let encoder id ext = let log = Log.make [id] in let is_metadata_restart = ref false in let is_stop = ref false in let buf = Strings.Mutable.empty () in let bytes = Bytes.create Utils.pagesize in let mutex = Mutex.create () in let condition = Condition.create () in let restart_decision = Mutex_utils.mutexify mutex (fun () -> let decision = match (!is_metadata_restart, !is_stop) with | _, true -> false | true, false -> true | false, false -> ext.restart_on_crash in is_metadata_restart := false; decision) in let header = if ext.video <> None then ( let width, height = Option.get ext.video in let width = Lazy.force width in let height = Lazy.force height in Avi.header ~width ~height ~channels:ext.channels ~samplerate:(Lazy.force ext.samplerate) ()) else if ext.header then Wav_aiff.wav_header ~channels:ext.channels ~sample_rate:(Lazy.force ext.samplerate) ~sample_size:16 () else "" in let on_stderr puller = let len = puller bytes 0 Utils.pagesize in log#debug "stderr: %s" (Bytes.unsafe_to_string (Bytes.sub bytes 0 len)); `Continue in let on_start pusher = Process_handler.really_write (Bytes.of_string header) pusher; `Continue in let on_stop = function | `Status s -> begin match s with | Unix.WEXITED 0 -> () | Unix.WEXITED c -> log#important "Process exited with code %d" c | Unix.WSIGNALED s -> log#important "Process was killed by signal %d" s | Unix.WSTOPPED s -> log#important "Process was stopped by signal %d" s end; restart_decision () | `Exception e -> log#important "Error: %s" (Printexc.to_string e); restart_decision () in (* Signal end of process instead of always waiting for zero read. See: https://lkml.indiana.edu/hypermail/linux/kernel/0106.0/0768.html *) let on_stop v = Condition.signal condition; on_stop v in let log s = log#important "%s" s in let on_stdout = Mutex_utils.mutexify mutex (fun puller -> begin let len = puller bytes 0 Utils.pagesize in match len with | 0 when !is_stop -> Condition.signal condition | _ -> Strings.Mutable.add_subbytes buf bytes 0 len end; `Continue) in let process = Process_handler.run ~on_start ~on_stop ~on_stdout ~on_stderr ~log ext.process in let insert_metadata = Mutex_utils.mutexify mutex (fun _ -> if ext.restart = Metadata then ( is_metadata_restart := true; Process_handler.stop process)) in let converter = Audio_converter.Samplerate.create ext.channels in let ratio = float (Lazy.force ext.samplerate) /. float (Frame.audio_of_seconds 1.) in let encode frame = let channels = ext.channels in let samplerate = Lazy.force ext.samplerate in let sbuf = if ext.video <> None then ( let width, height = Option.get ext.video in let width = Lazy.force width in let height = Lazy.force height in Avi_encoder.encode_frame ~channels ~samplerate ~converter ~width ~height frame) else ( let b = AFrame.pcm frame in let len = AFrame.position frame in (* Resample if needed. *) let b, start, len = Audio_converter.Samplerate.resample converter ratio b 0 len in let slen = 2 * len * Array.length b in let sbuf = Bytes.create slen in Audio.S16LE.of_audio b start sbuf 0 len; Strings.unsafe_of_bytes sbuf) in Mutex_utils.mutexify mutex (fun () -> try Process_handler.on_stdin process (fun push -> Strings.iter (fun s offset length -> Process_handler.really_write ~offset ~length (Bytes.unsafe_of_string s) push) sbuf) with | Process_handler.Finished when ext.restart_on_crash || !is_metadata_restart -> ()) (); Strings.Mutable.flush buf in let stop = Mutex_utils.mutexify mutex (fun () -> is_stop := true; Process_handler.stop process; Condition.wait condition mutex; Strings.Mutable.flush buf) in { Encoder.insert_metadata; header = (fun () -> Strings.empty); hls = Encoder.dummy_hls encode; encode; stop; } let () = Plug.register Encoder.plug "external" ~doc:"Encode using external programs." (function | Encoder.External m -> Some (fun ?hls:_ ~pos:_ s _ -> encoder s m) | _ -> None) liquidsoap-2.3.2/src/core/encoder/encoders/fdkaac_encoder.ml000066400000000000000000000103141477303350200240750ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (** FDK-AAC encoder *) let create_encoder ~pos params = let encoder = Fdkaac.Encoder.create params.Fdkaac_format.channels in let bandwidth = match params.Fdkaac_format.bandwidth with `Auto -> 0 | `Fixed b -> b in let params = [ `Aot params.Fdkaac_format.aot; `Bandwidth bandwidth; `Samplerate (Lazy.force params.Fdkaac_format.samplerate); `Transmux params.Fdkaac_format.transmux; `Afterburner params.Fdkaac_format.afterburner; ] @ (if params.Fdkaac_format.aot = `Mpeg_4 `AAC_ELD then [`Sbr_mode params.Fdkaac_format.sbr_mode] else []) @ match params.Fdkaac_format.bitrate_mode with | `Variable vbr -> [`Bitrate_mode (`Variable vbr)] | `Constant -> [`Bitrate (params.Fdkaac_format.bitrate * 1000)] in let string_of_param = function | `Aot _ -> "aot" | `Afterburner _ -> "afterburner" | `Bandwidth _ -> "bandwidth" | `Bitrate _ -> "bitrate" | `Bitrate_mode _ -> "bitrate mode" | `Granule_length _ -> "granule length" | `Samplerate _ -> "samplerate" | `Sbr_mode _ -> "sbr mode" | `Transmux _ -> "transmux" in let set p = try Fdkaac.Encoder.set encoder p with | Fdkaac.Encoder.Invalid_config -> Lang_encoder.raise_error ~pos ("Invalid configuration: " ^ string_of_param p) | Fdkaac.Encoder.Unsupported_parameter -> Lang_encoder.raise_error ~pos ("Unsupported parameter: " ^ string_of_param p) | e -> raise e in List.iter set params; encoder let encoder ~pos aac = let enc = create_encoder ~pos aac in let channels = aac.Fdkaac_format.channels in let samplerate = Lazy.force aac.Fdkaac_format.samplerate in let samplerate_converter = Audio_converter.Samplerate.create channels in let src_freq = float (Frame.audio_of_seconds 1.) in let dst_freq = float samplerate in let n = Utils.pagesize in let buf = Strings.Mutable.empty () in let encode frame = let b = AFrame.pcm frame in let len = AFrame.position frame in let b, start, len = Audio_converter.Samplerate.resample samplerate_converter (dst_freq /. src_freq) b 0 len in let encoded = Strings.Mutable.empty () in Strings.Mutable.add buf (Audio.S16LE.make b start len); while Strings.Mutable.length buf >= n do let data = Bytes.create n in Strings.blit (Strings.sub (Strings.Mutable.to_strings buf) 0 n) data 0; let data = Bytes.unsafe_to_string data in Strings.Mutable.drop buf n; Strings.Mutable.add encoded (Fdkaac.Encoder.encode enc data 0 n) done; Strings.Mutable.to_strings encoded in let stop () = let rem = Strings.Mutable.map (fun rem ofs len -> let rem = Fdkaac.Encoder.encode enc rem ofs len in (rem, 0, String.length rem)) buf in Strings.Mutable.add rem (Fdkaac.Encoder.flush enc); Strings.Mutable.to_strings rem in { Encoder.insert_metadata = (fun _ -> ()); header = (fun () -> Strings.empty); hls = Encoder_utils.mk_id3_hls ~pos encode; encode; stop; } let () = Plug.register Encoder.plug "fdkaac" ~doc:"" (function | Encoder.FdkAacEnc m -> Some (fun ?hls:_ ~pos _ _ -> encoder ~pos m) | _ -> None) liquidsoap-2.3.2/src/core/encoder/encoders/ffmpeg_copy_encoder.ml000066400000000000000000000151771477303350200251760ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** FFMPEG copy encoder *) open Avcodec open Ffmpeg_encoder_common let log = Log.make ["ffmpeg"; "copy"; "encoder"] let mk_stream_copy ~get_stream ~on_keyframe ~remove_stream ~keyframe_opt ~field output = let stream = ref None in let video_size_ref = ref None in let codec_attr = ref None in let bitrate = ref None in let main_time_base = Ffmpeg_utils.liq_main_ticks_time_base () in (* This should be the same for all streams but it is lazily set when the first stream is created. *) let intra_only = ref true in let initialized_stream = Av.new_uninitialized_stream_copy output in let mk_stream frame = let { Content.Video.params } = Ffmpeg_copy_content.get_data (Frame.get frame field) in let mk_stream params = let s = Av.initialize_stream_copy ~params initialized_stream in codec_attr := Av.codec_attr s; bitrate := Av.bitrate s; (match Avcodec.descriptor params with | None -> () | Some { properties } -> intra_only := List.mem `Intra_only properties); s in match Option.get params with | `Audio params -> stream := Some (`Audio (mk_stream params)) | `Video { Ffmpeg_copy_content.avg_frame_rate; params } -> let width = Avcodec.Video.get_width params in let height = Avcodec.Video.get_height params in video_size_ref := Some (width, height); let s = mk_stream params in Av.set_avg_frame_rate s avg_frame_rate; stream := Some (`Video (mk_stream params)) in let codec_attr () = !codec_attr in let bitrate () = !bitrate in let video_size () = !video_size_ref in let to_main_time_base ~time_base v = Ffmpeg_utils.convert_time_base ~src:time_base ~dst:main_time_base v in let current_stream = ref (get_stream ~last_start:Int64.min_int ~ready:false 0L) in let stream_started = ref false in let waiting_for_keyframe = ref false in let last_dts = ref None in let last_position = ref 0L in (* [true] if we should process new packets for this stream *) let check_stream ~packet stream_idx = if !current_stream.idx <> stream_idx then ( waiting_for_keyframe := keyframe_opt = `Wait_for_keyframe && not !intra_only; remove_stream !current_stream; last_position := !current_stream.position; (* Mark the stream as ready if it is not waiting for keyframes. *) let stream = get_stream ~last_start:Int64.min_int ~ready:(not !waiting_for_keyframe) stream_idx in stream.ready <- not !waiting_for_keyframe; current_stream := stream; stream_started := false); let is_keyframe = List.mem `Keyframe Avcodec.Packet.(get_flags packet) in if !waiting_for_keyframe then is_keyframe else !stream_started || !current_stream.ready in let begin_stream ~dts idx = let offset = Option.value ~default:0L dts in let last_start = Int64.sub !last_position offset in (* Mark the stream as ready if it was waiting for keyframes. *) current_stream := get_stream ~last_start ~ready:!waiting_for_keyframe idx; stream_started := true; waiting_for_keyframe := false in let adjust_ts = Option.map (fun ts -> Int64.add ts !current_stream.last_start) in let check_dts dts = let offset = if !stream_started then !current_stream.last_start else Int64.sub !last_position (Option.value ~default:0L dts) in match (dts, !last_dts) with | None, _ -> log#important "Packet has no dts!"; true | Some d, Some d' when Int64.add offset d <= d' -> log#important "Dropping packet with non-monotomic dts!"; false | _ -> true in let push ~time_base ~stream ~idx packet = let to_main = to_main_time_base ~time_base in let pts = Option.map to_main (Avcodec.Packet.get_pts packet) in let dts = Option.map to_main (Avcodec.Packet.get_dts packet) in let duration = Option.map to_main (Avcodec.Packet.get_duration packet) in if check_dts dts then ( if not !stream_started then begin_stream ~dts idx; let pts = adjust_ts pts in let dts = adjust_ts dts in (match dts with | None -> () | Some dts -> !current_stream.position <- max (Int64.add (Option.value ~default:1L duration) dts) !current_stream.position); last_dts := dts; (match ( on_keyframe, List.mem `Keyframe Avcodec.Packet.(get_flags packet) || !intra_only ) with | Some on_keyframe, true -> if not !intra_only then Av.flush output; on_keyframe () | _ -> ()); let packet = Avcodec.Packet.dup packet in Packet.set_pts packet pts; Packet.set_dts packet dts; Packet.set_duration packet duration; Av.write_packet stream main_time_base packet) in let process ~packet ~stream_idx ~time_base stream = if check_stream ~packet stream_idx then push ~time_base ~stream ~idx:stream_idx packet in let encode frame = let content = Frame.get frame field in let data = (Ffmpeg_copy_content.get_data content).Content.Video.data in List.iter (fun (_, { Ffmpeg_copy_content.packet; time_base; stream_idx }) -> match (packet, !stream) with | `Audio packet, Some (`Audio stream) -> process ~packet ~stream_idx ~time_base stream | `Video packet, Some (`Video stream) -> process ~packet ~stream_idx ~time_base stream | _ -> assert false) data in { Ffmpeg_encoder_common.mk_stream; encode; flush = (fun () -> ()); codec_attr; bitrate; video_size; } liquidsoap-2.3.2/src/core/encoder/encoders/ffmpeg_encoder.ml000066400000000000000000000127351477303350200241410ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** FFMPEG encoder *) open Ffmpeg_encoder_common let replace_default opts name default = Hashtbl.replace opts name (Option.value ~default (Hashtbl.find_opt opts name)) let () = Plug.register Encoder.plug "ffmpeg" ~doc:"" (function | Encoder.Ffmpeg ffmpeg -> Some (fun ?(hls = false) ~pos _ -> (* Inject hls params. *) let ffmpeg = if hls then ( let opts = Hashtbl.copy ffmpeg.Ffmpeg_format.opts in replace_default opts "flush_packets" (`Int 1); (match ffmpeg.Ffmpeg_format.format with | Some "mp4" -> replace_default opts "movflags" (`String "+dash+skip_sidx+skip_trailer+frag_custom"); replace_default opts "frag_duration" (`Int 10) | _ -> ()); let streams = List.map (function | ( lbl, `Encode ({ Ffmpeg_format.opts } as stream : Ffmpeg_format.encoded_stream) ) -> let opts = Hashtbl.copy opts in replace_default opts "flags" (`String "+global_header"); (lbl, `Encode { stream with Ffmpeg_format.opts }) | s -> s) ffmpeg.Ffmpeg_format.streams in { ffmpeg with Ffmpeg_format.streams; opts }) else ffmpeg in let copy_count = List.fold_left (fun cur (_, c) -> match c with `Copy _ -> cur + 1 | _ -> cur) 0 ffmpeg.streams in let get_stream, remove_stream = mk_stream_store copy_count in let keyframes = List.map (fun (field, _) -> (field, Atomic.make false)) ffmpeg.streams in let on_keyframe = Atomic.make (fun () -> ()) in let on_stream_keyframe field = if hls then Some (fun () -> let on_keyframe = Atomic.get on_keyframe in on_keyframe (); Atomic.set (List.assoc field keyframes) true) else None in let mk_streams output = List.fold_left (fun streams (field, stream) -> match stream with | `Drop -> streams | `Copy keyframe_opt -> Frame.Fields.add field (Ffmpeg_copy_encoder.mk_stream_copy ~on_keyframe:(on_stream_keyframe field) ~get_stream ~remove_stream ~keyframe_opt ~field output) streams | `Encode Ffmpeg_format.{ codec = None } -> Lang_encoder.raise_error ~pos (Printf.sprintf "Codec unspecified for %%ffmpeg stream %%%s!" (Frame.Fields.string_of_field field)) | `Encode Ffmpeg_format. { mode; codec = Some codec; options = `Audio params; opts = options; } -> Frame.Fields.add field (Ffmpeg_internal_encoder.mk_audio ~pos ~on_keyframe:(on_stream_keyframe field) ~mode ~params ~options ~codec ~field output) streams | `Encode Ffmpeg_format. { mode; codec = Some codec; options = `Video params; opts = options; } -> Frame.Fields.add field (Ffmpeg_internal_encoder.mk_video ~pos ~on_keyframe:(on_stream_keyframe field) ~mode ~params ~options ~codec ~field output) streams) Frame.Fields.empty ffmpeg.streams in encoder ~pos ~on_keyframe ~keyframes ~mk_streams ffmpeg) | _ -> None) liquidsoap-2.3.2/src/core/encoder/encoders/ffmpeg_encoder_common.ml000066400000000000000000000273761477303350200255200ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** FFMPEG encoder *) let log = Ffmpeg_utils.log type encoder = { mk_stream : Frame.t -> unit; encode : Frame.t -> unit; flush : unit -> unit; codec_attr : unit -> string option; bitrate : unit -> int option; video_size : unit -> (int * int) option; } type handler = { output : Avutil.output Avutil.container; streams : encoder Frame.Fields.t; format : (Avutil.output, Avutil.media_type) Avutil.format option; mutable insert_id3 : frame_position:int -> sample_position:int -> (string * string) list -> string option; started : bool Atomic.t; } type stream_data = { idx : Int64.t; mutable last_start : Int64.t; mutable position : Int64.t; mutable ready_count : int; mutable ready : bool; } module Stream = Weak.Make (struct type t = stream_data let equal x y = x.idx = y.idx let hash x = Int64.to_int x.idx end) (* We lazily store last_start when concatenating streams. The idea is to always have the greatest possible offset, for instance if we concatenate: a: ------>als v: ----------->vls the last start should be vls so we end up with: a: ------> --------> v: ----------->--------> Also, if one or more stream is waiting for a key frame, we make the whole set of streams wait for the first key frame. This makes sure that we avoid e.g. starting an audio track with no keyframe before the video has started. *) let mk_stream_store total_count = let store = Stream.create 1 in let add ~last_start ~ready idx = let data = Stream.merge store { idx; ready_count = 0; last_start; position = 0L; ready = false } in if ready then data.ready_count <- data.ready_count + 1; if data.ready_count = total_count then data.ready <- true; if data.last_start < last_start then data.last_start <- last_start; data in let remove data = let data = Stream.merge store data in if data.ready_count = 1 then Stream.remove store data else data.ready_count <- data.ready_count - 1 in (add, remove) let mk_format ffmpeg = match (ffmpeg.Ffmpeg_format.format, ffmpeg.Ffmpeg_format.output) with | short_name, `Url filename -> Av.Format.guess_output_format ~filename ?short_name () | Some short_name, _ -> Av.Format.guess_output_format ~short_name () | _ -> None let encode ~encoder frame = if not (Atomic.exchange encoder.started true) then Frame.Fields.iter (fun _ { mk_stream } -> mk_stream frame) encoder.streams; Frame.Fields.iter (fun _ { encode } -> encode frame) encoder.streams (* Convert ffmpeg-specific options. *) let convert_options opts = let convert name fn = match Hashtbl.find_opt opts name with | None -> () | Some v -> Hashtbl.replace opts name (fn v) in convert "sample_fmt" (function | `String fmt -> `Int Avutil.Sample_format.(get_id (find fmt)) | _ -> assert false); convert "channel_layout" (function | `String layout -> `String Avutil.Channel_layout.(get_description (find layout)) | _ -> assert false) let encoder ~pos ~on_keyframe ~keyframes ~mk_streams ffmpeg meta = let buf = Strings.Mutable.empty () in let make () = let options = Hashtbl.copy ffmpeg.Ffmpeg_format.opts in convert_options options; let write str ofs len = Strings.Mutable.add_subbytes buf str ofs len; len in let format = mk_format ffmpeg in let interleaved = match ffmpeg.interleaved with | `Default -> 1 < List.length ffmpeg.streams | `True -> true | `False -> false in let output = match ffmpeg.Ffmpeg_format.output with | `Stream -> if format = None then ( match ffmpeg.Ffmpeg_format.format with | None -> Lang_encoder.raise_error ~pos "Format is required!" | Some fmt -> Lang_encoder.raise_error ~pos (Printf.sprintf "No ffmpeg format could be found for format=%S" fmt)); Av.open_output_stream ~interleaved ~opts:options write (Option.get format) | `Url url -> Av.open_output ?format ~interleaved ~opts:options url in let streams = mk_streams output in if Hashtbl.length options > 0 then Lang_encoder.raise_error ~pos (Printf.sprintf "Unrecognized options: %s" (Ffmpeg_format.string_of_options options)); { format; output; streams; insert_id3 = (fun ~frame_position:_ ~sample_position:_ _ -> None); started = Atomic.make false; } in let encoder = Atomic.make (make ()) in let codec_attrs () = let encoder = Atomic.get encoder in match Frame.Fields.fold (fun _ c cur -> match c.codec_attr () with Some c -> c :: cur | None -> cur) encoder.streams [] with | [] -> None | l -> Some (String.concat "," l) in let bitrate () = let encoder = Atomic.get encoder in let ( + ) v v' = match (v, v') with | None, None -> None | None, Some v | Some v, None -> Some v | Some v, Some v' -> Some (v + v') in Frame.Fields.fold (fun _ c cur -> cur + c.bitrate ()) encoder.streams None in let video_size () = let encoder = Atomic.get encoder in match Frame.Fields.fold (fun _ stream cur -> match stream.video_size () with | Some (width, height) -> (width, height) :: cur | _ -> cur) encoder.streams [] with | (width, height) :: [] -> Some (width, height) | _ -> None in let sent = Frame.Fields.map (fun _ -> ref false) (Atomic.get encoder).streams in let init ?id3_enabled ?id3_version () = let encoder = Atomic.get encoder in match Option.map Av.Format.get_output_name encoder.format with | Some "mpegts" -> if id3_enabled <> Some false then ( let id3_version = Option.value ~default:3 id3_version in let time_base = Ffmpeg_utils.liq_audio_sample_time_base () in let stream = Av.new_data_stream ~time_base ~codec:`Timed_id3 encoder.output in encoder.insert_id3 <- (fun ~frame_position ~sample_position m -> if Atomic.get encoder.started then ( let tag = Utils.id3v2_of_metadata ~version:id3_version m in let packet = Avcodec.Packet.create tag in let position = Int64.of_int (Frame.audio_of_main ((frame_position * Lazy.force Frame.size) + sample_position)) in Avcodec.Packet.set_pts packet (Some position); Avcodec.Packet.set_dts packet (Some position); Av.write_packet stream time_base packet); None); true) else false | Some "adts" | Some "mp3" | Some "ac3" | Some "eac3" -> if id3_enabled = Some false then Lang_encoder.raise_error ~pos "Format requires ID3 metadata!"; encoder.insert_id3 <- (fun ~frame_position ~sample_position m -> Some (Encoder_utils.mk_hls_id3 ?id3_version ~frame_position ~sample_position m)); true | Some _ when id3_enabled = Some true -> Lang_encoder.raise_error ~pos "Format does not support HLS ID3 metadata!" | Some _ -> false | None -> Lang_encoder.raise_error ~pos "Format is required!" in let insert_id3 ~frame_position ~sample_position m = let encoder = Atomic.get encoder in encoder.insert_id3 ~frame_position ~sample_position m in let init_encode frame = let encoder = Atomic.get encoder in match ffmpeg.Ffmpeg_format.format with | Some "mp4" -> encode ~encoder frame; let frame = Frame.Fields.filter (fun _ c -> not (Content_timed.Track_marks.is_data c || Content_timed.Metadata.is_data c)) frame in Frame.Fields.iter (fun field c -> match Frame.Fields.find_opt field sent with | None -> () | Some sent -> sent := !sent || not (Content.is_empty c)) frame; if Frame.Fields.exists (fun _ c -> not !c) sent then raise Encoder.Not_enough_data; Av.flush encoder.output; let init = Strings.Mutable.flush buf in (Some init, Strings.empty) | Some "webm" -> Av.flush encoder.output; let init = Strings.Mutable.flush buf in encode ~encoder frame; (Some init, Strings.Mutable.flush buf) | _ -> encode ~encoder frame; (None, Strings.Mutable.flush buf) in let split_encode frame = let encoder = Atomic.get encoder in let can_split () = List.for_all (fun (_, keyframe) -> Atomic.get keyframe) keyframes in let flushed = if can_split () then Atomic.make (Some (Strings.Mutable.flush buf)) else ( let flushed = Atomic.make None in Atomic.set on_keyframe (fun () -> match (can_split (), Atomic.get flushed) with | true, None -> Atomic.set flushed (Some (Strings.Mutable.flush buf)) | _ -> ()); flushed) in Fun.protect (fun () -> encode ~encoder frame) ~finally:(fun () -> Atomic.set on_keyframe (fun () -> ())); let encoded = Strings.Mutable.flush buf in match Atomic.get flushed with | Some flushed -> List.iter (fun (_, keyframe) -> Atomic.set keyframe false) keyframes; `Ok (flushed, encoded) | None -> `Nope encoded in let encode frame = encode ~encoder:(Atomic.get encoder) frame; Strings.Mutable.flush buf in let flush () = Frame.Fields.iter (fun _ { flush } -> flush ()) (Atomic.get encoder).streams in let insert_metadata m = let m = Frame.Metadata.to_list (Frame.Metadata.Export.to_metadata m) in match (ffmpeg.Ffmpeg_format.output, ffmpeg.Ffmpeg_format.format) with | _ when not (Atomic.get (Atomic.get encoder).started) -> Av.set_output_metadata (Atomic.get encoder).output m | `Stream, Some "ogg" -> flush (); Av.close (Atomic.get encoder).output; Atomic.set encoder (make ()); Av.set_output_metadata (Atomic.get encoder).output m | _ -> () in insert_metadata meta; let stop () = flush (); (try Av.close (Atomic.get encoder).output with _ -> ()); Strings.Mutable.flush buf in let hls = { Encoder.init; init_encode; split_encode; insert_id3; codec_attrs; bitrate; video_size; } in { Encoder.insert_metadata; header = (fun () -> Strings.empty); hls; encode; stop; } liquidsoap-2.3.2/src/core/encoder/encoders/ffmpeg_internal_encoder.ml000066400000000000000000000422371477303350200260350ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (** FFMPEG internal encoder *) module type InternalResampler_type = sig type t module Content : sig type data val get_data : Content.data -> data end val create : ?options:Swresample.options list -> Avutil.Channel_layout.t -> ?in_sample_format:Avutil__Sample_format.t -> int -> Avutil.Channel_layout.t -> ?out_sample_format:Avutil__Sample_format.t -> int -> t val convert : ?offset:int -> ?length:int -> t -> Content.data -> Swresample.Frame.t end module InternalResampler = struct module Content = Content_audio include Swresample.Make (Swresample.PlanarFloatArray) (Swresample.Frame) end module InternalResampler_pcm_s16 = struct module Content = Content_pcm_s16 include Swresample.Make (Swresample.S16PlanarBigArray) (Swresample.Frame) end module InternalResampler_pcm_f32 = struct module Content = Content_pcm_f32 include Swresample.Make (Swresample.FltPlanarBigArray) (Swresample.Frame) end module RawResampler = Swresample.Make (Swresample.Frame) (Swresample.Frame) module InternalScaler = Swscale.Make (Swscale.BigArray) (Swscale.Frame) module RawScaler = Swscale.Make (Swscale.Frame) (Swscale.Frame) let log = Log.make ["ffmpeg"; "encoder"; "internal"] (* mk_stream is used for the copy encoder, where stream creation has to be delayed until the first packet is passed. This is not needed here. *) let mk_stream _ = () let get_channel_layout ~pos channels = try Avutil.Channel_layout.get_default channels with Not_found -> Lang_encoder.raise_error ~pos (Printf.sprintf "%%ffmpeg encoder: could not find a default channel configuration for \ %d channels.." channels) (* This function optionally splits frames into [frame_size] and also adds PTS based on targeted [time_base], [sample_rate] and number of channel. *) let write_audio_frame ~time_base ~sample_rate ~channel_layout ~sample_format ~frame_size write_frame = let src_time_base = { Avutil.num = 1; den = sample_rate } in let convert_pts = Ffmpeg_utils.convert_time_base ~src:src_time_base ~dst:time_base in let add_frame_pts () = let nb_samples = ref 0L in fun frame -> let frame_pts = convert_pts !nb_samples in nb_samples := Int64.add !nb_samples (Int64.of_int (Avutil.Audio.frame_nb_samples frame)); Avutil.Frame.set_pts frame (Some frame_pts) in let add_final_frame_pts = add_frame_pts () in let write_frame frame = add_final_frame_pts frame; write_frame frame in match frame_size with | None -> write_frame | Some out_frame_size -> let in_params = { Avfilter.Utils.sample_rate; channel_layout; sample_format } in let converter = Avfilter.Utils.init_audio_converter ~in_params ~in_time_base:time_base ~out_frame_size () in let add_filter_frame_pts = add_frame_pts () in fun frame -> add_filter_frame_pts frame; Avfilter.Utils.convert_audio converter write_frame (`Frame frame) let mk_audio ~pos ~on_keyframe ~mode ~codec ~params ~options ~field output = let internal_resampler = match params.Ffmpeg_format.pcm_kind with | pcm_kind when Content_audio.is_kind pcm_kind -> (module InternalResampler : InternalResampler_type) | pcm_kind when Content_pcm_s16.is_kind pcm_kind -> (module InternalResampler_pcm_s16 : InternalResampler_type) | pcm_kind when Content_pcm_f32.is_kind pcm_kind -> (module InternalResampler_pcm_f32 : InternalResampler_type) | _ -> raise Content_base.Invalid in let module InternalResampler = (val internal_resampler : InternalResampler_type) in let codec = try Avcodec.Audio.find_encoder_by_name codec with e -> log#severe "Cannot find encoder %s: %s." codec (Printexc.to_string e); raise e in let target_samplerate = Lazy.force params.Ffmpeg_format.samplerate in let target_liq_audio_sample_time_base = { Avutil.num = 1; den = target_samplerate } in let target_channels = params.Ffmpeg_format.channels in let target_channel_layout = get_channel_layout ~pos target_channels in let target_sample_format = match params.Ffmpeg_format.sample_format with | Some format -> Avutil.Sample_format.find format | None -> `Dbl in let target_sample_format = Avcodec.Audio.find_best_sample_format codec target_sample_format in let internal_converter () = let src_samplerate = Lazy.force Frame.audio_rate in (* The typing system ensures that this is the number of channels in the frame. *) let src_channels = params.Ffmpeg_format.channels in let src_channel_layout = get_channel_layout ~pos src_channels in let resampler = InternalResampler.create ~out_sample_format:target_sample_format src_channel_layout src_samplerate target_channel_layout target_samplerate in fun frame -> let alen = AFrame.position frame in let content = Frame.get frame field in let pcm = InternalResampler.Content.get_data content in [InternalResampler.convert ~length:alen ~offset:0 resampler pcm] in let raw_converter = let resampler = ref None in let resample frame = let src_samplerate = Avutil.Audio.frame_get_sample_rate frame in let resampler = match !resampler with | Some f -> f | None -> let src_channel_layout = Avutil.Audio.frame_get_channel_layout frame in let src_sample_format = Avutil.Audio.frame_get_sample_format frame in let f = if src_samplerate <> target_samplerate || (not (Avutil.Channel_layout.compare src_channel_layout target_channel_layout)) || src_sample_format <> target_sample_format then ( let fn = RawResampler.create ~in_sample_format:src_sample_format ~out_sample_format:target_sample_format src_channel_layout src_samplerate target_channel_layout target_samplerate in RawResampler.convert fn) else fun f -> f in resampler := Some f; f in resampler frame in fun frame -> let frames = Ffmpeg_raw_content.Audio.(get_data (Frame.get frame field)) .Content.Video.data in let len = Frame.position frame in let frames = List.filter (fun (pos, _) -> 0 <= pos && pos < len) frames in List.map (fun (_, { Ffmpeg_raw_content.frame }) -> resample frame) frames in let converter = match mode with | `Internal -> internal_converter () | `Raw -> raw_converter | _ -> assert false in let opts = Hashtbl.copy options in let stream = try Av.new_audio_stream ~sample_rate:target_samplerate ~time_base:target_liq_audio_sample_time_base ~channel_layout:target_channel_layout ~sample_format:target_sample_format ~opts ~codec output with e -> log#severe "Cannot create audio stream (samplerate: %d, time_base: %s, channel \ layout: %s, sample format: %s, options: %s): %s." target_samplerate (Avutil.string_of_rational target_liq_audio_sample_time_base) (Avutil.Channel_layout.get_description target_channel_layout) (Option.value ~default:"" (Avutil.Sample_format.get_name target_sample_format)) (Avutil.string_of_opts opts) (Printexc.to_string e); raise e in let options = Hashtbl.copy options in Hashtbl.filter_map_inplace (fun l v -> if Hashtbl.mem opts l then Some v else None) options; if Hashtbl.length options > 0 then Lang_encoder.raise_error ~pos (Printf.sprintf "Unrecognized options: %s" (Ffmpeg_format.string_of_options options)); let intra_only = let params = Av.get_codec_params stream in match Avcodec.descriptor params with | None -> true | Some { Avcodec.properties } -> List.mem `Intra_only properties in let on_keyframe = Option.map (fun on_keyframe () -> if not intra_only then Av.flush output; on_keyframe ()) on_keyframe in let codec_attr () = Av.codec_attr stream in let bitrate () = Av.bitrate stream in let video_size () = None in let frame_size = if List.mem `Variable_frame_size (Avcodec.capabilities codec) then None else Some (Av.get_frame_size stream) in let write_frame = try write_audio_frame ~time_base:(Av.get_time_base stream) ~sample_rate:target_samplerate ~channel_layout:target_channel_layout ~sample_format:target_sample_format ~frame_size (Av.write_frame ?on_keyframe stream) with e -> log#severe "Error writing audio frame: %s." (Printexc.to_string e); raise e in let encode frame = List.iter write_frame (converter frame) in { Ffmpeg_encoder_common.mk_stream; encode; flush = (fun () -> ()); codec_attr; bitrate; video_size; } let mk_video ~pos ~on_keyframe ~mode ~codec ~params ~options ~field output = let codec = try Avcodec.Video.find_encoder_by_name codec with e -> log#severe "Cannot find encoder %s: %s." codec (Printexc.to_string e); raise e in let pixel_aspect = { Avutil.num = 1; den = 1 } in let target_fps = Lazy.force params.Ffmpeg_format.framerate in let target_video_frame_time_base = { Avutil.num = 1; den = target_fps } in let target_width = Lazy.force params.Ffmpeg_format.width in let target_height = Lazy.force params.Ffmpeg_format.height in let target_pixel_format = Ffmpeg_utils.pixel_format codec params.Ffmpeg_format.pixel_format in let flag = match Ffmpeg_utils.conf_scaling_algorithm#get with | "fast_bilinear" -> Swscale.Fast_bilinear | "bilinear" -> Swscale.Bilinear | "bicubic" -> Swscale.Bicubic | _ -> Lang_encoder.raise_error ~pos "Invalid value set for ffmpeg scaling algorithm!" in let opts = Hashtbl.copy options in let hwaccel = params.Ffmpeg_format.hwaccel in let hwaccel_device = params.Ffmpeg_format.hwaccel_device in let hwaccel_pixel_format = Option.map Avutil.Pixel_format.of_string params.Ffmpeg_format.hwaccel_pixel_format in let hardware_context, stream_pixel_format = Ffmpeg_utils.mk_hardware_context ~hwaccel ~hwaccel_pixel_format ~hwaccel_device ~opts ~target_pixel_format ~target_width ~target_height codec in let stream = Av.new_video_stream ~time_base:target_video_frame_time_base ~pixel_format:stream_pixel_format ?hardware_context ~frame_rate:{ Avutil.num = target_fps; den = 1 } ~width:target_width ~height:target_height ~opts ~codec output in let options = Hashtbl.copy options in Hashtbl.filter_map_inplace (fun l v -> if Hashtbl.mem opts l then Some v else None) options; if Hashtbl.length options > 0 then Lang_encoder.raise_error ~pos (Printf.sprintf "Unrecognized options: %s" (Ffmpeg_format.string_of_options options)); let intra_only = let params = Av.get_codec_params stream in match Avcodec.descriptor params with | None -> true | Some { Avcodec.properties } -> List.mem `Intra_only properties in let on_keyframe = Option.map (fun on_keyframe () -> if not intra_only then Av.flush output; on_keyframe ()) on_keyframe in let codec_attr () = Av.codec_attr stream in let bitrate () = Av.bitrate stream in let video_size () = let p = Av.get_codec_params stream in Some (Avcodec.Video.get_width p, Avcodec.Video.get_height p) in let converter = ref None in let start_pts = ref 0L in let mk_converter ~pixel_format ~time_base ~stream_idx () = let c = Ffmpeg_avfilter_utils.Fps.init ~start_pts:!start_pts ~width:target_width ~height:target_height ~pixel_format ~time_base ~pixel_aspect ~target_fps () in converter := Some (pixel_format, time_base, stream_idx, c); c in let get_converter ~pixel_format ~time_base ~stream_idx () = match !converter with | None -> mk_converter ~stream_idx ~pixel_format ~time_base () | Some (p, t, i, _) when (p, t, i) <> (pixel_format, time_base, stream_idx) -> mk_converter ~stream_idx ~pixel_format ~time_base () | Some (_, _, _, c) -> c in let stream_time_base = Av.get_time_base stream in let write_frame ~time_base frame = let frame_pts = Option.map (fun pts -> Ffmpeg_utils.convert_time_base ~src:time_base ~dst:stream_time_base pts) (Avutil.Frame.pts frame) in Avutil.Frame.set_pts frame frame_pts; start_pts := Int64.succ !start_pts; Av.write_frame ?on_keyframe stream frame in let fps_converter ~stream_idx ~time_base frame = let converter = get_converter ~time_base ~stream_idx ~pixel_format:(Avutil.Video.frame_get_pixel_format frame) () in let time_base = Ffmpeg_avfilter_utils.Fps.time_base converter in Ffmpeg_avfilter_utils.Fps.convert converter frame (write_frame ~time_base) in let flush () = match !converter with | None -> () | Some (_, _, _, converter) -> let time_base = Ffmpeg_avfilter_utils.Fps.time_base converter in Ffmpeg_avfilter_utils.Fps.eof converter (write_frame ~time_base) in let internal_converter cb = let src_width = Lazy.force Frame.video_width in let src_height = Lazy.force Frame.video_height in let scaler = InternalScaler.create [flag] src_width src_height (Ffmpeg_utils.liq_frame_pixel_format ()) target_width target_height target_pixel_format in let nb_frames = ref 0L in let time_base = Ffmpeg_utils.liq_video_sample_time_base () in let stream_idx = 1L in fun frame -> let content = Frame.get frame field in let buf = Content.Video.get_data content in List.iter (fun (_, img) -> let f = img (* TODO: we could scale instead of aggressively changing the viewport *) |> Video.Canvas.Image.viewport src_width src_height |> Video.Canvas.Image.render ~transparent:false in let vdata = Ffmpeg_utils.pack_image f in let frame = InternalScaler.convert scaler vdata in Avutil.Frame.set_pts frame (Some !nb_frames); nb_frames := Int64.succ !nb_frames; cb ~stream_idx ~time_base frame) buf.Content.Video.data in let raw_converter cb = let scaler = ref None in let scale frame = let scaler = match !scaler with | Some f -> f | None -> let src_width = Avutil.Video.frame_get_width frame in let src_height = Avutil.Video.frame_get_height frame in let src_pixel_format = Avutil.Video.frame_get_pixel_format frame in let f = if src_width <> target_width || src_height <> target_height then ( let scaler = RawScaler.create [flag] src_width src_height src_pixel_format target_width target_height src_pixel_format in fun frame -> let scaled = RawScaler.convert scaler frame in Avutil.Frame.set_pts scaled (Avutil.Frame.pts frame); scaled) else fun f -> f in scaler := Some f; f in scaler frame in fun frame -> let len = Frame.position frame in let { Ffmpeg_raw_content.VideoSpecs.data } = Ffmpeg_raw_content.Video.get_data (Frame.get frame field) in List.iter (fun (pos, { Ffmpeg_raw_content.time_base; frame; stream_idx }) -> if 0 <= pos && pos < len then cb ~stream_idx ~time_base (scale frame)) data in let converter = match mode with `Internal -> internal_converter | `Raw -> raw_converter in let encode = converter fps_converter in { Ffmpeg_encoder_common.mk_stream; encode; flush; codec_attr; bitrate; video_size; } liquidsoap-2.3.2/src/core/encoder/encoders/flac_encoder.ml000066400000000000000000000051471477303350200236010ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (** FLAC encoder *) let encoder flac meta = let comments = Frame.Metadata.to_list (Frame.Metadata.Export.to_metadata meta) in let channels = flac.Flac_format.channels in let samplerate_converter = Audio_converter.Samplerate.create channels in let samplerate = Lazy.force flac.Flac_format.samplerate in let src_freq = float (Frame.audio_of_seconds 1.) in let dst_freq = float samplerate in let p = { Flac.Encoder.channels; bits_per_sample = flac.Flac_format.bits_per_sample; sample_rate = samplerate; compression_level = Some flac.Flac_format.compression; total_samples = None; } in let buf = Strings.Mutable.empty () in let write chunk = Strings.Mutable.add_bytes buf chunk in let enc = Flac.Encoder.create ~comments ~write p in let enc = ref enc in let encode frame = let b = AFrame.pcm frame in let len = AFrame.position frame in let b, start, len = Audio_converter.Samplerate.resample samplerate_converter (dst_freq /. src_freq) b 0 len in let b = Audio.sub b start len in Flac.Encoder.process !enc b; Strings.Mutable.flush buf in let stop () = Flac.Encoder.finish !enc; Strings.Mutable.flush buf in { Encoder.insert_metadata = (fun _ -> ()); (* Flac encoder do not support header * for now. It will probably never do.. *) header = (fun () -> Strings.empty); hls = Encoder.dummy_hls encode; encode; stop; } let () = Plug.register Encoder.plug "flac" ~doc:"Flac encoder." (function | Encoder.Flac m -> Some (fun ?hls:_ ~pos:_ _ -> encoder m) | _ -> None) liquidsoap-2.3.2/src/core/encoder/encoders/lame_encoder.ml000066400000000000000000000107741477303350200236140ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** MP3 encoder *) open Encoder open Mp3_format let () = let create_encoder mp3 = let enc = Lame.create_encoder () in (* Input settings *) Lame.set_in_samplerate enc (Lazy.force Frame.audio_rate); Lame.set_num_channels enc (if mp3.Mp3_format.stereo then 2 else 1); (* Internal quality *) Lame.set_quality enc mp3.Mp3_format.internal_quality; (* Output settings *) if not mp3.Mp3_format.stereo then Lame.set_mode enc Lame.Mono else ( match mp3.Mp3_format.stereo_mode with | Mp3_format.Default -> () | Mp3_format.Stereo -> Lame.set_mode enc Lame.Stereo | Mp3_format.Joint_stereo -> Lame.set_mode enc Lame.Joint_stereo); begin let apply_constaints enc { Mp3_format.quality; mean_bitrate; min_bitrate; max_bitrate; hard_min; } = let f (s, v) = match v with Some v -> s enc v | None -> () in f (Lame.set_vbr_hard_min, hard_min); List.iter f [ (Lame.set_vbr_quality, quality); (Lame.set_vbr_mean_bitrate, mean_bitrate); (Lame.set_vbr_min_bitrate, min_bitrate); (Lame.set_vbr_max_bitrate, max_bitrate); ] in match mp3.Mp3_format.bitrate_control with | Mp3_format.VBR c -> Lame.set_vbr_mode enc Lame.Vbr_mtrh; apply_constaints enc c | Mp3_format.CBR br -> Lame.set_brate enc br | Mp3_format.ABR c -> Lame.set_vbr_mode enc Lame.Vbr_abr; apply_constaints enc c end; Lame.set_out_samplerate enc (Lazy.force mp3.Mp3_format.samplerate); Lame.set_bWriteVbrTag enc false; Lame.init_params enc; enc in let mp3_encoder ~pos mp3 metadata = let enc = create_encoder mp3 in let channels = if mp3.Mp3_format.stereo then 2 else 1 in (* Lame accepts data of a fixed length.. *) let frame_size = Frame.main_of_audio (Lame.frame_size enc) in let pending_data = Generator.create (Frame.Fields.make ~audio:(Content.Audio.format_of_channels channels) ()) in let encoded = Strings.Mutable.empty () in ignore (Option.map (fun version -> Strings.Mutable.add encoded (Utils.id3v2_of_metadata ~version (Frame.Metadata.to_list (Frame.Metadata.Export.to_metadata metadata)))) mp3.id3v2); let encode frame = Generator.put pending_data Frame.Fields.audio (Frame.get frame Frame.Fields.audio); while Generator.length pending_data > frame_size do let pcm = Content.Audio.get_data (Frame.Fields.find Frame.Fields.audio (Generator.slice pending_data frame_size)) in Strings.Mutable.add encoded (if channels = 1 then Lame.encode_buffer_float_part enc pcm.(0) pcm.(0) 0 (Array.length pcm.(0)) else Lame.encode_buffer_float_part enc pcm.(0) pcm.(1) 0 (Array.length pcm.(0))) done; Strings.Mutable.flush encoded in let stop () = Strings.of_string (Lame.encode_flush enc) in { insert_metadata = (fun _ -> ()); hls = Encoder_utils.mk_id3_hls ~pos encode; encode; header = (fun () -> Strings.empty); stop; } in Plug.register Encoder.plug "lame" ~doc:"LAME mp3 encoder." (function | Encoder.MP3 mp3 -> Some (fun ?hls:_ ~pos _ meta -> mp3_encoder ~pos mp3 meta) | _ -> None) liquidsoap-2.3.2/src/core/encoder/encoders/ndi_encoder.ml000066400000000000000000000032321477303350200234370ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** NDI encoder. This encoder does nothing and can only by used with `output.ndi` *) let encoder ~pos _ = let error () = Runtime_error.raise ~pos:(match pos with Some p -> [p] | None -> []) ~message:"The NDI encoder can only be used with `output.ndi`!" "invalid" in { Encoder.insert_metadata = (fun _ -> error ()); header = error; hls = Encoder.dummy_hls (fun _ -> error ()); encode = (fun _ -> error ()); stop = error; } let () = Plug.register Encoder.plug "ndi" ~doc:"NDI encoder. Only used with `output.ndi`." (function | Encoder.NDI m -> Some (fun ?hls:_ ~pos _ _ -> encoder ~pos m) | _ -> None) liquidsoap-2.3.2/src/core/encoder/encoders/ogg_encoder.ml000066400000000000000000000114261477303350200234450ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** OGG encoder *) open Mm type track = { encode : Ogg_muxer.t -> nativeint -> Frame.t -> unit; reset : Ogg_muxer.t -> Frame.Metadata.Export.t -> nativeint; mutable id : nativeint option; } let audio_encoders = Hashtbl.create 3 let theora_encoder = ref None (** Helper to encode audio *) let encode_audio ~channels ~src_freq ~dst_freq () = let samplerate_converter = Audio_converter.Samplerate.create channels in (* start and len are in main ticks. *) let encode encoder id frame = let b = AFrame.pcm frame in let len = AFrame.position frame in let buf, start, len = Audio_converter.Samplerate.resample samplerate_converter (dst_freq /. src_freq) b 0 len in let data = Ogg_muxer.Audio_data { Ogg_muxer.data = buf; offset = start; length = len } in Ogg_muxer.encode encoder id data in encode (** Helper to encode video. *) let encode_video encoder id frame = let content = Frame.get frame Frame.Fields.video in let buf = Content.Video.get_data content in let data = List.map (fun (_, img) -> Video.Canvas.Image.render img) buf.Content.Video.data in match data with | [] -> () | data -> let length = List.length data in let data = Ogg_muxer.Video_data { Ogg_muxer.data = Array.of_list data; offset = 0; length } in Ogg_muxer.encode encoder id data let encoder_name = function | Ogg_format.Vorbis _ -> "vorbis" | Ogg_format.Opus _ -> "opus" | Ogg_format.Flac _ -> "flac" | Ogg_format.Speex _ -> "speex" let get_encoder ~pos name = try Hashtbl.find audio_encoders name with Not_found -> Lang_encoder.raise_error ~pos (Printf.sprintf "Could not find any %s encoder." name) let encoder ~pos { Ogg_format.audio; video } = ignore (Option.map (fun p -> get_encoder ~pos (encoder_name p)) audio); ignore (Option.map (fun _ -> assert (!theora_encoder <> None)) video); fun name meta -> let tracks = [] in let tracks = match audio with | Some params -> let enc = get_encoder ~pos (encoder_name params) in enc params :: tracks | None -> tracks in let tracks = match video with | Some params -> let enc = Option.get !theora_encoder in enc params :: tracks | None -> tracks in (* We add a skeleton only * if there are more than one stream for now. *) let skeleton = List.length tracks > 1 in let ogg_enc = Ogg_muxer.create ~skeleton name in let rec enc = { Encoder.insert_metadata; hls = Encoder.dummy_hls (fun _ -> assert false); encode; header = (fun () -> Ogg_muxer.get_header ogg_enc); stop; } and streams_start () = let f track = match track.id with | Some _ -> () | None -> track.id <- Some (track.reset ogg_enc meta) in List.iter f tracks; Ogg_muxer.streams_start ogg_enc and encode frame = (* We do a lazy start, to * avoid empty streams at beginning.. *) if Ogg_muxer.state ogg_enc <> Ogg_muxer.Streaming then streams_start (); let f track = track.encode ogg_enc (Option.get track.id) frame in List.iter f tracks; Ogg_muxer.get_data ogg_enc and ogg_stop () = let f track = track.id <- None in List.iter f tracks; Ogg_muxer.end_of_stream ogg_enc and stop () = ogg_stop (); Ogg_muxer.get_data ogg_enc and insert_metadata m = ogg_stop (); let f track = track.id <- Some (track.reset ogg_enc m) in List.iter f tracks in { enc with hls = Encoder.dummy_hls encode } let () = Plug.register Encoder.plug "ogg" ~doc:"ogg encoder." (function | Encoder.Ogg m -> Some (fun ?hls:_ ~pos v -> encoder ~pos m v) | _ -> None) liquidsoap-2.3.2/src/core/encoder/encoders/shine_encoder.ml000066400000000000000000000055501477303350200240000ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Fixed-point MP3 encoder *) open Shine_format let create_encoder ~samplerate ~bitrate ~channels = Shine.create { Shine.channels; samplerate; bitrate } let encoder ~pos shine = let channels = shine.channels in let samplerate = Lazy.force shine.samplerate in let enc = create_encoder ~samplerate ~bitrate:shine.bitrate ~channels in let samplerate_converter = Audio_converter.Samplerate.create channels in let src_freq = float (Frame.audio_of_seconds 1.) in let dst_freq = float samplerate in (* Shine accepts data of a fixed length.. *) let samples = Frame.main_of_audio (Shine.samples_per_pass enc) in let buf = Generator.create (Frame.Fields.make ~audio:(Content.Audio.format_of_channels channels) ()) in let encoded = Strings.Mutable.empty () in let encode frame = let b = AFrame.pcm frame in let len = AFrame.position frame in let b, start, len = Audio_converter.Samplerate.resample samplerate_converter (dst_freq /. src_freq) b 0 len in let start = Frame.main_of_audio start in let len = Frame.main_of_audio len in Generator.put buf Frame.Fields.audio (Content.sub (Content.Audio.lift_data b) start len); while Generator.length buf > samples do let pcm = Content.Audio.get_data (Frame.Fields.find Frame.Fields.audio (Generator.slice buf samples)) in Strings.Mutable.add encoded (Shine.encode_buffer enc pcm) done; Strings.Mutable.flush encoded in let stop () = Strings.of_string (Shine.flush enc) in { Encoder.insert_metadata = (fun _ -> ()); header = (fun () -> Strings.empty); hls = Encoder_utils.mk_id3_hls ~pos encode; encode; stop; } let () = Plug.register Encoder.plug "shine" ~doc:"SHINE fixed-point mp3 encoder." (function | Encoder.Shine m -> Some (fun ?hls:_ ~pos _ _ -> encoder ~pos m) | _ -> None) liquidsoap-2.3.2/src/core/encoder/encoders/wav_encoder.ml000066400000000000000000000055101477303350200234630ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (** WAV encoder *) open Wav_format let encoder ~pos wav = let channels = wav.channels in let sample_rate = Lazy.force wav.samplerate in let sample_size = wav.samplesize in let ratio = float sample_rate /. float (Lazy.force Frame.audio_rate) in let converter = Audio_converter.Samplerate.create channels in let len = match wav.duration with | None -> None | Some d -> Some (int_of_float (d *. float channels *. float sample_rate *. float sample_size /. 8.)) in let header = Wav_aiff.wav_header ?len ~channels ~sample_rate ~sample_size () in let need_header = ref wav.header in let encode frame = let b = AFrame.pcm frame in let len = AFrame.position frame in (* Resample if needed. *) let b, start, len = Audio_converter.Samplerate.resample converter ratio b 0 len in let s = Bytes.create (sample_size / 8 * len * channels) in let of_audio = match sample_size with | 32 -> fun buf s off -> Audio.S32LE.of_audio buf s off | 24 -> fun buf s off -> Audio.S24LE.of_audio buf s off | 16 -> Audio.S16LE.of_audio | 8 -> fun buf s off -> Audio.U8.of_audio buf s off | _ -> Lang_encoder.raise_error ~pos "Unsupported sample size" in of_audio b start s 0 len; let s = Bytes.unsafe_to_string s in if !need_header then ( need_header := false; Strings.of_list [header; s]) else Strings.of_string s in { Encoder.insert_metadata = (fun _ -> ()); hls = Encoder.dummy_hls encode; encode; header = (fun () -> Strings.of_string header); stop = (fun () -> Strings.empty); } let () = Plug.register Encoder.plug "wav" ~doc:"Native wav encoder." (function | Encoder.WAV w -> Some (fun ?hls:_ ~pos _ _ -> encoder ~pos w) | _ -> None) liquidsoap-2.3.2/src/core/encoder/formats/000077500000000000000000000000001477303350200205055ustar00rootroot00000000000000liquidsoap-2.3.2/src/core/encoder/formats/avi_format.ml000066400000000000000000000027321477303350200231720ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) type t = { (* Samplerate is lazy in order to avoid forcing the evaluation of the samplerate at typing time, see #933. For channels this is pointless since we really need that for typing. *) samplerate : int Lazy.t; channels : int; width : int Lazy.t; height : int Lazy.t; } let to_string w = Printf.sprintf "%%avi(samplerate=%d,channels=%d,width=%d,height=%d)" (Lazy.force w.samplerate) w.channels (Lazy.force w.width) (Lazy.force w.height) liquidsoap-2.3.2/src/core/encoder/formats/external_encoder_format.ml000066400000000000000000000036421477303350200257350ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) exception No_process type restart_condition = Delay of int | Metadata | No_condition type t = { channels : int; samplerate : int Lazy.t; video : (int Lazy.t * int Lazy.t) option; header : bool; restart_on_crash : bool; restart : restart_condition; process : string; } let to_string e = let string_of_restart_condition c = match c with | Delay d -> Printf.sprintf "restart_after_delay=%i" d | Metadata -> "restart_on_metadata" | No_condition -> "" in let video = match e.video with | None -> "video=false" | Some (w, h) -> Printf.sprintf "video=true,width=%d,height=%d" (Lazy.force w) (Lazy.force h) in Printf.sprintf "%%external(channels=%i,samplerate=%i,%s,header=%b,restart_on_crash=%b,%s,process=%s)" e.channels (Lazy.force e.samplerate) video e.header e.restart_on_crash (string_of_restart_condition e.restart) e.process liquidsoap-2.3.2/src/core/encoder/formats/fdkaac_format.ml000066400000000000000000000060731477303350200236260ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) type mpeg2_aac = [ `AAC_LC | `HE_AAC | `HE_AAC_v2 ] type mpeg4_aac = [ mpeg2_aac | `AAC_LD | `AAC_ELD ] type aot = [ `Mpeg_4 of mpeg4_aac | `Mpeg_2 of mpeg2_aac ] type bandwidth = [ `Auto | `Fixed of int ] type bitrate_mode = [ `Constant | `Variable of int ] type transmux = [ `Raw | `Adif | `Adts | `Latm | `Latm_out_of_band | `Loas ] type t = { afterburner : bool; aot : aot; bandwidth : bandwidth; bitrate_mode : bitrate_mode; bitrate : int; channels : int; samplerate : int Lazy.t; sbr_mode : bool; transmux : transmux; } let string_of_aot = function | `Mpeg_4 `AAC_LC -> "mpeg4_aac_lc" | `Mpeg_4 `HE_AAC -> "mpeg4_he_aac" | `Mpeg_4 `HE_AAC_v2 -> "mpeg4_he_aac_v2" | `Mpeg_4 `AAC_LD -> "mpeg4_aac_ld" | `Mpeg_4 `AAC_ELD -> "mpeg4_aac_eld" | `Mpeg_2 `AAC_LC -> "mpeg2_aac_lc" | `Mpeg_2 `HE_AAC -> "mpeg2_he_aac" | `Mpeg_2 `HE_AAC_v2 -> "mpeg2_he_aac_v2" let aot_of_string = function | "mpeg4_aac_lc" -> `Mpeg_4 `AAC_LC | "mpeg4_he_aac" -> `Mpeg_4 `HE_AAC | "mpeg4_he_aac_v2" -> `Mpeg_4 `HE_AAC_v2 | "mpeg4_aac_ld" -> `Mpeg_4 `AAC_LD | "mpeg4_aac_eld" -> `Mpeg_4 `AAC_ELD | "mpeg2_aac_lc" -> `Mpeg_2 `AAC_LC | "mpeg2_he_aac" -> `Mpeg_2 `HE_AAC | "mpeg2_he_aac_v2" -> `Mpeg_2 `HE_AAC_v2 | _ -> raise Not_found let string_of_transmux = function | `Raw -> "raw" | `Adif -> "adif" | `Adts -> "adts" | `Latm -> "latm" | `Latm_out_of_band -> "latm_out_of_band" | `Loas -> "loas" let transmux_of_string = function | "raw" -> `Raw | "adif" -> `Adif | "adts" -> `Adts | "latm" -> `Latm | "latm_out_of_band" -> `Latm_out_of_band | "loas" -> `Loas | _ -> raise Not_found let to_string m = let br_info = match m.bitrate_mode with | `Variable vbr -> Printf.sprintf "vbr=%d" vbr | `Constant -> Printf.sprintf "bitrate=%d" m.bitrate in Printf.sprintf "%%fdkaac(afterburner=%b,aot=%S,%s,channels=%d,samplerate=%d,sbr_mode=%b,transmux=%S)" m.afterburner (string_of_aot m.aot) br_info m.channels (Lazy.force m.samplerate) m.sbr_mode (string_of_transmux m.transmux) let bitrate m = m.bitrate * 1000 liquidsoap-2.3.2/src/core/encoder/formats/ffmpeg_format.ml000066400000000000000000000135231477303350200236570ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) type opt_val = [ `String of string | `Int of int | `Int64 of int64 | `Float of float ] type copy_opt = [ `Wait_for_keyframe | `Ignore_keyframe ] type output = [ `Stream | `Url of string ] type opts = (string, opt_val) Hashtbl.t type hwaccel = [ `None | `Auto | `Internal | `Device | `Frame ] let string_of_hwaccel = function | `None -> "none" | `Auto -> "auto" | `Internal -> "internal" | `Device -> "device" | `Frame -> "frame" type audio_options = { pcm_kind : Content.kind; channels : int; samplerate : int Lazy.t; sample_format : string option; } type video_options = { framerate : int Lazy.t; width : int Lazy.t; height : int Lazy.t; pixel_format : string option; hwaccel : hwaccel; hwaccel_device : string option; hwaccel_pixel_format : string option; } type options = [ `Audio of audio_options | `Video of video_options ] type encoded_stream = { mode : [ `Raw | `Internal ]; codec : string option; options : options; opts : opts; } type stream = [ `Copy of copy_opt | `Encode of encoded_stream | `Drop ] type t = { format : string option; output : output; streams : (Frame.field * stream) list; interleaved : [ `Default | `True | `False ]; opts : opts; } let string_of_options (options : (string, [< `Var of string | opt_val ]) Hashtbl.t) = let _v = function | `Var v -> v | `String s -> Printf.sprintf "%S" s | `Int i -> string_of_int i | `Int64 i -> Int64.to_string i | `Float f -> string_of_float f in String.concat "," (Hashtbl.fold (fun k v c -> let v = Printf.sprintf "%s=%s" k (_v v) in v :: c) options []) let string_of_copy_opt = function | `Wait_for_keyframe -> "wait_for_keyframe" | `Ignore_keyframe -> "ignore_keyframe" let to_string m = let opts = [] in let opts = if Hashtbl.length m.opts > 0 then string_of_options m.opts :: opts else opts in let opts = List.fold_left (fun opts (field, stream) -> let name = Frame.Fields.string_of_field field in let name = match stream with | `Drop -> "%" ^ name ^ ".drop" | `Copy _ -> "%" ^ name ^ ".copy" | `Encode { mode = `Raw } -> "%" ^ name ^ ".raw" | _ -> "%" ^ name in match stream with | `Drop -> name :: opts | `Copy opt -> Printf.sprintf "%s(%s)" name (string_of_copy_opt opt) :: opts | `Encode { codec; options = `Video options; opts = stream_opts } -> let stream_opts = Hashtbl.fold (fun lbl v h -> Hashtbl.replace h lbl (v :> [ `Var of string | opt_val ]); h) stream_opts (Hashtbl.create 10) in ignore (Option.map (fun codec -> Hashtbl.replace stream_opts "codec" (`String codec)) codec); Hashtbl.replace stream_opts "framerate" (`Int (Lazy.force options.framerate)); Hashtbl.replace stream_opts "width" (`Int (Lazy.force options.width)); Hashtbl.replace stream_opts "height" (`Int (Lazy.force options.height)); Hashtbl.replace stream_opts "hwaccel" (`Var (string_of_hwaccel options.hwaccel)); Hashtbl.replace stream_opts "hwaccel_device" (match options.hwaccel_device with | None -> `Var "none" | Some d -> `String d); Printf.sprintf "%%%s(%s%s)" name (if Re.Pcre.pmatch ~rex:(Re.Pcre.regexp "video") name then "" else "video_content,") (string_of_options stream_opts) :: opts | `Encode { codec; options = `Audio options; opts = stream_opts } -> let stream_opts = Hashtbl.copy stream_opts in ignore (Option.map (fun codec -> Hashtbl.replace stream_opts "codec" (`String codec)) codec); Hashtbl.replace stream_opts "channels" (`Int options.channels); Hashtbl.replace stream_opts "samplerate" (`Int (Lazy.force options.samplerate)); Printf.sprintf "%s(%s%s)" name (if Re.Pcre.pmatch ~rex:(Re.Pcre.regexp "audio") name then "" else "audio_content,") (string_of_options stream_opts) :: opts) opts m.streams in let opts = Printf.sprintf "interleaved=%s" (match m.interleaved with | `Default -> "\"default\"" | `True -> "true" | `False -> "false") :: opts in let opts = match m.format with | Some f -> Printf.sprintf "format=%S" f :: opts | None -> opts in Printf.sprintf "%%ffmpeg(%s)" (String.concat "," opts) liquidsoap-2.3.2/src/core/encoder/formats/flac_format.ml000066400000000000000000000024661477303350200233240ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) type t = { channels : int; bits_per_sample : int; samplerate : int Lazy.t; compression : int; fill : int option; } let to_string m = Printf.sprintf "%%flac(channels=%i,bits_per_sample=%i,samplerate=%d,compression=%i)" m.channels m.bits_per_sample (Lazy.force m.samplerate) m.compression liquidsoap-2.3.2/src/core/encoder/formats/mp3_format.ml000066400000000000000000000053441477303350200231140ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) type stereo_mode = Default | Stereo | Joint_stereo type bitrate_constraints = { quality : int option; min_bitrate : int option; mean_bitrate : int option; max_bitrate : int option; hard_min : bool option; } let string_of_bitrate_constraints { quality; min_bitrate; mean_bitrate; max_bitrate; hard_min } = let hard_min = match hard_min with None -> "" | Some b -> Printf.sprintf "hard_min=%b" b in let f (v, x) = match x with Some x -> Printf.sprintf "%s=%d" v x | None -> "" in String.concat "," (List.filter (fun s -> s <> "") (List.map f [ ("quality", quality); ("bitrate", mean_bitrate); ("min_bitrate", min_bitrate); ("max_bitrate", max_bitrate); ] @ [hard_min])) type bitrate_control = | ABR of bitrate_constraints | VBR of bitrate_constraints | CBR of int let string_of_bitrate_control = function | ABR c | VBR c -> string_of_bitrate_constraints c | CBR br -> Printf.sprintf "bitrate=%d" br type t = { stereo : bool; stereo_mode : stereo_mode; bitrate_control : bitrate_control; internal_quality : int; samplerate : int Lazy.t; id3v2 : int option; } let to_string m = let name = match m.bitrate_control with | VBR _ -> "%mp3.vbr" | ABR _ -> "%mp3.abr" | CBR _ -> "%mp3" in Printf.sprintf "%s(%s,%s,samplerate=%d,id3v2=%s)" name (Encoder_formats.string_of_stereo m.stereo) (string_of_bitrate_control m.bitrate_control) (Lazy.force m.samplerate) (match m.id3v2 with None -> "none" | Some v -> string_of_int v) let bitrate m = match m.bitrate_control with | VBR _ -> raise Not_found | CBR n -> n * 1000 | ABR c -> Option.get c.mean_bitrate * 1000 liquidsoap-2.3.2/src/core/encoder/formats/ndi_format.ml000066400000000000000000000023601477303350200231620ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) type t = { audio : bool; video : bool } let to_string { audio; video } = Printf.sprintf "%%ndi(%s)" (String.concat "," ((if audio then "%audio" else "%audio.none") :: (if video then ["%video"] else ["%video.none"]))) liquidsoap-2.3.2/src/core/encoder/formats/ogg_format.ml000066400000000000000000000032531477303350200231660ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) type audio = | Speex of Speex_format.t | Vorbis of Vorbis_format.t | Flac of Flac_format.t | Opus of Opus_format.t let string_of_audio = function | Vorbis v -> Vorbis_format.to_string v | Flac v -> Flac_format.to_string v | Speex s -> Speex_format.to_string s | Opus o -> Opus_format.to_string o type video = Theora_format.t let string_of_video = Theora_format.to_string type t = { audio : audio option; video : video option } let to_string { audio; video } = let l = match video with Some e -> [string_of_video e] | None -> [] in let l = match audio with Some e -> string_of_audio e :: l | None -> [] in Printf.sprintf "%%ogg(%s)" (String.concat "," l) liquidsoap-2.3.2/src/core/encoder/formats/opus_format.ml000066400000000000000000000061741477303350200234050ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) type application = [ `Voip | `Audio | `Restricted_lowdelay ] type bitrate = [ `Auto | `Bitrate_max | `Bitrate of int ] type mode = | VBR of bool (* Variable bitrate, constrained or not. *) | CBR (* Constant bitrate. *) type max_bandwidth = [ `Narrow_band | `Medium_band | `Wide_band | `Super_wide_band | `Full_band ] type signal = [ `Auto | `Voice | `Music ] type t = { application : application option; bitrate : bitrate; complexity : int option; channels : int; frame_size : float; max_bandwidth : max_bandwidth option; mode : mode; samplerate : int; signal : signal option; fill : int option; dtx : bool; phase_inversion : bool; } let string_of_bitrate = function | `Auto -> "birate=\"auto\"," | `Bitrate_max -> "birate=\"max\"," | `Bitrate b -> Printf.sprintf "bitrate=%d," b let string_of_mode = function | CBR -> "vbr=\"none\"" | VBR b -> Printf.sprintf "vbr=%S" (if b then "constrained" else "unconstrained") let string_of_application = function | None -> "" | Some `Voip -> "application=\"voip\"," | Some `Audio -> "application=\"audio\"," | Some `Restricted_lowdelay -> "application=\"restricted_lowdelay\"," let string_of_bandwidth = function | None -> "" | Some `Narrow_band -> "max_bandwidth=\"narrow_band\"," | Some `Medium_band -> "max_bandwidth=\"medium_band\"," | Some `Wide_band -> "max_bandwidth=\"wide_band\"," | Some `Super_wide_band -> "max_bandwidth=\"super_wide_band\"," | Some `Full_band -> "max_bandwidth=\"full_band\"," let string_of_signal = function | None -> "" | Some `Auto -> "signal=\"auto\"," | Some `Voice -> "signal=\"voice\"," | Some `Music -> "signal=\"music\"," let to_string v = Printf.sprintf "%%opus(%s,%schannels=%d,%s%s%s%ssamplerate=%d,frame_size=%.02f,dtx=%B,phase_inversion=%b)" (string_of_mode v.mode) (string_of_bitrate v.bitrate) v.channels (string_of_application v.application) (match v.complexity with | None -> "" | Some i -> Printf.sprintf "complexity=\"%d\"," i) (string_of_bandwidth v.max_bandwidth) (string_of_signal v.signal) v.samplerate v.frame_size v.dtx v.phase_inversion liquidsoap-2.3.2/src/core/encoder/formats/shine_format.ml000066400000000000000000000023571477303350200235240ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) type t = { channels : int; samplerate : int Lazy.t; bitrate : int } let to_string m = Printf.sprintf "%%shine(channels=%d,samplerate=%d,bitrate=%d)" m.channels (Lazy.force m.samplerate) m.bitrate let bitrate m = m.bitrate * 1000 liquidsoap-2.3.2/src/core/encoder/formats/speex_format.ml000066400000000000000000000041121477303350200235310ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) type bitrate_control = Quality of int | Vbr of int | Abr of int type mode = Narrowband | Wideband | Ultra_wideband type t = { bitrate_control : bitrate_control; samplerate : int Lazy.t; stereo : bool; mode : mode; frames_per_packet : int; complexity : int option; fill : int option; dtx : bool; vad : bool; } let string_of_br_ctl x = match x with | Vbr x -> Printf.sprintf "vbr,quality=%d" x | Abr x -> Printf.sprintf "abr,bitrate=%d" x | Quality x -> Printf.sprintf "quality=%d" x let string_of_mode x = match x with | Narrowband -> "narrowband" | Wideband -> "widebande" | Ultra_wideband -> "ultra-wideband" let string_of_complexity x = match x with None -> "" | Some x -> Printf.sprintf ",complexity=%d" x let to_string m = Printf.sprintf "%%speex(%s,%s,samplerate=%d,mode=%s,frames_per_packet=%d%s,dtx=%B,vad=%B)" (Encoder_formats.string_of_stereo m.stereo) (string_of_br_ctl m.bitrate_control) (Lazy.force m.samplerate) (string_of_mode m.mode) m.frames_per_packet (string_of_complexity m.complexity) m.dtx m.vad liquidsoap-2.3.2/src/core/encoder/formats/theora_format.ml000066400000000000000000000046521477303350200237000ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) type bitrate_control = Quality of int | Bitrate of int type t = { (* TODO: framerate ! *) bitrate_control : bitrate_control; width : int Lazy.t; height : int Lazy.t; picture_width : int Lazy.t; picture_height : int Lazy.t; picture_x : int; picture_y : int; aspect_numerator : int; aspect_denominator : int; keyframe_frequency : int; vp3_compatible : bool option; soft_target : bool; buffer_delay : int option; speed : int option; fill : int option; } let bit_ctl_to_string bit_ctl = match bit_ctl with | Quality x -> Printf.sprintf "quality=%d" x | Bitrate x -> Printf.sprintf "bitrate=%d" x let print_some_bool v x = match x with None -> "" | Some x -> Printf.sprintf "%s=%b" v x let print_some_int v x = match x with None -> "" | Some x -> Printf.sprintf "%s=%i" v x let to_string th = let f = Lazy.force in Printf.sprintf "%%theora(%s,width=%d,height=%d,picture_width=%d,picture_height=%d,picture_x=%d,picture_y=%d,aspect_numerator=%d,aspect_denominator=%d,keyframe_frequency=%d,%s,soft_target=%b,%s,%s)" (bit_ctl_to_string th.bitrate_control) (f th.width) (f th.height) (f th.picture_width) (f th.picture_height) th.picture_x th.picture_y th.aspect_numerator th.aspect_denominator th.keyframe_frequency (print_some_bool "vp3_compatible" th.vp3_compatible) th.soft_target (print_some_int "buffer_delay" th.buffer_delay) (print_some_int "speed" th.speed) liquidsoap-2.3.2/src/core/encoder/formats/vorbis_format.ml000066400000000000000000000035021477303350200237130ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) type quality = float type bitrate = int type mode = | VBR of quality (* Variable bitrate. *) | CBR of bitrate (* Constant bitrate. *) | ABR of bitrate option * bitrate option * bitrate option (* Average: min,avg,max. *) type t = { channels : int; mode : mode; samplerate : int Lazy.t; fill : int option; } let string_of_mode = function | ABR (min, avg, max) -> let f v x = match x with Some x -> Printf.sprintf "%s=%d," v x | None -> "" in Printf.sprintf ".abr(%s%s%s" (f "min_bitrate" min) (f "bitrate" avg) (f "max_bitrate" max) | CBR bitrate -> Printf.sprintf ".cbr(bitrate=%d" bitrate | VBR q -> Printf.sprintf "(quality=%.2f" q let to_string v = Printf.sprintf "%%vorbis%s,channels=%d,samplerate=%d)" (string_of_mode v.mode) v.channels (Lazy.force v.samplerate) liquidsoap-2.3.2/src/core/encoder/formats/wav_format.ml000066400000000000000000000026321477303350200232070ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) type t = { samplerate : int Lazy.t; samplesize : int; channels : int; duration : float option; header : bool; } let to_string w = let duration = match w.duration with | None -> "" | Some d -> Printf.sprintf ",duration=%f" d in Printf.sprintf "%%wav(samplerate=%d,channels=%d,samplesize=%d,header=%b%s)" (Lazy.force w.samplerate) w.channels w.samplesize w.header duration liquidsoap-2.3.2/src/core/encoder/lang/000077500000000000000000000000001477303350200177535ustar00rootroot00000000000000liquidsoap-2.3.2/src/core/encoder/lang/lang_avi.ml000066400000000000000000000041351477303350200220700ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Value let make params = let defaults = { (* We use a hardcoded value in order not to force the evaluation of the number of channels too early, see #933. *) Avi_format.channels = 2; samplerate = Frame.audio_rate; width = Frame.video_width; height = Frame.video_height; } in let avi = List.fold_left (fun f -> function | `Labelled ("channels", Int { value = c }) -> { f with Avi_format.channels = c } | `Labelled ("samplerate", Int { value = i; _ }) -> { f with Avi_format.samplerate = Lazy.from_val i } | `Labelled ("width", Int { value = i; _ }) -> { f with Avi_format.width = Lazy.from_val i } | `Labelled ("height", Int { value = i; _ }) -> { f with Avi_format.height = Lazy.from_val i } | t -> Lang_encoder.raise_generic_error t) defaults params in Encoder.AVI avi let type_of_encoder p = Encoder.audio_video_type ~pcm_kind:Content.Audio.kind (Lang_encoder.channels_of_params p) let () = Lang_encoder.register "avi" type_of_encoder make liquidsoap-2.3.2/src/core/encoder/lang/lang_external_encoder.ml000066400000000000000000000117771477303350200246440ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Value let type_of_encoder p = let channels = Lang_encoder.channels_of_params p in match List.find_map (function `Labelled ("video", p) -> Some p | _ -> None) p with | Some { Term.term = `Bool true } -> Encoder.audio_video_type ~pcm_kind:Content.Audio.kind channels | Some ({ t = { Type.pos } } as tm) -> Lang_encoder.raise_error ~pos (Printf.sprintf "Invalid value %s for value mode. Only `true` or `false is \ allowed." (Term.to_string tm)) | _ -> Encoder.audio_type ~pcm_kind:Content.Audio.kind channels let make params = let defaults = { (* We use a hardcoded value in order not to force the evaluation of the number of channels too early, see #933. *) External_encoder_format.channels = 2; samplerate = Frame.audio_rate; video = None; header = true; restart_on_crash = false; restart = External_encoder_format.No_condition; process = ""; } in let ext = List.fold_left (fun f -> function | `Labelled ("stereo", Value.Bool { value = b; _ }) -> { f with External_encoder_format.channels = (if b then 2 else 1) } | `Labelled ("mono", Value.Bool { value = b; _ }) -> { f with External_encoder_format.channels = (if b then 1 else 2) } | `Anonymous s when String.lowercase_ascii s = "mono" -> { f with External_encoder_format.channels = 1 } | `Anonymous s when String.lowercase_ascii s = "stereo" -> { f with External_encoder_format.channels = 2 } | `Labelled ("channels", Value.Int { value = c }) -> { f with External_encoder_format.channels = c } | `Labelled ("samplerate", Value.Int { value = i; _ }) -> { f with External_encoder_format.samplerate = Lazy.from_val i } | `Labelled ("video", Value.Bool { value = b; _ }) -> let w, h = match f.External_encoder_format.video with | None -> (Frame.video_width, Frame.video_height) | Some (w, h) -> (w, h) in { f with External_encoder_format.video = (if b then Some (w, h) else None); } | `Labelled ("width", Int { value = w; _ }) -> let _, h = match f.External_encoder_format.video with | None -> (Frame.video_width, Frame.video_height) | Some (w, h) -> (w, h) in let w = Lazy.from_val w in { f with External_encoder_format.video = Some (w, h) } | `Labelled ("height", Int { value = h }) -> let w, _ = match f.External_encoder_format.video with | None -> (Frame.video_width, Frame.video_height) | Some (w, h) -> (w, h) in let h = Lazy.from_val h in { f with External_encoder_format.video = Some (w, h) } | `Labelled ("header", Bool { value = h }) -> { f with External_encoder_format.header = h } | `Labelled ("restart_on_crash", Bool { value = h }) -> { f with External_encoder_format.restart_on_crash = h } | `Anonymous s when String.lowercase_ascii s = "restart_on_metadata" -> { f with External_encoder_format.restart = External_encoder_format.Metadata; } | `Labelled ("restart_after_delay", Value.Int { value = i; _ }) -> { f with External_encoder_format.restart = External_encoder_format.Delay i; } | `Labelled ("process", String { value = s; _ }) -> { f with External_encoder_format.process = s } | `Anonymous s -> { f with External_encoder_format.process = s } | t -> Lang_encoder.raise_generic_error t) defaults params in if ext.External_encoder_format.process = "" then raise External_encoder_format.No_process; Encoder.External ext let () = Lang_encoder.register "external" type_of_encoder make liquidsoap-2.3.2/src/core/encoder/lang/lang_fdkaac.ml000066400000000000000000000120241477303350200225160ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Value let type_of_encoder p = Encoder.audio_type ~pcm_kind:Content.Audio.kind (Lang_encoder.channels_of_params p) let make params = let valid_samplerates = [ 8000; 11025; 12000; 16000; 22050; 24000; 32000; 44100; 48000; 64000; 88200; 96000; ] in let check_samplerate ~pos i = Lazy.from_fun (fun () -> let i = Lazy.force i in if not (List.mem i valid_samplerates) then ( let err = Printf.sprintf "invalid samplerate value. Possible values: %s" (String.concat ", " (List.map string_of_int valid_samplerates)) in Lang_encoder.raise_error ~pos err); i) in let defaults = { Fdkaac_format.afterburner = false; aot = `Mpeg_4 `HE_AAC_v2; bandwidth = `Auto; bitrate = 64; bitrate_mode = `Constant; (* We use a hardcoded value in order not to force the evaluation of the number of channels too early, see #933. *) channels = 2; samplerate = check_samplerate ~pos:None Frame.audio_rate; sbr_mode = false; transmux = `Adts; } in let valid_vbr = [1; 2; 3; 4; 5] in let fdkaac = List.fold_left (fun f -> function | `Labelled ("afterburner", Value.Bool { value = b; _ }) -> { f with Fdkaac_format.afterburner = b } | `Labelled ("aot", Value.String { value = s; pos }) -> let aot = try Fdkaac_format.aot_of_string s with Not_found -> Lang_encoder.raise_error ~pos "invalid aot value" in { f with Fdkaac_format.aot } | `Labelled ("vbr", Value.Int { value = i; pos }) -> if not (List.mem i valid_vbr) then ( let err = Printf.sprintf "invalid vbr mode. Possible values: %s" (String.concat ", " (List.map string_of_int valid_vbr)) in Lang_encoder.raise_error ~pos err); { f with Fdkaac_format.bitrate_mode = `Variable i } | `Labelled ("bandwidth", Value.Int { value = i; _ }) -> { f with Fdkaac_format.bandwidth = `Fixed i } | `Labelled ("bandwidth", String { value = s; _ }) when String.lowercase_ascii s = "auto" -> { f with Fdkaac_format.bandwidth = `Auto } | `Labelled ("bitrate", Value.Int { value = i; _ }) -> { f with Fdkaac_format.bitrate = i } | `Labelled ("stereo", Value.Bool { value = b; _ }) -> { f with Fdkaac_format.channels = (if b then 2 else 1) } | `Labelled ("mono", Value.Bool { value = b; _ }) -> { f with Fdkaac_format.channels = (if b then 1 else 2) } | `Labelled ("channels", Value.Int { value = i; _ }) -> { f with Fdkaac_format.channels = i } | `Labelled ("samplerate", Value.Int { value = i; pos }) -> { f with Fdkaac_format.samplerate = check_samplerate ~pos (Lazy.from_val i); } | `Labelled ("sbr_mode", Value.Bool { value = b; _ }) -> { f with Fdkaac_format.sbr_mode = b } | `Labelled ("transmux", Value.String { value = s; pos }) -> let transmux = try Fdkaac_format.transmux_of_string s with Not_found -> Lang_encoder.raise_error ~pos "invalid transmux value" in { f with Fdkaac_format.transmux } | `Anonymous s when String.lowercase_ascii s = "mono" -> { f with Fdkaac_format.channels = 1 } | `Anonymous s when String.lowercase_ascii s = "stereo" -> { f with Fdkaac_format.channels = 2 } | t -> Lang_encoder.raise_generic_error t) defaults params in let aot = fdkaac.Fdkaac_format.aot in if aot = `Mpeg_4 `HE_AAC_v2 || aot = `Mpeg_2 `HE_AAC_v2 then if fdkaac.Fdkaac_format.channels <> 2 then failwith "HE-AAC v2 is only available with 2 channels."; Encoder.FdkAacEnc fdkaac let () = Lang_encoder.register "fdkaac" type_of_encoder make liquidsoap-2.3.2/src/core/encoder/lang/lang_ffmpeg.ml000066400000000000000000000463121477303350200225600ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Value type decode_type = [ `Raw | `Internal ] type content_type = [ `Audio | `Video ] type encoder_params = decode_type * content_type * (string * Liquidsoap_lang.Value.t) list type mode = [ `Drop | `Copy of Liquidsoap_lang.Value.t option | `Encode of encoder_params ] type parsed_encoder = Frame.field * mode let channels_of_channel_layout args = match List.assoc "channel_layout" args with (* 5.1 as float. *) | { Term.term = `Float layout } -> let layout = Printf.sprintf "%.1f" layout in Avutil.Channel_layout.(get_nb_channels (find layout)) | { Term.term = `String layout } -> Avutil.Channel_layout.(get_nb_channels (find layout)) | { t = { Type.pos } } as tm -> Lang_encoder.raise_error ~pos (Printf.sprintf "Invalid value %s for channel_layout parameter. Only static \ numbers are allowed." (Term.to_string tm)) let channels args = try channels_of_channel_layout args with _ -> ( try let name, channels = try ("channels", List.assoc "channels" args) with Not_found -> ("ac", List.assoc "ac" args) in match channels with | { Term.term = `Int n } -> n | { t = { Type.pos } } as tm -> Lang_encoder.raise_error ~pos (Printf.sprintf "Invalid value %s for %s parameter. Only static numbers are \ allowed." name (Term.to_string tm)) with Not_found -> 2) let to_int t = match t with | Value.Int { value = i } -> i | Value.String { value = s } -> int_of_string s | Value.Float { value = f } -> int_of_float f | _ -> Lang_encoder.raise_error ~pos:(Value.pos t) "integer expected" let to_string t = match t with | Value.Int { value = i } -> Printf.sprintf "%i" i | Value.String { value = s } -> s | Value.Float { value = f } -> Printf.sprintf "%f" f | _ -> Lang_encoder.raise_error ~pos:(Value.pos t) "string expected" let to_float t = match t with | Value.Int { value = i } -> float i | Value.String { value = s } -> float_of_string s | Value.Float { value = f } -> f | _ -> Lang_encoder.raise_error ~pos:(Value.pos t) "float expected" let to_copy_opt t = match t with | Value.String { value = "wait_for_keyframe" } -> `Wait_for_keyframe | Value.String { value = "ignore_keyframe" } -> `Ignore_keyframe | _ -> Lang_encoder.raise_error ~pos:(Value.pos t) ("Invalid value for copy encoder parameter: " ^ Value.to_string t) let has_content ~to_static_string name p = List.exists (fun (lbl, v) -> lbl = "" && to_static_string v = Some name) p (* The following conventions are used to infer media type from an encoder: - encoder has "audio" or "video" in its name, e.g. dolby_audio, video_1 etc. - encoder has "audio_content" or "video_content" in its arguments: %track(audio_content, ...) or %track(video_content, ...) - encoder has a static codec string, e.g. %track(codec="libmp3lame") *) let stream_media_type ~to_pos ~to_static_string name args = let raise pos = Lang_encoder.raise_error ~pos {|Unable to find a track media content type. Please use one of the available convention: - Use `"audio"` or `"video"` in the track name, e.g. `%dolby_audio` - Add `audio_content` or `video_content` to the track parameters, e.g. `%track(audio_content)` - Use a static codec string, e.g. `%track(codec="libmp3lame")`|} in match (name, args) with | _ when has_content ~to_static_string "audio_content" args -> `Audio | _ when has_content ~to_static_string "video_content" args -> `Video | _ when Re.Pcre.pmatch ~rex:(Re.Pcre.regexp "audio") name -> `Audio | _ when Re.Pcre.pmatch ~rex:(Re.Pcre.regexp "video") name -> `Video | _ -> ( match List.assoc_opt "codec" args with | Some t -> ( let codec = to_static_string t in try ignore (Avcodec.Audio.find_encoder_by_name (Option.get codec)); `Audio with _ -> ( try ignore (Avcodec.Video.find_encoder_by_name (Option.get codec)); `Video with _ -> raise (to_pos t))) | None -> raise None) let copy_param = function | [] -> None | [("", t)] -> Some t | [(l, v)] | _ :: (l, v) :: _ -> Lang_encoder.raise_generic_error (match l with | "" -> `Anonymous (Lang.to_string v) | l -> `Labelled (l, v)) let parse_encoder_name name = let field, mode = match String.split_on_char '.' name with | [field] -> (field, "") | field :: mode :: _ -> (field, mode) | _ -> failwith "invalid content field" in let mode = match mode with | "copy" -> `Copy | "raw" -> `Raw | "drop" -> `Drop | "" -> `Internal | _ -> failwith "invalid content field" in (field, mode) let to_static_string_value = function | Value.String { value = s } -> Some s | _ -> None let parse_encoder_params ~to_pos (name, p) : parsed_encoder = let field, mode = parse_encoder_name name in let mode = match mode with | `Drop -> `Drop | `Copy -> `Copy (copy_param p) | `Raw -> `Encode ( `Raw, stream_media_type ~to_static_string:to_static_string_value ~to_pos name p, p ) | `Internal -> `Encode ( `Internal, stream_media_type ~to_static_string:to_static_string_value ~to_pos name p, p ) in let field = Frame.Fields.register field in (field, mode) let to_static_string_term = function | Term.{ term = `String s } -> Some s | _ -> None let term_pos { Term.t = { Type.pos } } = pos let type_of_encoder = List.fold_left (fun content_type p -> match p with | `Encoder (name, args) -> let args = List.filter_map (function | `Anonymous s -> Some ("", Term.make (`String s)) | `Labelled (l, v) -> Some (l, v) | `Encoder _ -> None) args in let field, mode = parse_encoder_name name in let format = match mode with | `Drop -> Type.var ~constraints:[Format_type.track] () | `Copy -> Type.make (Format_type.descr (`Format (Content.default_format Ffmpeg_copy_content.kind))) | `Raw -> Type.make (Format_type.descr (`Format (match stream_media_type ~to_pos:term_pos ~to_static_string:to_static_string_term name args with | `Audio -> Content.default_format Ffmpeg_raw_content.Audio.kind | `Video -> Content.default_format Ffmpeg_raw_content.Video.kind))) | `Internal -> Type.make (Format_type.descr (`Format (match stream_media_type ~to_pos:term_pos ~to_static_string:to_static_string_term name args with | `Audio -> let channels = channels args in let pcm_kind = List.fold_left (fun pcm_kind -> function | "", { Term.term = `String "pcm" } -> Content.Audio.kind | "", { Term.term = `String "pcm_s16" } -> Content_pcm_s16.kind | "", { Term.term = `String "pcm_f32" } -> Content_pcm_f32.kind | _ -> pcm_kind) Content.Audio.kind args in Frame_base.format_of_channels ~pcm_kind channels | `Video -> Content.(default_format Video.kind)))) in let field = Frame.Fields.register field in Frame.Fields.add field format content_type | _ -> content_type) Frame.Fields.empty let flag_qscale = ref 0 let qp2lambda = ref 0 (* Looks like this is how ffmpeg CLI does it. See: https://github.com/FFmpeg/FFmpeg/blob/4782124b90cf915ede2cebd871be82fc0267a135/fftools/ffmpeg_opt.c#L1567-L1570 *) let set_global_quality q opts = let flags = match Hashtbl.find_opt opts "flags" with | Some (`Int i) -> `Int (i lor Avcodec.flag_qscale) | Some (`Int64 i) -> `Int64 (Int64.logor i (Int64.of_int Avcodec.flag_qscale)) | Some (`String s) -> `String (s ^ "+qscale") | Some _ -> assert false | None -> `Int Avcodec.flag_qscale in Hashtbl.replace opts "flags" flags; Hashtbl.replace opts "global_quality" (`Float (float Avutil.qp2lambda *. q)) let ffmpeg_gen params = let defaults = { Ffmpeg_format.format = None; output = `Stream; streams = []; interleaved = `Default; opts = Hashtbl.create 0; } in let default_audio = { Ffmpeg_format.pcm_kind = Content_audio.kind; channels = 2; samplerate = Frame.audio_rate; sample_format = None; } in let default_video = { Ffmpeg_format.framerate = Frame.video_rate; width = Frame.video_width; height = Frame.video_height; pixel_format = None; hwaccel = `Auto; hwaccel_device = None; hwaccel_pixel_format = None; } in let parse_opts opts = function | "q", t | "qscale", t -> set_global_quality (to_float t) opts | "", String { value = "audio_content"; _ } | "", String { value = "video_content"; _ } | "codec", _ -> () | k, String { value = s; _ } -> Hashtbl.replace opts k (`String s) | k, Value.Int { value = i; _ } -> Hashtbl.replace opts k (`Int i) | k, Float { value = fl; _ } -> Hashtbl.replace opts k (`Float fl) | _, t -> Lang_encoder.raise_error ~pos:(Value.pos t) "unexpected option" in let rec parse_audio_args ~opts options = function | [] -> options (* Audio options *) | ("", t) :: args when to_string t = "pcm" -> parse_audio_args ~opts { options with Ffmpeg_format.pcm_kind = Content_audio.kind } args | ("", t) :: args when to_string t = "pcm_s16" -> parse_audio_args ~opts { options with Ffmpeg_format.pcm_kind = Content_pcm_s16.kind } args | ("", t) :: args when to_string t = "pcm_f32" -> parse_audio_args ~opts { options with Ffmpeg_format.pcm_kind = Content_pcm_f32.kind } args (* Set channels but keep argument for encoders. *) | (("channel_layout", t) as arg) :: args -> (* Handle 5.1 as float *) let layout = match t with | Value.Float { value = f } -> Printf.sprintf "%.1f" f | _ -> to_string t in parse_opts opts arg; parse_audio_args ~opts { options with Ffmpeg_format.channels = Avutil.Channel_layout.(get_nb_channels (find layout)); } args | ("channels", t) :: args -> parse_audio_args ~opts { options with Ffmpeg_format.channels = to_int t } args | ("ac", t) :: args -> parse_audio_args ~opts { options with Ffmpeg_format.channels = to_int t } args | ("samplerate", t) :: args -> parse_audio_args ~opts { options with Ffmpeg_format.samplerate = Lazy.from_val (to_int t) } args | ("ar", t) :: args -> parse_audio_args ~opts { options with Ffmpeg_format.samplerate = Lazy.from_val (to_int t) } args | ("sample_format", t) :: args -> parse_audio_args ~opts { options with Ffmpeg_format.sample_format = Some (to_string t) } args | arg :: args -> parse_opts opts arg; parse_audio_args ~opts options args in let rec parse_video_args ~opts options = function | [] -> options | ("framerate", t) :: args -> parse_video_args ~opts { options with Ffmpeg_format.framerate = Lazy.from_val (to_int t) } args | ("r", t) :: args -> parse_video_args ~opts { options with Ffmpeg_format.framerate = Lazy.from_val (to_int t) } args | ("width", t) :: args -> parse_video_args ~opts { options with Ffmpeg_format.width = Lazy.from_val (to_int t) } args | ("height", t) :: args -> parse_video_args ~opts { options with Ffmpeg_format.height = Lazy.from_val (to_int t) } args | ("pixel_format", String { value = "guess" }) :: args -> parse_video_args ~opts { options with Ffmpeg_format.pixel_format = None } args | ("pixel_format", t) :: args -> parse_video_args ~opts { options with Ffmpeg_format.pixel_format = Some (to_string t) } args | ("hwaccel", String { value = "auto" }) :: args -> parse_video_args ~opts { options with Ffmpeg_format.hwaccel = `Auto } args | ("hwaccel", String { value = "none"; _ }) :: args -> parse_video_args ~opts { options with Ffmpeg_format.hwaccel = `None } args | ("hwaccel", String { value = "internal"; _ }) :: args -> parse_video_args ~opts { options with Ffmpeg_format.hwaccel = `Internal } args | ("hwaccel", String { value = "device"; _ }) :: args -> parse_video_args ~opts { options with Ffmpeg_format.hwaccel = `Device } args | ("hwaccel", String { value = "frame"; _ }) :: args -> parse_video_args ~opts { options with Ffmpeg_format.hwaccel = `Frame } args | ("hwaccel_device", String { value = "none"; _ }) :: args -> parse_video_args ~opts { options with Ffmpeg_format.hwaccel_device = None } args | ("hwaccel_device", t) :: args -> parse_video_args ~opts { options with Ffmpeg_format.hwaccel_device = Some (to_string t) } args | ("hwaccel_pixel_format", String { value = "guess" }) :: args -> parse_video_args ~opts { options with Ffmpeg_format.hwaccel_pixel_format = None } args | ("hwaccel_pixel_format", t) :: args -> parse_video_args ~opts { options with Ffmpeg_format.hwaccel_pixel_format = Some (to_string t); } args | arg :: args -> parse_opts opts arg; parse_video_args ~opts options args in let parse_stream ~content_type args = let opts = Hashtbl.create 0 in let codec = Option.map to_string (List.assoc_opt "codec" args) in match content_type with | `Audio -> let options = parse_audio_args ~opts default_audio args in (codec, `Audio options, opts) | `Video -> let options = parse_video_args ~opts default_video args in (codec, `Video options, opts) in List.fold_left (fun f -> function | `Encoder (field, `Drop) -> { f with Ffmpeg_format.streams = f.Ffmpeg_format.streams @ [(field, `Drop)]; } | `Encoder (field, `Copy None) -> { f with Ffmpeg_format.streams = f.Ffmpeg_format.streams @ [(field, `Copy `Wait_for_keyframe)]; } | `Encoder (field, `Copy (Some t)) -> { f with Ffmpeg_format.streams = f.Ffmpeg_format.streams @ [(field, `Copy (to_copy_opt t))]; } | `Encoder (field, `Encode (mode, content_type, args)) -> let codec, options, opts = parse_stream ~content_type args in { f with Ffmpeg_format.streams = f.Ffmpeg_format.streams @ [(field, `Encode { Ffmpeg_format.mode; options; codec; opts })]; } | `Option ("format", String { value = "none"; _ }) -> { f with Ffmpeg_format.format = None } | `Option ("format", String { value = fmt }) -> { f with Ffmpeg_format.format = Some fmt } | `Option ("interleaved", Value.Bool { value = b; _ }) -> { f with Ffmpeg_format.interleaved = (if b then `True else `False) } | `Option ("interleaved", String { value = "default" }) -> { f with Ffmpeg_format.interleaved = `Default } | `Option (k, String { value = s; _ }) -> Hashtbl.replace f.Ffmpeg_format.opts k (`String s); f | `Option (k, Value.Int { value = i; _ }) -> Hashtbl.replace f.Ffmpeg_format.opts k (`Int i); f | `Option (k, Float { value = i }) -> Hashtbl.replace f.Ffmpeg_format.opts k (`Float i); f | `Option (l, v) -> Lang_encoder.raise_generic_error (match l with | "" -> `Anonymous (Lang.to_string v) | l -> `Labelled (l, v))) defaults params let make params = let params = List.map (function | `Encoder (name, args) -> let args = List.filter_map (function | `Anonymous s -> Some ("", Lang.string s) | `Labelled (l, v) -> Some (l, v) | `Encoder _ -> None) args in `Encoder (parse_encoder_params ~to_pos:Value.pos (name, args)) | `Anonymous s -> `Option ("", Lang.string s) | `Labelled (l, v) -> `Option (l, v)) params in Encoder.Ffmpeg (ffmpeg_gen params) let () = Lang_encoder.register "ffmpeg" type_of_encoder make liquidsoap-2.3.2/src/core/encoder/lang/lang_flac.ml000066400000000000000000000057601477303350200222230ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Value let type_of_encoder p = Encoder.audio_type ~pcm_kind:Content.Audio.kind (Lang_encoder.channels_of_params p) let accepted_bits_per_sample = [8; 16; 24; 32] let flac_gen params = let defaults = { Flac_format.fill = None; (* We use a hardcoded value in order not to force the evaluation of the number of channels too early, see #933. *) channels = 2; samplerate = Frame.audio_rate; bits_per_sample = 16; compression = 5; } in List.fold_left (fun f -> function | `Labelled ("stereo", Bool { value = b; _ }) -> { f with Flac_format.channels = (if b then 2 else 1) } | `Labelled ("mono", Bool { value = b; _ }) -> { f with Flac_format.channels = (if b then 1 else 2) } | `Labelled ("channels", Int { value = i; _ }) -> { f with Flac_format.channels = i } | `Labelled ("samplerate", Int { value = i; _ }) -> { f with Flac_format.samplerate = Lazy.from_val i } | `Labelled ("compression", Int { value = i; pos }) -> if i < 0 || i > 8 then Lang_encoder.raise_error ~pos "invalid compression value"; { f with Flac_format.compression = i } | `Labelled ("bits_per_sample", Int { value = i; pos }) -> if not (List.mem i accepted_bits_per_sample) then Lang_encoder.raise_error ~pos "invalid bits_per_sample value"; { f with Flac_format.bits_per_sample = i } | `Labelled ("bytes_per_page", Int { value = i; _ }) -> { f with Flac_format.fill = Some i } | `Anonymous s when String.lowercase_ascii s = "mono" -> { f with Flac_format.channels = 1 } | `Anonymous s when String.lowercase_ascii s = "stereo" -> { f with Flac_format.channels = 2 } | t -> Lang_encoder.raise_generic_error t) defaults params let make_ogg params = Ogg_format.Flac (flac_gen params) let make params = Encoder.Flac (flac_gen params) let () = Lang_encoder.register "flac" type_of_encoder make liquidsoap-2.3.2/src/core/encoder/lang/lang_mp3.ml000066400000000000000000000205641477303350200220140ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Value let type_of_encoder p = Encoder.audio_type ~pcm_kind:Content.Audio.kind (Lang_encoder.channels_of_params p) let allowed_bitrates = [ 8; 16; 24; 32; 40; 48; 56; 64; 80; 96; 112; 128; 144; 160; 192; 224; 256; 320; ] let check_samplerate ~pos i = Lazy.from_fun (fun () -> let i = Lazy.force i in let allowed = [8000; 11025; 12000; 16000; 22050; 24000; 32000; 44100; 48000] in if not (List.mem i allowed) then Lang_encoder.raise_error ~pos "invalid samplerate value"; i) let mp3_base_defaults () = { Mp3_format.stereo = true; stereo_mode = Mp3_format.Joint_stereo; samplerate = check_samplerate ~pos:None Frame.audio_rate; bitrate_control = Mp3_format.CBR 128; internal_quality = 2; id3v2 = None; } let mp3_base f = function | `Labelled ("stereo", Value.Bool { value = b; _ }) -> { f with Mp3_format.stereo = b } | `Labelled ("mono", Value.Bool { value = b; _ }) -> { f with Mp3_format.stereo = not b } | `Labelled ("stereo_mode", String { value = m; pos }) -> let mode = match m with | "default" -> Mp3_format.Default | "joint_stereo" -> Mp3_format.Joint_stereo | "stereo" -> Mp3_format.Stereo | _ -> Lang_encoder.raise_error ~pos "invalid stereo mode" in { f with Mp3_format.stereo_mode = mode } | `Labelled ("internal_quality", Int { value = q; pos }) -> if q < 0 || q > 9 then Lang_encoder.raise_error ~pos "internal quality must be a value between 0 and 9"; { f with Mp3_format.internal_quality = q } | `Labelled ("samplerate", Value.Int { value = i; pos }) -> { f with Mp3_format.samplerate = check_samplerate ~pos (Lazy.from_val i) } | `Labelled ("id3v2", Bool { value = true; _ }) -> { f with Mp3_format.id3v2 = Some 3 } | `Labelled ("id3v2", Bool { value = false; _ }) -> { f with Mp3_format.id3v2 = None } | `Labelled ("id3v2", Int { value = v }) -> { f with Mp3_format.id3v2 = Some v } | `Labelled ("id3v2", String { value = "none"; _ }) -> { f with Mp3_format.id3v2 = None } | `Anonymous s when String.lowercase_ascii s = "mono" -> { f with Mp3_format.stereo = false } | `Anonymous s when String.lowercase_ascii s = "stereo" -> { f with Mp3_format.stereo = true } | t -> Lang_encoder.raise_generic_error t let make_cbr params = let defaults = { (mp3_base_defaults ()) with Mp3_format.bitrate_control = Mp3_format.CBR 128; } in let set_bitrate f b = match f.Mp3_format.bitrate_control with | Mp3_format.CBR _ -> { f with Mp3_format.bitrate_control = Mp3_format.CBR b } | _ -> assert false in let mp3 = List.fold_left (fun f -> function | `Labelled ("bitrate", Value.Int { value = i; pos }) -> if not (List.mem i allowed_bitrates) then Lang_encoder.raise_error ~pos "invalid bitrate value"; set_bitrate f i | x -> mp3_base f x) defaults params in Encoder.MP3 mp3 let make_abr_vbr ~default params = let set_min_bitrate f b = match f.Mp3_format.bitrate_control with | Mp3_format.VBR vbr -> { f with Mp3_format.bitrate_control = Mp3_format.VBR { vbr with Mp3_format.min_bitrate = b }; } | Mp3_format.ABR abr -> { f with Mp3_format.bitrate_control = Mp3_format.ABR { abr with Mp3_format.min_bitrate = b }; } | _ -> assert false in let set_max_bitrate f b = match f.Mp3_format.bitrate_control with | Mp3_format.VBR vbr -> { f with Mp3_format.bitrate_control = Mp3_format.VBR { vbr with Mp3_format.max_bitrate = b }; } | Mp3_format.ABR abr -> { f with Mp3_format.bitrate_control = Mp3_format.ABR { abr with Mp3_format.max_bitrate = b }; } | _ -> assert false in let set_mean_bitrate f b = match f.Mp3_format.bitrate_control with | Mp3_format.VBR vbr -> { f with Mp3_format.bitrate_control = Mp3_format.VBR { vbr with Mp3_format.mean_bitrate = b }; } | Mp3_format.ABR abr -> { f with Mp3_format.bitrate_control = Mp3_format.ABR { abr with Mp3_format.mean_bitrate = b }; } | _ -> assert false in let set_hard_min f b = match f.Mp3_format.bitrate_control with | Mp3_format.VBR vbr -> { f with Mp3_format.bitrate_control = Mp3_format.VBR { vbr with Mp3_format.hard_min = b }; } | Mp3_format.ABR abr -> { f with Mp3_format.bitrate_control = Mp3_format.ABR { abr with Mp3_format.hard_min = b }; } | _ -> assert false in let set_quality f q = match f.Mp3_format.bitrate_control with | Mp3_format.VBR vbr -> { f with Mp3_format.bitrate_control = Mp3_format.VBR { vbr with Mp3_format.quality = q }; } | _ -> assert false in let mp3 = let is_vbr f = match f.Mp3_format.bitrate_control with | Mp3_format.VBR _ -> true | _ -> false in List.fold_left (fun f -> function | `Labelled ("quality", Int { value = q; pos }) when is_vbr f -> if q < 0 || q > 9 then Lang_encoder.raise_error ~pos "quality should be in [0..9]"; set_quality f (Some q) | `Labelled ("hard_min", Value.Bool { value = b; _ }) -> set_hard_min f (Some b) | `Labelled ("bitrate", Value.Int { value = i; pos }) -> if not (List.mem i allowed_bitrates) then Lang_encoder.raise_error ~pos "invalid bitrate value"; set_mean_bitrate f (Some i) | `Labelled ("min_bitrate", Value.Int { value = i; pos }) -> if not (List.mem i allowed_bitrates) then Lang_encoder.raise_error ~pos "invalid bitrate value"; set_min_bitrate f (Some i) | `Labelled ("max_bitrate", Value.Int { value = i; pos }) -> if not (List.mem i allowed_bitrates) then Lang_encoder.raise_error ~pos "invalid bitrate value"; set_max_bitrate f (Some i) | x -> mp3_base f x) default params in Encoder.MP3 mp3 let make_abr p = make_abr_vbr ~default: { (mp3_base_defaults ()) with Mp3_format.bitrate_control = Mp3_format.ABR { Mp3_format.quality = None; min_bitrate = None; mean_bitrate = Some 128; max_bitrate = None; hard_min = Some false; }; } p let make_vbr p = make_abr_vbr ~default: { (mp3_base_defaults ()) with Mp3_format.bitrate_control = Mp3_format.VBR { Mp3_format.quality = Some 4; min_bitrate = None; mean_bitrate = None; max_bitrate = None; hard_min = None; }; } p let () = Lang_encoder.register "mp3" type_of_encoder make_cbr; Lang_encoder.register "mp3.cbr" type_of_encoder make_cbr; Lang_encoder.register "mp3.abr" type_of_encoder make_abr; Lang_encoder.register "mp3.vbr" type_of_encoder make_vbr liquidsoap-2.3.2/src/core/encoder/lang/lang_ndi.ml000066400000000000000000000047431477303350200220700ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Ndi_format let type_of_encoder p = match List.fold_left (fun (audio, video) -> function | `Encoder ("audio", []) -> (true, video) | `Encoder ("audio.none", []) -> (false, video) | `Encoder ("video", []) -> (audio, true) | `Encoder ("video.none", []) -> (audio, false) | _ -> (audio, video)) (true, true) p with | true, true -> Encoder.audio_video_type ~pcm_kind:Content.Audio.kind 2 | false, true -> Encoder.video_type () | true, false -> Encoder.audio_type ~pcm_kind:Content.Audio.kind 2 | _ -> Lang_encoder.raise_error ~pos:None "Invalid %%ndi encoder parameter!" let make params = let defaults = { audio = true; video = true } in let ndi = List.fold_left (fun ndi -> function | `Encoder ("audio", []) -> { ndi with audio = true } | `Encoder ("audio.none", []) -> { ndi with audio = false } | `Encoder ("video", []) -> { ndi with video = true } | `Encoder ("video.none", []) -> { ndi with video = false } | `Labelled (_, v) -> Lang_encoder.raise_error ~pos:(Value.pos v) "Invalid parameter!" | _ -> Lang_encoder.raise_error ~pos:None "Invalid %%ndi encoder parameter!") defaults params in if (not ndi.audio) && not ndi.video then Lang_encoder.raise_error ~pos:None "%%ndi encoder needs at least one audio or video field!"; Encoder.NDI ndi let () = Lang_encoder.register "ndi" type_of_encoder make liquidsoap-2.3.2/src/core/encoder/lang/lang_ogg.ml000066400000000000000000000047401477303350200220670ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let type_of_encoder p = let audio = ["vorbis"; "vorbis.cbr"; "vorbis.abr"; "opus"; "speex"; "flac"] in let audio = List.find_map (function | `Encoder (e, p) -> if List.mem e audio then Some p else None | _ -> None) p in let channels = match audio with None -> 0 | Some p -> Lang_encoder.channels_of_params p in let video = List.exists (function `Encoder ("theora", _) -> true | _ -> false) p in if not video then Encoder.audio_type ~pcm_kind:Content.Audio.kind channels else Encoder.audio_video_type ~pcm_kind:Content.Audio.kind channels let make p = let ogg_audio e p = match e with | "vorbis" -> Lang_vorbis.make p | "vorbis.cbr" -> Lang_vorbis.make_cbr p | "vorbis.abr" -> Lang_vorbis.make_abr p | "opus" -> Lang_opus.make p | "speex" -> Lang_speex.make p | "flac" -> Lang_flac.make_ogg p | _ -> raise Not_found in let ogg_audio_opt e p = try Some (ogg_audio e p) with Not_found -> None in let ogg_video e p = match e with "theora" -> Lang_theora.make p | _ -> raise Not_found in let ogg_video_opt e p = try Some (ogg_video e p) with Not_found -> None in let audio = List.find_map (function `Encoder (e, p) -> ogg_audio_opt e p | _ -> None) p in let video = List.find_map (function `Encoder (e, p) -> ogg_video_opt e p | _ -> None) p in Encoder.Ogg { Ogg_format.audio; video } let () = Lang_encoder.register "ogg" type_of_encoder make liquidsoap-2.3.2/src/core/encoder/lang/lang_opus.ml000066400000000000000000000145331477303350200223020ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Value let type_of_encoder p = Encoder.audio_type ~pcm_kind:Content.Audio.kind (Lang_encoder.channels_of_params p) let make params = let defaults = { Opus_format.application = None; complexity = None; max_bandwidth = None; mode = Opus_format.VBR true; bitrate = `Auto; fill = None; channels = 2; samplerate = 48000; signal = None; frame_size = 20.; dtx = false; phase_inversion = true; } in let opus = List.fold_left (fun f -> function | `Labelled ("application", String { value = "voip"; _ }) -> { f with Opus_format.application = Some `Voip } | `Labelled ("application", String { value = "audio" }) -> { f with Opus_format.application = Some `Audio } | `Labelled ("application", String { value = "restricted_lowdelay" }) -> { f with Opus_format.application = Some `Restricted_lowdelay } | `Labelled ("complexity", Int { value = c; pos }) -> (* Doc say this should be from 0 to 10. *) if c < 0 || c > 10 then Lang_encoder.raise_error ~pos "Opus complexity should be in 0..10"; { f with Opus_format.complexity = Some c } | `Labelled ("max_bandwidth", String { value = "narrow_band" }) -> { f with Opus_format.max_bandwidth = Some `Narrow_band } | `Labelled ("max_bandwidth", String { value = "medium_band" }) -> { f with Opus_format.max_bandwidth = Some `Medium_band } | `Labelled ("max_bandwidth", String { value = "wide_band" }) -> { f with Opus_format.max_bandwidth = Some `Wide_band } | `Labelled ("max_bandwidth", String { value = "super_wide_band" }) -> { f with Opus_format.max_bandwidth = Some `Super_wide_band } | `Labelled ("max_bandwidth", String { value = "full_band" }) -> { f with Opus_format.max_bandwidth = Some `Full_band } | `Labelled ("frame_size", Float { value = size; pos }) -> let frame_sizes = [2.5; 5.; 10.; 20.; 40.; 60.] in if not (List.mem size frame_sizes) then Lang_encoder.raise_error ~pos "Opus frame size should be one of 2.5, 5., 10., 20., 40. or 60."; { f with Opus_format.frame_size = size } | `Labelled ("samplerate", Value.Int { value = i; pos }) -> let samplerates = [8000; 12000; 16000; 24000; 48000] in if not (List.mem i samplerates) then Lang_encoder.raise_error ~pos "Opus samplerate should be one of 8000, 12000, 16000, 24000 or \ 48000"; { f with Opus_format.samplerate = i } | `Labelled ("bitrate", Value.Int { value = i; pos }) -> let i = i * 1000 in (* Doc say this should be from 500 to 512000. *) if i < 500 || i > 512000 then Lang_encoder.raise_error ~pos "Opus bitrate should be in 5..512"; { f with Opus_format.bitrate = `Bitrate i } | `Labelled ("bitrate", String { value = "auto" }) -> { f with Opus_format.bitrate = `Auto } | `Labelled ("bitrate", String { value = "max" }) -> { f with Opus_format.bitrate = `Bitrate_max } | `Labelled ("stereo", Value.Bool { value = b; _ }) -> { f with Opus_format.channels = (if b then 2 else 1) } | `Labelled ("mono", Value.Bool { value = b; _ }) -> { f with Opus_format.channels = (if b then 1 else 2) } | `Anonymous s when String.lowercase_ascii s = "mono" -> { f with Opus_format.channels = 1 } | `Anonymous s when String.lowercase_ascii s = "stereo" -> { f with Opus_format.channels = 2 } | `Labelled ("channels", Value.Int { value = i; pos }) -> if i < 1 || i > 2 then Lang_encoder.raise_error ~pos "only mono and stereo streams are supported for now"; { f with Opus_format.channels = i } | `Labelled ("vbr", String { value = "none"; _ }) -> { f with Opus_format.mode = Opus_format.CBR } | `Labelled ("vbr", String { value = "constrained" }) -> { f with Opus_format.mode = Opus_format.VBR true } | `Labelled ("vbr", String { value = "unconstrained" }) -> { f with Opus_format.mode = Opus_format.VBR false } | `Labelled ("signal", String { value = "voice" }) -> { f with Opus_format.signal = Some `Voice } | `Labelled ("signal", String { value = "music" }) -> { f with Opus_format.signal = Some `Music } | `Labelled ("bytes_per_page", Value.Int { value = i; _ }) -> { f with Opus_format.fill = Some i } | `Labelled ("dtx", Value.Bool { value = b; _ }) -> { f with Opus_format.dtx = b } | `Labelled ("phase_inversion", Value.Bool { value = b; _ }) -> { f with Opus_format.phase_inversion = b } | `Anonymous s when String.lowercase_ascii s = "mono" -> { f with Opus_format.channels = 1 } | `Anonymous s when String.lowercase_ascii s = "stereo" -> { f with Opus_format.channels = 2 } | t -> Lang_encoder.raise_generic_error t) defaults params in Ogg_format.Opus opus let () = let make p = Encoder.Ogg { Ogg_format.audio = Some (make p); video = None } in Lang_encoder.register "opus" type_of_encoder make liquidsoap-2.3.2/src/core/encoder/lang/lang_shine.ml000066400000000000000000000052741477303350200224240ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Value let type_of_encoder p = Encoder.audio_type ~pcm_kind:Content.Audio.kind (Lang_encoder.channels_of_params p) let make params = let defaults = { (* We use a hardcoded value in order not to force the evaluation of the number of channels too early, see #933. *) Shine_format.channels = 2; samplerate = Frame.audio_rate; bitrate = 128; } in let shine = List.fold_left (fun f -> function | `Labelled ("stereo", Bool { value = b; _ }) -> { f with Shine_format.channels = (if b then 2 else 1) } | `Labelled ("mono", Bool { value = b; _ }) -> { f with Shine_format.channels = (if b then 1 else 2) } | `Anonymous s when String.lowercase_ascii s = "mono" -> { f with Shine_format.channels = 1 } | `Anonymous s when String.lowercase_ascii s = "stereo" -> { f with Shine_format.channels = 2 } | `Labelled ("channels", Int { value = i; _ }) -> { f with Shine_format.channels = i } | `Labelled ("samplerate", Int { value = i; _ }) -> { f with Shine_format.samplerate = Lazy.from_val i } | `Labelled ("bitrate", Int { value = i; _ }) -> { f with Shine_format.bitrate = i } | `Anonymous s when String.lowercase_ascii s = "mono" -> { f with Shine_format.channels = 1 } | `Anonymous s when String.lowercase_ascii s = "stereo" -> { f with Shine_format.channels = 2 } | t -> Lang_encoder.raise_generic_error t) defaults params in Encoder.Shine shine let () = Lang_encoder.register "shine" type_of_encoder make; Lang_encoder.register "mp3.fxp" type_of_encoder make liquidsoap-2.3.2/src/core/encoder/lang/lang_speex.ml000066400000000000000000000102301477303350200224260ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Value let type_of_encoder p = Encoder.audio_type ~pcm_kind:Content.Audio.kind (Lang_encoder.channels_of_params p) let make params = let defaults = { Speex_format.stereo = true; fill = None; samplerate = Frame.audio_rate; bitrate_control = Speex_format.Quality 7; mode = Speex_format.Narrowband; frames_per_packet = 1; complexity = None; dtx = false; vad = false; } in let speex = List.fold_left (fun f -> function | `Labelled ("stereo", Value.Bool { value = b; _ }) -> { f with Speex_format.stereo = b } | `Labelled ("mono", Value.Bool { value = b; _ }) -> { f with Speex_format.stereo = not b } | `Labelled ("samplerate", Value.Int { value = i; _ }) -> { f with Speex_format.samplerate = Lazy.from_val i } | `Labelled ("abr", Value.Int { value = i; _ }) -> { f with Speex_format.bitrate_control = Speex_format.Abr i } | `Labelled ("quality", Int { value = q; pos }) -> (* Doc say this should be from 0 to 10. *) if q < 0 || q > 10 then Lang_encoder.raise_error ~pos "Speex quality should be in 0..10"; { f with Speex_format.bitrate_control = Speex_format.Quality q } | `Labelled ("vbr", Int { value = q; _ }) -> { f with Speex_format.bitrate_control = Speex_format.Vbr q } | `Labelled ("mode", String { value = s; _ }) when String.lowercase_ascii s = "wideband" -> { f with Speex_format.mode = Speex_format.Wideband } | `Labelled ("mode", String { value = s; _ }) when String.lowercase_ascii s = "narrowband" -> { f with Speex_format.mode = Speex_format.Narrowband } | `Labelled ("mode", String { value = s; _ }) when String.lowercase_ascii s = "ultra-wideband" -> { f with Speex_format.mode = Speex_format.Ultra_wideband } | `Labelled ("frames_per_packet", Value.Int { value = i; _ }) -> { f with Speex_format.frames_per_packet = i } | `Labelled ("complexity", Value.Int { value = i; pos }) -> (* Doc says this should be between 1 and 10. *) if i < 1 || i > 10 then Lang_encoder.raise_error ~pos "Speex complexity should be in 1..10"; { f with Speex_format.complexity = Some i } | `Labelled ("bytes_per_page", Value.Int { value = i; _ }) -> { f with Speex_format.fill = Some i } | `Labelled ("dtx", Value.Bool { value = b; _ }) -> { f with Speex_format.dtx = b } | `Labelled ("vad", Value.Bool { value = b; _ }) -> { f with Speex_format.vad = b } | `Anonymous s when String.lowercase_ascii s = "mono" -> { f with Speex_format.stereo = false } | `Anonymous s when String.lowercase_ascii s = "stereo" -> { f with Speex_format.stereo = true } | t -> Lang_encoder.raise_generic_error t) defaults params in Ogg_format.Speex speex let () = let make p = Encoder.Ogg { Ogg_format.audio = Some (make p); video = None } in Lang_encoder.register "speex" type_of_encoder make liquidsoap-2.3.2/src/core/encoder/lang/lang_theora.ml000066400000000000000000000141471477303350200225770ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Value let type_of_encoder _ = Encoder.video_type () let make params = let defaults = { Theora_format.bitrate_control = Theora_format.Quality 40; fill = None; width = Frame.video_width; height = Frame.video_height; picture_width = Frame.video_width; picture_height = Frame.video_height; picture_x = 0; picture_y = 0; aspect_numerator = 1; aspect_denominator = 1; keyframe_frequency = 64; vp3_compatible = None; soft_target = false; buffer_delay = None; speed = None; } in List.fold_left (fun f -> function | `Labelled ("quality", Value.Int { value = i; pos }) -> (* According to the doc, this should be a value between * 0 and 63. *) if i < 0 || i > 63 then Lang_encoder.raise_error ~pos "Theora quality should be in 0..63"; { f with Theora_format.bitrate_control = Theora_format.Quality i } | `Labelled ("bitrate", Value.Int { value = i; _ }) -> { f with Theora_format.bitrate_control = Theora_format.Bitrate i } | `Labelled ("width", Value.Int { value = i; pos }) -> (* According to the doc: must be a multiple of 16, and less than 1048576. *) if i mod 16 <> 0 || i >= 1048576 then Lang_encoder.raise_error ~pos "invalid frame width value (should be a multiple of 16)"; { f with Theora_format.width = Lazy.from_val i; picture_width = Lazy.from_val i; } | `Labelled ("height", Value.Int { value = i; pos }) -> (* According to the doc: must be a multiple of 16, and less than 1048576. *) if i mod 16 <> 0 || i >= 1048576 then Lang_encoder.raise_error ~pos "invalid frame height value (should be a multiple of 16)"; { f with Theora_format.height = Lazy.from_val i; picture_height = Lazy.from_val i; } | `Labelled ("picture_width", Value.Int { value = i; pos }) -> (* According to the doc: must not be larger than width. *) if i > Lazy.force f.Theora_format.width then Lang_encoder.raise_error ~pos "picture width must not be larger than width"; { f with Theora_format.picture_width = Lazy.from_val i } | `Labelled ("picture_height", Value.Int { value = i; pos }) -> (* According to the doc: must not be larger than height. *) if i > Lazy.force f.Theora_format.height then Lang_encoder.raise_error ~pos "picture height must not be larger than height"; { f with Theora_format.picture_height = Lazy.from_val i } | `Labelled ("picture_x", Value.Int { value = i; pos }) -> (* According to the doc: must be no larger than width-picture_width * or 255, whichever is smaller. *) if i > min (Lazy.force f.Theora_format.width - Lazy.force f.Theora_format.picture_width) 255 then Lang_encoder.raise_error ~pos "picture x must not be larger than width - picture width or 255, \ whichever is smaller"; { f with Theora_format.picture_x = i } | `Labelled ("picture_y", Value.Int { value = i; pos }) -> (* According to the doc: must be no larger than width-picture_width * and frame_height-pic_height-pic_y must be no larger than 255. *) if i > Lazy.force f.Theora_format.height - Lazy.force f.Theora_format.picture_height then Lang_encoder.raise_error ~pos "picture y must not be larger than height - picture height"; if Lazy.force f.Theora_format.picture_height - i > 255 then Lang_encoder.raise_error ~pos "picture height - picture y must not be larger than 255"; { f with Theora_format.picture_y = i } | `Labelled ("aspect_numerator", Value.Int { value = i; _ }) -> { f with Theora_format.aspect_numerator = i } | `Labelled ("aspect_denominator", Value.Int { value = i; _ }) -> { f with Theora_format.aspect_denominator = i } | `Labelled ("keyframe_frequency", Value.Int { value = i; _ }) -> { f with Theora_format.keyframe_frequency = i } | `Labelled ("vp3_compatible", Bool { value = i }) -> { f with Theora_format.vp3_compatible = Some i } | `Labelled ("soft_target", Bool { value = i }) -> { f with Theora_format.soft_target = i } | `Labelled ("buffer_delay", Value.Int { value = i; _ }) -> { f with Theora_format.buffer_delay = Some i } | `Labelled ("speed", Value.Int { value = i; _ }) -> { f with Theora_format.speed = Some i } | `Labelled ("bytes_per_page", Value.Int { value = i; _ }) -> { f with Theora_format.fill = Some i } | t -> Lang_encoder.raise_generic_error t) defaults params let () = let make p = Encoder.Ogg { Ogg_format.audio = None; video = Some (make p) } in Lang_encoder.register "theora" type_of_encoder make liquidsoap-2.3.2/src/core/encoder/lang/lang_vorbis.ml000066400000000000000000000147011477303350200226150ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Value let type_of_encoder p = Encoder.audio_type ~pcm_kind:Content.Audio.kind (Lang_encoder.channels_of_params p) let make_cbr params = let defaults = { Vorbis_format.mode = Vorbis_format.CBR 128; fill = None; (* We use a hardcoded value in order not to force the evaluation of the number of channels too early, see #933. *) channels = 2; samplerate = Frame.audio_rate; } in let vorbis = List.fold_left (fun f -> function | `Labelled ("samplerate", Int { value = i; _ }) -> { f with Vorbis_format.samplerate = Lazy.from_val i } | `Labelled ("bitrate", Int { value = i; _ }) -> { f with Vorbis_format.mode = Vorbis_format.CBR i } | `Labelled ("stereo", Bool { value = b; _ }) -> { f with Vorbis_format.channels = (if b then 2 else 1) } | `Labelled ("mono", Bool { value = b; _ }) -> { f with Vorbis_format.channels = (if b then 1 else 2) } | `Anonymous s when String.lowercase_ascii s = "mono" -> { f with Vorbis_format.channels = 1 } | `Anonymous s when String.lowercase_ascii s = "stereo" -> { f with Vorbis_format.channels = 2 } | `Labelled ("channels", Int { value = i; _ }) -> { f with Vorbis_format.channels = i } | `Labelled ("bytes_per_page", Int { value = i; _ }) -> { f with Vorbis_format.fill = Some i } | t -> Lang_encoder.raise_generic_error t) defaults params in Ogg_format.Vorbis vorbis let make_abr params = let defaults = { Vorbis_format.mode = Vorbis_format.ABR (None, None, None); channels = 2; fill = None; samplerate = Frame.audio_rate; } in let get_rates x = match x.Vorbis_format.mode with | Vorbis_format.ABR (x, y, z) -> (x, y, z) | _ -> assert false in let vorbis = List.fold_left (fun f -> function | `Labelled ("samplerate", Int { value = i; _ }) -> { f with Vorbis_format.samplerate = Lazy.from_val i } | `Labelled ("bitrate", Int { value = i; _ }) -> let x, _, y = get_rates f in { f with Vorbis_format.mode = Vorbis_format.ABR (x, Some i, y) } | `Labelled ("max_bitrate", Int { value = i; _ }) -> let x, y, _ = get_rates f in { f with Vorbis_format.mode = Vorbis_format.ABR (x, y, Some i) } | `Labelled ("min_bitrate", Int { value = i; _ }) -> let _, x, y = get_rates f in { f with Vorbis_format.mode = Vorbis_format.ABR (Some i, x, y) } | `Labelled ("stereo", Bool { value = b; _ }) -> { f with Vorbis_format.channels = (if b then 2 else 1) } | `Labelled ("mono", Bool { value = b; _ }) -> { f with Vorbis_format.channels = (if b then 1 else 2) } | `Anonymous s when String.lowercase_ascii s = "mono" -> { f with Vorbis_format.channels = 1 } | `Anonymous s when String.lowercase_ascii s = "stereo" -> { f with Vorbis_format.channels = 2 } | `Labelled ("channels", Int { value = i; _ }) -> { f with Vorbis_format.channels = i } | `Labelled ("bytes_per_page", Int { value = i; _ }) -> { f with Vorbis_format.fill = Some i } | t -> Lang_encoder.raise_generic_error t) defaults params in Ogg_format.Vorbis vorbis let make params = let defaults = { Vorbis_format.mode = Vorbis_format.VBR 0.3; channels = 2; fill = None; samplerate = Frame.audio_rate; } in let vorbis = List.fold_left (fun f -> function | `Labelled ("samplerate", Int { value = i; _ }) -> { f with Vorbis_format.samplerate = Lazy.from_val i } | `Labelled ("quality", Float { value = q; pos }) -> if q < -0.2 || q > 1. then Lang_encoder.raise_error ~pos "quality should be in [(-0.2)..1]"; { f with Vorbis_format.mode = Vorbis_format.VBR q } | `Labelled ("quality", Int { value = i; pos }) -> if i <> 0 && i <> 1 then Lang_encoder.raise_error ~pos "quality should be in [-(0.2)..1]"; let q = float i in { f with Vorbis_format.mode = Vorbis_format.VBR q } | `Labelled ("stereo", Bool { value = b; _ }) -> { f with Vorbis_format.channels = (if b then 2 else 1) } | `Labelled ("mono", Bool { value = b; _ }) -> { f with Vorbis_format.channels = (if b then 1 else 2) } | `Anonymous s when String.lowercase_ascii s = "mono" -> { f with Vorbis_format.channels = 1 } | `Anonymous s when String.lowercase_ascii s = "stereo" -> { f with Vorbis_format.channels = 2 } | `Labelled ("channels", Int { value = i; _ }) -> { f with Vorbis_format.channels = i } | `Labelled ("bytes_per_page", Int { value = i; _ }) -> { f with Vorbis_format.fill = Some i } | t -> Lang_encoder.raise_generic_error t) defaults params in Ogg_format.Vorbis vorbis let () = let make p = Encoder.Ogg { Ogg_format.audio = Some (make p); video = None } in let make_abr p = Encoder.Ogg { Ogg_format.audio = Some (make_abr p); video = None } in let make_cbr p = Encoder.Ogg { Ogg_format.audio = Some (make_cbr p); video = None } in Lang_encoder.register "vorbis" type_of_encoder make; Lang_encoder.register "vorbis.abr" type_of_encoder make_abr; Lang_encoder.register "vorbis.cbr" type_of_encoder make_cbr liquidsoap-2.3.2/src/core/encoder/lang/lang_wav.ml000066400000000000000000000054111477303350200221040ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Value let type_of_encoder p = Encoder.audio_type ~pcm_kind:Content.Audio.kind (Lang_encoder.channels_of_params p) let make params = let defaults = { Wav_format.samplesize = 16; header = true; duration = None; (* We use a hardcoded value in order not to force the evaluation of the number of channels too early, see #933. *) channels = 2; samplerate = Frame.audio_rate; } in let wav = List.fold_left (fun f -> function | `Labelled ("stereo", Bool { value = b; _ }) -> { f with Wav_format.channels = (if b then 2 else 1) } | `Labelled ("mono", Bool { value = b; _ }) -> { f with Wav_format.channels = (if b then 1 else 2) } | `Anonymous s when String.lowercase_ascii s = "stereo" -> { f with Wav_format.channels = 2 } | `Anonymous s when String.lowercase_ascii s = "mono" -> { f with Wav_format.channels = 1 } | `Labelled ("channels", Int { value = c }) -> { f with Wav_format.channels = c } | `Labelled ("duration", Float { value = d }) -> { f with Wav_format.duration = Some d } | `Labelled ("samplerate", Int { value = i; _ }) -> { f with Wav_format.samplerate = Lazy.from_val i } | `Labelled ("samplesize", Int { value = i; pos }) -> if i <> 8 && i <> 16 && i <> 24 && i <> 32 then Lang_encoder.raise_error ~pos "invalid sample size"; { f with Wav_format.samplesize = i } | `Labelled ("header", Bool { value = b; _ }) -> { f with Wav_format.header = b } | t -> Lang_encoder.raise_generic_error t) defaults params in Encoder.WAV wav let () = Lang_encoder.register "wav" type_of_encoder make liquidsoap-2.3.2/src/core/encoder/lang_encoder.ml000066400000000000000000000107771477303350200220200ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Liquidsoap_lang open Term module V = Lang_core.MkCustom (struct type content = Encoder.format let name = "encoder" let to_string = Encoder.string_of_format let to_json ~pos _ = Runtime_error.raise ~pos ~message:(Printf.sprintf "Encoders cannot be represented as json") "json" let compare = Stdlib.compare end) module L = struct (** Type of audio formats that can encode frame of a given kind. *) let format_t ?pos k = Type.make ?pos (Type.Constr { Type.constructor = "format"; params = [(`Covariant, k)] }) let to_format = V.of_value let format = V.to_value end let raise_error ~pos message = Runtime_error.raise ~message ~pos:(match pos with None -> [] | Some pos -> [pos]) "encoder" let raise_generic_error = function | `Anonymous s -> raise_error ~pos:None ("Unknown encoder parameter: " ^ s) | `Labelled (l, v) -> raise_error ~pos:(Value.pos v) (Printf.sprintf "unknown parameter name (%s) or invalid parameter value (%s)" l (Value.to_string v)) | `Encoder _ -> raise_error ~pos:None "unexpected subencoder" (* An encoder. *) type encoder = { (* Compute the kind of the encoder. *) type_of_encoder : Term.encoder_params -> Type.t Frame.Fields.t; (* Actually create the encoder. *) make : Hooks.encoder_params -> Encoder.format; } let encoders = ref [] (** Register an encoder. *) let register name type_of_encoder make = encoders := (name, { type_of_encoder; make }) :: !encoders (** Find an encoder with given name. *) let find_encoder name = List.assoc name !encoders let channels_of_params ?(default = 2) p = match List.find_map (function | `Anonymous s when String.lowercase_ascii s = "mono" -> Some 1 | `Anonymous s when String.lowercase_ascii s = "stereo" -> Some 2 | `Labelled ("stereo", { term = `Bool b; _ }) -> Some (if b then 2 else 1) | `Labelled ("stereo", ({ t = { Type.pos } } as tm)) -> raise_error ~pos (Printf.sprintf "Invalid value %s for stereo mode. Only static `true` or \ `false` are allowed." (Term.to_string tm)) | `Labelled ("mono", { term = `Bool b; _ }) -> Some (if b then 1 else 2) | `Labelled ("mono", ({ t = { Type.pos } } as tm)) -> raise_error ~pos (Printf.sprintf "Invalid value %s for mono mode. Only static `true` or \ `false` are allowed." (Term.to_string tm)) | `Labelled ("channels", { term = `Int n }) -> Some n | `Labelled ("channels", ({ t = { Type.pos } } as tm)) -> raise_error ~pos (Printf.sprintf "Invalid value %s for channels mode. Only static numbers are \ allowed." (Term.to_string tm)) | _ -> None) p with | Some n -> n | None -> default (** Compute a kind from a non-fully evaluated format. This should give the same result than [Encoder.type_of_format] once evaluated... *) let type_of_encoder ((e, p) : Term.encoder) = (find_encoder e).type_of_encoder p let type_of_encoder ~pos e = let fields = type_of_encoder e in let frame_t = Frame_type.make Liquidsoap_lang.Lang.unit_t fields in L.format_t ?pos frame_t let make_encoder ~pos ((e, p) : Hooks.encoder) = try let e = (find_encoder e).make p in let (_ : Encoder.factory) = Encoder.get_factory e in V.to_value ?pos e with Not_found -> raise_error ~pos "unsupported format" liquidsoap-2.3.2/src/core/error.ml000066400000000000000000000000361477303350200170750ustar00rootroot00000000000000include Liquidsoap_lang.Error liquidsoap-2.3.2/src/core/file_watcher.inotify.ml000066400000000000000000000057141477303350200220700ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Event to watch. *) type event = [ `Modify ] (** Type for unwatching function. *) type unwatch = unit -> unit (** Type for watching function. *) type watch = pos:Liquidsoap_lang.Pos.t list -> event list -> string -> (unit -> unit) -> unwatch let fd = ref (None : Unix.file_descr option) let handlers = ref [] let m = Mutex.create () let log = Log.make ["inotify"] let rec watchdog () = let fd = Option.get !fd in let handler = Mutex_utils.mutexify m (fun _ -> let events = Inotify.read fd in List.iter (fun (wd, _, _, _) -> match List.assoc wd !handlers with | f -> f () | exception Not_found -> ( try Inotify.rm_watch fd wd with _ -> ())) events; [watchdog ()]) in { Duppy.Task.priority = `Maybe_blocking; events = [`Read fd]; handler } let watch : watch = fun ~pos e file f -> if not (Sys.file_exists file) then Lang.raise_error ~pos "not_found"; Mutex_utils.mutexify m (fun () -> if !fd = None then ( fd := Some (Inotify.create ()); Duppy.Task.add Tutils.scheduler (watchdog ())); let fd = Option.get !fd in let event_conv = function | `Modify -> [ Inotify.S_Moved_to; Inotify.S_Moved_from; Inotify.S_Delete; Inotify.S_Create; ] @ if Sys.is_directory file then [] else [Inotify.S_Modify] in let e = List.flatten (List.map event_conv e) in let wd = Inotify.add_watch fd file e in handlers := (wd, f) :: !handlers; Mutex_utils.mutexify m (fun () -> (try Inotify.rm_watch fd wd with exn -> let bt = Printexc.get_backtrace () in Utils.log_exception ~log ~bt (Printf.sprintf "Error while removing file watch handler: %s" (Printexc.to_string exn))); handlers := List.remove_assoc wd !handlers)) () liquidsoap-2.3.2/src/core/file_watcher.mtime.ml000066400000000000000000000055731477303350200215250ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Event to watch. *) type event = [ `Modify ] (** Type for unwatching function. *) type unwatch = unit -> unit (** Type for watching function. *) type watch = pos:Liquidsoap_lang.Pos.t list -> event list -> string -> (unit -> unit) -> unwatch type watched_files = { file : string; callback : unit -> unit; mutable mtime : float; } let log = Log.make ["file_watcher"; "native"] let launched = ref false let watched = ref [] let m = Mutex.create () let file_mtime file = (Unix.stat file).Unix.st_mtime let rec handler _ = Mutex_utils.mutexify m (fun () -> List.iter (fun ({ file; callback; mtime } as w) -> try let mtime' = try file_mtime file with _ -> mtime in if mtime' <> mtime then callback (); w.mtime <- mtime' with exn -> let bt = Printexc.get_backtrace () in Utils.log_exception ~log ~bt (Printf.sprintf "Error while executing file watcher callback: %s" (Printexc.to_string exn))) !watched; [{ Duppy.Task.priority = `Maybe_blocking; events = [`Delay 1.]; handler }]) () let watch : watch = fun ~pos e file callback -> if not (Sys.file_exists file) then Lang.raise_error ~pos "not_found"; if List.mem `Modify e then Mutex_utils.mutexify m (fun () -> if not !launched then begin launched := true; Duppy.Task.add Tutils.scheduler { Duppy.Task.priority = `Maybe_blocking; events = [`Delay 1.]; handler; } end; let mtime = try file_mtime file with _ -> 0. in watched := { file; mtime; callback } :: !watched; let unwatch = Mutex_utils.mutexify m (fun () -> watched := List.filter (fun w -> w.file <> file) !watched) in unwatch) () else fun () -> () liquidsoap-2.3.2/src/core/harbor/000077500000000000000000000000001477303350200166705ustar00rootroot00000000000000liquidsoap-2.3.2/src/core/harbor/harbor.ml000066400000000000000000001224321477303350200205030ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2016 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Harbor_base module Monad = Duppy.Monad module Type = Liquidsoap_lang.Type module Http = Liq_http let ( let* ) = Duppy.Monad.bind module type Monad_t = module type of Monad with module Io := Monad.Io module type Transport_t = sig type socket = Http.socket val file_descr_of_socket : socket -> Unix.file_descr val read : socket -> bytes -> int -> int -> int val write : socket -> bytes -> int -> int -> int val close : socket -> unit module Duppy : sig module Io : Duppy.Io_t with type socket = socket module Monad : sig module Io : Duppy.Monad.Monad_io_t with type socket = socket and module Io = Io include Monad_t end end module Websocket : Websocket.Websocket_t with type socket = socket end module Http_transport = struct type socket = Http.socket let file_descr_of_socket socket = socket#file_descr let read socket = socket#read let write socket = socket#write let close socket = socket#close module Duppy_transport : Duppy.Transport_t with type t = Http.socket = struct type t = Http.socket type bigarray = (char, Bigarray.int8_unsigned_elt, Bigarray.c_layout) Bigarray.Array1.t let sock socket = socket#file_descr let read = read let write = write let ba_write _ _ _ _ = failwith "Not implemented!" end module Duppy = struct module Io = Duppy.MakeIo (Duppy_transport) module Monad = struct module Io = Duppy.Monad.MakeIo (Io) include (Monad : Monad_t) end end module Websocket_transport = struct type socket = Http.socket let read = read let read_retry socket = Extralib.read_retry socket#read let write = write end module Websocket = Websocket.Make (Websocket_transport) end module type T = sig type socket exception Retry exception Assoc of string exception Not_authenticated exception Unknown_codec exception Mount_taken exception Websocket_closed exception Protocol_not_supported of string (* Generic *) val file_descr_of_socket : socket -> Unix.file_descr val read : socket -> bytes -> int -> int -> int val write : socket -> bytes -> int -> int -> int val close : socket -> unit (* Http Server *) type http_verb = [ `Get | `Post | `Put | `Delete | `Head | `Options ] type reply = | Close of (unit -> string) | Relay of string * (unit -> unit) | Custom type http_handler = protocol:string -> meth:http_verb -> data:(float -> string) -> headers:(string * string) list -> query:(string * string) list -> socket:socket -> string -> (reply, reply) Duppy.Monad.t val verb_of_string : string -> http_verb val string_of_verb : http_verb -> string val mk_simple : string -> unit -> string val simple_reply : string -> ('a, reply) Duppy.Monad.t val reply : (unit -> string) -> ('a, reply) Duppy.Monad.t val custom : unit -> ('a, reply) Duppy.Monad.t val add_http_handler : pos:Liquidsoap_lang.Pos.t list -> transport:Http.transport -> port:int -> verb:http_verb -> uri:Lang.regexp -> http_handler -> unit val remove_http_handler : port:int -> verb:http_verb -> uri:Lang.regexp -> unit -> unit (* Source input *) class virtual source : object inherit Source.source method virtual relay : string -> (string * string) list -> ?read:(socket -> bytes -> int -> int -> int) -> socket -> unit method virtual insert_metadata : Frame.metadata -> unit method virtual login : string * (socket:socket -> string -> string -> bool) method virtual icy_charset : string option method virtual meta_charset : string option method virtual get_mime_type : string option end val http_auth_check : ?query:(string * string) list -> login:string * (socket:socket -> string -> string -> bool) -> socket -> (string * string) list -> (unit, reply) Duppy.Monad.t val relayed : string -> (unit -> unit) -> ('a, reply) Duppy.Monad.t val add_source : pos:Liquidsoap_lang.Pos.t list -> transport:Http.transport -> port:int -> mountpoint:string -> icy:bool -> source -> unit val remove_source : port:int -> mountpoint:string -> unit -> unit end module Make (T : Transport_t) : T with type socket = T.socket = struct module Websocket = T.Websocket module Task = Duppy.Task module Duppy = T.Duppy type socket = T.socket let file_descr_of_socket = T.file_descr_of_socket let read = T.read let write = T.write let close = T.close let protocol_name h = Printf.sprintf "%s/%s" h.Duppy.Monad.Io.socket#transport#protocol h.Duppy.Monad.Io.socket#transport#name (* Define what we need as a source *) (** Raised when source needs to retry read. *) exception Retry class virtual source = object inherit Source.source ~name:"input.harbor" () method virtual relay : string -> (string * string) list -> ?read:(socket -> bytes -> int -> int -> int) -> socket -> unit method virtual insert_metadata : Frame.metadata -> unit method virtual login : string * (socket:socket -> string -> string -> bool) method virtual icy_charset : string option method virtual meta_charset : string option method virtual get_mime_type : string option end type sources = (string, source) Hashtbl.t type http_verb = [ `Get | `Post | `Put | `Delete | `Head | `Options ] type source_type = [ `Put | `Post | `Source | `Xaudiocast | `Shout ] type verb = [ `Get | `Post | `Put | `Delete | `Head | `Options | `Source | `Shout ] let verb_of_string s = match String.uppercase_ascii s with | "GET" -> `Get | "POST" -> `Post | "PUT" -> `Put | "DELETE" -> `Delete | "HEAD" -> `Head | "OPTIONS" -> `Options | _ -> raise Not_found let verb_or_source_of_string s = match String.uppercase_ascii s with | "SOURCE" -> `Source | _ -> verb_of_string s let string_of_verb = function | `Get -> "GET" | `Post -> "POST" | `Put -> "PUT" | `Delete -> "DELETE" | `Head -> "HEAD" | `Options -> "OPTIONS" | _ -> assert false let string_of_any_verb = function | `Source -> "SOURCE" | `Shout -> "ICY" | `Get -> "GET" | `Post -> "POST" | `Put -> "PUT" | `Delete -> "DELETE" | `Head -> "HEAD" | `Options -> "OPTIONS" type protocol = [ `Http_10 | `Http_11 | `Ice_10 | `Icy | `Xaudiocast_uri of string | `Websocket ] let string_of_protocol : protocol -> string = function | `Http_10 -> "HTTP/1.0" | `Http_11 -> "HTTP/1.1" | `Ice_10 -> "ICE/1.0" | `Icy -> "ICY" | `Xaudiocast_uri uri -> Printf.sprintf "X-AUDIOCAST (%s)" uri | `Websocket -> "WEBSOCKET" type reply = | Close of (unit -> string) | Relay of string * (unit -> unit) | Custom let mk_simple s = let ret = Atomic.make s in fun () -> Atomic.exchange ret "" let simple_reply s = Duppy.Monad.raise (Close (mk_simple s)) let reply s = Duppy.Monad.raise (Close s) let relayed s f = Duppy.Monad.raise (Relay (s, f)) let custom () = Duppy.Monad.raise Custom type http_handler = protocol:string -> meth:http_verb -> data:(float -> string) -> headers:(string * string) list -> query:(string * string) list -> socket:socket -> string -> (reply, reply) Duppy.Monad.t type http_handlers = (http_verb * Lang.regexp * http_handler) list Atomic.t type handler = { sources : sources; http : http_handlers } type open_port = { handler : handler; transport : Http.transport; fds : Unix.file_descr list; } let opened_ports : (int, open_port) Hashtbl.t = Hashtbl.create 1 let find_handler = Hashtbl.find opened_ports let find_source mount port = Hashtbl.find (find_handler port).handler.sources mount exception Assoc of string let assoc_uppercase x y = try List.iter (fun (l, v) -> if String.uppercase_ascii l = x then raise (Assoc v) else ()) y; raise Not_found with Assoc s -> s exception Not_authenticated exception Unknown_codec exception Mount_taken let http_error_page code status msg = "HTTP/1.0 " ^ string_of_int code ^ " " ^ status ^ "\r\n\ Content-Type: text/html\r\n\ \r\n\ \n\ \n\ Liquidsoap source \ harbor

    " ^ msg ^ "

    " (* The error numbers are specified here: http://tools.ietf.org/html/rfc6455#section-7.4.1 *) let websocket_error n msg = Websocket.to_string (`Close (Some (n, msg))) let parse_icy_request_line ~port h r = let { Liq_http.user = requested_user; password } = try Liq_http.parse_auth r with Not_found -> { Liq_http.user = ""; password = r } in let* s = try Duppy.Monad.return (find_source "/" (port - 1)) with Not_found -> log#info "ICY error: no / mountpoint"; simple_reply "No / mountpoint\r\n\r\n" in (* Authentication can be blocking. *) Duppy.Monad.Io.exec ~priority:`Maybe_blocking h (let user, auth_f = s#login in let user = if requested_user = "" then user else requested_user in if auth_f ~socket:h.Duppy.Monad.Io.socket user password then Duppy.Monad.return (`Shout, "/", `Icy) else ( log#info "ICY error: invalid password"; simple_reply "Invalid password\r\n\r\n")) exception Protocol_not_supported of string let parse_http_request_line r = try let data = Re.Pcre.split ~rex:(Re.Pcre.regexp "[ \t]+") r in let protocol = verb_or_source_of_string (List.nth data 0) in Duppy.Monad.return ( protocol, List.nth data 1, match String.uppercase_ascii (List.nth data 2) with | "HTTP/1.0" -> `Http_10 | "HTTP/1.1" -> `Http_11 | "ICE/1.0" -> `Ice_10 | s when protocol = `Source -> `Xaudiocast_uri s | s -> raise (Protocol_not_supported s) ) with | Protocol_not_supported p -> log#info "Protocol not supported for request %s: %s" r p; simple_reply "HTTP 505 Protocol Not Supported\r\n\r\n" | e -> log#info "Invalid request line %s: %s" r (Printexc.to_string e); simple_reply "HTTP 500 Invalid request\r\n\r\n" let parse_headers headers = let split_header h l = try let rex = Re.Pcre.regexp "([^:\\r\\n]+):\\s*([^\\r\\n]+)" in let sub = Re.Pcre.exec ~rex h in (Re.Pcre.get_substring sub 1, Re.Pcre.get_substring sub 2) :: l with Not_found -> l in let f x = String.uppercase_ascii x in let headers = List.fold_right split_header headers [] in let display_headers = List.filter (fun (x, _) -> conf_pass_verbose#get || f x <> "AUTHORIZATION") headers in List.iter (fun (h, v) -> log#info "Header: %s, value: %s." h v) display_headers; headers let auth_check ~auth_f socket user pass = (* OK *) if conf_pass_verbose#get then log#info "Requested username: %s, password: %s." user pass else (); if not (auth_f ~socket user pass) then raise Not_authenticated else (); log#info "Client logged in."; Duppy.Monad.return () let http_auth_check ?query ~login socket headers = (* 401 error model *) let http_reply s = simple_reply (http_error_page 401 "Unauthorized\r\nWWW-Authenticate: Basic realm=\"Liquidsoap harbor\"" s) in let valid_user, auth_f = login in try let user, pass = try (* HTTP authentication *) let auth = assoc_uppercase "AUTHORIZATION" headers in let data = Re.Pcre.split ~rex:(Re.Pcre.regexp "[ \t]+") auth in match data with | "Basic" :: x :: _ -> let { Liq_http.user; password } = Liq_http.parse_auth (Lang_string.decode64 x) in (user, password) | _ -> raise Not_found with Not_found -> ( match query with | Some query -> (* ICY updates are done with * password sent in GET args * and user being valid_user * or user, if given. *) let user = try List.assoc "user" query with Not_found -> valid_user in (user, List.assoc "pass" query) | _ -> raise Not_found) in auth_check ~auth_f socket user pass with | Not_authenticated -> log#info "Returned 401: wrong auth."; http_reply "Wrong Authentication data" | Not_found -> log#info "Returned 401: bad authentication."; http_reply "No login / password supplied." let exec_http_auth_check ?args ~login h headers = let query = Option.map (fun query -> Hashtbl.fold (fun lbl k query -> (lbl, k) :: query) query []) args in Duppy.Monad.Io.exec ~priority:`Maybe_blocking h (http_auth_check ?query ~login h.Duppy.Monad.Io.socket headers) (* We do not implement anything with this handler for now. *) let handle_asterisk_options_request ~hprotocol:_ ~headers:_ _ = log#info "Returned 405: Method Not Allowed"; simple_reply "HTTP/1.0 405 Method Not Allowed\r\n\ Allow: OPTIONS, GET, POST, PUT\r\n\ \r\n" let handle_source_request ~port ~auth ~smethod hprotocol h uri headers = (* ICY request are on port+1 *) let source_port = if smethod = `Shout then port - 1 else port in let* s = try Duppy.Monad.return (find_source uri source_port) with Not_found -> log#info "Request failed: no mountpoint '%s'!" uri; simple_reply (http_error_page 404 "Not found" "This mountpoint isn't available.") in let* () = if (* ICY and Xaudiocast auth check was done before.. *) not auth then exec_http_auth_check ~login:s#login h headers else Duppy.Monad.return () in try let sproto = match (smethod : source_type) with | `Shout -> "ICY" | `Source -> "SOURCE" | `Put -> "PUT (source)" | `Post -> "POST (source)" | `Xaudiocast -> "X-AUDIOCAST" in log#info "%s request on %s." sproto uri; let stype = try assoc_uppercase "CONTENT-TYPE" headers with | Not_found when smethod = `Shout || smethod = `Xaudiocast -> "audio/mpeg" | Not_found -> raise Unknown_codec in let chunked = try assoc_uppercase "TRANSFER-ENCODING" headers = "chunked" with Not_found -> false in let read = if chunked then ( let buf = Buffer.create Utils.pagesize in let read connection b ofs len = if Buffer.length buf < len then ( let s, len = Http.read_chunked ~timeout:conf_timeout#get connection in Buffer.add_substring buf s 0 len); let len = min len (Buffer.length buf) in Buffer.blit buf 0 b ofs len; Utils.buffer_drop buf len; len in Some read) else None in let f () = s#relay ?read stype headers h.Duppy.Monad.Io.socket in log#info "Adding source on mountpoint %S with type %S." uri stype; log#debug "Relaying %s." (string_of_protocol hprotocol); let protocol = match hprotocol with | `Icy -> "ICY" | `Ice_10 | `Http_10 -> "HTTP/1.0" | `Http_11 -> "HTTP/1.1" | _ -> assert false in relayed (Printf.sprintf "%s 200 OK\r\n\r\n" protocol) f with | Mount_taken -> log#info "Returned 403: Mount taken"; simple_reply (http_error_page 403 "Mountpoint already taken\r\n\ WWW-Authenticate: Basic realm=\"Liquidsoap harbor\"" "Mountpoint in use") | Not_found -> log#info "Returned 404 for '%s'." uri; simple_reply (http_error_page 404 "Not found" "This mountpoint isn't available.") | Unknown_codec -> log#info "Returned 501: unknown audio codec"; simple_reply (http_error_page 501 "Not Implemented" "This stream's format is not recognized.") | e -> log#info "Returned 500 for '%s': %s" uri (Printexc.to_string e); simple_reply (http_error_page 500 "Internal Server Error" "The server could not handle your request.") exception Websocket_closed let handle_websocket_request ~port h mount headers = let json_string_of = function `String s -> s | _ -> raise Not_found in let extract_packet s = let json = match Json.from_string s with | `Assoc json -> json | _ -> raise Not_found in let packet_type = match List.assoc "type" json with | `String s -> s | _ -> raise Not_found in let data = match List.assoc "data" json with `Assoc data -> Some data | _ -> None in (packet_type, data) in let websocket_read = Websocket.read () in let read_hello s = let error () = simple_reply (websocket_error 1002 "Invalid hello.") in try match websocket_read s with | `Text s -> ( log#debug "Hello packet: %s\n%!" s; match extract_packet s with | "hello", data -> let data = Option.get data in let mime = json_string_of (List.assoc "mime" data) in let user = json_string_of (List.assoc "user" data) in let password = json_string_of (List.assoc "password" data) in Duppy.Monad.return (mime, mount, user, password) | _ -> error ()) | _ -> error () with _ -> error () in let* () = Duppy.Monad.Io.write ?timeout:(Some conf_timeout#get) ~priority:`Non_blocking h (Bytes.of_string (Websocket.upgrade headers)) in let* stype, huri, user, password = Duppy.Monad.Io.exec ~priority:`Blocking h (read_hello h.Duppy.Monad.Io.socket) in log#info "Mime type: %s" stype; log#info "Mount point: %s" huri; let* source = try Duppy.Monad.return (find_source huri port) with Not_found -> log#info "Request failed: no mountpoint '%s'!" huri; simple_reply (websocket_error 1011 "This mountpoint isn't available.") in let _, auth_f = source#login in let* () = try auth_check ~auth_f h.Duppy.Monad.Io.socket user password with Not_authenticated -> log#info "Authentication failed!"; simple_reply (websocket_error 1011 "Authentication failed.") in let binary_data = Buffer.create Utils.pagesize in let read_socket socket = match websocket_read socket with | `Binary buf -> Buffer.add_string binary_data buf | `Text s -> ( match extract_packet s with | "metadata", data -> log#debug "Metadata packet: %s\n%!" s; let data = Option.get data in let m = List.map (fun (l, v) -> (l, json_string_of v)) data in let m = Frame.Metadata.from_list m in source#insert_metadata m; raise Retry | _ -> raise Retry) | `Close _ -> raise Websocket_closed | _ -> raise Retry in let read socket buf ofs len = if Buffer.length binary_data = 0 then read_socket socket else (); let len = min (Buffer.length binary_data) len in Buffer.blit binary_data 0 buf ofs len; Utils.buffer_drop binary_data len; len in let f () = source#relay stype headers ~read h.Duppy.Monad.Io.socket in relayed "" f exception Handled of (http_verb * (string * string) list * http_handler) let ans_404 uri = log#info "Returned 404 for '%s'." uri; simple_reply (http_error_page 404 "Not found" "This page isn't available.") let ans_500 uri = log#info "Returned 500 for '%s'." uri; simple_reply (http_error_page 500 "Internal Server Error" "There was an error processing your request.") let ans_400 ~uri s = log#info "Returned 400 for '%s': %s." uri s; simple_reply (http_error_page 400 "Bad Request" s) let admin ~icy ~port ~uri ~headers ~args h = let* mode = try Duppy.Monad.return (Hashtbl.find args "mode") with Not_found -> ans_400 ~uri "unrecognised command" in let len = try int_of_string (assoc_uppercase "CONTENT-LENGTH" headers) with _ -> 0 in let* data = if len > 0 then Duppy.Monad.Io.read ?timeout:(Some conf_timeout#get) ~priority:`Non_blocking ~marker:(Duppy.Io.Length len) h else Duppy.Monad.return "" in (try if assoc_uppercase "CONTENT-TYPE" headers = "application/x-www-form-urlencoded" then Hashtbl.iter (Hashtbl.replace args) (Http.args_split data) with Not_found -> ()); match mode with | "updinfo" -> let mount = try Hashtbl.find args "mount" with Not_found -> "/" in log#info "Request to update metadata for mount %s on port %i" mount port; let* s = try Duppy.Monad.return (find_source mount port) with Not_found -> ans_400 ~uri "Source is not available" in let* () = exec_http_auth_check ~args ~login:s#login h headers in let* () = if not (List.mem (Option.value ~default:"" s#get_mime_type) conf_icy_metadata#get) then ( log#info "Returned 405 for '%s': Source format does not support ICY \ metadata update" uri; simple_reply (http_error_page 405 "Method Not Allowed" "Method Not Allowed")) else Duppy.Monad.return () in Hashtbl.remove args "mount"; Hashtbl.remove args "mode"; let in_enc = try let enc = Charset.of_string (Hashtbl.find args "charset") in Hashtbl.remove args "charset"; Some enc with Not_found -> (if icy then s#icy_charset else s#meta_charset) |> Option.map Charset.of_string in (* Recode tags.. *) let g x = Charset.convert ?source:in_enc x in let f x y m = let add, x = match x with | "song" when not conf_map_song_metadata#get -> (true, "song") | "song" -> ( not (Hashtbl.mem args "title" || Hashtbl.mem args "artist"), "title" ) | "url" -> (true, "metadata_url") | _ -> (true, x) in if add then Frame.Metadata.add (g x) (g y) m else m in let args = Hashtbl.fold f args Frame.Metadata.empty in s#insert_metadata args; simple_reply (Printf.sprintf "HTTP/1.0 200 OK\r\n\r\nUpdated metadatas for mount %s" mount) | _ -> ans_500 uri let handle_http_request ~hmethod ~hprotocol ~port h uri headers = let rex = Re.Pcre.regexp "^(.+)\\?(.+)$" in let base_uri, args = try let sub = Re.Pcre.exec ~rex uri in (Re.Pcre.get_substring sub 1, Re.Pcre.get_substring sub 2) with Not_found -> (uri, "") in let smethod = string_of_verb hmethod in log#info "%s %s request on %s." (protocol_name h) smethod base_uri; let args = Http.args_split args in (* Filter out password *) let log_args = if conf_pass_verbose#get then args else ( let log_args = Hashtbl.copy args in Hashtbl.remove log_args "pass"; log_args) in Hashtbl.iter (log#info "%s Arg: %s, value: %s." (protocol_name h)) log_args; (* First, try with a registered handler. *) let { handler; _ } = find_handler port in let f (verb, regex, handler) = let rex = regex.Liquidsoap_lang.Builtins_regexp.regexp in let sub = Lazy.from_fun (fun () -> try Some (Re.Pcre.exec ~rex base_uri) with _ -> None) in if (verb :> verb) = hmethod && Lazy.force sub <> None then ( let sub = Option.get (Lazy.force sub) in let groups = List.fold_left (fun groups name -> try (name, Re.Pcre.get_named_substring rex name sub) :: groups with Not_found -> groups) [] (Array.to_list (Re.Pcre.names rex)) in log#info "Found handler '%s %s' on port %d%s." smethod (Lang.descr_of_regexp regex) port (match groups with | [] -> "" | groups -> Printf.sprintf " with matches: %s" (String.concat ", " (List.map (fun (lbl, v) -> [%string "%{lbl}: %{v}"]) groups))); raise (Handled (verb, groups, handler))) else () in try List.iter f (Atomic.get handler.http); (* Otherwise, try with a standard handler. *) let is_admin s = try String.sub s 0 6 = "/admin" with _ -> false in match base_uri with (* Icecast *) | "/admin/metadata" -> admin ~icy:false ~port ~uri ~headers ~args h (* Shoutcast *) | "/admin.cgi" -> admin ~icy:true ~port ~uri ~headers ~args h | s when is_admin s -> ans_400 ~uri "unrecognised command" | _ -> ans_404 uri with | Handled (meth, groups, handler) -> let protocol = match hprotocol with | `Http_10 -> "1.0" | `Http_11 -> "1.1" | _ -> assert false in let query = groups @ Hashtbl.fold (fun lbl k query -> (lbl, k) :: query) args [] in let headers = List.map (fun (k, v) -> (String.lowercase_ascii k, v)) headers in let content_type = try Some (assoc_uppercase "CONTENT-TYPE" headers) with _ -> None in let content_length = try Some (int_of_string (assoc_uppercase "CONTENT-LENGTH" headers)) with _ -> None in let transfer_encoding = try Some (assoc_uppercase "TRANSFER-ENCODING" headers) with _ -> None in let socket : Http.socket = let rem_data = h.Duppy.Monad.Io.data in let rem_len = String.length rem_data in let rem_ofs = Atomic.make 0 in let socket = h.Duppy.Monad.Io.socket in object method typ = socket#typ method transport = socket#transport method file_descr = socket#file_descr method write = socket#write method close = socket#close method wait_for ?log event timeout = match (event, Atomic.get rem_ofs) with | `Read, ofs when ofs < rem_len -> () | _ -> socket#wait_for ?log event timeout method read buf dst_ofs len = if Atomic.get rem_ofs < rem_len then ( let src_ofs = Atomic.get rem_ofs in let len = min (rem_len - src_ofs) len in Bytes.blit_string rem_data src_ofs buf dst_ofs len; Atomic.set rem_ofs (src_ofs + len); len) else socket#read buf dst_ofs len end in let data = match (content_type, content_length, transfer_encoding) with | _, Some len, _ when len > 0 -> let buflen = min len 4096 in let len = Atomic.make len in fun timeout -> let ret = Http.read ~timeout socket (min (Atomic.get len) buflen) in Atomic.set len (Atomic.get len - String.length ret); ret | _, _, Some "chunked" -> fun timeout -> fst (Http.read_chunked ~timeout socket) | _ -> fun _ -> "" in Duppy.Monad.Io.exec ~priority:`Maybe_blocking h (handler ~protocol ~meth ~headers ~data ~socket:h.Duppy.Monad.Io.socket ~query base_uri) | e -> let bt = Printexc.get_backtrace () in Utils.log_exception ~log ~bt (Printf.sprintf "%s %s request on uri '%s' failed: %s" (protocol_name h) smethod (Printexc.to_string e) uri); ans_500 uri let handle_client ~port ~icy h = (* Read and process lines *) let* s = Duppy.Monad.Io.read ?timeout:(Some conf_timeout#get) ~priority:`Non_blocking ~marker: (match icy with | true -> Duppy.Io.Split "[\r]?\n" | false -> Duppy.Io.Split "[\r]?\n[\r]?\n") h in let lines = Re.Pcre.split ~rex:(Re.Pcre.regexp "[\r]?\n") s in let* hmethod, huri, hprotocol = let s = List.hd lines in if icy then parse_icy_request_line ~port h s else parse_http_request_line s in log#info "Method: %s, uri: %s, protocol: %s" (string_of_any_verb hmethod) huri (string_of_protocol hprotocol); let headers = parse_headers (List.tl lines) in let hprotocol = try if hmethod = `Get && assoc_uppercase "UPGRADE" headers <> "websocket" then raise Exit else (); if assoc_uppercase "SEC-WEBSOCKET-PROTOCOL" headers <> "webcast" then raise Exit else (); `Websocket with _ -> hprotocol in let is_source = try ignore (find_source huri port); true with Not_found -> false in let handle_source smethod = let* auth, huri, smethod = (* X-audiocast sends lines of the form: [SOURCE password path] *) match hprotocol with | `Xaudiocast_uri uri -> let password = huri in (* We check authentication here *) let* s = try Duppy.Monad.return (find_source uri port) with Not_found -> log#info "Request failed: no mountpoint '%s'!" uri; simple_reply (http_error_page 404 "Not found" "This mountpoint isn't available.") in (* Authentication can be blocking *) Duppy.Monad.Io.exec ~priority: (* ICY = true means that authentication has already happened *) `Maybe_blocking h (let valid_user, auth_f = s#login in if not (auth_f ~socket:h.Duppy.Monad.Io.socket valid_user password) then simple_reply "Invalid password!" else Duppy.Monad.return (true, uri, `Xaudiocast)) | _ -> Duppy.Monad.return (false, huri, smethod) in handle_source_request ~port ~auth ~smethod hprotocol h huri headers in match hmethod with | `Put when is_source -> handle_source `Put | `Post when is_source -> handle_source `Post | `Source when not icy -> handle_source `Source | `Get when hprotocol = `Websocket -> handle_websocket_request ~port h huri headers | `Options when huri = "*" -> handle_asterisk_options_request ~hprotocol ~headers h | (`Get | `Post | `Put | `Delete | `Options | `Head) when not icy -> handle_http_request ~hmethod ~hprotocol ~port h huri headers | `Shout when icy -> let* () = Duppy.Monad.Io.write ?timeout:(Some conf_timeout#get) ~priority:`Non_blocking h (Bytes.of_string "OK2\r\nicy-caps:11\r\n\r\n") in (* Now parsing headers *) let* s = Duppy.Monad.Io.read ?timeout:(Some conf_timeout#get) ~priority:`Non_blocking ~marker:(Duppy.Io.Split "[\r]?\n[\r]?\n") h in let lines = Re.Pcre.split ~rex:(Re.Pcre.regexp "[\r]?\n") s in let headers = parse_headers lines in handle_source_request ~port ~auth:true ~smethod:`Shout hprotocol h huri headers | _ -> log#info "Returned 501: not implemented"; simple_reply (http_error_page 501 "Not Implemented" "The server did not understand your request.") (* {1 The server} *) (* Open a port and listen to it. *) let open_port ~transport ~icy port = log#info "Opening port %d with icy = %b" port icy; let max_conn = conf_harbor_max_conn#get in let server = transport#server in let process_client sock = try let socket, caller = server#accept ?timeout:(Some conf_accept_timeout#get) sock in let ip = Utils.name_of_sockaddr ~rev_dns:conf_revdns#get caller in log#info "New client on port %i: %s" port ip; let unix_socket = T.file_descr_of_socket socket in Unix.setsockopt unix_socket Unix.TCP_NODELAY true; let on_error e = (match e with | Duppy.Io.Io_error -> log#info "Client %s disconnected" ip | Duppy.Io.Timeout -> log#info "Timeout while communicating with client %s." ip | Duppy.Io.Unix (c, p, m) -> log#info "%s" (Printexc.to_string (Unix.Unix_error (c, p, m))) | Duppy.Io.Unknown e -> log#info "%s" (Printexc.to_string e)); (* Sending an HTTP response in case of timeout * even though ICY connections are not HTTP.. *) if e = Duppy.Io.Timeout then Close (mk_simple (http_error_page 408 "Request Time-out" "The server timed out waiting for the request.")) else Close (mk_simple "") in let h = { Duppy.Monad.Io.scheduler = Tutils.scheduler; socket; data = ""; on_error; } in let rec reply r = let close () = try close socket with _ -> () in let s, exec = match r with | Custom -> ("", fun () -> ()) | Relay (s, exec) -> (s, exec) | Close fn -> let s = fn () in let exec = if s = "" then close else fun () -> reply (Close fn) in (s, exec) in let on_error e = ignore (on_error e); close () in Duppy.Io.write ~timeout:conf_timeout#get ~priority:`Non_blocking ~on_error ~string:(Bytes.of_string s) ~exec Tutils.scheduler socket in Duppy.Monad.run ~return:reply ~raise:reply (handle_client ~port ~icy h) with e -> let bt = Printexc.get_backtrace () in Utils.log_exception ~log ~bt (Printf.sprintf "Failed to accept new client: %s" (Printexc.to_string e)) in let rec incoming ~port ~icy events (out_s : Unix.file_descr) e = if List.mem (`Read out_s) e then ( try List.iter (function `Read s -> Unix.close s | _ -> assert false) events; [] with _ -> []) else ( let get_sock = function `Read sock -> sock | _ -> assert false in List.iter process_client (List.map get_sock e); [ { Task.priority = `Non_blocking; events; handler = incoming ~port ~icy events out_s; }; ]) in let open_socket port bind_addr = let bind_addr_inet = Unix.inet_addr_of_string bind_addr in let bind_addr = Unix.ADDR_INET (bind_addr_inet, port) in let domain = Unix.domain_of_sockaddr bind_addr in let sock = Unix.socket ~cloexec:true domain Unix.SOCK_STREAM 0 in (* Set TCP_NODELAY on the socket *) Unix.setsockopt sock Unix.SO_REUSEADDR true; Unix.setsockopt sock Unix.TCP_NODELAY true; Unix.bind sock bind_addr; Unix.listen sock max_conn; `Read sock in let bind_addrs = List.fold_left (fun cur bind_addr -> if bind_addr <> "" then bind_addr :: cur else cur) [] conf_harbor_bind_addrs#get in let in_s, out_s = Unix.pipe ~cloexec:true () in let events = `Read in_s :: List.map (open_socket port) bind_addrs in Task.add Tutils.scheduler { Task.priority = `Non_blocking; events; handler = incoming ~port ~icy events in_s; }; out_s (* This, contrary to the find_xx functions creates the handlers when they are missing. *) let get_handler ~pos ~transport ~icy port = try let { handler; fds; transport = t } = Hashtbl.find opened_ports port in if transport#name <> t#name then Lang.raise_error ~pos ~message:"Port is already opened with a different transport" "http"; (* If we have only one socket and icy=true, * we need to open a second one. *) if List.length fds = 1 && icy then ( let fds = open_port ~transport ~icy (port + 1) :: fds in Hashtbl.replace opened_ports port { handler; fds; transport }) else (); handler with Not_found -> (* First the port without icy *) let fds = [open_port ~transport ~icy:false port] in (* Now the port with icy, is requested.*) let fds = if icy then open_port ~transport ~icy (port + 1) :: fds else fds in let handler = { sources = Hashtbl.create 1; http = Atomic.make [] } in Hashtbl.replace opened_ports port { handler; fds; transport }; handler (* Add sources... This is tied up to sources lifecycle so no need to prevent early start *) let add_source ~pos ~transport ~port ~mountpoint ~icy source = let sources = let handler = get_handler ~pos ~transport ~icy port in if Hashtbl.mem handler.sources mountpoint then Lang.raise_error ~pos ~message:"Mountpoint is already taken!" "http" else (); handler.sources in log#important "Adding mountpoint '%s' on port %i" mountpoint port; Hashtbl.replace sources mountpoint source (* Remove source. *) let remove_source ~port ~mountpoint () = let { handler; fds; _ } = Hashtbl.find opened_ports port in assert (Hashtbl.mem handler.sources mountpoint); log#important "Removing mountpoint '%s' on port %i" mountpoint port; Hashtbl.remove handler.sources mountpoint; if Hashtbl.length handler.sources = 0 && List.length (Atomic.get handler.http) = 0 then ( log#important "Nothing more on port %i: closing sockets." port; let f in_s = ignore (Unix.write in_s (Bytes.of_string " ") 0 1); Unix.close in_s in List.iter f fds; Hashtbl.remove opened_ports port) else () (* Add http_handler... *) let add_http_handler ~pos ~transport ~port ~verb ~uri h = let exec () = let handler = get_handler ~pos ~transport ~icy:false port in let suri = Lang.descr_of_regexp uri in log#important "Adding handler for '%s %s' on port %i" (string_of_verb verb) suri port; Atomic.set handler.http (Atomic.get handler.http @ [(verb, uri, h)]) in Server.on_start exec (* Remove http_handler. *) let remove_http_handler ~port ~verb ~uri () = let exec () = let { handler; fds; _ } = Hashtbl.find opened_ports port in let suri = Lang.descr_of_regexp uri in let handlers, removed = List.partition (fun (v, u, _) -> v = verb && suri = Lang.descr_of_regexp u) (Atomic.get handler.http) in if removed <> [] then log#important "Removing handler for '%s %s' on port %i" (string_of_verb verb) suri port; Atomic.set handler.http handlers; if Hashtbl.length handler.sources = 0 && List.length (Atomic.get handler.http) = 0 then ( log#info "Nothing more on port %i: closing sockets." port; let f in_s = ignore (Unix.write in_s (Bytes.of_string " ") 0 1); Unix.close in_s in List.iter f fds; Hashtbl.remove opened_ports port) else () in Server.on_start exec end module Harbor = Make (Http_transport) include Harbor liquidsoap-2.3.2/src/core/harbor/harbor_base.ml000066400000000000000000000055651477303350200215040ustar00rootroot00000000000000(* -*- mode: tuareg; -*- *) (***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2016 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let conf_harbor = Dtools.Conf.void ~p:(Configure.conf#plug "harbor") "Harbor settings (Icecast/shoutcast stream receiver)." let conf_harbor_bind_addrs = Dtools.Conf.list ~p:(conf_harbor#plug "bind_addrs") ~d:["0.0.0.0"] "IP addresses on which the harbor should listen." let conf_harbor_max_conn = Dtools.Conf.int ~p:(conf_harbor#plug "max_connections") ~d:128 "Maximum of pending source requests per port." let conf_pass_verbose = Dtools.Conf.bool ~p:(conf_harbor#plug "verbose") ~d:false "Display passwords, for debugging." let conf_revdns = Dtools.Conf.bool ~p:(conf_harbor#plug "reverse_dns") ~d:false "Perform reverse DNS lookup to get the client's hostname from its IP." let conf_icy_metadata = Dtools.Conf.list ~p:(conf_harbor#plug "icy_formats") ~d: [ "audio/mpeg"; "audio/aacp"; "audio/aac"; "audio/x-aac"; "audio/wav"; "audio/wave"; "audio/flac"; "audio/x-flac"; ] "Content-type (mime) of formats which allow shout metadata update." let conf_map_song_metadata = Dtools.Conf.bool ~p:(conf_harbor#plug "map_song_metadata") ~d:true "If `true`, `\"song\"` metadata in icecast metadata update is mapped to \ `\"title\"` unless on of: `\"artist\"` or `\"title\"` metadata is also \ passed in which case `\"song\"` metadata is removed as it usually \ contains redundant info that confuses the system. Metadata are passed \ as-is when `false`." let conf_timeout = Dtools.Conf.float ~p:(conf_harbor#plug "timeout") ~d:120. "Timeout for network operations (in seconds)." let conf_accept_timeout = Dtools.Conf.float ~p:(conf_harbor#plug "accept_timeout") ~d:3. "Timeout for network accept operations (in seconds)." let log = Log.make ["harbor"] liquidsoap-2.3.2/src/core/hooks_implementations.ml000066400000000000000000000201311477303350200223550ustar00rootroot00000000000000module Hooks = Liquidsoap_lang.Hooks module Lang = Liquidsoap_lang.Lang module Cache = Liquidsoap_lang.Cache (* For source eval check there are cases of: source('a) <: (source('a).{ source methods })? b/c of source.dynamic so we want to dig deeper than the regular demeth. *) let rec deep_demeth t = match Type.demeth t with | Type.{ descr = Nullable t } -> deep_demeth t | t -> t let strip_tracks ty = let ty = Type.hide_meth "track_marks" ty in Type.hide_meth "metadata" ty let strip_source_tracks ty = match Type.deref ty with | Type. { descr = Constr { constructor = "source"; params = [(`Invariant, frame_t)] }; } -> Type. { ty with descr = Constr { constructor = "source"; params = [(`Invariant, strip_tracks frame_t)]; }; } | _ -> ty let eval_check ~env:_ ~tm v = if Lang_source.Source_val.is_value v then ( let s = Lang_source.Source_val.of_value v in if not s#has_content_type then ( let ty = Type.fresh (deep_demeth tm.Term.t) in Typing.( Lang_source.source_t ~methods:false (strip_tracks s#frame_type) <: strip_source_tracks ty); s#content_type_computation_allowed)) else if Source_tracks.is_value v then ( let s = Source_tracks.source v in Typing.(strip_tracks s#frame_type <: strip_tracks (Type.fresh tm.Term.t))) else if Track.is_value v then ( let field, source = Lang_source.to_track v in if not source#has_content_type then ( match field with | _ when field = Frame.Fields.metadata -> () | _ when field = Frame.Fields.track_marks -> () | _ -> let ty = Type.fresh (deep_demeth tm.Term.t) in let frame_t = Frame_type.make (Lang.univ_t ()) (Frame.Fields.add field ty Frame.Fields.empty) in Typing.(source#frame_type <: frame_t))) let render_string = function | `Verbatim s -> s | `String (pos, (sep, s)) -> Liquidsoap_lang.Lexer.render_string ~pos ~sep s let mk_field_t ?pos kind params = let err_pos = Option.value ~default:(Lexing.dummy_pos, Lexing.dummy_pos) pos in let pos = Option.map Pos.of_lexing_pos pos in match kind with | "any" -> Type.var ?pos () | "none" | "never" -> Type.make ?pos Type.Never | _ -> ( try let k = Content.kind_of_string kind in match params with | [] -> Type.make ?pos (Format_type.descr (`Kind k)) | [("", `Verbatim "any")] -> Type.var ?pos () | [("", `Verbatim "internal")] -> Type.var ?pos ~constraints:[Format_type.internal_tracks] () | param :: params -> let mk_format (label, value) = let value = render_string value in Content.parse_param k label value in let f = mk_format param in List.iter (fun param -> Content.merge f (mk_format param)) params; assert (k = Content.kind f); Type.make ?pos (Format_type.descr (`Format f)) with _ -> let params = params |> List.map (fun (l, v) -> l ^ "=" ^ render_string v) |> String.concat "," in let t = kind ^ "(" ^ params ^ ")" in raise (Liquidsoap_lang.Term_base.Parse_error (err_pos, "Unknown type constructor: " ^ t ^ "."))) let () = Hooks.mk_clock_ty := fun ?pos () -> Type.make ?pos:(Option.map Liquidsoap_lang.Pos.of_lexing_pos pos) Lang_source.ClockValue.base_t.Type.descr let mk_source_ty ?pos name { Liquidsoap_lang.Parsed_term.extensible; tracks } = if name <> "source" then ( let pos = Option.value ~default:(Lexing.dummy_pos, Lexing.dummy_pos) pos in raise (Liquidsoap_lang.Term_base.Parse_error (pos, "Unknown type constructor: " ^ name ^ "."))); match tracks with | [] -> Lang_source.source_t ?pos (Lang.univ_t ()) | tracks -> let fields = List.fold_left (fun fields { Liquidsoap_lang.Parsed_term.track_name; track_type; track_params; } -> Frame.Fields.add (Frame.Fields.field_of_string track_name) (mk_field_t ?pos track_type track_params) fields) Frame.Fields.empty tracks in let base = if extensible then Lang.univ_t () else Lang.unit_t in Lang_source.source_t ?pos (Frame_type.make base fields) let register () = Hooks.liq_libs_dir := Configure.liq_libs_dir; let on_change v = Hooks.log_path := if v then (try Some Dtools.Log.conf_file_path#get with _ -> None) else None in Dtools.Log.conf_file#on_change on_change; ignore (Option.map on_change Dtools.Log.conf_file#get_d); (Hooks.make_log := fun name -> (Log.make name :> Hooks.log)); Hooks.type_of_encoder := Lang_encoder.type_of_encoder; Hooks.make_encoder := Lang_encoder.make_encoder; Hooks.eval_check := eval_check; (Hooks.has_encoder := fun fmt -> try let (_ : Encoder.factory) = Encoder.get_factory (Lang_encoder.V.of_value fmt) in true with _ -> false); Hooks.mk_source_ty := mk_source_ty; Hooks.getpwnam := Unix.getpwnam; Hooks.source_methods_t := fun () -> Lang_source.source_t ~methods:true (Lang.univ_t ()) let cache_max_days = try int_of_string (Sys.getenv "LIQ_CACHE_MAX_DAYS") with _ -> 10 let cache_max_files = try int_of_string (Sys.getenv "LIQ_CACHE_MAX_FILES") with _ -> 20 let () = (try Liquidsoap_lang.Cache.system_dir_perms := int_of_string (Sys.getenv "LIQ_CACHE_SYSTEM_DIR_PERMS") with _ -> ()); (try Liquidsoap_lang.Cache.system_file_perms := int_of_string (Sys.getenv "LIQ_CACHE_SYSTEM_FILE_PERMS") with _ -> ()); (try Liquidsoap_lang.Cache.user_dir_perms := int_of_string (Sys.getenv "LIQ_CACHE_USER_DIR_PERMS") with _ -> ()); try Liquidsoap_lang.Cache.user_file_perms := int_of_string (Sys.getenv "LIQ_CACHE_USER_FILE_PERMS") with _ -> () module Term_cache = Liquidsoap_lang.Term_cache let cache_log = Log.make ["cache"] let cache_maintenance dirtype = let max_timestamp = Unix.time () -. (float cache_max_days *. 86400.) in try match Cache.dir dirtype with | Some dir when Sys.file_exists dir && Sys.is_directory dir -> let files = Array.fold_left (fun files fname -> if String.ends_with ~suffix:".liq-cache" fname then ( let filename = Filename.concat dir fname in let stats = Unix.stat filename in match Unix.stat filename with | { Unix.st_atime } when st_atime < max_timestamp -> cache_log#info "File %s is too old, deleting.." fname; Unix.unlink filename; files | _ -> (stats, filename) :: files) else files) [] (Sys.readdir dir) in let len = List.length files in if cache_max_files < len then ( let len = len - cache_max_files in cache_log#info "Too many cached files! Deleting %d oldest ones.." len; let files = List.sort (fun ({ Unix.st_atime = t }, _) ({ Unix.st_atime = t' }, _) -> Stdlib.compare t t') files in List.iteri (fun pos (_, filename) -> if pos < len then ( cache_log#info "Deleting %s.." (Filename.basename filename); Unix.unlink filename)) files) | _ -> () with exn -> let bt = Printexc.get_backtrace () in Utils.log_exception ~log:cache_log ~bt (Printf.sprintf "Error while cleaning up cache: %s" (Printexc.to_string exn)) let () = Hooks.cache_maintenance := cache_maintenance liquidsoap-2.3.2/src/core/hooks_implementations.mli000066400000000000000000000000341477303350200225260ustar00rootroot00000000000000val register : unit -> unit liquidsoap-2.3.2/src/core/io/000077500000000000000000000000001477303350200160225ustar00rootroot00000000000000liquidsoap-2.3.2/src/core/io/alsa_io.ml000066400000000000000000000340571477303350200177740ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Alsa exception Error of string let handle lbl f x = try f x with e -> failwith (Printf.sprintf "Error while setting %s: %s" lbl (string_of_error e)) class virtual base ~buffer_size:buffer_size_seconds ~self_sync dev mode = let samples_per_second = Lazy.force Frame.audio_rate in let periods = Alsa_settings.periods#get in let buffer_size = Frame.audio_of_seconds buffer_size_seconds in object (self) method virtual log : Log.t method virtual audio_channels : int val mutable alsa_rate = -1 val mutable pcm = None val mutable write = Pcm.writen_float val mutable read = Pcm.readn_float val mutable alsa_buffer_size = buffer_size method private alsa_buffer_size = alsa_buffer_size method virtual content_type : Frame.content_type val mutable gen = None method generator = match gen with | Some g -> g | None -> let g = Generator.create self#content_type in gen <- Some g; g method self_sync : Clock.self_sync = if self_sync then (`Dynamic, if pcm <> None then Some Alsa_settings.sync_source else None) else (`Static, None) method open_device = self#log#important "Using ALSA %s." (Alsa.get_version ()); try let dev = match pcm with | None -> handle "open_pcm" (Pcm.open_pcm dev mode) [] | Some d -> d in let params = Pcm.get_params dev in (try handle "access" (Pcm.set_access dev params) Pcm.Access_rw_noninterleaved; handle "format" (Pcm.set_format dev params) Pcm.Format_float with _ -> ( (* If we can't get floats we fallback on interleaved s16le *) self#log#important "Falling back on interleaved S16LE"; handle "format" (Pcm.set_format dev params) Pcm.Format_s16_le; try Pcm.set_access dev params Pcm.Access_rw_interleaved; write <- (fun pcm buf ofs len -> let sbuf = Bytes.create (2 * len * Array.length buf) in Audio.S16LE.of_audio buf ofs sbuf 0 len; Pcm.writei pcm sbuf 0 len); read <- (fun pcm buf ofs len -> let sbuf = Bytes.make (2 * 2 * len) (Char.chr 0) in let r = Pcm.readi pcm sbuf 0 len in Audio.S16LE.to_audio (Bytes.unsafe_to_string sbuf) 0 buf ofs r; r) with Alsa.Invalid_argument -> self#log#important "Falling back on non-interleaved S16LE"; handle "access" (Pcm.set_access dev params) Pcm.Access_rw_noninterleaved; write <- (fun pcm buf ofs len -> let sbuf = Array.init self#audio_channels (fun _ -> Bytes.make (2 * len) (Char.chr 0)) in for c = 0 to Audio.channels buf - 1 do Audio.S16LE.of_audio [| buf.(c) |] ofs sbuf.(c) 0 len done; Pcm.writen pcm sbuf 0 len); read <- (fun pcm buf ofs len -> let sbuf = Array.init self#audio_channels (fun _ -> Bytes.make (2 * len) (Char.chr 0)) in let r = Pcm.readn pcm sbuf 0 len in for c = 0 to Audio.channels buf - 1 do Audio.S16LE.to_audio (Bytes.unsafe_to_string sbuf.(c)) 0 [| buf.(c) |] ofs len done; r))); handle "channels" (Pcm.set_channels dev params) self#audio_channels; let rate = handle "rate" (Pcm.set_rate_near dev params samples_per_second) Dir_eq in alsa_buffer_size <- handle "buffer size" (Pcm.set_buffer_size_near dev params) (Frame.audio_of_main buffer_size); let periods = if periods > 0 then ( handle "periods" (Pcm.set_periods dev params periods) Dir_eq; periods) else fst (Pcm.get_periods_max params) in alsa_rate <- rate; if rate <> samples_per_second then self#log#important "Could not set sample rate to 'frequency' (%d Hz), got %d." samples_per_second rate; if buffer_size <> alsa_buffer_size then self#log#important "Could not set buffer size to: %.02fs (%d samples), got: %.02f (%d \ samples)." (Frame.seconds_of_audio buffer_size) buffer_size (Frame.seconds_of_audio alsa_buffer_size) alsa_buffer_size; self#log#important "Samplefreq=%dHz, Bufsize=%dB, Frame=%dB, Periods=%d" alsa_rate alsa_buffer_size (Pcm.get_frame_size params) periods; (try Pcm.set_params dev params with Alsa.Invalid_argument as e -> self#log#critical "Setting alsa parameters failed (invalid argument)!"; raise e); handle "non-blocking" (Pcm.set_nonblock dev) false; pcm <- Some dev with Unknown_error _ as e -> raise (Error (string_of_error e)) method close_device = match pcm with | Some d -> Pcm.close d; pcm <- None | None -> () method reset = self#close_device; self#open_device end class output ~buffer_size ~self_sync ~start ~infallible ~register_telnet ~on_stop ~on_start dev val_source = let samples_per_second = Lazy.force Frame.audio_rate in let name = Printf.sprintf "alsa_out(%s)" dev in object (self) inherit Output.output ~infallible ~register_telnet ~on_stop ~on_start ~name ~output_kind:"output.alsa" val_source start inherit! base ~buffer_size ~self_sync dev [Pcm.Playback] val mutable samplerate_converter = None method samplerate_converter = match samplerate_converter with | Some samplerate_converter -> samplerate_converter | None -> let sc = Audio_converter.Samplerate.create self#audio_channels in samplerate_converter <- Some sc; sc method start = self#open_device method stop = (match (pcm, 0 < Generator.length self#generator) with | Some _, true -> self#write_frame (Generator.slice self#generator (Generator.length self#generator)) | _ -> ()); self#close_device method send_frame memo = let gen = self#generator in Generator.append gen memo; let buffer_size = Frame.main_of_audio self#alsa_buffer_size in if buffer_size <= Generator.length gen then self#write_frame (Generator.slice gen buffer_size) method private write_frame frame = let pcm = Option.get pcm in let buf = AFrame.pcm frame in let len = Audio.length buf in let buf, ofs, len = if alsa_rate = samples_per_second then (buf, 0, len) else Audio_converter.Samplerate.resample self#samplerate_converter (float alsa_rate /. float samples_per_second) buf 0 len in let rec pcm_write ofs len = if 0 < len then ( let written = write pcm buf ofs len in pcm_write (ofs + written) (len - written)) in try pcm_write ofs len with e -> begin match e with | Buffer_xrun -> self#log#severe "Underrun! You may minimize them by increasing the buffer \ size." | _ -> self#log#severe "Alsa error: %s" (string_of_error e) end; if e = Buffer_xrun || e = Suspended || e = Interrupted then ( self#log#severe "Trying to recover.."; Pcm.recover pcm e; self#output) else raise e end class input ~buffer_size ~self_sync ~start ~on_stop ~on_start ~fallible dev = object (self) inherit base ~buffer_size ~self_sync dev [Pcm.Capture] inherit! Start_stop.active_source ~name:(Printf.sprintf "alsa_in(%s)" dev) ~on_start ~on_stop ~fallible ~autostart:start () as active_source method private start = self#open_device method private stop = self#close_device method remaining = -1 method abort_track = () method seek_source = (self :> Source.source) method private can_generate_frame = active_source#started (* TODO: convert samplerate *) method private generate_frame = let pcm = Option.get pcm in let length = Lazy.force Frame.size in let alsa_buffer_size = self#alsa_buffer_size in let gen = self#generator in let format = Frame.Fields.find Frame.Fields.audio self#content_type in try while Generator.length gen < length do let c = Content.make ~length:(Frame.main_of_audio alsa_buffer_size) format in let read = read pcm (Content.Audio.get_data c) 0 alsa_buffer_size in Generator.put gen Frame.Fields.audio (Content.sub c 0 (Frame.main_of_audio read)) done; Generator.slice gen length with e -> begin match e with | Buffer_xrun -> self#log#severe "Overrun! You may minimize them by increasing the buffer \ size." | _ -> self#log#severe "Alsa error: %s" (string_of_error e) end; if e = Buffer_xrun || e = Suspended || e = Interrupted then ( self#log#severe "Trying to recover.."; Pcm.recover pcm e; self#generate_frame) else raise e end let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:Modules.output "alsa" (Output.proto @ [ ( "self_sync", Lang.bool_t, Some (Lang.bool true), Some "Mark the source as being synchronized by the ALSA driver." ); ( "buffer_size", Lang.nullable_t Lang.float_t, Some Lang.null, Some "ALSA buffer size in seconds. Defaults to frame duration when \ `null`." ); ( "device", Lang.string_t, Some (Lang.string "default"), Some "Alsa device to use" ); ("", Lang.source_t frame_t, None, None); ]) ~return_t:frame_t ~category:`Output ~meth:Output.meth ~descr:"Output the source's stream to an ALSA output device." (fun p -> let e f v = f (List.assoc v p) in let self_sync = e Lang.to_bool "self_sync" in let device = e Lang.to_string "device" in let source = List.assoc "" p in let infallible = not (Lang.to_bool (List.assoc "fallible" p)) in let register_telnet = Lang.to_bool (List.assoc "register_telnet" p) in let buffer_size = match Lang.to_valued_option Lang.to_float (List.assoc "buffer_size" p) with | None -> Lazy.force Frame.duration | Some v -> v in let start = Lang.to_bool (List.assoc "start" p) in let on_start = let f = List.assoc "on_start" p in fun () -> ignore (Lang.apply f []) in let on_stop = let f = List.assoc "on_stop" p in fun () -> ignore (Lang.apply f []) in (new output ~buffer_size ~self_sync ~infallible ~register_telnet ~start ~on_start ~on_stop device source :> Output.output)) let _ = let return_t = Lang.frame_t Lang.unit_t (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:Modules.input "alsa" (Start_stop.active_source_proto ~fallible_opt:(`Yep false) @ [ ( "self_sync", Lang.bool_t, Some (Lang.bool true), Some "Mark the source as being synchronized by the ALSA driver." ); ( "buffer_size", Lang.nullable_t Lang.float_t, Some Lang.null, Some "ALSA buffer size in seconds. Defaults to frame duration when \ `null`." ); ( "device", Lang.string_t, Some (Lang.string "default"), Some "Alsa device to use" ); ]) ~meth:(Start_stop.meth ()) ~return_t ~category:`Input ~descr:"Stream from an ALSA input device." (fun p -> let e f v = f (List.assoc v p) in let self_sync = e Lang.to_bool "self_sync" in let device = e Lang.to_string "device" in let buffer_size = match Lang.to_valued_option Lang.to_float (List.assoc "buffer_size" p) with | None -> Lazy.force Frame.duration | Some v -> v in let start = Lang.to_bool (List.assoc "start" p) in let fallible = Lang.to_bool (List.assoc "fallible" p) in let on_start = let f = List.assoc "on_start" p in fun () -> ignore (Lang.apply f []) in let on_stop = let f = List.assoc "on_stop" p in fun () -> ignore (Lang.apply f []) in (new input ~buffer_size ~self_sync ~on_start ~on_stop ~fallible ~start device :> Start_stop.active_source)) liquidsoap-2.3.2/src/core/io/ffmpeg_filter_io.ml000066400000000000000000000306431477303350200216620ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Connect sources to FFmpeg filters. *) exception Not_ready let noop () = () type 'a _duration_converter = { idx : int64; time_base : Avutil.rational; converter : 'a Avutil.Frame.t Ffmpeg_utils.Duration.t; } let track_mark_metadata = "liquidsoap_track_mark" class virtual ['a] duration_converter = object (self) method virtual log : Log.t val mutable duration_converter : 'a _duration_converter option = None val mutable last_duration : int64 option = None method convert_duration ~stream_idx ~convert_ts ~time_base frame = let duration_converter = match duration_converter with | Some { idx; time_base = converter_time_base; converter } when idx = stream_idx && time_base = converter_time_base -> converter | _ -> let last_ts, offset = match duration_converter with | None -> (None, 0L) | Some { idx; converter } -> if idx = stream_idx then self#log#important "Unexpected time_base change!"; let last_ts = Ffmpeg_utils.Duration.last_ts converter in let frame_ts = Option.value ~default:0L (Avutil.Frame.pts frame) in let position = Int64.add (Option.value ~default:0L last_ts) (Option.value ~default:0L last_duration) in let offset = Int64.sub position frame_ts in (last_ts, offset) in let converter = Ffmpeg_utils.Duration.init ~offset ?last_ts ~mode:`PTS ~src:time_base ~convert_ts ~get_ts:Avutil.Frame.pts ~set_ts:Avutil.Frame.set_pts () in duration_converter <- Some { idx = stream_idx; time_base; converter }; converter in last_duration <- Avutil.Frame.duration frame; Ffmpeg_utils.Duration.push duration_converter frame end class virtual ['a] base_output ~pass_metadata ~name ~frame_t ~field source = object (self) inherit Output.output ~clock:(Clock.create ~sync:`Passive ~id:name ()) ~infallible:false ~register_telnet:false ~on_stop:noop ~on_start:noop ~name ~output_kind:"ffmpeg.filter.input" (Lang.source source) true as super inherit ['a] duration_converter initializer Typing.( self#frame_type <: frame_t; source#frame_type <: self#frame_type) val mutable input : [ `Frame of 'a Avutil.frame | `Flush ] -> unit = fun _ -> () method self_sync = source#self_sync method set_input fn = input <- fn val mutable init : 'a Avutil.frame -> unit = fun _ -> assert false method set_init v = init <- v method start = () method stop = () method! reset = () val is_up = Atomic.make false method! can_generate_frame = Atomic.get is_up && super#can_generate_frame initializer self#on_wake_up (fun () -> Atomic.set is_up true) initializer self#on_sleep (fun () -> Atomic.set is_up false) method virtual raw_ffmpeg_frames : Content.data -> 'a Ffmpeg_raw_content.frame list method send_frame memo = let content = Frame.get memo field in let frames = self#raw_ffmpeg_frames content in (match frames with | { Ffmpeg_raw_content.frame } :: _ -> init frame | _ -> ()); List.iter (fun { Ffmpeg_raw_content.frame; time_base; stream_idx } -> match self#convert_duration ~convert_ts:true ~stream_idx ~time_base frame with | None -> () | Some (_, frames) -> List.iteri (fun pos (_, frame) -> if pos = 0 then ( let metadata = if pass_metadata then ( (* Pass only one metadata. *) match Frame.get_all_metadata memo with | (_, m) :: _ -> Frame.Metadata.to_list m | _ -> []) else [] in let metadata = if Frame.has_track_marks memo then (track_mark_metadata, "1") :: metadata else metadata in if metadata <> [] then Avutil.Frame.set_metadata frame metadata; input (`Frame frame))) frames) frames end (** From the script perspective, the operator sending data to a filter graph is an output. *) class audio_output ~pass_metadata ~name ~frame_t ~field source = object inherit [[ `Audio ]] base_output ~pass_metadata ~name ~frame_t ~field source method raw_ffmpeg_frames content = List.map snd Ffmpeg_raw_content.((Audio.get_data content).AudioSpecs.data) end class video_output ~pass_metadata ~name ~frame_t ~field source = object inherit [[ `Video ]] base_output ~pass_metadata ~name ~frame_t ~field source method raw_ffmpeg_frames content = List.map snd Ffmpeg_raw_content.((Video.get_data content).VideoSpecs.data) end class virtual ['a] input_base ~name ~pass_metadata ~self_sync ~is_ready ~pull frame_t = let stream_idx = Ffmpeg_content_base.new_stream_idx () in object (self) inherit ['a] duration_converter inherit Source.source ~name () initializer Typing.(self#frame_type <: frame_t) method seek_source = (self :> Source.source) method fallible = true method remaining = Generator.remaining self#buffer method abort_track = () method virtual buffer : Generator.t method virtual put_data : length:int -> (int * 'a Ffmpeg_raw_content.frame) list -> unit val mutable output = None method private metadata_timestamps ~time_base frame = let get_time d = string_of_float (Frame.seconds_of_main (Int64.to_int (Ffmpeg_utils.convert_time_base ~src:time_base ~dst:(Ffmpeg_utils.liq_main_ticks_time_base ()) d))) in List.fold_left (fun result (label, fn) -> match fn frame with | None -> result | Some v -> ("lavfi.liq." ^ label, get_time v) :: result) [] [ ("pts", Avutil.Frame.pts); ("duration", Avutil.Frame.duration); ("best_effort_timestamp", Avutil.Frame.best_effort_timestamp); ] method private flush_buffer output = let time_base = Avfilter.(time_base output.context) in fun () -> let frame = output.Avfilter.handler () in match self#convert_duration ~convert_ts:false ~stream_idx ~time_base frame with | Some (length, frames) -> let frames = List.map (fun (pos, frame) -> if pass_metadata then ( let metadata = Avutil.Frame.metadata frame in if metadata <> [] then ( let m = List.filter (fun (k, _) -> k <> track_mark_metadata) metadata in let pos = Generator.length self#buffer + pos in Generator.add_metadata ~pos self#buffer (Frame.Metadata.from_list (m @ self#metadata_timestamps ~time_base frame)); if List.mem_assoc track_mark_metadata metadata then Generator.add_track_mark ~pos self#buffer)); (pos, { Ffmpeg_raw_content.time_base; stream_idx; frame })) frames in self#put_data ~length frames | None -> () method self_sync : Clock.self_sync = self_sync self method pull = try (* Init is driven by the pull. *) let output = while output = None do if not (is_ready ()) then raise Not_ready; pull () done; Option.get output in let flush = self#flush_buffer output in let rec f () = try while true do flush () done with Avutil.Error `Eagain -> if Generator.length self#buffer < Lazy.force Frame.size && is_ready () then ( pull (); f ()) in f () with Not_ready -> () method private can_generate_frame = Generator.length self#buffer >= Lazy.force Frame.size || is_ready () method private generate_frame = let size = Lazy.force Frame.size in if Generator.length self#buffer < Lazy.force Frame.size then self#pull; Generator.slice self#buffer size end type audio_config = { format : Avutil.Sample_format.t; rate : int; channels : int; } (* Same thing here. *) class audio_input ~field ~self_sync ~is_ready ~pull ~pass_metadata frame_t = object (self) inherit [[ `Audio ]] input_base ~name:"ffmpeg.filter.audio.output" ~pass_metadata ~self_sync ~is_ready ~pull frame_t initializer Typing.(self#frame_type <: frame_t) method set_output v = let output_format = { Ffmpeg_raw_content.AudioSpecs.channel_layout = Some Avfilter.(channel_layout v.context); sample_rate = Some Avfilter.(sample_rate v.context); sample_format = Some Avfilter.(sample_format v.context); } in Content.merge (Option.get (Frame.Fields.find_opt Frame.Fields.audio self#content_type)) (Ffmpeg_raw_content.Audio.lift_params output_format); output <- Some v method put_data ~length = function | [] -> () | (_, frame) :: _ as data -> let params = Ffmpeg_raw_content.AudioSpecs.frame_params frame in let content = { Ffmpeg_raw_content.AudioSpecs.params; data; length } in Generator.put self#buffer field (Ffmpeg_raw_content.Audio.lift_data content) end type video_config = { width : int; height : int; pixel_format : Avutil.Pixel_format.t; } class video_input ~field ~self_sync ~is_ready ~pull ~pass_metadata frame_t = object (self) inherit [[ `Video ]] input_base ~name:"ffmpeg.filter.video.output" ~pass_metadata ~self_sync ~is_ready ~pull frame_t method set_output v = let output_format = { Ffmpeg_raw_content.VideoSpecs.width = Some Avfilter.(width v.context); height = Some Avfilter.(height v.context); pixel_format = Some Avfilter.(pixel_format v.context); pixel_aspect = Avfilter.(pixel_aspect v.context); } in Content.merge (Option.get (Frame.Fields.find_opt Frame.Fields.video self#content_type)) (Ffmpeg_raw_content.Video.lift_params output_format); output <- Some v method put_data ~length = function | [] -> () | (_, frame) :: _ as data -> let params = Ffmpeg_raw_content.VideoSpecs.frame_params frame in let content = { Ffmpeg_raw_content.VideoSpecs.params; data; length } in Generator.put self#buffer field (Ffmpeg_raw_content.Video.lift_data content) end liquidsoap-2.3.2/src/core/io/ffmpeg_io.ml000066400000000000000000000556321477303350200203220ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) exception Not_connected module Http = Liq_http module Metadata = struct include Map.Make (struct type t = string let compare = String.compare end) let of_metadata m = List.fold_left (fun m (k, v) -> add k v m) empty m let equal = equal String.equal let to_metadata = bindings end let normalize_metadata = List.map (fun (lbl, v) -> let lbl = match lbl with | "StreamTitle" -> "title" | "StreamUrl" -> "url" | _ -> lbl in let v = try Charset.convert ~target:Charset.utf8 v with _ -> v in (lbl, v)) exception Stopped type container = { input : Avutil.input Avutil.container; decoder : Decoder.buffer -> unit; buffer : Decoder.buffer; get_metadata : unit -> (string * string) list; closed : bool Atomic.t; } let shutdown = Atomic.make false let () = Lifecycle.before_core_shutdown ~name:"input.ffmpeg shutdown" (fun () -> Atomic.set shutdown true) class input ?(name = "input.ffmpeg") ~autostart ~self_sync ~poll_delay ~debug ~max_buffer ~on_error ~on_stop ~on_start ~on_connect ~metadata_filter ~on_disconnect ~new_track_on_metadata ?format ~opts ~trim_url url = let max_length = Some (Frame.main_of_seconds max_buffer) in object (self) inherit Start_stop.active_source ~name ~fallible:true ~on_start ~on_stop ~autostart () as super val connect_task = Atomic.make None method seek_source = (self :> Source.source) method remaining = -1 method abort_track = Generator.add_track_mark self#buffer val source_status : [ `Stopped | `Starting | `Polling | `Connected of string * container | `Stopping ] Atomic.t = Atomic.make `Stopped method source_status = Atomic.get source_status method private is_connected = match Atomic.get source_status with `Connected _ -> true | _ -> false method can_generate_frame = super#started && self#is_connected method private get_self_sync = match self_sync () with Some v -> v | None -> false method self_sync = (`Dynamic, self#source_sync (self#get_self_sync && self#is_connected)) method private start = self#connect method private stop = self#disconnect val mutable url = url method url = let u = url () in if trim_url then String.trim u else u method set_url u = url <- u method buffer_length = Frame.seconds_of_audio (Generator.length self#buffer) method private connect_task () = Generator.set_max_length self#buffer max_length; try if self#source_status = `Stopping then raise Stopped; assert (self#source_status = `Starting); Atomic.set source_status `Polling; let opts = Hashtbl.copy opts in let url = self#url in let closed = Atomic.make false in let input = Av.open_input ~interrupt:(fun () -> Atomic.get shutdown || Atomic.get closed) ?format ~opts url in if Hashtbl.length opts > 0 then failwith (Printf.sprintf "Unrecognized options: %s" (Ffmpeg_format.string_of_options opts)); let content_type = Ffmpeg_decoder.get_type ~format ~ctype:self#content_type ~url input in if not (Decoder.can_decode_type content_type self#content_type) then failwith (Printf.sprintf "url %S cannot produce content of type %s" url (Frame.string_of_content_type self#content_type)); let streams = Ffmpeg_decoder.mk_streams ~ctype:self#content_type ~decode_first_metadata:true input in let decoder = Ffmpeg_decoder.mk_decoder ~streams ~target_position:(ref None) input in let buffer = Decoder.mk_buffer ~ctype:self#content_type self#buffer in (* FFmpeg has memory leaks with chained ogg stream so we manually reset the metadata after fetching it. *) let get_metadata stream = let m = Av.get_metadata stream in Av.set_metadata stream []; m in let get_metadata () = normalize_metadata (Ffmpeg_decoder.Streams.fold (fun _ stream m -> m @ match stream with | `Audio_frame (stream, _) -> get_metadata stream | `Audio_packet (stream, _) -> get_metadata stream | `Video_frame (stream, _) -> get_metadata stream | `Video_packet (stream, _) -> get_metadata stream | `Data_packet _ -> []) streams (Av.get_input_metadata input)) in let last_meta = ref [] in let get_metadata () = let m = get_metadata () in if m <> !last_meta then ( last_meta := m; m) else [] in on_connect input; Generator.add_track_mark self#buffer; let container = { input; decoder; buffer; get_metadata; closed } in Atomic.set source_status (`Connected (url, container)); -1. with | Stopped -> Atomic.set source_status `Stopped; -1. | e -> let bt = Printexc.get_raw_backtrace () in Utils.log_exception ~log:self#log ~bt:(Printexc.raw_backtrace_to_string bt) (Printf.sprintf "Decoding failed: %s" (Printexc.to_string e)); on_error (Lang.runtime_error_of_exception ~bt ~kind:"ffmpeg" e); if debug then Printexc.raise_with_backtrace e bt; Atomic.set source_status `Starting; poll_delay method private connect = match self#source_status with | `Starting | `Polling | `Connected _ -> () | `Stopping | `Stopped -> ( Atomic.set source_status `Starting; match Atomic.get connect_task with | Some t -> Duppy.Async.wake_up t | None -> let t = Duppy.Async.add ~priority:`Blocking Tutils.scheduler self#connect_task in Atomic.set connect_task (Some t); Duppy.Async.wake_up t) method private disconnect = let stop_task () = match Atomic.get connect_task with | None -> () | Some t -> Atomic.set source_status `Stopping; Duppy.Async.wake_up t in match self#source_status with | `Stopping | `Stopped -> () | `Polling | `Starting -> stop_task () | `Connected (_, { input; closed }) -> Atomic.set closed true; self#mutexify (fun () -> try Av.close input with exn -> let bt = Printexc.get_backtrace () in Utils.log_exception ~log:self#log ~bt (Printf.sprintf "Error while disconnecting: %s" (Printexc.to_string exn))) (); on_disconnect (); stop_task () method private reconnect = self#disconnect; self#connect method private get_connected_container = match self#source_status with | `Connected (_, c) -> c | _ -> raise Not_connected method private generate_frame = let size = Lazy.force Frame.size in try let { decoder; buffer; closed } = self#get_connected_container in while Generator.length self#buffer < Lazy.force Frame.size do if Atomic.get shutdown || Atomic.get closed then raise Not_connected; self#mutexify (fun () -> decoder buffer) () done; let { get_metadata } = self#get_connected_container in let meta = get_metadata () in if meta <> [] then ( Generator.add_metadata self#buffer (Frame.Metadata.from_list meta); if new_track_on_metadata then Generator.add_track_mark self#buffer); let frame = Generator.slice self#buffer size in (* Metadata can be added by the decoder and the demuxer so we filter at the frame level. *) let metadata = List.fold_left (fun metadata (p, m) -> let m = metadata_filter m in if 0 < Frame.Metadata.cardinal m then (p, m) :: metadata else metadata) [] (Frame.get_all_metadata frame) in Frame.add_all_metadata frame metadata with exn -> let bt = Printexc.get_raw_backtrace () in Utils.log_exception ~log:self#log ~bt:(Printexc.raw_backtrace_to_string bt) (Printf.sprintf "Feeding failed: %s" (Printexc.to_string exn)); on_error (Lang.runtime_error_of_exception ~bt ~kind:"ffmpeg" exn); self#reconnect; Frame.append (Generator.slice self#buffer size) self#end_of_track end let http_log = Log.make ["input"; "http"] class http_input ~autostart ~self_sync ~poll_delay ~debug ~on_error ~max_buffer ~on_connect ~on_disconnect ?format ~opts ~user_agent ~timeout ~metadata_filter ~on_start ~on_stop ~new_track_on_metadata ~trim_url url = let () = Hashtbl.replace opts "icy" (`Int 1); Hashtbl.replace opts "user_agent" (`String user_agent); Hashtbl.replace opts "rw_timeout" (`Int64 (Int64.of_float (timeout *. 1000000.))) in let is_icy = Atomic.make false in let on_connect input = let icy_headers = try let icy_headers = Avutil.Options.get_string ~search_children:true ~name:"icy_metadata_headers" (Av.input_obj input) in let icy_headers = Re.Pcre.split ~rex:(Re.Pcre.regexp "[\r]?\n") icy_headers in List.fold_left (fun ret header -> if header <> "" then ( try let res = Re.Pcre.exec ~rex:(Re.Pcre.regexp "([^:]*):\\s*(.*)") header in (Re.Pcre.get_substring res 1, Re.Pcre.get_substring res 2) :: ret with Not_found -> ret) else ret) [] icy_headers with exn -> let bt = Printexc.get_raw_backtrace () in Utils.log_exception ~log:http_log ~bt:(Printexc.raw_backtrace_to_string bt) (Printf.sprintf "Error while fetching icy headers: %s" (Printexc.to_string exn)); on_error (Lang.runtime_error_of_exception ~bt ~kind:"ffmpeg" exn); [] in Atomic.set is_icy (icy_headers <> []); on_connect icy_headers in let self_sync () = match (self_sync (), Atomic.get is_icy) with | Some v, _ -> Some v | None, v -> Some v in object inherit input ~name:"input.http" ~autostart ~self_sync ~poll_delay ~debug ~max_buffer ~on_stop ~on_start ~on_disconnect ~on_connect ~on_error ~metadata_filter ?format ~opts ~new_track_on_metadata ~trim_url url end let parse_args ~t name p opts = let name = name ^ "_args" in let args = List.assoc name p in let args = Lang.to_list args in let extract_pair extractor v = let label, value = Lang.to_product v in Hashtbl.replace opts (Lang.to_string label) (extractor value) in let extract = match t with | `Int -> fun v -> extract_pair (fun v -> `Int (Lang.to_int v)) v | `Float -> fun v -> extract_pair (fun v -> `Float (Lang.to_float v)) v | `String -> fun v -> extract_pair (fun v -> `String (Lang.to_string v)) v in List.iter extract args let register_input is_http = let return_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in let args ?t name = let t = match t with | Some t -> Lang.product_t Lang.string_t t | None -> Lang.string_t in (name ^ "_args", Lang.list_t t, Some (Lang.list []), None) in let name, descr = if is_http then ("http", "Create a http stream using ffmpeg") else ("ffmpeg", "Create a stream using ffmpeg") in let on_error = Lang.eval ~cache:false ~stdlib:`Disabled ~typecheck:false "fun (_) -> ()" in ignore (Lang.add_operator ~base:Modules.input name ~descr ~category:`Input (Start_stop.active_source_proto ~fallible_opt:`Nope @ (if is_http then [ ( "user_agent", Lang.string_t, Some (Lang.string Http.user_agent), Some "User agent." ); ( "timeout", Lang.float_t, Some (Lang.float 10.), Some "Timeout for source connection." ); ] else []) @ (if is_http then [ ( "on_connect", Lang.fun_t [(false, "", Lang.metadata_t)] Lang.unit_t, Some (Lang.val_cst_fun [("", None)] Lang.unit), Some "Function to execute when a source is connected. Its \ receives the list of ICY-specific headers, if available." ); ] else [ ( "on_connect", Lang.fun_t [] Lang.unit_t, Some (Lang.val_cst_fun [] Lang.unit), Some "Function to execute when a source is connected." ); ]) @ [ args ~t:Lang.int_t "int"; args ~t:Lang.float_t "float"; args ~t:Lang.string_t "string"; ( "metadata_filter", Lang.nullable_t (Lang.fun_t [(false, "", Lang.metadata_t)] Lang.metadata_t), Some Lang.null, Some "Metadata filter function. Returned metadata are set a \ metadata. Default: filter `id3v2_priv` metadata." ); ( "deduplicate_metadata", Lang.bool_t, Some (Lang.bool true), Some "Prevent duplicated metadata." ); ( "new_track_on_metadata", Lang.bool_t, Some (Lang.bool true), Some "Treat new metadata as new track." ); ( "on_error", Lang.fun_t [(false, "", Lang.error_t)] Lang.unit_t, Some on_error, Some "Callback executed when an error occurs." ); ( "on_disconnect", Lang.fun_t [] Lang.unit_t, Some (Lang.val_cst_fun [] Lang.unit), Some "Function to execute when a source is disconnected" ); ( "max_buffer", Lang.float_t, Some (Lang.float 5.), Some "Maximum uration of buffered data" ); (if is_http then ( "self_sync", Lang.getter_t (Lang.nullable_t Lang.bool_t), Some Lang.null, Some "Should the source control its own timing? If `null`, the \ source will control its latency if it can be detected that \ it is connecting to an `icecast` or `shoutcast` server. \ Otherwise, see `input.ffmpeg` for more details about this \ option." ) else ( "self_sync", Lang.getter_t Lang.bool_t, Some (Lang.bool false), Some "Should the source control its own timing? Set to `true` if \ you are having synchronization issues. Should be `false` \ for most typical cases." )); ( "debug", Lang.bool_t, Some (Lang.bool false), Some "Run in debugging mode, not catching some exceptions." ); ( "poll_delay", Lang.float_t, Some (Lang.float 2.), Some "Polling delay when trying to connect to the stream." ); ( "format", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Force a specific input format. Autodetected when passed a null \ argument" ); ( "trim_url", Lang.bool_t, Some (Lang.bool true), Some "Trim input URL." ); ("", Lang.getter_t Lang.string_t, None, Some "URL to decode."); ]) ~return_t ~meth: Lang.( Start_stop.meth () @ [ ( "url", ([], fun_t [] string_t), "Return the source's current url.", fun s -> val_fun [] (fun _ -> string s#url) ); ( "set_url", ([], fun_t [(false, "", getter_t string_t)] unit_t), "Set the source's url.", fun s -> val_fun [("", "", None)] (fun p -> s#set_url (to_string_getter (List.assoc "" p)); unit) ); ( "status", ([], fun_t [] string_t), "Return the current status of the source, either \"stopped\" \ (the source isn't trying to relay the HTTP stream), \ \"starting\" (polling task is about to begin) \"polling\" \ (attempting to connect to the HTTP stream), \"connected \ \" (connected to , buffering or playing back the \ stream) or \"stopping\" (source is stopping).", fun s -> val_fun [] (fun _ -> string (match s#source_status with | `Stopped -> "stopped" | `Starting -> "starting" | `Stopping -> "stopping" | `Polling -> "polling" | `Connected (url, _) -> Printf.sprintf "connected %s" url)) ); ( "buffer_length", ([], fun_t [] float_t), "Get the buffer's length in seconds.", fun s -> val_fun [] (fun _ -> float s#buffer_length) ); ]) (fun p -> let format = Lang.to_option (List.assoc "format" p) in let format = Option.map (fun format -> let format = Lang.to_string format in match Av.Format.find_input_format format with | Some f -> f | None -> raise (Error.Invalid_value ( Lang.string format, "Could not find ffmpeg input format with that name" ))) format in let opts = Hashtbl.create 10 in parse_args ~t:`Int "int" p opts; parse_args ~t:`Float "float" p opts; parse_args ~t:`String "string" p opts; let max_buffer = Lang.to_float (List.assoc "max_buffer" p) in let debug = Lang.to_bool (List.assoc "debug" p) in let self_sync = Lang.to_getter (List.assoc "self_sync" p) in let self_sync () = if is_http then Lang.to_valued_option Lang.to_bool (self_sync ()) else Some (Lang.to_bool (self_sync ())) in let autostart = Lang.to_bool (List.assoc "start" p) in let on_error = let f = List.assoc "on_error" p in fun err -> ignore (Lang.apply f [("", Lang.error err)]) in let on_start = let f = List.assoc "on_start" p in fun _ -> ignore (Lang.apply f []) in let on_stop = let f = List.assoc "on_stop" p in fun () -> ignore (Lang.apply f []) in let on_disconnect () = ignore (Lang.apply (List.assoc "on_disconnect" p) []) in let metadata_filter = match Lang.to_option (List.assoc "metadata_filter" p) with | Some fn -> fun m -> Lang.to_metadata_list (Lang.apply fn [("", Lang.metadata_list m)]) | None -> List.filter (fun (k, _) -> not (Re.Pcre.pmatch ~rex:(Re.Pcre.regexp "^id3v2_priv") k)) in let deduplicate_metadata = Lang.to_bool (List.assoc "deduplicate_metadata" p) in let metadata_filter = if not deduplicate_metadata then metadata_filter else ( let last_meta = ref Metadata.empty in fun m -> let m = metadata_filter m in let m' = Metadata.of_metadata m in if m = [] || Metadata.equal !last_meta m' then [] else ( last_meta := m'; m)) in let metadata_filter m = let m = metadata_filter (Frame.Metadata.to_list m) in Frame.Metadata.from_list m in let new_track_on_metadata = Lang.to_bool (List.assoc "new_track_on_metadata" p) in let poll_delay = Lang.to_float (List.assoc "poll_delay" p) in let url = Lang.to_string_getter (Lang.assoc "" 1 p) in let trim_url = Lang.to_bool (List.assoc "trim_url" p) in if is_http then ( let timeout = Lang.to_float (List.assoc "timeout" p) in let user_agent = Lang.to_string (List.assoc "user_agent" p) in let on_connect l = let l = List.map (fun (x, y) -> Lang.product (Lang.string x) (Lang.string y)) l in let arg = Lang.list l in ignore (Lang.apply (List.assoc "on_connect" p) [("", arg)]) in (new http_input ~metadata_filter ~debug ~autostart ~self_sync ~poll_delay ~on_connect ~on_disconnect ~user_agent ~new_track_on_metadata ~max_buffer ?format ~opts ~timeout ~on_error ~on_start ~on_stop ~trim_url url :> input)) else ( let on_connect _ = ignore (Lang.apply (List.assoc "on_connect" p) []) in new input ~metadata_filter ~autostart ~debug ~self_sync ~poll_delay ~on_error ~on_start ~on_stop ~on_connect ~on_disconnect ~max_buffer ?format ~opts ~new_track_on_metadata ~trim_url url))) let () = register_input true; register_input false liquidsoap-2.3.2/src/core/io/oss_io.ml000066400000000000000000000163321477303350200176540ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm external set_format : Unix.file_descr -> int -> int = "caml_oss_dsp_setfmt" external set_channels : Unix.file_descr -> int -> int = "caml_oss_dsp_channels" external set_rate : Unix.file_descr -> int -> int = "caml_oss_dsp_speed" module SyncSource = Clock.MkSyncSource (struct type t = unit let to_string _ = "oss" end) let sync_source = SyncSource.make () (** Wrapper for calling set_* functions and checking that the desired value has been accepted. If not, the current behavior is a bit too violent. *) let force f fd x = let x' = f fd x in if x <> x' then failwith "cannot obtain desired OSS settings" class output ~self_sync ~on_start ~on_stop ~infallible ~register_telnet ~start dev val_source = let samples_per_second = Lazy.force Frame.audio_rate in let name = Printf.sprintf "oss_out(%s)" dev in object (self) inherit Output.output ~infallible ~register_telnet ~on_stop ~on_start ~name ~output_kind:"output.oss" val_source start val mutable fd = None method self_sync = if self_sync then (`Dynamic, if fd <> None then Some sync_source else None) else (`Static, None) method open_device = let descr = Unix.openfile dev [Unix.O_WRONLY; Unix.O_CLOEXEC] 0o200 in fd <- Some descr; force set_format descr 16; force set_channels descr self#audio_channels; force set_rate descr samples_per_second method close_device = match fd with | None -> () | Some x -> Unix.close x; fd <- None method start = self#open_device method stop = self#close_device method send_frame memo = let fd = Option.get fd in let buf = AFrame.pcm memo in let len = Audio.length buf in let r = Audio.S16LE.size (Audio.channels buf) len in let s = Bytes.create r in Audio.S16LE.of_audio buf 0 s 0 len; let w = Unix.write fd s 0 r in assert (w = r) end class input ~self_sync ~start ~on_stop ~on_start ~fallible dev = let samples_per_second = Lazy.force Frame.audio_rate in object (self) inherit Start_stop.active_source ~name:(Printf.sprintf "oss_in(%s)" dev) ~on_start ~on_stop ~fallible ~autostart:start () as active_source val mutable fd = None method self_sync = if self_sync then (`Dynamic, if fd <> None then Some sync_source else None) else (`Static, None) method abort_track = () method remaining = -1 method seek_source = (self :> Source.source) method private start = self#open_device method private can_generate_frame = active_source#started method private open_device = let descr = Unix.openfile dev [Unix.O_RDONLY; Unix.O_CLOEXEC] 0o400 in fd <- Some descr; force set_format descr 16; force set_channels descr self#audio_channels; force set_rate descr samples_per_second method private stop = self#close_device method private close_device = Unix.close (Option.get fd); fd <- None method generate_frame = let length = Lazy.force Frame.size in let frame = Frame.create ~length self#content_type in let buf = Content.Audio.get_data (Frame.get frame Frame.Fields.audio) in let fd = Option.get fd in let len = 2 * Array.length buf * Audio.Mono.length buf.(0) in let s = Bytes.create len in let r = Unix.read fd s 0 len in (* TODO: recursive read ? *) assert (len = r); Audio.S16LE.to_audio (Bytes.unsafe_to_string s) 0 buf 0 len; Frame.set_data frame Frame.Fields.audio Content.Audio.lift_data buf end let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in ignore (Lang.add_operator ~base:Modules.output "oss" (Output.proto @ [ ( "self_sync", Lang.bool_t, Some (Lang.bool true), Some "Mark the source as being synchronized by the OSS driver." ); ( "device", Lang.string_t, Some (Lang.string "/dev/dsp"), Some "OSS device to use." ); ("", Lang.source_t frame_t, None, None); ]) ~return_t:frame_t ~category:`Output ~meth:Output.meth ~descr:"Output the source's stream to an OSS output device." (fun p -> let e f v = f (List.assoc v p) in let infallible = not (Lang.to_bool (List.assoc "fallible" p)) in let register_telnet = Lang.to_bool (List.assoc "register_telnet" p) in let start = Lang.to_bool (List.assoc "start" p) in let on_start = let f = List.assoc "on_start" p in fun () -> ignore (Lang.apply f []) in let on_stop = let f = List.assoc "on_stop" p in fun () -> ignore (Lang.apply f []) in let self_sync = e Lang.to_bool "self_sync" in let device = e Lang.to_string "device" in let source = List.assoc "" p in (new output ~start ~on_start ~on_stop ~infallible ~register_telnet ~self_sync device source :> Output.output))); let return_t = Lang.frame_t Lang.unit_t (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:Modules.input "oss" (Start_stop.active_source_proto ~fallible_opt:(`Yep false) @ [ ( "self_sync", Lang.bool_t, Some (Lang.bool true), Some "Mark the source as being synchronized by the OSS driver." ); ( "device", Lang.string_t, Some (Lang.string "/dev/dsp"), Some "OSS device to use." ); ]) ~meth:(Start_stop.meth ()) ~return_t ~category:`Input ~descr:"Stream from an OSS input device." (fun p -> let e f v = f (List.assoc v p) in let self_sync = e Lang.to_bool "self_sync" in let device = e Lang.to_string "device" in let start = Lang.to_bool (List.assoc "start" p) in let fallible = Lang.to_bool (List.assoc "fallible" p) in let on_start = let f = List.assoc "on_start" p in fun () -> ignore (Lang.apply f []) in let on_stop = let f = List.assoc "on_stop" p in fun () -> ignore (Lang.apply f []) in new input ~start ~on_start ~on_stop ~fallible ~self_sync device) liquidsoap-2.3.2/src/core/io/oss_io_c.c000066400000000000000000000015141477303350200177640ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include CAMLprim value caml_oss_dsp_setfmt(value fd, value fmt) { int f = Int_val(fmt); int ret = ioctl(Int_val(fd), SNDCTL_DSP_SETFMT, &f); /* TODO: raise errors */ /* TODO: use format constants */ assert(ret != -1); return Val_int(f); } CAMLprim value caml_oss_dsp_channels(value fd, value chans) { int c = Int_val(chans); int ret = ioctl(Int_val(fd), SNDCTL_DSP_CHANNELS, &c); assert(ret != -1); return Val_int(c); } CAMLprim value caml_oss_dsp_speed(value fd, value speed) { int s = Int_val(speed); int ret = ioctl(Int_val(fd), SNDCTL_DSP_SPEED, &s); assert(ret != -1); return Val_int(s); } liquidsoap-2.3.2/src/core/io/portaudio_io.ml000066400000000000000000000270721477303350200210610ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm module SyncSource = Clock.MkSyncSource (struct type t = unit let to_string _ = "portaudio" end) let sync_source = SyncSource.make () let initialized = ref false let () = Extra_args.add ( ["--list-portaudio-devices"], Arg.Unit (fun () -> Portaudio.init (); let c = Portaudio.get_device_count () in Printf.printf "Portaudio has %d devices:\n%!" c; let rec f p = if p < c then ( let { Portaudio.d_name; d_host_api; d_max_input_channels; d_max_output_channels; d_default_low_input_latency; d_default_low_output_latency; d_default_high_input_latency; d_default_high_output_latency; d_default_sample_rate; } = Portaudio.get_device_info p in Printf.printf {| Device ID %d: - name: %s - host API: %d - max input channels: %d - max output channels: %d - default low input latency: %.02f - default low output latency: %.02f - default high input latency: %.02f - default high output latency: %.02f - default sample rate: %.02f |} p d_name d_host_api d_max_input_channels d_max_output_channels d_default_low_input_latency d_default_low_output_latency d_default_high_input_latency d_default_high_output_latency d_default_sample_rate; f (p + 1)) in f 0; Portaudio.terminate (); exit 0), "List all available portaudio devices" ) class virtual base = object (self) initializer if not !initialized then ( Portaudio.init (); initialized := true) method virtual log : Log.t (* TODO: inline this to be more efficient? *) method handle lbl f = try f () with | Portaudio.Error n -> let bt = Printexc.get_raw_backtrace () in let exn = Failure (Printf.sprintf "Portaudio error in %s: %s" lbl (Portaudio.string_of_error n)) in Printexc.raise_with_backtrace exn bt | Portaudio.Unanticipated_host_error -> let n, s = Portaudio.get_last_host_error () in if n = 0 then self#log#important "Unanticipated host error in %s. (ignoring)" lbl else self#log#important "Unanticipated host error %d in %s: %s. (ignoring)" n lbl s end let open_device ~mode ~latency ~channels ~buflen device_id = let samples_per_second = Lazy.force Frame.audio_rate in match device_id with | None -> Portaudio.open_default_stream ~format:Portaudio.format_float32 0 channels samples_per_second buflen | Some device -> let device_info = Portaudio.get_device_info device in let inparams, outparams = match mode with | `Input -> let latency = Option.value ~default:device_info.Portaudio.d_default_high_output_latency latency in ( Some { Portaudio.channels; device; sample_format = Portaudio.format_float32; latency; }, None ) | `Output -> let latency = Option.value ~default:device_info.Portaudio.d_default_high_input_latency latency in ( None, Some { Portaudio.channels; device; sample_format = Portaudio.format_float32; latency; } ) in Portaudio.open_stream inparams outparams (float samples_per_second) buflen [] class output ~self_sync ~start ~on_start ~on_stop ~infallible ~register_telnet ~device_id ~latency buflen val_source = object (self) inherit base inherit Output.output ~infallible ~register_telnet ~on_stop ~on_start ~name:"output.portaudio" ~output_kind:"output.portaudio" val_source start val mutable stream = None method self_sync = if self_sync then (`Dynamic, if stream <> None then Some sync_source else None) else (`Static, None) method private open_device = self#handle "open_device" (fun () -> stream <- Some (open_device ~mode:`Output ~latency ~channels:self#audio_channels ~buflen device_id)); self#handle "start_stream" (fun () -> Portaudio.start_stream (Option.get stream)) method private close_device = match stream with | None -> () | Some s -> Portaudio.close_stream s; stream <- None method start = self#open_device method stop = self#close_device method! reset = self#close_device; self#open_device method send_frame memo = let stream = Option.get stream in let buf = AFrame.pcm memo in self#handle "write_stream" (fun () -> let len = Audio.length buf in Portaudio.write_stream stream buf 0 len) end class input ~self_sync ~start ~on_start ~on_stop ~fallible ~device_id ~latency buflen = object (self) inherit base inherit Start_stop.active_source ~name:"input.portaudio" ~on_start ~on_stop ~fallible ~autostart:start () as active_source method private start = self#open_device method private stop = self#close_device val mutable stream = None method self_sync = if self_sync then (`Dynamic, if stream <> None then Some sync_source else None) else (`Static, None) method abort_track = () method remaining = -1 method seek_source = (self :> Source.source) method private can_generate_frame = active_source#started method private open_device = self#handle "open_device" (fun () -> stream <- Some (open_device ~mode:`Input ~latency ~channels:self#audio_channels ~buflen device_id)); self#handle "start_stream" (fun () -> Portaudio.start_stream (Option.get stream)) method private close_device = Portaudio.close_stream (Option.get stream); stream <- None method generate_frame = let size = Lazy.force Frame.size in let frame = Frame.create ~length:size self#content_type in let buf = Content.Audio.get_data (Frame.get frame Frame.Fields.audio) in let stream = Option.get stream in self#handle "read_stream" (fun () -> Portaudio.read_stream stream buf 0 (Array.length buf.(0))); Frame.set_data frame Frame.Fields.audio Content.Audio.lift_data buf end let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:Modules.output "portaudio" (Output.proto @ [ ( "self_sync", Lang.bool_t, Some (Lang.bool true), Some "Mark the source as being synchronized by the portaudio driver." ); ( "buflen", Lang.int_t, Some (Lang.int 256), Some "Length of a buffer in samples." ); ( "device_id", Lang.nullable_t Lang.int_t, Some Lang.null, Some "Device ID. Uses default device if `null`." ); ( "latency", Lang.nullable_t Lang.float_t, Some Lang.null, Some "Device latency. Only used when specifying device ID." ); ("", Lang.source_t frame_t, None, None); ]) ~return_t:frame_t ~category:`Output ~meth:Output.meth ~descr:"Output the source's stream to a portaudio output device." (fun p -> let e f v = f (List.assoc v p) in let buflen = e Lang.to_int "buflen" in let device_id = Lang.to_valued_option Lang.to_int (List.assoc "device_id" p) in let latency = Lang.to_valued_option Lang.to_float (List.assoc "latency" p) in let infallible = not (Lang.to_bool (List.assoc "fallible" p)) in let register_telnet = Lang.to_bool (List.assoc "register_telnet" p) in let start = Lang.to_bool (List.assoc "start" p) in let on_start = let f = List.assoc "on_start" p in fun () -> ignore (Lang.apply f []) in let on_stop = let f = List.assoc "on_stop" p in fun () -> ignore (Lang.apply f []) in let source = List.assoc "" p in let self_sync = Lang.to_bool (List.assoc "self_sync" p) in (new output ~start ~on_start ~on_stop ~infallible ~register_telnet ~self_sync ~device_id ~latency buflen source :> Output.output)) let _ = let return_t = Lang.frame_t Lang.unit_t (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:Modules.input "portaudio" (Start_stop.active_source_proto ~fallible_opt:(`Yep false) @ [ ( "buflen", Lang.int_t, Some (Lang.int 256), Some "Length of a buffer in samples." ); ( "self_sync", Lang.bool_t, Some (Lang.bool true), Some "Mark the source as being synchronized by the portaudio driver." ); ( "device_id", Lang.nullable_t Lang.int_t, Some Lang.null, Some "Device ID. Uses default device if `null`." ); ( "latency", Lang.nullable_t Lang.float_t, Some Lang.null, Some "Device latency. Only used when specifying device ID." ); ]) ~return_t ~category:`Input ~meth:(Start_stop.meth ()) ~descr:"Stream from a portaudio input device." (fun p -> let e f v = f (List.assoc v p) in let buflen = e Lang.to_int "buflen" in let device_id = Lang.to_valued_option Lang.to_int (List.assoc "device_id" p) in let latency = Lang.to_valued_option Lang.to_float (List.assoc "latency" p) in let self_sync = Lang.to_bool (List.assoc "self_sync" p) in let start = Lang.to_bool (List.assoc "start" p) in let fallible = Lang.to_bool (List.assoc "fallible" p) in let on_start = let f = List.assoc "on_start" p in fun () -> ignore (Lang.apply f []) in let on_stop = let f = List.assoc "on_stop" p in fun () -> ignore (Lang.apply f []) in new input ~self_sync ~start ~on_start ~on_stop ~fallible ~device_id ~latency buflen) liquidsoap-2.3.2/src/core/io/pulseaudio_io.ml000066400000000000000000000241311477303350200212160ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Pulseaudio module SyncSource = Clock.MkSyncSource (struct type t = unit let to_string _ = "pulseaudio" end) let sync_source = SyncSource.make () (** Error translator *) let error_translator e = match e with | Pulseaudio.Error n -> Some (Printf.sprintf "Pulseaudio error: %s" (Pulseaudio.string_of_error n)) | _ -> None let () = Printexc.register_printer error_translator class virtual base ~self_sync ~client ~device = object val client_name = client val dev = device val mutable stream = None method self_sync : Clock.self_sync = if self_sync then (`Dynamic, if stream <> None then Some sync_source else None) else (`Static, None) end let log = Log.make ["pulseaudio"] class output ~infallible ~register_telnet ~start ~on_start ~on_stop p = let client = Lang.to_string (List.assoc "client" p) in let device = Lang.to_valued_option Lang.to_string (List.assoc "device" p) in let device = if device = Some "" then ( log#important "Empty device name \"\" is deprecated! Please use `null()` instead.."; None) else device in let retry_delay = Lang.to_float (List.assoc "retry_delay" p) in let on_error = List.assoc "on_error" p in let on_error s = ignore (Lang.apply on_error [("", Lang.string s)]) in let name = Printf.sprintf "pulse_out(%s:%s)" client (match device with None -> "(default)" | Some s -> s) in let val_source = List.assoc "" p in let samples_per_second = Lazy.force Frame.audio_rate in let self_sync = Lang.to_bool (List.assoc "self_sync" p) in object (self) inherit base ~self_sync ~client ~device inherit Output.output ~infallible ~register_telnet ~on_stop ~on_start ~name ~output_kind:"output.pulseaudio" val_source start val mutable last_try = 0. method private open_device = let now = Unix.gettimeofday () in try if last_try +. retry_delay < now then ( last_try <- now; let ss = { sample_format = Sample_format_float32le; sample_rate = samples_per_second; sample_chans = self#audio_channels; } in stream <- Some (Pulseaudio.Simple.create ~client_name ~stream_name:self#id ?dev ~dir:Dir_playback ~sample:ss ())) with exn -> let bt = Printexc.get_backtrace () in let error = Printf.sprintf "Failed to open pulse audio device: %s" (Printexc.to_string exn) in on_error error; Utils.log_exception ~log:self#log ~bt error method close_device = match stream with | None -> () | Some s -> Pulseaudio.Simple.free s; stream <- None method start = self#open_device method stop = self#close_device method! reset = self#close_device; self#open_device method send_frame memo = if stream = None then self#open_device; match stream with | Some stream -> ( let buf = AFrame.pcm memo in let len = Audio.length buf in try Simple.write stream buf 0 len with exn -> let bt = Printexc.get_backtrace () in last_try <- Unix.gettimeofday (); self#close_device; let error = Printf.sprintf "Failed to send pulse audio data: %s" (Printexc.to_string exn) in on_error error; Utils.log_exception ~log:self#log ~bt error) | None -> () end class input p = let client = Lang.to_string (List.assoc "client" p) in let device = Lang.to_valued_option Lang.to_string (List.assoc "device" p) in let device = if device = Some "" then ( log#important "Empty device name \"\" is deprecated! Please use `null()` instead.."; None) else device in let retry_delay = Lang.to_float (List.assoc "retry_delay" p) in let on_error = List.assoc "on_error" p in let on_error s = ignore (Lang.apply on_error [("", Lang.string s)]) in let self_sync = Lang.to_bool (List.assoc "self_sync" p) in let start = Lang.to_bool (List.assoc "start" p) in let fallible = Lang.to_bool (List.assoc "fallible" p) in let on_start = let f = List.assoc "on_start" p in fun () -> ignore (Lang.apply f []) in let on_stop = let f = List.assoc "on_stop" p in fun () -> ignore (Lang.apply f []) in let samples_per_second = Lazy.force Frame.audio_rate in object (self) inherit Start_stop.active_source ~name:"input.pulseaudio" ~on_start ~on_stop ~autostart:start ~fallible () as active_source inherit base ~self_sync ~client ~device method private start = self#open_device method private stop = self#close_device method remaining = -1 method abort_track = () method seek_source = (self :> Source.source) method private can_generate_frame = match (active_source#started, stream) with | true, None -> self#open_device; stream <> None | v, _ -> v val mutable last_try = 0. method private open_device = let now = Unix.gettimeofday () in if last_try +. retry_delay < now then ( last_try <- now; let ss = { sample_format = Sample_format_float32le; sample_rate = samples_per_second; sample_chans = self#audio_channels; } in try stream <- Some (Pulseaudio.Simple.create ~client_name ~stream_name:self#id ~dir:Dir_record ?dev ~sample:ss ()) with exn when fallible -> let bt = Printexc.get_backtrace () in let error = Printf.sprintf "Error while connecting to pulseaudio: %s" (Printexc.to_string exn) in on_error error; Utils.log_exception ~log:self#log ~bt error) method private close_device = match stream with | Some device -> Pulseaudio.Simple.free device; stream <- None | None -> () method generate_frame = try let size = Lazy.force Frame.size in let frame = Frame.create ~length:size self#content_type in let buf = Content.Audio.get_data (Frame.get frame Frame.Fields.audio) in let stream = Option.get stream in Simple.read stream buf 0 (Frame.audio_of_main size); Frame.set_data frame Frame.Fields.audio Content.Audio.lift_data buf with exn -> let bt = Printexc.get_raw_backtrace () in last_try <- Unix.gettimeofday (); self#close_device; if fallible then ( let error = Printf.sprintf "Error while reading from pulseaudio: %s" (Printexc.to_string exn) in on_error error; Utils.log_exception ~log:self#log ~bt:(Printexc.raw_backtrace_to_string bt) error; self#empty_frame) else Printexc.raise_with_backtrace exn bt end let on_error = Lang.eval ~cache:false ~typecheck:false ~stdlib:`Disabled "fun (_) -> ()" let proto = [ ("client", Lang.string_t, Some (Lang.string "liquidsoap"), None); ( "device", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Device to use. Uses default if set to `null`." ); ( "retry_delay", Lang.float_t, Some (Lang.float 1.), Some "When fallible, time to wait before trying to connect again." ); ( "on_error", Lang.fun_t [(false, "", Lang.string_t)] Lang.unit_t, Some on_error, Some "Function executed when an operation with the pulseaudio server \ returns an error." ); ( "self_sync", Lang.bool_t, Some (Lang.bool true), Some "Mark the source as being synchronized by the pulseaudio driver." ); ] let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:Modules.output "pulseaudio" (Output.proto @ proto @ [("", Lang.source_t frame_t, None, None)]) ~return_t:frame_t ~category:`Output ~meth:Output.meth ~descr:"Output the source's stream to a pulseaudio output device." (fun p -> let infallible = not (Lang.to_bool (List.assoc "fallible" p)) in let register_telnet = Lang.to_bool (List.assoc "register_telnet" p) in let start = Lang.to_bool (List.assoc "start" p) in let on_start = let f = List.assoc "on_start" p in fun () -> ignore (Lang.apply f []) in let on_stop = let f = List.assoc "on_stop" p in fun () -> ignore (Lang.apply f []) in (new output ~infallible ~register_telnet ~on_start ~on_stop ~start p :> Output.output)) let _ = let return_t = Lang.frame_t Lang.unit_t (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:Modules.input "pulseaudio" (Start_stop.active_source_proto ~fallible_opt:(`Yep true) @ proto) ~return_t ~category:`Input ~meth:(Start_stop.meth ()) ~descr:"Stream from a pulseaudio input device." (fun p -> new input p) liquidsoap-2.3.2/src/core/io/srt_io.ml000066400000000000000000001327261477303350200176660ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** SRT input *) exception Done exception Not_connected type prefer_address = [ `System_default | `Ipv4 | `Ipv6 ] type socket_mode = [ `Connect | `Listen | `Incoming | `Close ] let conf_srt = Dtools.Conf.void ~p:(Configure.conf#plug "srt") "SRT configuration" let conf_prefer_address = Dtools.Conf.string ~p:(conf_srt#plug "prefer_address") ~d:"system" "Set preference for resolving addresses. One of: `\"system\"`, `\"ipv4\"` \ or `\"ipv6\"`." let conf_log = Dtools.Conf.bool ~p:(conf_srt#plug "log") ~d:true "Route srt logs through liquidsoap's logs" let conf_verbosity = Dtools.Conf.string ~p:(conf_log#plug "verbosity") "Verbosity" ~d:"warning" ~comments: [ "Set SRT log level, one of: \"critical\", \"error\", "; "\"warning\", \"notice\" or \"debug\""; ] let conf_level = Dtools.Conf.int ~p:(conf_log#plug "level") ~d:4 "Level" let conf_poll = Dtools.Conf.void ~p:(conf_srt#plug "poll") "Poll configuration" let conf_timeout = Dtools.Conf.float ~p:(conf_poll#plug "timeout") ~d:0.1 "Timeout for polling loop, in seconda." let conf_enforced_encryption = Dtools.Conf.bool ~p:(conf_srt#plug "enforced_encryption") ~d:true "Enforce consistent encryption settings on both end of any connection." let string_of_address = function | Unix.ADDR_UNIX _ -> assert false | Unix.ADDR_INET (addr, port) -> Printf.sprintf "%s:%d" (Unix.string_of_inet_addr addr) port let getaddrinfo ~(log : Log.t) ~prefer_address address port = let open Ctypes in let open Posix_socket in let hints = allocate_n Addrinfo.t ~count:1 in hints |-> Addrinfo.ai_flags <-@ ni_numerichost; hints |-> Addrinfo.ai_family <-@ af_unspec; hints |-> Addrinfo.ai_socktype <-@ sock_stream; match getaddrinfo ~hints ~port:(`Int port) address with | [] -> Runtime_error.raise ~pos:[] ~message: (Printf.sprintf "getaddrinfo could not resolve address: %s:%i" address port) "srt" | first_address :: _ as resolved_addresses -> let rec filter_address inet_type = function | [] -> first_address | sockaddr :: _ when !@(sockaddr |-> Sockaddr.sa_family) = inet_type -> sockaddr | _ :: resolved_addresses -> filter_address inet_type resolved_addresses in let sockaddr = match prefer_address with | `System_default -> first_address | `Ipv4 -> filter_address af_inet resolved_addresses | `Ipv6 -> filter_address af_inet6 resolved_addresses in if log#active 5 then log#f 5 "Address %s:%n resolved to: %s" address port (string_of_address (to_unix_sockaddr sockaddr)); sockaddr module SyncSource = Clock.MkSyncSource (struct type t = unit let to_string _ = "srt" end) let sync_source = SyncSource.make () let mode_of_value v = match Lang.to_string v with | "listener" -> `Listener | "caller" -> `Caller | _ -> raise (Error.Invalid_value (v, "Invalid mode!")) let string_of_mode = function `Listener -> "listener" | `Caller -> "caller" let common_options ~mode = [ ( "mode", Lang.string_t, Some (Lang.string (string_of_mode mode)), Some "Mode to operate on. One of: `\"listener\"` (waits for connection to \ come in) or `\"caller\"` (initiate connection to a remote server)" ); ( "listen_callback", Lang.nullable_t (Lang.fun_t [ (false, "hs_version", Lang.int_t); (false, "peeraddr", Lang.string_t); (false, "streamid", Lang.nullable_t Lang.string_t); (false, "", Builtins_srt.Socket_value.base_t); ] Lang.bool_t), Some Lang.null, Some "Callback used to decide whether to accept new incoming connections. \ Used in listener mode only." ); ( "streamid", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Set `streamid`. This value can be retrieved by the listener side when \ connecting to it. Used in caller mode only." ); ( "passphrase", Lang.nullable_t Lang.string_t, Some Lang.null, Some "When set to a non-empty string, this option enables encryption and \ sets the passphrase for it. See `libsrt` documentation for more \ details." ); ( "pbkeylen", Lang.nullable_t Lang.int_t, Some Lang.null, Some "Set encryption key length. See `libsrt` documentation for more \ details." ); ( "enforced_encryption", Lang.nullable_t Lang.bool_t, Some Lang.null, Some "Enforces that both connection parties have the same passphrase set, \ or both do not set the passphrase, otherwise the connection is \ rejected." ); ( "host", Lang.string_t, Some (Lang.string "localhost"), Some "Address to connect to. Used only in caller mode." ); ( "port", Lang.int_t, Some (Lang.int 8000), Some "Port to bind on the local machine (listener mode) or to connect to \ (caller mode). The term `port` as used in SRT is occasionally \ identical to the term `UDP port`. However SRT offers more flexibility \ than UDP because it manages ports as its own resources. For example, \ one port may be shared between various services." ); ( "bind_address", Lang.string_t, Some (Lang.string "0.0.0.0"), Some "Address to bind on the local machine. Used only in listener mode" ); ( "prefer_address", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Preferred address type when resolving hostnames. One of: \ `\"system\"`, `\"ipv4\"` or `\"ipv6\"`. Defaults to global \ `srt.prefer_connection` settings when `null`." ); ( "on_socket", Lang.nullable_t (Lang.fun_t [ (false, "mode", Lang.string_t); (false, "", Builtins_srt.Socket_value.base_t); ] Lang.unit_t), Some Lang.null, Some "Callback executed when a new SRT socket is created to set additional \ options, add monitoring, etc. `mode` should be one of: `\"connect\"` \ (socket created before connecting to a remote address), `\"listen\"` \ (socket created before binding for receiving new incoming \ connections), `\"incoming\"` (socket received as incoming connection) \ or `\"close\"` (socket is about to closed)." ); ( "polling_delay", Lang.float_t, Some (Lang.float 2.), Some "Delay between connection attempts. Used only in caller mode." ); ( "read_timeout", Lang.nullable_t Lang.float_t, Some (Lang.float 1.), Some "Timeout, in seconds, after which read operations are aborted if no \ data was received, indefinite if `null`." ); ( "write_timeout", Lang.nullable_t Lang.float_t, Some (Lang.float 1.), Some "Timeout, in seconds, after which write operations are aborted if no \ data was received, indefinite if `null`." ); ( "connection_timeout", Lang.nullable_t Lang.float_t, Some Lang.null, Some "Timeout, in seconds, after which initial connection operations are \ aborted if no data was received. Uses library's default if `null`. \ Used only in `client` mode." ); ("payload_size", Lang.int_t, Some (Lang.int 1316), Some "Payload size."); ("messageapi", Lang.bool_t, Some (Lang.bool true), Some "Use message api"); ( "on_connect", Lang.fun_t [] Lang.unit_t, Some (Lang.val_cst_fun [] Lang.unit), Some "Function to execute when connected." ); ( "on_disconnect", Lang.fun_t [] Lang.unit_t, Some (Lang.val_cst_fun [] Lang.unit), Some "Function to execute when disconnected" ); ] let meth () = [ ( "sockets", ( [], Lang.fun_t [] (Lang.list_t (Lang.product_t Lang.string_t Builtins_srt.Socket_value.base_t)) ), "List of `(connected_address, connected_socket)`", fun s -> Lang.val_fun [] (fun _ -> Lang.list (List.map (fun (origin, s) -> Lang.product (Lang.string (Utils.name_of_sockaddr origin)) (Builtins_srt.Socket_value.to_base_value s)) s#get_sockets)) ); ( "connect", ([], Lang.fun_t [] Lang.unit_t), "In sender mode, connect to remote server. In listener mode, setup \ listening socket.", fun s -> Lang.val_fun [] (fun _ -> s#set_should_stop false; s#connect; Lang.unit) ); ( "disconnect", ([], Lang.fun_t [] Lang.unit_t), "Disconnect all connected socket.", fun s -> Lang.val_fun [] (fun _ -> s#set_should_stop true; s#disconnect; Lang.unit) ); ] type common_options = { mode : [ `Listener | `Caller ]; hostname : string; port : int; bind_address : string; prefer_address : [ `System_default | `Ipv4 | `Ipv6 ]; on_socket : mode:socket_mode -> Srt.socket -> unit; listen_callback : Srt.listen_callback option; streamid : string option; pbkeylen : int option; enforced_encryption : bool option; passphrase : string option; polling_delay : float; read_timeout : int option; write_timeout : int option; connection_timeout : int option; payload_size : int; messageapi : bool; on_connect : (unit -> unit) ref; on_disconnect : (unit -> unit) ref; } let parse_common_options p = let bind_address = Lang.to_string (List.assoc "bind_address" p) in let prefer_address = let v = List.assoc "prefer_address" p in match Option.value ~default:conf_prefer_address#get (Lang.to_valued_option Lang.to_string v) with | "system" -> `System_default | "ipv4" -> `Ipv4 | "ipv6" -> `Ipv6 | _ -> raise (Error.Invalid_value (v, "Valid values are: `\"system\"`, `\"ipv4\"` or `\"ipv6\"`.")) in let on_socket = match Lang.to_option (List.assoc "on_socket" p) with | None -> fun ~mode:_ _ -> () | Some fn -> fun ~mode s -> let mode = match mode with | `Connect -> "connect" | `Listen -> "listen" | `Incoming -> "incoming" | `Close -> "close" in ignore (Lang.apply fn [ ("mode", Lang.string mode); ("", Builtins_srt.Socket_value.to_base_value s); ]) in let passphrase_v = List.assoc "passphrase" p in let passphrase = Lang.to_valued_option Lang.to_string passphrase_v in (match passphrase with | Some s when String.length s < 10 -> raise (Error.Invalid_value (passphrase_v, "Passphrase must be at least 10 characters long!")) | Some s when String.length s > 79 -> raise (Error.Invalid_value (passphrase_v, "Passphrase must be at most 79 characters long!")) | _ -> ()); let streamid = Lang.to_valued_option Lang.to_string (List.assoc "streamid" p) in let pbkeylen = Lang.to_valued_option Lang.to_int (List.assoc "pbkeylen" p) in let enforced_encryption = Lang.to_valued_option Lang.to_bool (List.assoc "enforced_encryption" p) in let listen_callback = let fn = List.assoc "listen_callback" p in Option.map (fun fn socket hs_version peeraddr streamid -> Lang.to_bool (Lang.apply fn [ ("hs_version", Lang.int hs_version); ("peeraddr", Lang.string (Utils.name_of_sockaddr peeraddr)); ( "streamid", match streamid with | None -> Lang.null | Some s -> Lang.string s ); ("", Builtins_srt.Socket_value.to_base_value socket); ])) (Lang.to_option fn) in let on_connect = List.assoc "on_connect" p in let on_disconnect = List.assoc "on_disconnect" p in let polling_delay = Lang.to_float (List.assoc "polling_delay" p) in let read_timeout = Lang.to_valued_option (fun v -> int_of_float (1000. *. Lang.to_float v)) (List.assoc "read_timeout" p) in let write_timeout = Lang.to_valued_option (fun v -> int_of_float (1000. *. Lang.to_float v)) (List.assoc "write_timeout" p) in let connection_timeout = Lang.to_valued_option (fun v -> int_of_float (1000. *. Lang.to_float v)) (List.assoc "connection_timeout" p) in { mode = mode_of_value (List.assoc "mode" p); hostname = Lang.to_string (List.assoc "host" p); port = Lang.to_int (List.assoc "port" p); bind_address; prefer_address; on_socket; listen_callback; pbkeylen; enforced_encryption; passphrase; streamid; polling_delay; read_timeout; write_timeout; connection_timeout; payload_size = Lang.to_int (List.assoc "payload_size" p); messageapi = Lang.to_bool (List.assoc "messageapi" p); on_connect = ref (fun () -> ignore (Lang.apply on_connect [])); on_disconnect = ref (fun () -> ignore (Lang.apply on_disconnect [])); } let log = Log.make ["srt"] let log_handler { Srt.Log.message } = let message = Re.Pcre.substitute ~rex:(Re.Pcre.regexp "[ \r\n]+$") ~subst:(fun _ -> "") message in log#f conf_level#get "%s" message (** Common polling task for all srt input/output. sockets entering a poll are always set to non-blocking and set back to blocking when exiting. They are also always removed from the poll when done. *) module Poll = struct type t = { p : Srt.Poll.t; handlers : (Srt.socket, Srt.Poll.flag * (Srt.socket -> unit)) Hashtbl.t; } let t = let p = Srt.Poll.create () in let handlers = Hashtbl.create 0 in { p; handlers } exception Empty let process () = try if List.length (Srt.Poll.sockets t.p) = 0 then raise Empty; let read, write = Srt.Poll.wait t.p ~timeout:(int_of_float (1000. *. conf_timeout#get)) in let apply fn s = try fn s with exn -> let bt = Printexc.get_backtrace () in Utils.log_exception ~log ~bt (Printf.sprintf "Error while executing asynchronous callback: %s" (Printexc.to_string exn)) in List.iter (fun (event, sockets) -> List.iter (fun fd -> Srt.Poll.remove_usock t.p fd; let event', fn = Hashtbl.find t.handlers fd in if event = event' then apply fn fd) sockets) [(`Read, read); (`Write, write)]; 0. with | Empty | Srt.Error (`Epollempty, _) -> -1. | Srt.Error (`Etimeout, _) -> 0. | exn -> let bt = Printexc.get_backtrace () in Utils.log_exception ~log ~bt (Printf.sprintf "Error while processing SRT socket pool: %s" (Printexc.to_string exn)); -1. let task = Duppy.Async.add ~priority:`Blocking Tutils.scheduler process let add_socket ~mode socket fn = Srt.setsockflag socket Srt.sndsyn false; Srt.setsockflag socket Srt.rcvsyn false; Hashtbl.replace t.handlers socket (mode, fn); Srt.Poll.add_usock t.p socket ~flags:[(mode :> Srt.Poll.flag)]; Duppy.Async.wake_up task let remove_socket socket = Hashtbl.remove t.handlers socket; if List.mem socket (Srt.Poll.sockets t.p) then Srt.Poll.remove_usock t.p socket end let init = lazy (Lifecycle.on_start ~name:"srt initialization" (fun () -> Srt.startup (); if conf_log#get then ( let level = match conf_verbosity#get with | "critical" -> `Critical | "error" -> `Error | "warning" -> `Warning | "notice" -> `Notice | "debug" -> `Debug | _ -> log#severe "Invalid value for \"srt.log.verbosity\"!"; `Error in Srt.Log.setloglevel level; Srt.Log.set_handler log_handler)); Lifecycle.on_final_cleanup ~name:"set cleanup" (fun () -> Srt.Poll.release Poll.t.Poll.p; Srt.cleanup ())) let string_of_address = function | Unix.ADDR_UNIX _ -> assert false | Unix.ADDR_INET (addr, port) -> Printf.sprintf "%s:%d" (Unix.string_of_inet_addr addr) port let mk_socket ~mode ~on_socket ~payload_size ~messageapi () = let s = Srt.create_socket () in Srt.setsockflag s Srt.payloadsize payload_size; Srt.setsockflag s Srt.transtype `Live; Srt.setsockflag s Srt.messageapi messageapi; Srt.setsockflag s Srt.enforced_encryption conf_enforced_encryption#get; on_socket ~mode s; s let close_socket ~on_socket s = on_socket ~mode:`Close s; Srt.close s let shutdown = Atomic.make false let () = Lifecycle.on_core_shutdown ~name:"srt shutdown" (fun () -> Atomic.set shutdown true) let id = let counter = Atomic.make 0 in fun () -> Atomic.fetch_and_add counter 1 class virtual base () = let () = Lazy.force init in object val should_stop = Atomic.make false val id = id () method private should_stop = Atomic.get shutdown || Atomic.get should_stop method set_should_stop = Atomic.set should_stop method srt_id = id end class virtual networking_agent = object method virtual private connect : unit method virtual private disconnect : unit method virtual private is_connected : bool method virtual private mutexify : 'a 'b. ('a -> 'b) -> 'a -> 'b method virtual private should_stop : bool end class virtual input_networking_agent = object inherit networking_agent method virtual private get_socket : Unix.sockaddr * Srt.socket end class virtual output_networking_agent = object inherit networking_agent method virtual get_sockets : (Unix.sockaddr * Srt.socket) list method virtual private client_error : Srt.socket -> exn -> Printexc.raw_backtrace -> unit end module ToDisconnect = Weak.Make (struct type t = < disconnect : unit ; srt_id : int > let equal t t' = t#srt_id = t'#srt_id let hash t = t#srt_id end) let to_disconnect = ToDisconnect.create 10 let () = Lifecycle.on_core_shutdown ~name:"Srt disconnect" (fun () -> ToDisconnect.iter (fun s -> s#disconnect) to_disconnect) class virtual caller ~enforced_encryption ~pbkeylen ~passphrase ~streamid ~polling_delay ~payload_size ~messageapi ~on_socket ~hostname ~port ~prefer_address ~connection_timeout ~read_timeout ~write_timeout ~on_connect ~on_disconnect = object (self) method virtual id : string method virtual should_stop : bool val mutable connect_task = None val task_should_stop = Atomic.make false val socket = Atomic.make None initializer ToDisconnect.add to_disconnect (self :> ToDisconnect.data) method private get_socket = match Atomic.get socket with Some s -> s | None -> raise Not_connected method virtual private log : Log.t method private is_connected = Atomic.get socket <> None method private connect_fn () = try let sockaddr = getaddrinfo ~log:self#log ~prefer_address hostname port in self#log#important "Connecting to srt://%s:%d.." hostname port; (match Atomic.exchange socket None with | None -> () | Some (_, s) -> close_socket ~on_socket s); let s = mk_socket ~mode:`Connect ~on_socket ~payload_size ~messageapi () in try Srt.setsockflag s Srt.sndsyn true; Srt.setsockflag s Srt.rcvsyn true; Utils.optional_apply (fun id -> Srt.(setsockflag s streamid id)) streamid; Utils.optional_apply (fun b -> Srt.(setsockflag s enforced_encryption b)) enforced_encryption; Utils.optional_apply (fun len -> Srt.(setsockflag s pbkeylen len)) pbkeylen; Utils.optional_apply (fun p -> Srt.(setsockflag s passphrase p)) passphrase; Utils.optional_apply (fun v -> Srt.(setsockflag s conntimeo v)) connection_timeout; Utils.optional_apply (fun v -> Srt.(setsockflag s sndtimeo v)) write_timeout; Utils.optional_apply (fun v -> Srt.(setsockflag s rcvtimeo v)) read_timeout; Srt.connect_posix_socket s sockaddr; self#log#important "Client connected!"; !on_connect (); Atomic.set socket (Some (Posix_socket.to_unix_sockaddr sockaddr, s)); -1. with exn -> let bt = Printexc.get_raw_backtrace () in Srt.close s; Printexc.raise_with_backtrace exn bt with exn -> self#log#important "Connect failed: %s" (Printexc.to_string exn); if not (Atomic.get task_should_stop) then polling_delay else -1. method connect = Atomic.set task_should_stop false; match connect_task with | Some t -> Duppy.Async.wake_up t | None -> let t = Duppy.Async.add ~priority:`Blocking Tutils.scheduler self#connect_fn in connect_task <- Some t; Duppy.Async.wake_up t method disconnect = (match Atomic.exchange socket None with | None -> () | Some (_, socket) -> close_socket ~on_socket socket; !on_disconnect ()); Atomic.set task_should_stop true; match connect_task with | None -> () | Some t -> Duppy.Async.stop t; connect_task <- None end class virtual listener ~enforced_encryption ~pbkeylen ~passphrase ~max_clients ~listen_callback ~payload_size ~messageapi ~bind_address ~port ~prefer_address ~on_socket ~read_timeout ~write_timeout ~on_connect ~on_disconnect () = object (self) val mutable client_sockets = [] method virtual id : string method virtual log : Log.t method virtual should_stop : bool method virtual mutexify : 'a 'b. ('a -> 'b) -> 'a -> 'b val listening_socket = Atomic.make None initializer ToDisconnect.add to_disconnect (self :> ToDisconnect.data) method private is_connected = self#mutexify (fun () -> client_sockets <> []) () method get_sockets = self#mutexify (fun () -> client_sockets) () method private listening_socket = match Atomic.get listening_socket with | Some s -> s | None -> ( let s = mk_socket ~mode:`Listen ~on_socket ~payload_size ~messageapi () in try Srt.bind_posix_socket s (getaddrinfo ~log:self#log ~prefer_address bind_address port); let max_clients_callback = Option.map (fun n _ _ _ _ -> self#mutexify (fun () -> List.length client_sockets < n) ()) max_clients in let listen_callback = List.fold_left (fun cur v -> match (cur, v) with | None, _ -> v | Some _, None -> cur | Some cur, Some fn -> Some (fun s hs_version peeraddr streamid -> cur s hs_version peeraddr streamid && fn s hs_version peeraddr streamid)) None [max_clients_callback; listen_callback] in Utils.optional_apply (fun fn -> Srt.listen_callback s fn) listen_callback; Utils.optional_apply (fun b -> Srt.(setsockflag s enforced_encryption b)) enforced_encryption; Utils.optional_apply (fun len -> Srt.(setsockflag s pbkeylen len)) pbkeylen; Utils.optional_apply (fun p -> Srt.(setsockflag s passphrase p)) passphrase; Srt.listen s (Option.value ~default:1 max_clients); self#log#info "Setting up socket to listen at %s:%i" bind_address port; Atomic.set listening_socket (Some s); s with exn -> let bt = Printexc.get_raw_backtrace () in Srt.close s; Printexc.raise_with_backtrace exn bt) method connect = let rec accept_connection s = try let client, origin = Srt.accept s in try Poll.add_socket ~mode:`Read s accept_connection; (try self#log#info "New connection from %s" (string_of_address origin) with exn -> self#log#important "Error while fetching connection source: %s" (Printexc.to_string exn)); Srt.(setsockflag client sndsyn true); Srt.(setsockflag client rcvsyn true); Utils.optional_apply (fun v -> Srt.(setsockflag client sndtimeo v)) write_timeout; Utils.optional_apply (fun v -> Srt.(setsockflag client rcvtimeo v)) read_timeout; on_socket ~mode:`Incoming client; if self#should_stop then ( close_socket ~on_socket client; raise Done); self#mutexify (fun () -> client_sockets <- (origin, client) :: client_sockets; !on_connect ()) () with exn -> let bt = Printexc.get_raw_backtrace () in Srt.close client; Printexc.raise_with_backtrace exn bt with exn -> self#log#debug "Failed to connect: %s" (Printexc.to_string exn) in if not self#should_stop then self#mutexify (fun () -> Poll.add_socket ~mode:`Read self#listening_socket accept_connection) () method disconnect = let should_stop = self#should_stop in self#mutexify (fun () -> List.iter (fun (_, s) -> close_socket ~on_socket s) client_sockets; client_sockets <- []; (match (should_stop, Atomic.get listening_socket) with | true, Some s -> Poll.remove_socket s; close_socket ~on_socket s; Atomic.set listening_socket None | _ -> ()); !on_disconnect ()) () end class virtual input_base ~max ~self_sync ~on_connect ~on_disconnect ~payload_size ~dump ~on_start ~on_stop ~autostart format = let max_length = Some (Frame.main_of_seconds max) in object (self) inherit input_networking_agent inherit base () inherit Start_stop.active_source ~name:"input.srt" ~on_start ~on_stop ~autostart ~fallible:true () as super val mutable decoder_data = None val mutable dump_chan = None initializer let on_connect_cur = !on_connect in (on_connect := fun () -> (match dump with | Some fname -> dump_chan <- Some (open_out_bin fname) | None -> ()); on_connect_cur ()); let on_disconnect_cur = !on_disconnect in on_disconnect := fun () -> (match decoder_data with | Some (d, _) -> d.Decoder.close () | None -> ()); decoder_data <- None; (match dump_chan with | Some chan -> close_out_noerr chan; dump_chan <- None | None -> ()); on_disconnect_cur () initializer self#on_wake_up (fun () -> Generator.set_max_length self#buffer max_length) method seek_source = (self :> Source.source) method remaining = -1 method abort_track = Generator.add_track_mark self#buffer method private can_generate_frame = super#started && (not self#should_stop) && self#is_connected method self_sync = if self_sync then (`Dynamic, if self#is_connected then Some sync_source else None) else (`Static, None) method private create_decoder socket = let create_decoder = match Decoder.get_stream_decoder ~ctype:self#content_type format with | Some d -> d | None -> raise Harbor.Unknown_codec in let buf = Buffer.create payload_size in let tmp = Bytes.create payload_size in let eof_seen = ref false in Srt.setsockflag socket Srt.sndsyn true; Srt.setsockflag socket Srt.rcvsyn true; let read bytes ofs len = if self#should_stop then raise Done; if !eof_seen && Buffer.length buf = 0 then raise End_of_file; if (not !eof_seen) && Buffer.length buf < len then ( let input = Srt.recvmsg socket tmp payload_size in if input = 0 then eof_seen := true; Buffer.add_subbytes buf tmp 0 input; match dump_chan with | Some chan -> output chan tmp 0 input | None -> ()); let len = min len (Buffer.length buf) in Buffer.blit buf 0 bytes ofs len; Utils.buffer_drop buf len; len in create_decoder { Decoder.read; tell = None; length = None; lseek = None } method private generate_frame = let size = Lazy.force Frame.size in try let _, socket = self#get_socket in let decoder, buffer = match decoder_data with | None -> let buffer = Decoder.mk_buffer ~ctype:self#content_type self#buffer in let decoder = self#create_decoder socket in decoder_data <- Some (decoder, buffer); Generator.add_track_mark self#buffer; (decoder, buffer) | Some d -> d in while Generator.length self#buffer < size do decoder.Decoder.decode buffer done; Generator.slice self#buffer size with exn -> let bt = Printexc.get_backtrace () in Utils.log_exception ~log:self#log ~bt (Printf.sprintf "Feeding failed: %s" (Printexc.to_string exn)); self#disconnect; if not self#should_stop then self#connect; Frame.append (Generator.slice self#buffer size) self#end_of_track method private start = self#set_should_stop false; self#connect method private stop = self#set_should_stop true; self#disconnect end class input_listener ~enforced_encryption ~pbkeylen ~passphrase ~listen_callback ~bind_address ~port ~prefer_address ~on_socket ~max ~payload_size ~self_sync ~on_connect ~on_disconnect ~read_timeout ~write_timeout ~messageapi ~dump ~on_start ~on_stop ~autostart format = object (self) inherit input_base ~max ~payload_size ~self_sync ~on_connect ~on_disconnect ~dump ~on_start ~on_stop ~autostart format inherit listener ~enforced_encryption ~pbkeylen ~passphrase ~listen_callback ~max_clients:(Some 1) ~bind_address ~port ~prefer_address ~on_socket ~payload_size ~read_timeout ~write_timeout ~messageapi ~on_connect ~on_disconnect () method private get_socket = match self#get_sockets with | [s] -> s | [] -> raise Not_connected | _ -> assert false end class input_caller ~enforced_encryption ~pbkeylen ~passphrase ~streamid ~polling_delay ~hostname ~port ~prefer_address ~max ~payload_size ~self_sync ~on_connect ~on_disconnect ~read_timeout ~write_timeout ~connection_timeout ~messageapi ~dump ~on_socket ~on_start ~on_stop ~autostart format = object (self) inherit input_base ~max ~payload_size ~self_sync ~on_connect ~on_disconnect ~dump ~on_start ~on_stop ~autostart format inherit caller ~enforced_encryption ~pbkeylen ~passphrase ~streamid ~polling_delay ~hostname ~port ~prefer_address ~payload_size ~read_timeout ~write_timeout ~connection_timeout ~messageapi ~on_connect ~on_disconnect ~on_socket method get_sockets = match self#get_socket with s -> [s] | exception Not_found -> [] end let _ = let return_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator ~base:Modules.input "srt" ~return_t ~category:`Input ~meth:(meth () @ Start_stop.meth ()) ~descr:"Receive a SRT stream from a distant agent." (common_options ~mode:`Listener @ Start_stop.active_source_proto ~fallible_opt:`Nope @ [ ( "max", Lang.float_t, Some (Lang.float 10.), Some "Maximum duration of the buffered data." ); ( "self_sync", Lang.bool_t, Some (Lang.bool true), Some "`true` if the source controls its own latency (i.e. the SRT \ stream is in `live` mode), `false` otherwise (i.e. the stream is \ in `file` mode." ); ( "dump", Lang.string_t, Some (Lang.string ""), Some "Dump received data to the given file for debugging. Unused is \ empty." ); ( "content_type", Lang.string_t, Some (Lang.string "application/ffmpeg"), Some "Content-Type (mime type) used to find a decoder for the input \ stream." ); ]) (fun p -> let { mode; hostname; port; prefer_address; streamid; enforced_encryption; pbkeylen; passphrase; bind_address; on_socket; listen_callback; polling_delay; read_timeout; write_timeout; connection_timeout; payload_size; messageapi; on_connect; on_disconnect; } = parse_common_options p in let dump = match Lang.to_string (List.assoc "dump" p) with | s when s = "" -> None | s -> Some s in let max = Lang.to_float (List.assoc "max" p) in let self_sync = Lang.to_bool (List.assoc "self_sync" p) in let on_start = let f = List.assoc "on_start" p in fun () -> ignore (Lang.apply f []) in let on_stop = let f = List.assoc "on_stop" p in fun () -> ignore (Lang.apply f []) in let autostart = Lang.to_bool (List.assoc "start" p) in let format = Lang.to_string (List.assoc "content_type" p) in match mode with | `Listener -> (new input_listener ~enforced_encryption ~pbkeylen ~passphrase ~listen_callback ~bind_address ~port ~prefer_address ~on_socket ~read_timeout ~write_timeout ~payload_size ~self_sync ~on_connect ~on_disconnect ~messageapi ~max ~dump ~on_start ~on_stop ~autostart format :> < Start_stop.active_source ; get_sockets : (Unix.sockaddr * Srt.socket) list ; set_should_stop : bool -> unit ; connect : unit ; disconnect : unit >) | `Caller -> (new input_caller ~enforced_encryption ~pbkeylen ~passphrase ~streamid ~polling_delay ~hostname ~port ~prefer_address ~payload_size ~self_sync ~on_connect ~read_timeout ~write_timeout ~connection_timeout ~on_disconnect ~messageapi ~max ~dump ~on_start ~on_socket ~on_stop ~autostart format :> < Start_stop.active_source ; get_sockets : (Unix.sockaddr * Srt.socket) list ; set_should_stop : bool -> unit ; connect : unit ; disconnect : unit >)) class virtual output_base ~payload_size ~messageapi ~on_start ~on_stop ~infallible ~register_telnet ~autostart ~on_disconnect ~encoder_factory source = let buffer = Strings.Mutable.empty () in let tmp = Bytes.create payload_size in object (self) inherit output_networking_agent inherit base () inherit [Strings.t] Output.encoded ~output_kind:"srt" ~on_start ~on_stop ~infallible ~register_telnet ~export_cover_metadata:false ~autostart ~name:"output.srt" source val mutable encoder = Atomic.make None initializer let on_disconnect_cur = !on_disconnect in on_disconnect := fun () -> ignore (Strings.Mutable.flush buffer); Atomic.set encoder None; on_disconnect_cur () method private send_chunk = self#mutexify (fun () -> Strings.Mutable.blit buffer 0 tmp 0 payload_size; Strings.Mutable.drop buffer payload_size) (); let send data socket = if messageapi then Srt.sendmsg socket data (-1) false else Srt.send socket data in let rec f pos socket = match pos with | pos when pos < payload_size -> let ret = send (Bytes.sub tmp pos (payload_size - pos)) socket in f (pos + ret) socket | _ -> () in let f pos (_, socket) = try f pos socket with exn -> let bt = Printexc.get_raw_backtrace () in self#stop_encoder; self#client_error socket exn bt in List.iter (f 0) self#get_sockets method private send_chunks = let len = self#mutexify (fun () -> Strings.Mutable.length buffer) in while payload_size <= len () do self#send_chunk done method private get_encoder = self#mutexify (fun () -> match Atomic.get encoder with | Some enc -> enc | None -> let enc = encoder_factory self#id Frame.Metadata.Export.empty in Atomic.set encoder (Some enc); enc) () method private start = self#mutexify (fun () -> Atomic.set should_stop false) (); self#connect method! private reset = self#start; self#stop method private stop = self#mutexify (fun () -> Atomic.set should_stop true) (); self#stop_encoder; self#send_chunks; self#disconnect method stop_encoder = if self#is_connected then ( try self#mutexify (Strings.Mutable.append_strings buffer) (self#get_encoder.Encoder.stop ()) with exn -> let bt = Printexc.get_backtrace () in Utils.log_exception ~log:self#log ~bt (Printf.sprintf "Error while stopping encoder: %s" (Printexc.to_string exn))); ignore (Strings.Mutable.flush buffer); Atomic.set encoder None method private encode frame = if self#is_connected then self#get_encoder.Encoder.encode frame else Strings.empty method private insert_metadata m = if self#is_connected then self#get_encoder.Encoder.insert_metadata m method private send data = if self#is_connected then ( self#mutexify (Strings.Mutable.append_strings buffer) data; self#send_chunks) end class output_caller ~enforced_encryption ~pbkeylen ~passphrase ~streamid ~polling_delay ~payload_size ~messageapi ~on_start ~on_stop ~infallible ~register_telnet ~autostart ~on_socket ~on_connect ~on_disconnect ~prefer_address ~port ~hostname ~read_timeout ~write_timeout ~connection_timeout ~encoder_factory source_val = let source = Lang.to_source source_val in object (self) inherit output_base ~payload_size ~messageapi ~on_start ~on_stop ~infallible ~register_telnet ~autostart ~on_disconnect ~encoder_factory source_val inherit caller ~enforced_encryption ~pbkeylen ~passphrase ~streamid ~polling_delay ~hostname ~port ~prefer_address ~payload_size ~read_timeout ~write_timeout ~connection_timeout ~messageapi ~on_connect ~on_disconnect ~on_socket method self_sync = source#self_sync method private get_sockets = try [self#get_socket] with Not_connected -> [] method private client_error _ exn bt = Utils.log_exception ~log:self#log ~bt:(Printexc.raw_backtrace_to_string bt) (Printf.sprintf "Error while sending client data: %s" (Printexc.to_string exn)); self#disconnect; if not self#should_stop then self#connect end class output_listener ~enforced_encryption ~pbkeylen ~passphrase ~listen_callback ~max_clients ~payload_size ~messageapi ~on_start ~on_stop ~infallible ~register_telnet ~autostart ~on_connect ~on_disconnect ~bind_address ~port ~prefer_address ~on_socket ~read_timeout ~write_timeout ~encoder_factory source_val = let source = Lang.to_source source_val in object (self) inherit output_base ~payload_size ~messageapi ~on_start ~on_stop ~infallible ~register_telnet ~autostart ~on_disconnect ~encoder_factory source_val inherit listener ~bind_address ~port ~prefer_address ~on_socket ~payload_size ~read_timeout ~write_timeout ~messageapi ~on_connect ~on_disconnect ~enforced_encryption ~pbkeylen ~passphrase ~listen_callback ~max_clients () method self_sync = source#self_sync method private client_error socket exn bt = Utils.log_exception ~log:self#log ~bt:(Printexc.raw_backtrace_to_string bt) (Printf.sprintf "Error while sending client data: %s" (Printexc.to_string exn)); self#mutexify (fun () -> close_socket ~on_socket socket; client_sockets <- List.filter (fun (_, s) -> s <> socket) client_sockets) () end let _ = let return_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in let output_meth = List.map (fun (a, b, c, fn) -> (a, b, c, fun s -> fn (s :> Output.output))) Output.meth in Lang.add_operator ~base:Modules.output "srt" ~return_t ~category:`Output ~meth:(meth () @ output_meth) ~descr:"Send a SRT stream to a distant agent." (Output.proto @ common_options ~mode:`Caller @ [ ( "max_clients", Lang.nullable_t Lang.int_t, Some Lang.null, Some "Max number of connected clients (listener mode only)" ); ("", Lang.format_t return_t, None, Some "Encoding format."); ("", Lang.source_t return_t, None, None); ]) (fun p -> let { mode; hostname; port; prefer_address; streamid; enforced_encryption; pbkeylen; passphrase; bind_address; on_socket; listen_callback; polling_delay; read_timeout; write_timeout; connection_timeout; payload_size; messageapi; on_connect; on_disconnect; } = parse_common_options p in let source = Lang.assoc "" 2 p in let max_clients = Lang.to_valued_option Lang.to_int (List.assoc "max_clients" p) in let infallible = not (Lang.to_bool (List.assoc "fallible" p)) in let register_telnet = Lang.to_bool (List.assoc "register_telnet" p) in let autostart = Lang.to_bool (List.assoc "start" p) in let on_start = let f = List.assoc "on_start" p in fun () -> ignore (Lang.apply f []) in let on_stop = let f = List.assoc "on_stop" p in fun () -> ignore (Lang.apply f []) in let format_val = Lang.assoc "" 1 p in let format = Lang.to_format format_val in let encoder_factory = try (Encoder.get_factory format) ~pos:(Value.pos format_val) with Not_found -> raise (Error.Invalid_value (format_val, "Cannot get a stream encoder for that format")) in match mode with | `Caller -> (new output_caller ~enforced_encryption ~pbkeylen ~passphrase ~streamid ~polling_delay ~hostname ~port ~prefer_address ~payload_size ~autostart ~on_start ~on_stop ~read_timeout ~write_timeout ~connection_timeout ~infallible ~register_telnet ~messageapi ~encoder_factory ~on_socket ~on_connect ~on_disconnect source :> < Output.output ; get_sockets : (Unix.sockaddr * Srt.socket) list ; set_should_stop : bool -> unit ; connect : unit ; disconnect : unit >) | `Listener -> (new output_listener ~enforced_encryption ~pbkeylen ~passphrase ~bind_address ~port ~prefer_address ~on_socket ~read_timeout ~write_timeout ~payload_size ~autostart ~on_start ~on_stop ~infallible ~register_telnet ~messageapi ~encoder_factory ~on_connect ~on_disconnect ~listen_callback ~max_clients source :> < Output.output ; get_sockets : (Unix.sockaddr * Srt.socket) list ; set_should_stop : bool -> unit ; connect : unit ; disconnect : unit >)) liquidsoap-2.3.2/src/core/io/udp_io.ml000066400000000000000000000234041477303350200176360ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let max_packet_size = if Configure.host = "osx" then 9216 else 65535 module Tutils = struct include Tutils (* Thread with preemptive kill/wait mechanism, see mli for details. *) (** Preemptive stoppable thread. The thread function receives a [should_stop,has_stop] pair on startup. It should regularly poll the [should_stop] and stop when asked to. Before stopping it should call [has_stopped]. The function returns a [kill,wait] pair. The first function should be called to request that the thread stops, and the second to wait that it has effectively stopped. *) let stoppable_thread f name = let cond = Condition.create () in let lock = Mutex.create () in let should_stop = ref false in let has_stopped = ref false in let kill = Mutex_utils.mutexify lock (fun () -> should_stop := true) in let wait () = wait cond lock (fun () -> !has_stopped) in let should_stop = Mutex_utils.mutexify lock (fun () -> !should_stop) in let has_stopped = Mutex_utils.mutexify lock (fun () -> has_stopped := true; Condition.signal cond) in let _ = create f (should_stop, has_stopped) name in (kill, wait) end class output ~on_start ~on_stop ~register_telnet ~infallible ~autostart ~hostname ~port ~encoder_factory source_val = let source = Lang.to_source source_val in object (self) inherit [Strings.t] Output.encoded ~output_kind:"udp" ~on_start ~on_stop ~register_telnet ~infallible ~autostart ~export_cover_metadata:false ~name:(Printf.sprintf "udp://%s:%d" hostname port) source_val val mutable socket_send = None val mutable encoder = None method self_sync = source#self_sync method private start = let socket = Unix.socket ~cloexec:true Unix.PF_INET Unix.SOCK_DGRAM (Unix.getprotobyname "udp").Unix.p_proto in let ipaddr = (Unix.gethostbyname hostname).Unix.h_addr_list.(0) in let portaddr = Unix.ADDR_INET (ipaddr, port) in socket_send <- Some (fun msg off len -> let msg = Bytes.of_string msg in let rec f pos = if off + pos < len then ( let len = min (len - pos) max_packet_size in let len = Unix.sendto socket msg (off + pos) len [] portaddr in f (pos + len)) in f 0); encoder <- Some (encoder_factory self#id Frame.Metadata.Export.empty) method! private reset = self#start; self#stop method private stop = socket_send <- None; encoder <- None method private encode frame = (Option.get encoder).Encoder.encode frame method private insert_metadata m = (Option.get encoder).Encoder.insert_metadata m method private send data = let socket_send = Option.get socket_send in Strings.iter (fun s o l -> ignore (socket_send s o l)) data end class input ~hostname ~port ~get_stream_decoder ~bufferize = let max_length = Some (2 * Frame.main_of_seconds bufferize) in object (self) inherit Generated.source ~empty_on_abort:false ~bufferize () as generated inherit! Start_stop.active_source ~name:"input.udp" ~fallible:true ~on_start:ignore ~on_stop:ignore ~autostart:true () as super val mutable kill_feeding = None val mutable wait_feeding = None val mutable decoder_factory = None method private decoder_factory = Option.get decoder_factory method! private can_generate_frame = super#started && generated#can_generate_frame method private start = begin let decoder args = let buffer = Decoder.mk_buffer ~ctype:self#content_type self#buffer in (get_stream_decoder self#content_type args, buffer) in decoder_factory <- Some decoder; match wait_feeding with | None -> () | Some f -> f (); wait_feeding <- None end; let kill, wait = Tutils.stoppable_thread self#feed "UDP input" in kill_feeding <- Some kill; wait_feeding <- Some wait initializer self#on_wake_up (fun () -> Generator.set_max_length self#buffer max_length) method private stop = (Option.get kill_feeding) (); kill_feeding <- None; decoder_factory <- None method private feed (should_stop, has_stopped) = let socket = Unix.socket ~cloexec:true Unix.PF_INET Unix.SOCK_DGRAM (Unix.getprotobyname "udp").Unix.p_proto in let ipaddr = (Unix.gethostbyname hostname).Unix.h_addr_list.(0) in let addr = Unix.ADDR_INET (ipaddr, port) in Unix.bind socket addr; (* Wait until there's something to read or we must stop. *) let rec wait () = if should_stop () then failwith "stop"; let l, _, _ = Utils.select [socket] [] [] 1. in if l = [] then wait () in (* Read data from the network. *) let read buf ofs len = wait (); let n, _ = Unix.recvfrom socket buf ofs len [] in n in let input = { Decoder.read; tell = None; length = None; lseek = None } in let decoder, buffer = self#decoder_factory input in try (* Feeding loop. *) while true do if should_stop () then failwith "stop"; decoder.Decoder.decode buffer done with e -> decoder.Decoder.close (); Generator.add_track_mark self#buffer; (* Closing the socket is slightly overkill but we need to recreate the decoder anyway, which might loose some data too. *) Unix.close socket; begin match e with | Failure s -> self#log#severe "Feeding stopped: %s." s | e -> self#log#severe "Feeding stopped: %s." (Printexc.to_string e) end; if should_stop () then has_stopped () else self#feed (should_stop, has_stopped) end let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator ~base:Modules.output "udp" ~descr:"Output encoded data to UDP, without any control whatsoever." ~category:`Output ~flags:[`Hidden; `Deprecated; `Experimental] (Output.proto @ [ ("port", Lang.int_t, None, None); ("host", Lang.string_t, None, None); ("", Lang.format_t frame_t, None, Some "Encoding format."); ("", Lang.source_t frame_t, None, None); ]) ~return_t:frame_t (fun p -> (* Generic output parameters *) let autostart = Lang.to_bool (List.assoc "start" p) in let infallible = not (Lang.to_bool (List.assoc "fallible" p)) in let register_telnet = Lang.to_bool (List.assoc "register_telnet" p) in let on_start = let f = List.assoc "on_start" p in fun () -> ignore (Lang.apply f []) in let on_stop = let f = List.assoc "on_stop" p in fun () -> ignore (Lang.apply f []) in (* Specific UDP parameters *) let port = Lang.to_int (List.assoc "port" p) in let hostname = Lang.to_string (List.assoc "host" p) in let fmt = let fmt = Lang.assoc "" 1 p in try (Encoder.get_factory (Lang.to_format fmt)) ~pos:(Value.pos fmt) with Not_found -> raise (Error.Invalid_value (fmt, "Cannot get a stream encoder for that format")) in let source = Lang.assoc "" 2 p in (new output ~on_start ~on_stop ~register_telnet ~infallible ~autostart ~hostname ~port ~encoder_factory:fmt source :> Source.source)) let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator ~base:Modules.input "udp" ~descr:"Input encoded data from UDP, without any control whatsoever." ~category:`Input ~flags:[`Hidden; `Deprecated; `Experimental] [ ("port", Lang.int_t, None, None); ("host", Lang.string_t, None, None); ( "buffer", Lang.float_t, Some (Lang.float 1.), Some "Duration of buffered data before starting playout." ); ("", Lang.string_t, None, Some "Mime type."); ] ~return_t:frame_t (fun p -> (* Specific UDP parameters *) let port = Lang.to_int (List.assoc "port" p) in let hostname = Lang.to_string (List.assoc "host" p) in let bufferize = Lang.to_float (List.assoc "buffer" p) in let mime = Lang.to_string (Lang.assoc "" 1 p) in let get_stream_decoder ctype = match Decoder.get_stream_decoder ~ctype mime with | None -> raise (Error.Invalid_value ( Lang.assoc "" 1 p, "Cannot get a stream decoder for this MIME" )) | Some decoder_factory -> decoder_factory in (new input ~hostname ~port ~bufferize ~get_stream_decoder :> Source.source)) liquidsoap-2.3.2/src/core/json.ml000066400000000000000000000000351477303350200167140ustar00rootroot00000000000000include Liquidsoap_lang.Json liquidsoap-2.3.2/src/core/lang.ml000066400000000000000000000041301477303350200166640ustar00rootroot00000000000000include Liquidsoap_lang.Lang include Lang_source include Lang_encoder.L module Flags = Liquidsoap_lang.Flags module Http = Liq_http let source_t = source_t ?pos:None let () = Hooks_implementations.register () (** Helpers for defining protocols. *) let add_protocol ~syntax ~doc ~static name resolver = Doc.Protocol.add ~name ~doc ~syntax; let spec = { Request.static; resolve = resolver } in Plug.register Request.protocols ~doc name spec let frame_t base_type fields = Frame_type.make base_type fields let internal_tracks_t () = Frame_type.internal_tracks () let pcm_audio_t () = Frame_type.pcm_audio () let format_t t = Type.make (Type.Constr (* The type has to be invariant because we don't want the sup mechanism to be used here, see #2806. *) { Type.constructor = "format"; Type.params = [(`Invariant, t)] }) module HttpTransport = struct include Value.MkCustom (struct type content = Http.transport let name = "http_transport" let to_json ~pos _ = Runtime_error.raise ~pos ~message:"Http transport cannot be represented as json" "json" let to_string transport = Printf.sprintf "<%s_transport>" transport#name let compare = Stdlib.compare end) let meths = [ ( "name", ([], string_t), "Transport name", fun transport -> string transport#name ); ( "protocol", ([], string_t), "Transport protocol", fun transport -> string transport#protocol ); ( "default_port", ([], int_t), "Transport default port", fun transport -> int transport#default_port ); ] let base_t = t let t = method_t t (List.map (fun (lbl, t, descr, _) -> (lbl, t, descr)) meths) let to_base_value = to_value let to_value transport = meth (to_value transport) (List.map (fun (lbl, _, _, m) -> (lbl, m transport)) meths) end let http_transport_t = HttpTransport.t let http_transport_base_t = HttpTransport.base_t let to_http_transport = HttpTransport.of_value let http_transport = HttpTransport.to_value let base_http_transport = HttpTransport.to_base_value ?pos:None liquidsoap-2.3.2/src/core/lang.mli000066400000000000000000000243441477303350200170460ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Values and types of the liquidsoap language. *) val log : Log.t (** The type of a value. *) type t = Liquidsoap_lang.Type.t type module_name = Liquidsoap_lang.Lang.module_name type scheme = Liquidsoap_lang.Type.scheme type regexp = Liquidsoap_lang.Lang.regexp (** {2 Values} *) module Custom = Value.Custom module Methods = Term.Methods module Flags = Liquidsoap_lang.Flags type in_value = Liquidsoap_lang.Value.in_value type env = Liquidsoap_lang.Value.env type value = Liquidsoap_lang.Value.t val demeth : value -> value val split_meths : value -> (string * value) list * value (** Iter a function over all sources contained in a value. This only applies to statically referenced objects, i.e. it does not explore inside reference cells. [on_imprecise] is used when we encounter a value cell that may contain a source. If not passed, we display a warning log. *) val iter_sources : ?on_imprecise:(unit -> unit) -> (Source.source -> unit) -> value -> unit (** {2 Computation} *) (** Multiapply a value to arguments. The argument [t] is the type of the result of the application. *) val apply : ?pos:Liquidsoap_lang.Pos.t list -> value -> env -> value (** {3 Helpers for registering protocols} *) val add_protocol : syntax:string -> doc:string -> static:(string -> bool) -> string -> Request.resolver -> unit (** {3 Helpers for source builtins} *) type proto = (string * t * value option * string option) list (** Add a builtin to the language, high-level version for functions. *) val add_builtin : category:Doc.Value.category -> descr:string -> ?flags:Doc.Value.flag list -> ?meth:(string * Type.scheme * string * value) list -> ?examples:string list -> ?base:module_name -> string -> proto -> t -> (env -> value) -> module_name (** Add a builtin value to the language *) val add_builtin_value : category:Doc.Value.category -> descr:string -> ?flags:Doc.Value.flag list -> ?base:module_name -> string -> value -> t -> module_name (** Add a builtin to the language, more rudimentary version. *) val add_builtin_base : category:Doc.Value.category -> descr:string -> ?flags:Doc.Value.flag list -> ?base:module_name -> string -> Liquidsoap_lang.Value.in_value -> t -> module_name (** Declare a new module. *) val add_module : ?base:module_name -> string -> module_name val module_name : module_name -> string type 'a operator_method = string * scheme * string * ('a -> value) (** Add an operator to the language and to the documentation. *) val add_operator : category:Doc.Value.source -> descr:string -> ?flags:Doc.Value.flag list -> ?meth:(< Source.source ; .. > as 'a) operator_method list -> ?base:module_name -> string -> proto -> return_t:t -> (env -> 'a) -> module_name (** Add a track operator to the language and to the documentation. *) val add_track_operator : category:Doc.Value.source -> descr:string -> ?flags:Doc.Value.flag list -> ?meth:(< Source.source ; .. > as 'a) operator_method list -> ?base:module_name -> string -> proto -> return_t:t -> (env -> Frame.field * 'a) -> module_name (** {2 Manipulation of values} *) val to_unit : value -> unit val to_bool : value -> bool val to_bool_getter : value -> unit -> bool val to_string : value -> string val to_string_getter : value -> unit -> string val to_regexp : value -> regexp val to_float : value -> float val to_float_getter : value -> unit -> float val to_error : value -> Runtime_error.runtime_error val to_source : value -> Source.source val to_format : value -> Encoder.format val to_track : value -> Frame.field * Source.source val to_int : value -> int val to_int_getter : value -> unit -> int val to_num : value -> [ `Int of int | `Float of float ] val to_list : value -> value list val to_option : value -> value option val to_valued_option : (value -> 'a) -> value -> 'a option val to_default_option : default:'a -> (value -> 'a) -> value -> 'a val to_product : value -> value * value val to_tuple : value -> value list val to_metadata_list : value -> (string * string) list val to_metadata : value -> Frame.metadata val to_string_list : value -> string list val to_int_list : value -> int list val to_source_list : value -> Source.source list val to_fun : value -> (string * value) list -> value val to_getter : value -> unit -> value val to_ref : value -> (unit -> value) * (value -> unit) val to_valued_ref : (value -> 'a) -> ('a -> value) -> value -> (unit -> 'a) * ('a -> unit) val to_http_transport : value -> Liq_http.transport (** [assoc x n l] returns the [n]-th [y] such that [(x,y)] is in the list [l]. This is useful for retrieving arguments of a function. *) val assoc : 'a -> int -> ('a * 'b) list -> 'b val int_t : t val unit_t : t val float_t : t val bool_t : t val string_t : t val regexp_t : t val product_t : t -> t -> t val of_product_t : t -> t * t val tuple_t : t list -> t val of_tuple_t : t -> t list val record_t : (string * t) list -> t val optional_record_t : (string * t) list -> t val method_t : t -> (string * scheme * string) list -> t val optional_method_t : t -> (string * scheme * string) list -> t val list_t : t -> t val of_list_t : t -> t val nullable_t : t -> t val ref_t : t -> t val error_t : t val source_t : ?methods:bool -> t -> t val of_source_t : t -> t val format_t : t -> t val metadata_track_t : t val track_marks_t : t (* [frame_t base_type fields] returns a frame with [base_type] as its base type and [fields] as explicit fields. Equivalent to: [base_type.{fields}] *) val frame_t : t -> t Frame.Fields.t -> t (* Return a generic frame type with the internal media constraint applied. Equivalent to: ['a where 'a is an internal media type] *) val internal_tracks_t : unit -> t (* Return a generic frame type with the pcm audio constraint applied. *) val pcm_audio_t : unit -> t (** [fun_t args r] is the type of a function taking [args] as parameters and returning values of type [r]. The elements of [r] are of the form [(b,l,t)] where [b] indicates if the argument is optional, [l] is the label of the argument ([""] means no label) and [t] is the type of the argument. *) val fun_t : (bool * string * t) list -> t -> t val univ_t : ?constraints:Liquidsoap_lang.Type.constr list -> unit -> t (** A shortcut for lists of pairs of strings. *) val metadata_t : t (** A getter on an arbitrary type. *) val getter_t : t -> t (** Custom http transport with all methods *) val http_transport_t : t (** Same with no methods. *) val http_transport_base_t : t val unit : value val int : int -> value val octal_int : int -> value val hex_int : int -> value val bool : bool -> value val float : float -> value val string : string -> value val regexp : regexp -> value val list : value list -> value val null : value val error : Runtime_error.runtime_error -> value val source : Source.source -> value val track : Frame.field * Source.source -> value val product : value -> value -> value val tuple : value list -> value val meth : value -> (string * value) list -> value val record : (string * value) list -> value val reference : (unit -> value) -> (value -> unit) -> value val http_transport : Liq_http.transport -> value val base_http_transport : Liq_http.transport -> value (** Build a function from an OCaml function. Items in the prototype indicate the label and optional values. Second string value is used when renaming argument name, e.g. `fun (foo=_, ...) -> ` *) val val_fun : (string * string * value option) list -> (env -> value) -> value (** Build a function from a term. *) val term_fun : (string * string * value option) list -> Term.t -> value (** Build a constant function. It is slightly less opaque and allows the printing of the closure when the constant is ground. *) val val_cst_fun : (string * value option) list -> value -> value (** Extract position from the environment. Used inside function execution. *) val pos : env -> Liquidsoap_lang.Pos.t list (** Convert a metadata packet to a list associating strings to strings. *) val metadata : Frame.metadata -> value val metadata_list : (string * string) list -> value (** Raise an error. *) val raise_error : ?bt:Printexc.raw_backtrace -> ?message:string -> pos:Liquidsoap_lang.Pos.List.t -> string -> 'a (** Convert an exception into a runtime error. *) val runtime_error_of_exception : bt:Printexc.raw_backtrace -> kind:string -> exn -> Runtime_error.runtime_error (** Re-raise an error as a runtime error. *) val raise_as_runtime : bt:Printexc.raw_backtrace -> kind:string -> exn -> 'a (** Return the process' environment. *) val environment : unit -> (string * string) list (** Return an unescaped string description of a regexp, i.e. ^foo/bla$ *) val descr_of_regexp : regexp -> string (** Return a string description of a regexp value i.e. r/^foo\/bla$/g *) val string_of_regexp : regexp -> string type stdlib = [ `Disabled | `If_present | `Force | `Override of string ] (** Type a term, possibly returning the cached term instead. *) val type_term : ?name:string -> ?cache:bool -> ?trim:bool -> ?deprecated:bool -> ?ty:t -> stdlib:stdlib -> parsed_term:Liquidsoap_lang.Parsed_term.t -> Term.t -> Term.t (** Evaluate a term. *) val eval : ?toplevel:bool -> ?typecheck:bool -> ?cache:bool -> ?deprecated:bool -> ?ty:t -> ?name:string -> stdlib:stdlib -> string -> value liquidsoap-2.3.2/src/core/lang_source.ml000066400000000000000000000644371477303350200202640ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) module Lang = Liquidsoap_lang.Lang module Flags = Liquidsoap_lang.Flags open Lang module ClockValue = struct include Value.MkCustom (struct type content = Clock.t let name = "clock" let to_string = Clock.descr let to_json ~pos _ = Lang.raise_error ~message:"Clocks cannot be represented as json" ~pos "json" let compare = Stdlib.compare end) let base_t = t let to_base_value = to_value let methods = [ ( "id", Lang.ref_t Lang.string_t, "The clock's id", fun c -> let get () = Lang.string (Clock.id c) in let set v = Clock.set_id c (Lang.to_string v) in Lang.reference get set ); ( "sync", Lang.fun_t [] Lang.string_t, "The clock's current sync mode. One of: `\"stopped\"`, `\"stopping\"`, \ `\"auto\"`, `\"CPU\"`, `\"unsynced\"` or `\"passive\"`.", fun c -> Lang.val_fun [] (fun _ -> Lang.string Clock.(string_of_sync_mode (sync c))) ); ( "start", Lang.fun_t [(true, "force", Lang.bool_t)] Lang.unit_t, "Start the clock.", fun c -> Lang.val_fun [("force", "force", Some (Lang.bool true))] (fun p -> let pos = Lang.pos p in let force = Lang.to_bool (List.assoc "force" p) in try Clock.start ~force c; Lang.unit with Clock.Invalid_state -> Runtime_error.raise ~message: (Printf.sprintf "Invalid clock state: %s" Clock.(string_of_sync_mode (sync c))) ~pos "clock") ); ( "stop", Lang.fun_t [] Lang.unit_t, "Stop the clock. Does nothing if the clock is stopping or stopped.", fun c -> Lang.val_fun [] (fun _ -> Clock.stop c; Lang.unit) ); ( "self_sync", Lang.fun_t [] Lang.bool_t, "`true` if the clock is in control of its latency.", fun c -> Lang.val_fun [] (fun _ -> Lang.bool (Clock.self_sync c)) ); ( "unify", Lang.fun_t [(false, "", base_t)] Lang.unit_t, "Unify the clock with another one. One of the two clocks should be in \ `\"stopped\"` sync mode.", fun c -> Lang.val_fun [("", "", None)] (fun p -> let pos = match Lang.pos p with p :: _ -> Some p | [] -> None in let c' = of_value (List.assoc "" p) in Clock.unify ~pos c c'; Lang.unit) ); ( "tick", Lang.fun_t [] Lang.unit_t, "Animate the clock and run one tick", fun c -> Lang.val_fun [] (fun _ -> Clock.tick c; Lang.unit) ); ( "ticks", Lang.fun_t [] Lang.int_t, "The total number of times the clock has ticked.", fun c -> Lang.val_fun [] (fun _ -> Lang.int (Clock.ticks c)) ); ] let t = Lang.method_t base_t (List.map (fun (lbl, typ, descr, _) -> (lbl, ([], typ), descr)) methods) let to_value c = Lang.meth (to_base_value c) (List.map (fun (lbl, _, _, v) -> (lbl, v c)) methods) end let log = Log.make ["lang"] let metadata_t = list_t (product_t string_t string_t) let to_metadata_list t = let pop v = let f (a, b) = (to_string a, to_string b) in f (to_product v) in List.map pop (to_list t) let to_metadata t = Frame.Metadata.from_list (to_metadata_list t) let metadata_list m = list (List.map (fun (k, v) -> product (string k) (string v)) m) let metadata m = metadata_list (Frame.Metadata.to_list m) let metadata_track_t = Format_type.metadata let track_marks_t = Format_type.track_marks module Source_val = Liquidsoap_lang.Lang_core.MkCustom (struct type content = Source.source let name = "source" let to_string s = Printf.sprintf "" s#id (Type.to_string s#frame_type) let to_json ~pos _ = Runtime_error.raise ~pos ~message:(Printf.sprintf "Sources cannot be represented as json") "json" let compare s1 s2 = Stdlib.compare s1#id s2#id end) let source_methods = [ ( "id", ([], fun_t [] string_t), "Identifier of the source.", fun s -> val_fun [] (fun _ -> string s#id) ); ( "is_ready", ([], fun_t [] bool_t), "Indicate if a source is ready to stream. This does not mean that the \ source is currently streaming, just that its resources are all properly \ initialized.", fun s -> val_fun [] (fun _ -> bool s#is_ready) ); ( "reset_last_metadata_on_track", ([], ref_t bool_t), "If `true`, the source's `last_metadata` is reset on each new track. If \ a metadata is present along with the track mark, then it becomes the \ new `last_metadata`, otherwise, `last_metadata becomes `null`.", fun s -> reference (fun () -> bool s#reset_last_metadata_on_track) (fun b -> s#set_reset_last_metadata_on_track (to_bool b)) ); ( "buffered", ([], fun_t [] (list_t (product_t string_t float_t))), "Length of buffered data.", fun s -> val_fun [] (fun _ -> let l = Frame.Fields.fold (fun field _ l -> ( Frame.Fields.string_of_field field, Frame.seconds_of_main (Generator.field_length s#buffer field) ) :: l) s#content_type [] in list (List.map (fun (lbl, v) -> product (string lbl) (float v)) l)) ); ( "last_metadata", ([], fun_t [] (nullable_t metadata_t)), "Return the last metadata from the source.", fun s -> val_fun [] (fun _ -> match s#last_metadata with None -> null | Some m -> metadata m) ); ( "register_command", ( [], fun_t [ (true, "usage", Lang.nullable_t Lang.string_t); (false, "description", Lang.string_t); (false, "", Lang.string_t); (false, "", Lang.fun_t [(false, "", Lang.string_t)] Lang.string_t); ] unit_t ), "Register a server command for this source. Command is registered under \ the source's id namespace when it gets up and de-registered when it \ gets down.", fun s -> val_fun [ ("usage", "usage", Some Lang.null); ("description", "description", None); ("", "", None); ("", "", None); ] (fun p -> let usage = Lang.to_valued_option Lang.to_string (List.assoc "usage" p) in let descr = Lang.to_string (List.assoc "description" p) in let command = Lang.to_string (Lang.assoc "" 1 p) in let f = Lang.assoc "" 2 p in let f x = Lang.to_string (Lang.apply f [("", Lang.string x)]) in s#register_command ?usage ~descr command f; unit) ); ( "on_metadata", ([], fun_t [(false, "", fun_t [(false, "", metadata_t)] unit_t)] unit_t), "Call a given handler on metadata packets.", fun s -> val_fun [("", "", None)] (fun p -> let f = assoc "" 1 p in s#on_metadata (fun m -> ignore (apply f [("", metadata m)])); unit) ); ( "on_wake_up", ([], fun_t [(false, "", fun_t [] unit_t)] unit_t), "Register a function to be called after the source is asked to get \ ready. This is when, for instance, the source's final ID is set.", fun s -> val_fun [("", "", None)] (fun p -> let f = assoc "" 1 p in s#on_wake_up (fun () -> ignore (apply f [])); unit) ); ( "on_shutdown", ([], fun_t [(false, "", fun_t [] unit_t)] unit_t), "Register a function to be called when source shuts down.", fun s -> val_fun [("", "", None)] (fun p -> let f = assoc "" 1 p in s#on_sleep (fun () -> ignore (apply f [])); unit) ); ( "on_track", ([], fun_t [(false, "", fun_t [(false, "", metadata_t)] unit_t)] unit_t), "Call a given handler on new tracks.", fun s -> val_fun [("", "", None)] (fun p -> let f = assoc "" 1 p in s#on_track (fun m -> ignore (apply f [("", metadata m)])); unit) ); ( "remaining", ([], fun_t [] float_t), "Estimation of remaining time in the current track.", fun s -> val_fun [] (fun _ -> float (let r = s#remaining in if r < 0 then infinity else Frame.seconds_of_main r)) ); ( "elapsed", ([], fun_t [] float_t), "Elapsed time in the current track.", fun s -> val_fun [] (fun _ -> float (let e = s#elapsed in if e < 0 then infinity else Frame.seconds_of_main e)) ); ( "duration", ([], fun_t [] float_t), "Estimation of the duration of the current track.", fun s -> val_fun [] (fun _ -> float (let d = s#duration in if d < 0 then infinity else Frame.seconds_of_main d)) ); ( "self_sync", ([], fun_t [] bool_t), "Is the source currently controlling its own real-time loop.", fun s -> val_fun [] (fun _ -> bool (snd s#self_sync <> None)) ); ( "log", ([], record_t [("level", ref_t int_t)]), "Get or set the source's log level, from `1` to `5`.", fun s -> record [ ( "level", reference (fun () -> int s#log#level) (fun v -> s#log#set_level (to_int v)) ); ] ); ( "is_up", ([], fun_t [] bool_t), "Indicate that the source can be asked to produce some data at any time. \ This is `true` when the source is currently being used or if it could \ be used at any time, typically inside a `switch` or `fallback`.", fun s -> val_fun [] (fun _ -> bool s#is_up) ); ( "is_active", ([], fun_t [] bool_t), "`true` if the source is active, i.e. it is continuously animated by its \ own clock whenever it is ready. Typically, `true` for outputs and \ sources such as `input.http`.", fun s -> val_fun [] (fun _ -> bool (match s#source_type with `Passive -> false | _ -> true)) ); ( "seek", ([], fun_t [(false, "", float_t)] float_t), "Seek forward, in seconds (returns the amount of time effectively \ seeked).", fun s -> val_fun [("", "", None)] (fun p -> float (Frame.seconds_of_main (s#seek (Frame.main_of_seconds (to_float (List.assoc "" p)))))) ); ( "skip", ([], fun_t [] unit_t), "Skip to the next track.", fun s -> val_fun [] (fun _ -> s#abort_track; unit) ); ( "fallible", ([], bool_t), "Indicate if a source may fail, i.e. may not be ready to stream.", fun s -> bool s#fallible ); ( "clock", ([], ClockValue.base_t), "The source's clock", fun s -> ClockValue.to_base_value s#clock ); ( "time", ([], fun_t [] float_t), "Get a source's time, based on its assigned clock.", fun s -> val_fun [] (fun _ -> let ticks = Clock.ticks s#clock in let frame_position = Lazy.force Frame.duration *. float_of_int ticks in let in_frame_position = if s#is_ready then Frame.(seconds_of_main (position s#get_frame)) else 0. in float (frame_position +. in_frame_position)) ); ] let make_t ?pos = Type.make ?pos:(Option.map Pos.of_lexing_pos pos) let source_methods_t t = method_t t (List.map (fun (name, t, doc, _) -> (name, t, doc)) source_methods) let source_t ?(pos : Liquidsoap_lang.Term_base.parsed_pos option) ?(methods = false) frame_t = let t = make_t ?pos (Type.Constr (* The type has to be invariant because we don't want the sup mechanism to be used here, see #2806. *) { Type.constructor = "source"; params = [(`Invariant, frame_t)] }) in if methods then source_methods_t t else t let of_source_t t = match (Type.demeth t).Type.descr with | Type.Constr { Type.constructor = "source"; params = [(_, t)] } -> t | _ -> assert false let source_tracks_t frame_t = Type.meth "track_marks" ([], Format_type.track_marks) (Type.meth "metadata" ([], Format_type.metadata) frame_t) let source_methods ~base s = meth base (List.map (fun (name, _, _, fn) -> (name, fn s)) source_methods) let source s = source_methods ~base:(Source_val.to_value s) s let track = Track.to_value ?pos:None let to_source = Source_val.of_value let to_source_list l = List.map to_source (to_list l) let to_track = Track.of_value (** A method: name, type scheme, documentation and implementation (which takes the currently defined source as argument). *) type 'a operator_method = string * scheme * string * ('a -> value) let has_value_flag v flag = match v with | Value.Float _ | Value.String _ | Value.Bool _ | Value.Null _ -> true | v -> Value.has_flag v flag let add_value_flag v flag = match v with | Value.Float _ | Value.String _ | Value.Bool _ | Value.Null _ -> () | v -> Value.add_flag v flag (** Ensure that the frame contents of all the sources occurring in the value agree with [t]. *) let check_content v t = let check t t' = Typing.(t <: t') in let rec check_value v t = if not (has_value_flag v Flags.checked_value) then ( add_value_flag v Flags.checked_value; match (v, (Type.deref t).Type.descr) with | _, Type.Var _ -> () | _ when Source_val.is_value v -> let source_t = source_t (Source_val.of_value v)#frame_type in check source_t t | _ when Track.is_value v -> let field, s = Track.of_value v in if field <> Frame.Fields.track_marks && field <> Frame.Fields.metadata then ( let t = Frame_type.make (Type.var ()) (Frame.Fields.add field t Frame.Fields.empty) in check s#frame_type t) | _ when Lang_encoder.V.is_value v -> let content_t = Encoder.type_of_format (Lang_encoder.V.of_value v) in let frame_t = Frame_type.make unit_t content_t in let encoder_t = Lang_encoder.L.format_t frame_t in check encoder_t t | Value.Int _, _ | Value.Float _, _ | Value.String _, _ | Value.Bool _, _ | Value.Custom _, _ -> () | Value.List { value = l }, Type.List { Type.t } -> List.iter (fun v -> check_value v t) l | Value.Tuple { value = l }, Type.Tuple t -> List.iter2 check_value l t | Value.Null _, _ -> () | _, Type.Nullable t -> check_value v t (* Value can have more methods than the type requires so check from the type here. *) | _, Type.Meth _ -> let meths, v = Value.split_meths v in let meths_t, t = Type.split_meths t in List.iter (fun { Type.meth; optional; scheme = generalized, t } -> let names = List.map (fun v -> v.Type.name) generalized in let handler = Type.Fresh.init ~selector:(fun v -> List.mem v.Type.name names) () in let t = Type.Fresh.make handler t in try check_value (List.assoc meth meths) t with Not_found when optional -> ()) meths_t; check_value v t | Value.Fun { fun_args = []; fun_body = ret }, Type.Getter t -> Typing.(ret.Term.t <: t) | Value.FFI ({ ffi_args = []; ffi_fn } as ffi), Type.Getter t -> ffi.ffi_fn <- (fun env -> let v = ffi_fn env in check_value v t; v) | ( Value.Fun { fun_args = args; fun_body = ret }, Type.Arrow (args_t, ret_t) ) -> List.iter (fun typ -> match typ with | true, lbl_t, typ -> List.iter (fun arg -> match arg with | lbl, _, Some v when lbl = lbl_t -> check_value v typ | _ -> ()) args | _ -> ()) args_t; Typing.(ret.Term.t <: ret_t) | Value.FFI ({ ffi_args; ffi_fn } as ffi), Type.Arrow (args_t, ret_t) -> List.iter (fun typ -> match typ with | true, lbl_t, typ -> List.iter (fun arg -> match arg with | lbl, _, Some v when lbl = lbl_t -> check_value v typ | _ -> ()) ffi_args | _ -> ()) args_t; ffi.ffi_fn <- (fun env -> let v = ffi_fn env in check_value v ret_t; v) | _ -> failwith (Printf.sprintf "Unhandled value in check_content: %s, type: %s." (Value.to_string v) (Type.to_string t))) in check_value v t (** An operator is a builtin function that builds a source. It is registered using the wrapper [add_operator]. Creating the associated function type (and function) requires some work: - Specify which content_kind the source will carry: a given fixed number of channels, any fixed, a variable number? - The content_kind can also be linked to a type variable, e.g. the parameter of a format type. From this high-level description a type is created. Often it will carry a type constraint. Once the type has been inferred, the function might be executed, and at this point the type might still not be known completely so we have to force its value within the acceptable range. *) let _meth = meth let check_arguments ~env ~return_t arguments = let handler = Type.Fresh.init () in let return_t = Type.Fresh.make handler return_t in let arguments = List.map (fun (lbl, t, _, _) -> (lbl, Type.Fresh.make handler t)) arguments in let arguments = List.stable_sort (fun (l, _) (l', _) -> Stdlib.compare l l') arguments in (* Generalize all terms inside the arguments *) let map = let open Liquidsoap_lang.Value in let rec map v = let v = match v with | Int _ | Float _ | String _ | Bool _ | Custom _ | Null _ -> v | List ({ value = l } as v) -> List { v with value = List.map map l } | Tuple ({ value = l } as v) -> Tuple { v with value = List.map map l } | Fun ({ fun_args = args; fun_body = ret } as fun_v) -> Fun { fun_v with fun_args = List.map (fun (l, l', v) -> (l, l', Option.map map v)) args; fun_body = Term.fresh ~handler ret; } | FFI ffi -> FFI { ffi with ffi_args = List.map (fun (l, l', v) -> (l, l', Option.map map v)) ffi.ffi_args; ffi_fn = (fun env -> let v = ffi.ffi_fn env in map v); } in map_methods v (Methods.map map) in map in let env = List.map (fun (lbl, v) -> (lbl, map v)) env in (* Negotiate content for all sources and formats in the arguments. *) let () = let env = List.stable_sort (fun (l, _) (l', _) -> Stdlib.compare l l') (List.filter (fun (lbl, _) -> lbl <> Liquidsoap_lang.Lang_core.pos_var) env) in List.iter2 (fun (name, typ) (name', v) -> assert (name = name'); check_content v typ) arguments env in (return_t, env) let add_operator ~(category : Doc.Value.source) ~descr ?(flags = []) ?(meth = ([] : 'a operator_method list)) ?base name arguments ~return_t f = let compare (x, _, _, _) (y, _, _, _) = match (x, y) with | "", "" -> 0 | _, "" -> -1 | "", _ -> 1 | x, y -> Stdlib.compare x y in let arguments = ( "id", nullable_t string_t, Some null, Some "Force the value of the source ID." ) :: List.stable_sort compare arguments in let f env = let return_t, env = check_arguments ~return_t ~env arguments in let src : < Source.source ; .. > = f env in src#set_stack (Liquidsoap_lang.Lang_core.pos env); Typing.(src#frame_type <: return_t); ignore (Option.map (fun id -> src#set_id id) (to_valued_option to_string (List.assoc "id" env))); let v = let src = (src :> Source.source) in if category = `Output then source_methods ~base:unit src else source src in _meth v (List.map (fun (name, _, _, fn) -> (name, fn src)) meth) in let base_t = if category = `Output then unit_t else source_t ~methods:false return_t in let return_t = source_methods_t base_t in let return_t = method_t return_t (List.map (fun (name, typ, doc, _) -> (name, typ, doc)) meth) in let category = `Source category in add_builtin ~category ~descr ~flags ?base name arguments return_t f let add_track_operator ~(category : Doc.Value.source) ~descr ?(flags = []) ?(meth = ([] : 'a operator_method list)) ?base name arguments ~return_t f = let arguments = ( "id", nullable_t string_t, Some null, Some "Force the value of the track ID." ) :: arguments in let f env = let return_t, env = check_arguments ~return_t ~env arguments in let field, (src : < Source.source ; .. >) = f env in src#set_stack (Liquidsoap_lang.Lang_core.pos env); (if field <> Frame.Fields.track_marks && field <> Frame.Fields.metadata then Typing.( src#frame_type <: method_t (univ_t ()) [(Frame.Fields.string_of_field field, ([], return_t), "")])); ignore (Option.map (fun id -> src#set_id id) (to_valued_option to_string (List.assoc "id" env))); let v = Track.to_value (field, (src :> Source.source)) in _meth v (List.map (fun (name, _, _, fn) -> (name, fn src)) meth) in let return_t = method_t return_t (List.map (fun (name, typ, doc, _) -> (name, typ, doc)) meth) in let category = `Track category in add_builtin ~category ~descr ~flags ?base name arguments return_t f let iter_sources ?(on_imprecise = fun () -> ()) f v = let rec iter_term env v = let iter_base_term env v = match v.Term.term with | `Cache_env _ | `Int _ | `Float _ | `Bool _ | `String _ | `Custom _ | `Encoder _ -> () | `List l -> List.iter (iter_term env) l | `Tuple l -> List.iter (iter_term env) l | `Null -> () | `Hide (a, _) -> iter_term env a | `Cast { Term.cast = a } -> iter_term env a | `Invoke { Term.invoked = a } -> iter_term env a | `Open (a, b) -> iter_term env a; iter_term env b | `Let { Term.def = a; body = b; _ } | `Seq (a, b) -> iter_term env a; iter_term env b | `Var v -> ( try (* If it's locally bound it won't be in [env]. *) (* TODO since inner-bound variables don't mask outer ones in [env], * we are actually checking values that may be out of reach. *) let v = List.assoc v env in iter_value v with Not_found -> ()) | `App (a, l) -> iter_term env a; List.iter (fun (_, v) -> iter_term env v) l | `Fun { Term.arguments; body } | `RFun (_, { Term.arguments; body }) -> iter_term env body; List.iter (function | { Term.default = Some v } -> iter_term env v | _ -> ()) arguments in Term.Methods.iter (fun _ meth_term -> iter_term env meth_term) v.Term.methods; iter_base_term env v and iter_value v = if not (has_value_flag v Flags.itered_value) then ( add_value_flag v Flags.itered_value; Value.Methods.iter (fun _ v -> iter_value v) (Value.methods v); match v with | _ when Source_val.is_value v -> f (Source_val.of_value v) | Value.Int _ | Value.String _ | Value.Float _ | Value.Bool _ | Value.Custom _ -> () | Value.List { value = l } -> List.iter iter_value l | Value.Tuple { value = l } -> List.iter iter_value l | Value.Null _ -> () | Value.Fun { fun_args = proto; fun_env = env; fun_body = body } -> (* The following is necessarily imprecise: we might see sources that will be unused in the execution of the function. *) iter_term env body; List.iter (function _, _, Some v -> iter_value v | _ -> ()) proto | Value.FFI { ffi_args = proto; _ } -> on_imprecise (); List.iter (function _, _, Some v -> iter_value v | _ -> ()) proto) in iter_value v let iter_sources = iter_sources liquidsoap-2.3.2/src/core/lang_string.ml000066400000000000000000000014571477303350200202630ustar00rootroot00000000000000include Liquidsoap_lang.Lang_string type base_id = { name : string; mutable counter : int } module IdMap = Weak.Make (struct type t = base_id let equal id id' = id.name = id'.name let hash { name } = Hashtbl.hash name end) (** Generate an identifier from the name of the source. *) let generate_id = let m = Mutex.create () in let h = IdMap.create 10 in Mutex_utils.mutexify m (fun name -> let base_id = IdMap.merge h { name; counter = 0 } in let id = Bytes.( unsafe_to_string (of_string (match base_id.counter with | 0 -> name | n -> name ^ "." ^ string_of_int n))) in base_id.counter <- base_id.counter + 1; Gc.finalise_last (fun () -> ignore (Sys.opaque_identity base_id)) id; id) liquidsoap-2.3.2/src/core/lang_string.mli000066400000000000000000000001271477303350200204250ustar00rootroot00000000000000include module type of Liquidsoap_lang.Lang_string val generate_id : string -> string liquidsoap-2.3.2/src/core/liquidsoap_paths.mli000066400000000000000000000010331477303350200214640ustar00rootroot00000000000000type mode = [ `Default | `Standalone | `Posix ] val mode : mode val rundir : unit -> string val rundir_descr : string val logdir : unit -> string val logdir_descr : string val liq_libs_dir : unit -> string val liq_libs_dir_descr : string val bin_dir : unit -> string val bin_dir_descr : string val camomile_dir : unit -> string val camomile_dir_descr : string val user_cache_override : unit -> string option val user_cache_override_descr : string val system_cache_override : unit -> string option val system_cache_override_descr : string liquidsoap-2.3.2/src/core/modules.ml000066400000000000000000000032131477303350200174140ustar00rootroot00000000000000include Liquidsoap_lang.Modules let configure = Lang.add_module "configure" let decoder = Lang.add_module "decoder" let encoder = Lang.add_module "encoder" let file = Lang.add_module "file" let file_mime = Lang.add_module ~base:file "mime" let harbor = Lang.add_module "harbor" let http = Lang.add_module "http" let http_transport = Lang.add_module ~base:http "transport" let input = Lang.add_module "input" let input_external = Lang.add_module ~base:input "external" let metadata = Lang.add_module "metadata" let midi = Lang.add_module "midi" let output = Lang.add_module "output" let osc = Lang.add_module "osc" let playlist = Lang.add_module "playlist" let process = Lang.add_module "process" let protocol = Lang.add_module "protocol" let request = Lang.add_module "request" let runtime = Lang.add_module "runtime" let server = Lang.add_module "server" let synth = Lang.add_module "synth" let synth_all = Lang.add_module ~base:synth "all" let thread = Lang.add_module "thread" let track = Lang.add_module "track" let track_encode = Lang.add_module ~base:track "encode" let track_decode = Lang.add_module ~base:track "decode" let track_audio = Lang.add_module ~base:track "audio" let track_encode_audio = Lang.add_module ~base:track_encode "audio" let track_decode_audio = Lang.add_module ~base:track_decode "audio" let track_video = Lang.add_module ~base:track "video" let track_metadata = Lang.add_module ~base:track "metadata" let video = Lang.add_module "video" let video_external = Lang.add_module ~base:video "external" let video_frame = Lang.add_module ~base:video "frame" let video_testsrc = Lang.add_module ~base:video "testsrc" let visu = Lang.add_module "visu" liquidsoap-2.3.2/src/core/ogg_formats/000077500000000000000000000000001477303350200177225ustar00rootroot00000000000000liquidsoap-2.3.2/src/core/ogg_formats/liq_flac_ogg_decoder.ml000066400000000000000000000020441477303350200243470ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let () = Flac_decoder.register () liquidsoap-2.3.2/src/core/ogg_formats/liq_opus_decoder.ml000066400000000000000000000027721477303350200236040ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let samplerates = [8000; 12000; 16000; 24000; 48000] let () = Lifecycle.on_start ~name:"opus decoder initialization" (fun () -> let rate = Lazy.force Frame.audio_rate in let rec f = function | [] -> 48000 | x :: l when x < rate -> f l | x :: _ -> x in Opus_decoder.decoder_samplerate := f samplerates); Opus_decoder.register () (* Register audio/opus mime *) let () = Liq_ogg_decoder.mime_types#set ("audio/opus" :: Liq_ogg_decoder.mime_types#get) liquidsoap-2.3.2/src/core/ogg_formats/liq_speex_decoder.ml000066400000000000000000000020451477303350200237330ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let () = Speex_decoder.register () liquidsoap-2.3.2/src/core/ogg_formats/liq_theora_decoder.ml000066400000000000000000000020461477303350200240720ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let () = Theora_decoder.register () liquidsoap-2.3.2/src/core/ogg_formats/liq_vorbis_decoder.ml000066400000000000000000000020461477303350200241140ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let () = Vorbis_decoder.register () liquidsoap-2.3.2/src/core/ogg_formats/ogg_flac_encoder.ml000066400000000000000000000104601477303350200235150ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) external set_stream_eos : Ogg.Stream.stream -> unit = "liq_ocaml_ogg_stream_set_eos" let create_encoder ~flac ~comments () = let samplerate = Lazy.force flac.Flac_format.samplerate in let p = { Flac.Encoder.channels = flac.Flac_format.channels; bits_per_sample = flac.Flac_format.bits_per_sample; sample_rate = samplerate; compression_level = Some flac.Flac_format.compression; total_samples = None; } in let enc = ref None in let started = ref false in let m = Mutex.create () in let pages = ref [] in let write_cb = Mutex_utils.mutexify m (fun p -> pages := p :: !pages) in let flush_pages = Mutex_utils.mutexify m (fun () -> let p = !pages in pages := []; List.rev p) in let get_enc os = match !enc with | Some x -> x | None -> let x = Flac_ogg.Encoder.create ~comments ~serialno:(Ogg.Stream.serialno os) ~write:write_cb p in enc := Some x; x in let empty_data () = Array.make (Lazy.force Frame.audio_channels) (Array.make 1 0.) in let header_encoder os = match get_enc os with | { Flac_ogg.Encoder.first_pages = p :: _ } -> p | _ -> raise Ogg_muxer.Invalid_data in let fisbone_packet os = Some (Flac_ogg.Skeleton.fisbone ~serialno:(Ogg.Stream.serialno os) ~samplerate:(Int64.of_int samplerate) ()) in let stream_start os = match get_enc os with | { Flac_ogg.Encoder.first_pages = _ :: pages } -> pages | _ -> raise Ogg_muxer.Invalid_data in let data_encoder data os write_page = if not !started then started := true; let b, ofs, len = (data.Ogg_muxer.data, data.Ogg_muxer.offset, data.Ogg_muxer.length) in let b = Array.map (fun x -> Array.sub x ofs len) b in let { Flac_ogg.Encoder.encoder } = get_enc os in Flac.Encoder.process encoder b; List.iter write_page (flush_pages ()) in let end_of_page p = let granulepos = Ogg.Page.granulepos p in if granulepos < Int64.zero then Ogg_muxer.Unknown else Ogg_muxer.Time (Int64.to_float granulepos /. float samplerate) in let end_of_stream os = let { Flac_ogg.Encoder.encoder } = get_enc os in (* Assert that at least some data was encoded.. *) if not !started then ( let b = empty_data () in Flac.Encoder.process encoder b); Flac.Encoder.finish encoder; set_stream_eos os; flush_pages () in { Ogg_muxer.header_encoder; fisbone_packet; stream_start; data_encoder = Ogg_muxer.Audio_encoder data_encoder; end_of_page; end_of_stream; } let create_flac = function | Ogg_format.Flac flac -> let reset ogg_enc m = let comments = Frame.Metadata.to_list (Frame.Metadata.Export.to_metadata m) in let enc = create_encoder ~flac ~comments () in Ogg_muxer.register_track ?fill:flac.Flac_format.fill ogg_enc enc in let src_freq = float (Frame.audio_of_seconds 1.) in let dst_freq = float (Lazy.force flac.Flac_format.samplerate) in let channels = flac.Flac_format.channels in let encode = Ogg_encoder.encode_audio ~channels ~dst_freq ~src_freq () in { Ogg_encoder.encode; reset; id = None } | _ -> assert false let () = Hashtbl.replace Ogg_encoder.audio_encoders "flac" create_flac liquidsoap-2.3.2/src/core/ogg_formats/ogg_flac_encoder_stubs.c000066400000000000000000000004241477303350200245460ustar00rootroot00000000000000#include #include #include CAMLprim value liq_ocaml_ogg_stream_set_eos(value o_stream_state) { CAMLparam1(o_stream_state); ogg_stream_state *os = Stream_state_val(o_stream_state); os->e_o_s = 1; CAMLreturn(Val_unit); } liquidsoap-2.3.2/src/core/ogg_formats/ogg_muxer.ml000066400000000000000000000321231477303350200222510ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (** Ogg Stream Encoder *) let log = Log.make ["ogg.muxer"] exception Invalid_data exception Invalid_usage type audio = Audio.t type video = Video.buffer type 'a data = { data : 'a; offset : int; length : int } type track_data = Audio_data of audio data | Video_data of video data type position = Unknown | Time of float type 'a track_encoder = 'a data -> Ogg.Stream.stream -> (Ogg.Page.t -> unit) -> unit type page_end_time = Ogg.Page.t -> position type header_encoder = Ogg.Stream.stream -> Ogg.Page.t type fisbone_packet = Ogg.Stream.stream -> Ogg.Stream.packet option type stream_start = Ogg.Stream.stream -> Ogg.Page.t list type end_of_stream = Ogg.Stream.stream -> Ogg.Page.t list type 'a stream = { os : Ogg.Stream.stream; encoder : 'a track_encoder; end_pos : page_end_time; page_fill : int option; available : Ogg.Page.t Queue.t; mutable remaining : (float * Ogg.Page.t) option; fisbone_data : fisbone_packet; start_page : stream_start; stream_end : end_of_stream; } type track = Audio_track of audio stream | Video_track of video stream (** You may register new tracks on state Eos or Bos. You can't register new track on state Streaming. You may finalize at any state, provided at least single track is registered. However, this is not recommended. *) type state = Eos | Streaming | Bos type t = { id : string; mutable skeleton : Ogg.Stream.stream option; header : Strings.Mutable.t; encoded : Strings.Mutable.t; mutable position : float; tracks : (nativeint, track) Hashtbl.t; mutable state : state; } type data_encoder = | Audio_encoder of audio track_encoder | Video_encoder of video track_encoder type stream_encoder = { header_encoder : header_encoder; fisbone_packet : fisbone_packet; stream_start : stream_start; data_encoder : data_encoder; end_of_page : page_end_time; end_of_stream : end_of_stream; } let os_of_ogg_track x = match x with Audio_track x -> x.os | Video_track x -> x.os let fisbone_data_of_ogg_track x = match x with | Audio_track x -> x.fisbone_data | Video_track x -> x.fisbone_data let stream_start_of_ogg_track x = match x with Audio_track x -> x.start_page | Video_track x -> x.start_page let remaining_of_ogg_track x = match x with Audio_track x -> x.remaining | Video_track x -> x.remaining let set_remaining_of_ogg_track x v = match x with | Audio_track x -> x.remaining <- v | Video_track x -> x.remaining <- v (** As per specifications, we need a random injective sequence of nativeint. This might not be assumed here, but chances are very low.. *) let random_state = Random.State.make_self_init () let get_serial () = Random.State.nativeint random_state (Nativeint.of_int 0x3FFFFFFF) let state encoder = encoder.state (** Get and remove encoded data.. *) let get_data encoder = Strings.Mutable.flush encoder.encoded (** Peek encoded data without removing it. *) let peek_data encoder = Strings.Mutable.to_strings encoder.encoded (** Add an ogg page. *) let add_page encoder ?(header = false) (h, v) = Strings.Mutable.add encoder.encoded h; Strings.Mutable.add encoder.encoded v; if header then ( Strings.Mutable.add encoder.header h; Strings.Mutable.add encoder.header v) let flush_pages os = let rec f os l = try f os (Ogg.Stream.flush_page os :: l) with Ogg.Not_enough_data -> let compare x y = compare (Ogg.Page.pageno x) (Ogg.Page.pageno y) in List.sort compare l in f os [] let add_flushed_pages ?header encoder os = List.iter (add_page ?header encoder) (flush_pages os) let init_skeleton encoder = let serial = get_serial () in let os = Ogg.Stream.create ~serial () in Ogg.Stream.put_packet os (Ogg.Skeleton.fishead ()); (* Output first page at beginning of content. *) add_page encoder ~header:true (Ogg.Stream.get_page os); os let create ~skeleton id = let skeleton encoder = if skeleton then Some (init_skeleton encoder) else None in let encoder = { id; skeleton = None; header = Strings.Mutable.empty (); encoded = Strings.Mutable.empty (); position = 0.; tracks = Hashtbl.create 10; state = Bos; } in encoder.skeleton <- skeleton encoder; encoder let register_track ?fill encoder track_encoder = if encoder.state = Streaming then ( log#info "%s: Invalid new track: ogg stream already started.." encoder.id; raise Invalid_usage); if encoder.state = Eos then ( log#info "%s: Starting new sequentialized ogg stream." encoder.id; if encoder.skeleton <> None then encoder.skeleton <- Some (init_skeleton encoder); encoder.state <- Bos); (* Initiate a new logical stream *) let serial = get_serial () in let os = Ogg.Stream.create ~serial () in (* Encode headers *) let p = track_encoder.header_encoder os in add_page ~header:true encoder p; let track = match track_encoder.data_encoder with | Audio_encoder encoder -> Audio_track { os; encoder; end_pos = track_encoder.end_of_page; page_fill = fill; available = Queue.create (); remaining = None; fisbone_data = track_encoder.fisbone_packet; start_page = track_encoder.stream_start; stream_end = track_encoder.end_of_stream; } | Video_encoder encoder -> Video_track { os; encoder; end_pos = track_encoder.end_of_page; page_fill = fill; available = Queue.create (); remaining = None; fisbone_data = track_encoder.fisbone_packet; start_page = track_encoder.stream_start; stream_end = track_encoder.end_of_stream; } in Hashtbl.replace encoder.tracks serial track; serial (** Start streams, set state to Streaming. *) let streams_start encoder = log#info "%s: Starting %d track(s)" encoder.id (Hashtbl.length encoder.tracks); (* Add skeleton information first. *) begin match encoder.skeleton with | Some os -> Hashtbl.iter (fun _ x -> let sos = os_of_ogg_track x in let f = fisbone_data_of_ogg_track x in match f sos with | Some p -> Ogg.Stream.put_packet os p | None -> ()) encoder.tracks; add_flushed_pages ~header:true encoder os | None -> () end; Hashtbl.iter (fun _ t -> let os = os_of_ogg_track t in let stream_start = stream_start_of_ogg_track t in let pages = stream_start os in List.iter (add_page ~header:true encoder) pages) encoder.tracks; (* Finish skeleton stream now. *) begin match encoder.skeleton with | Some os -> Ogg.Stream.put_packet os (Ogg.Skeleton.eos ()); let p = Ogg.Stream.flush_page os in add_page ~header:true encoder p | None -> () end; encoder.state <- Streaming (** Get the first pages of each streams. *) let get_header x = Strings.Mutable.to_strings x.header (** Is a track empty ?*) let is_empty x = Ogg.Stream.eos x.os && x.remaining = None && Queue.length x.available = 0 && try let p = Ogg.Stream.get_page ?fill:x.page_fill x.os in Queue.add p x.available; false with Ogg.Not_enough_data -> true (** Get the least remaining page of all tracks. *) let least_remaining encoder = let f _ t x = match (remaining_of_ogg_track t, x) with | Some (new_pos, p), Some (_, pos, _) when new_pos <= pos -> Some (t, new_pos, p) | Some (pos, p), None -> Some (t, pos, p) | _ -> x in Hashtbl.fold f encoder.tracks None (** Get the number of remaining page *) let remaining_pages encoder = let f _ t x = if remaining_of_ogg_track t <> None then x + 1 else x in Hashtbl.fold f encoder.tracks 0 (** Fill output data with available pages. The algorithm works as follow: + The encoder has a global position + Each time a page ends at a position that is ahead of the encoder position, the page is kept as remaining. + When there is one remaining page per track, we take the least of them and add it. The encoder position is then bumped. + As soon as the encoder's position is ahead of a page, then this page can be written *) let add_available src encoder = let rec fill src dst = (* First we check if there is a remaining * page that we can now output. *) if remaining_pages encoder = Hashtbl.length encoder.tracks then ( match least_remaining encoder with | None -> () | Some (track, pos, p) -> add_page encoder p; encoder.position <- pos; set_remaining_of_ogg_track track None); (* Then, we proceed only if the track * is the only one left, or there is no * remaining page. *) if Hashtbl.length encoder.tracks <= 1 || src.remaining = None then ( try let p = try Queue.take src.available with Queue.Empty -> Ogg.Stream.get_page ?fill:src.page_fill src.os in let pos = src.end_pos p in match pos with (* Is the new position ahead ? *) | Time pos -> if pos > encoder.position then (* We don't output the page now * since we want to let the possibility * for another stream to add pages * for a position between the current * position and this new one. *) src.remaining <- Some (pos, p) else ( add_page encoder p; fill src dst) | Unknown -> add_page encoder p; fill src dst with Ogg.Not_enough_data -> ()) in fill src encoder; if is_empty src then Hashtbl.remove encoder.tracks (Ogg.Stream.serialno src.os) let queue_add src p = Queue.add p src.available (** Encode data. Implicitly calls [streams_start] if not called before. *) let encode encoder id data = if encoder.state = Bos then streams_start encoder; if encoder.state = Eos then ( log#info "%s: Cannot encode: ogg stream finished.." encoder.id; raise Invalid_usage); match data with | Audio_data x -> ( match Hashtbl.find encoder.tracks id with | Audio_track t -> t.encoder x t.os (queue_add t); add_available t encoder | _ -> raise Invalid_data) | Video_data x -> ( match Hashtbl.find encoder.tracks id with | Video_track t -> t.encoder x t.os (queue_add t); add_available t encoder | _ -> raise Invalid_data) (** Finish a track. Not all data will necessarily be outputted here. It is possible that muxing needs also another track to end.. *) let end_of_track encoder id = let track = Hashtbl.find encoder.tracks id in if encoder.state = Bos then ( log#info "%s: Stream finished without calling streams_start !" encoder.id; streams_start encoder); match track with | Video_track x -> if not (Ogg.Stream.eos x.os) then ( log#info "%s: Setting end of track %nx." encoder.id id; List.iter (queue_add x) (x.stream_end x.os)); add_available x encoder | Audio_track x -> if not (Ogg.Stream.eos x.os) then ( log#info "%s: Setting end of track %nx." encoder.id id; List.iter (queue_add x) (x.stream_end x.os)); add_available x encoder (** Flush data from all tracks in the stream. *) let flush encoder = begin match encoder.skeleton with | Some os -> add_flushed_pages encoder os | None -> () end; while Hashtbl.length encoder.tracks > 0 do Hashtbl.iter (fun id _ -> end_of_track encoder id) encoder.tracks done (** Set end of stream on the encoder. *) let eos encoder = if encoder.state <> Streaming then streams_start encoder; if Hashtbl.length encoder.tracks <> 0 then raise Invalid_usage; log#info "%s: Every ogg logical tracks have ended: setting end of stream." encoder.id; ignore (Strings.Mutable.flush encoder.header); encoder.position <- 0.; encoder.state <- Eos (** End all tracks in the stream. *) let end_of_stream encoder = flush encoder; eos encoder liquidsoap-2.3.2/src/core/ogg_formats/ogg_muxer.mli000066400000000000000000000124451477303350200224270ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (** Ogg Stream Encoder *) val log : Log.t (** {2 Types} *) exception Invalid_data exception Invalid_usage (** Audio data type *) type audio = Audio.t (** Video data type *) type video = Video.buffer (** A data unit *) type 'a data = { data : 'a; offset : int; length : int } (** A track data is a data unit of either audio or video. *) type track_data = Audio_data of audio data | Video_data of video data (** A track encoder takes the track data, the ogg logical stream, and fills the stream. If the encoding process outputs ogg pages, then the encoder should use the last argument to add its pages to the stream. *) type 'a track_encoder = 'a data -> Ogg.Stream.stream -> (Ogg.Page.t -> unit) -> unit (** Returns the first page of the stream, to be placed at the very beginning. *) type header_encoder = Ogg.Stream.stream -> Ogg.Page.t (** Return the end time of a page, in milliseconds. *) type position = Unknown | Time of float (** Type for a function returning a page's ending time. *) type page_end_time = Ogg.Page.t -> position (** Returns an optional fisbone packet, which will contain the data for this stream to put in the ogg skeleton, if enabled in the encoder. *) type fisbone_packet = Ogg.Stream.stream -> Ogg.Stream.packet option (** Returns the remaining header data, before data encoding starts. *) type stream_start = Ogg.Stream.stream -> Ogg.Page.t list (** Ends the track. *) type end_of_stream = Ogg.Stream.stream -> Ogg.Page.t list (** A data encoder is an encoder for either a audio or a video track. *) type data_encoder = | Audio_encoder of audio track_encoder | Video_encoder of video track_encoder (** The full stream encoder type. *) type stream_encoder = { header_encoder : header_encoder; fisbone_packet : fisbone_packet; stream_start : stream_start; data_encoder : data_encoder; end_of_page : page_end_time; end_of_stream : end_of_stream; } (** Main type for the ogg encoder *) type t (** You may register new tracks on state Eos or Bos. You can't register new track on state Streaming. *) type state = Eos | Streaming | Bos (** {2 API} *) (** Usage: Encoding: - [create ~skeleton name] : create a new encoder - [register_track encoder stream_encoder] : register a new track - ibid - (...) - [streams_start encoder] : start the tracks (optional) - [encode encoder track_serial track_data] : encode data for one track - ibid - (...) - (encode data for other tracks) - [end_of_track encoder track_serial] : ends a track. (track end do not need to be simultaneous) - (...) - [end_of_stream encoder]: ends all tracks as well as the encoder. Set Eos state on the encoder. - [register_track encoder stream_encoder] : register a new track, starts a new sequentialized stream - And so on.. You get encoded data by calling [get_data], [peek_data]. See: http://xiph.org/ogg/doc/oggstream.html for more details on the specifications of an ogg stream. This API reflects exactly what is recommended to do. *) (** Create a new encoder. Add an ogg skeleton if [skeleton] is [true]. *) val create : skeleton:bool -> string -> t (** Get the state of an encoder. *) val state : t -> state (** Get and remove encoded data.. *) val get_data : t -> Strings.t (** Get header of a stream. *) val get_header : t -> Strings.t (** Peek encoded data without removing it. *) val peek_data : t -> Strings.t (** Register a new track to the stream. The state needs to be [Bos] or [Eos]. * [fill] parameter is used to try to control ogg logical page's size. See [Ogg.get_page] for more details. * Returns the serial number of the registered ogg stream. *) val register_track : ?fill:int -> t -> stream_encoder -> nativeint (** Start streams, set state to [Streaming]. *) val streams_start : t -> unit (** Encode data. Implicitly calls [streams_start] if not called before. Fails if state is not [Streaming] *) val encode : t -> nativeint -> track_data -> unit (** Finish a track. Raises [Not_found] if no such track exists. *) val end_of_track : t -> nativeint -> unit (** Ends all tracks, flush remaining encoded data. Set state to [Eos]. *) val end_of_stream : t -> unit (** {2 Utils} *) (** flush all availables pages from an ogg stream *) val flush_pages : Ogg.Stream.stream -> Ogg.Page.t list liquidsoap-2.3.2/src/core/ogg_formats/opus_encoder.ml000066400000000000000000000123031477303350200227400ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let create_encoder ~opus ~comments () = let samplerate = opus.Opus_format.samplerate in let channels = opus.Opus_format.channels in let frame_size = int_of_float (opus.Opus_format.frame_size *. float samplerate /. 1000.) in let frame_size_in_main = Frame.main_of_audio frame_size in let gen = Generator.create (Frame.Fields.make ~audio:(Content.Audio.format_of_channels channels) ()) in let application = match opus.Opus_format.application with None -> `Audio | Some a -> a in let enc = ref None in let started = ref false in let get_enc os = match !enc with | Some x -> x | None -> let x = Opus.Encoder.create ~comments ~channels ~samplerate ~application os in Opus.Encoder.apply_control (`Set_bitrate opus.Opus_format.bitrate) x; begin match opus.Opus_format.mode with | Opus_format.CBR -> Opus.Encoder.apply_control (`Set_vbr false) x | Opus_format.VBR b -> Opus.Encoder.apply_control (`Set_vbr true) x; Opus.Encoder.apply_control (`Set_vbr_constraint b) x end; let maybe name value = ignore (Option.map (fun value -> Opus.Encoder.apply_control (name value) x) value) in maybe (fun v -> `Set_complexity v) opus.Opus_format.complexity; maybe (fun v -> `Set_max_bandwidth v) opus.Opus_format.max_bandwidth; maybe (fun v -> `Set_signal v) opus.Opus_format.signal; Opus.Encoder.apply_control (`Set_dtx opus.Opus_format.dtx) x; Opus.Encoder.apply_control (`Set_phase_inversion_disabled (not opus.Opus_format.phase_inversion)) x; enc := Some x; x in let header_encoder os = let enc = get_enc os in Ogg.Stream.put_packet os (Opus.Encoder.header enc); Ogg.Stream.flush_page os in let fisbone_packet _ = None in let stream_start os = let enc = get_enc os in Ogg.Stream.put_packet os (Opus.Encoder.comments enc); Ogg_muxer.flush_pages os in let data_encoder { Ogg_muxer.data; offset; length } os _ = started := true; let enc = get_enc os in let content = Content.Audio.lift_data data in let offset = Frame.main_of_audio offset in let length = Frame.main_of_audio length in Generator.put gen Frame.Fields.audio (Content.sub content offset length); while Generator.length gen >= frame_size_in_main do let content = Frame.Fields.find Frame.Fields.audio (Generator.slice gen frame_size_in_main) in let pcm = Content.Audio.get_data content in let ret = Opus.Encoder.encode_float ~frame_size:opus.Opus_format.frame_size enc pcm 0 frame_size in assert (ret = frame_size) done in let empty_data () = { Ogg_muxer.offset = 0; length = 1; data = Array.make (Lazy.force Frame.audio_channels) (Array.make 1 0.); } in let end_of_page p = let granulepos = Ogg.Page.granulepos p in if granulepos < Int64.zero then Ogg_muxer.Unknown else Ogg_muxer.Time (Int64.to_float granulepos /. 48000.) in let end_of_stream os = (* Assert that at least some data was encoded.. *) if not !started then data_encoder (empty_data ()) os (); Ogg.Stream.terminate os in { Ogg_muxer.header_encoder; fisbone_packet; stream_start; data_encoder = Ogg_muxer.Audio_encoder data_encoder; end_of_page; end_of_stream; } let create_opus = function | Ogg_format.Opus opus -> let reset ogg_enc m = let comments = Frame.Metadata.to_list (Frame.Metadata.Export.to_metadata m) in let enc = create_encoder ~opus ~comments () in Ogg_muxer.register_track ?fill:opus.Opus_format.fill ogg_enc enc in let src_freq = float (Frame.audio_of_seconds 1.) in let dst_freq = float opus.Opus_format.samplerate in let channels = opus.Opus_format.channels in let encode = Ogg_encoder.encode_audio ~channels ~dst_freq ~src_freq () in { Ogg_encoder.encode; reset; id = None } | _ -> assert false let () = Hashtbl.replace Ogg_encoder.audio_encoders "opus" create_opus liquidsoap-2.3.2/src/core/ogg_formats/speex_encoder.ml000066400000000000000000000151321477303350200231010ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) exception Internal exception Invalid_settings of string let create speex ~metadata () = let frames_per_packet = speex.Speex_format.frames_per_packet in let mode = match speex.Speex_format.mode with | Speex_format.Narrowband -> Speex.Narrowband | Speex_format.Wideband -> Speex.Wideband | Speex_format.Ultra_wideband -> Speex.Ultra_wideband in let vbr = match speex.Speex_format.bitrate_control with | Speex_format.Vbr _ -> true | _ -> false in let channels = if speex.Speex_format.stereo then 2 else 1 in let rate = Lazy.force speex.Speex_format.samplerate in let header = Speex.Header.init ~frames_per_packet ~mode ~vbr ~nb_channels:channels ~rate () in let enc = Speex.Encoder.init mode frames_per_packet in begin match speex.Speex_format.bitrate_control with | Speex_format.Vbr x -> Speex.Encoder.set enc Speex.SPEEX_SET_VBR 1; Speex.Encoder.set enc Speex.SPEEX_SET_VBR_QUALITY x | Speex_format.Abr x -> Speex.Encoder.set enc Speex.SPEEX_SET_ABR 1; Speex.Encoder.set enc Speex.SPEEX_SET_BITRATE x | Speex_format.Quality x -> Speex.Encoder.set enc Speex.SPEEX_SET_QUALITY x end; begin match speex.Speex_format.complexity with | Some complexity -> Speex.Encoder.set enc Speex.SPEEX_SET_COMPLEXITY complexity | _ -> () end; if speex.Speex_format.dtx then Speex.Encoder.set enc Speex.SPEEX_SET_DTX 1; if speex.Speex_format.vad then Speex.Encoder.set enc Speex.SPEEX_SET_VAD 1; Speex.Encoder.set enc Speex.SPEEX_SET_SAMPLING_RATE rate; let frame_size = Speex.Encoder.get enc Speex.SPEEX_GET_FRAME_SIZE in let p1, p2 = Speex.Header.encode_header_packetout header metadata in let header_encoder os = Ogg.Stream.put_packet os p1; Ogg.Stream.flush_page os in let fisbone_packet os = let serialno = Ogg.Stream.serialno os in Some (Speex.Skeleton.fisbone ~header ~serialno ()) in let stream_start os = Ogg.Stream.put_packet os p2; Ogg_muxer.flush_pages os in let remaining_init = if channels > 1 then [| [||]; [||] |] else [| [||] |] in let remaining = ref remaining_init in let data_encoder data os add_page = let b, ofs, len = (data.Ogg_muxer.data, data.Ogg_muxer.offset, data.Ogg_muxer.length) in let buf = Array.map (fun x -> Array.sub x ofs len) b in let buf = if channels > 1 then [| Array.append !remaining.(0) buf.(0); Array.append !remaining.(1) buf.(1); |] else [| Array.append !remaining.(0) buf.(0) |] in let len = Array.length buf.(0) in let status = ref 0 in let feed () = let n = !status in if (frame_size * n) + frame_size < len then ( status := n + 1; (* Speex float API are values in - 32768. <= x <= 32767. .. I don't really trust this, it must be a bug, so using the int API. *) let f x = let x = int_of_float x in max (-32768) (min 32767 x) in let f x = Array.map (fun x -> f (32767. *. x)) x in if channels > 1 then [| f (Array.sub buf.(0) (frame_size * n) frame_size); f (Array.sub buf.(1) (frame_size * n) frame_size); |] else [| f (Array.sub buf.(0) (frame_size * n) frame_size) |]) else raise Internal in try while true do let page = if channels > 1 then Speex.Encoder.encode_page_int_stereo enc os feed else ( let feed () = let x = feed () in x.(0) in Speex.Encoder.encode_page_int enc os feed) in add_page page done with Internal -> let n = !status in remaining := if frame_size * n < len then if channels > 1 then [| Array.sub buf.(0) (frame_size * n) (len - (frame_size * n)); Array.sub buf.(1) (frame_size * n) (len - (frame_size * n)); |] else [| Array.sub buf.(0) (frame_size * n) (len - (frame_size * n)) |] else remaining_init in let end_of_page p = let granulepos = Ogg.Page.granulepos p in if granulepos < Int64.zero then Ogg_muxer.Unknown else Ogg_muxer.Time (Int64.to_float granulepos /. float rate) in let end_of_stream os = Ogg.Stream.terminate os in { Ogg_muxer.header_encoder; fisbone_packet; stream_start; data_encoder = Ogg_muxer.Audio_encoder data_encoder; end_of_page; end_of_stream; } let create_speex = function | Ogg_format.Speex speex -> let reset ogg_enc m = let m = Frame.Metadata.to_list (Frame.Metadata.Export.to_metadata m) in let title = try List.assoc "title" m with Not_found -> ( try let s = List.assoc "uri" m in let title = Filename.basename s in try String.sub title 0 (String.rindex title '.') with Not_found -> title with Not_found -> "Unknown") in let metadata = [("title", title)] @ List.remove_assoc "title" m in let enc = create speex ~metadata () in Ogg_muxer.register_track ?fill:speex.Speex_format.fill ogg_enc enc in let channels = if speex.Speex_format.stereo then 2 else 1 in let src_freq = float (Frame.audio_of_seconds 1.) in let dst_freq = float (Lazy.force speex.Speex_format.samplerate) in let encode = Ogg_encoder.encode_audio ~channels ~dst_freq ~src_freq () in { Ogg_encoder.reset; encode; id = None } | _ -> assert false let () = Hashtbl.replace Ogg_encoder.audio_encoders "speex" create_speex liquidsoap-2.3.2/src/core/ogg_formats/theora_encoder.ml000066400000000000000000000135201477303350200232360ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm module Img = Image.Generic let create_encoder ~theora ~metadata () = let quality, bitrate = match theora.Theora_format.bitrate_control with | Theora_format.Bitrate x -> (0, x) | Theora_format.Quality x -> (x, 0) in let width = Lazy.force theora.Theora_format.width in let height = Lazy.force theora.Theora_format.height in let picture_width = Lazy.force theora.Theora_format.picture_width in let picture_height = Lazy.force theora.Theora_format.picture_height in let picture_x = theora.Theora_format.picture_x in let picture_y = theora.Theora_format.picture_y in let aspect_numerator = theora.Theora_format.aspect_numerator in let aspect_denominator = theora.Theora_format.aspect_denominator in let fps = Frame.video_of_seconds 1. in let version_major, version_minor, version_subminor = Theora.version_number in let keyframe_frequency = Some theora.Theora_format.keyframe_frequency in let vp3_compatible = theora.Theora_format.vp3_compatible in let soft_target = Some theora.Theora_format.soft_target in let buffer_delay = theora.Theora_format.buffer_delay in let speed = theora.Theora_format.speed in let info = { Theora.frame_width = width; frame_height = height; picture_width; picture_height; picture_x; picture_y; fps_numerator = fps; fps_denominator = 1; aspect_numerator; aspect_denominator; colorspace = Theora.CS_unspecified; keyframe_granule_shift = Theora.default_granule_shift; target_bitrate = bitrate; quality; version_major; version_minor; version_subminor; pixel_fmt = Theora.PF_420; } in let params = { Theora.Encoder.keyframe_frequency; vp3_compatible; soft_target; buffer_delay; speed; } in let enc = if width mod 16 <> 0 || height mod 16 <> 0 then failwith "Invalid theora width/height (should be a multiple of 16)."; Theora.Encoder.create info params metadata in let started = ref false in let header_encoder os = Theora.Encoder.encode_header enc os; Ogg.Stream.flush_page os in let fisbone_packet os = let serialno = Ogg.Stream.serialno os in Some (Theora.Skeleton.fisbone ~serialno ~info ()) in let stream_start os = Ogg_muxer.flush_pages os in let yuv = Image.YUV420.create width height in let data_encoder data os _ = if not !started then started := true; let b, ofs, len = (data.Ogg_muxer.data, data.Ogg_muxer.offset, data.Ogg_muxer.length) in for i = ofs to ofs + len - 1 do let img = Video.get b i in let theora_yuv = { Theora.y_width = width; Theora.y_height = height; Theora.y_stride = Image.YUV420.y_stride img; Theora.u_width = width / 2; Theora.u_height = height / 2; Theora.u_stride = Image.YUV420.uv_stride img; Theora.v_width = width / 2; Theora.v_height = height / 2; Theora.v_stride = Image.YUV420.uv_stride img; Theora.y = Image.YUV420.y img; Theora.u = Image.YUV420.u img; Theora.v = Image.YUV420.v img; } in Theora.Encoder.encode_buffer enc os theora_yuv done in let end_of_page p = let granulepos = Ogg.Page.granulepos p in if granulepos < Int64.zero then Ogg_muxer.Unknown else if granulepos <> Int64.zero then ( let index = Int64.succ (Theora.Encoder.frames_of_granulepos enc granulepos) in Ogg_muxer.Time (Int64.to_float index /. float fps)) else Ogg_muxer.Time 0. in let end_of_stream os = (* Encode at least some data.. *) if not !started then ( let theora_yuv = { Theora.y_width = width; Theora.y_height = height; Theora.y_stride = width; Theora.u_width = width / 2; Theora.u_height = height / 2; Theora.u_stride = width / 2; Theora.v_width = width / 2; Theora.v_height = height / 2; Theora.v_stride = width / 2; Theora.y = Image.Data.alloc width; Theora.u = Image.Data.alloc (width / 2); Theora.v = Image.Data.alloc (width / 2); } in Image.YUV420.blank_all yuv; Theora.Encoder.encode_buffer enc os theora_yuv); Ogg.Stream.terminate os in { Ogg_muxer.header_encoder; fisbone_packet; stream_start; data_encoder = Ogg_muxer.Video_encoder data_encoder; end_of_page; end_of_stream; } let create_theora theora = let reset ogg_enc m = let metadata = Frame.Metadata.to_list (Frame.Metadata.Export.to_metadata m) in let enc = create_encoder ~theora ~metadata () in Ogg_muxer.register_track ?fill:theora.Theora_format.fill ogg_enc enc in { Ogg_encoder.encode = Ogg_encoder.encode_video; reset; id = None } let () = Ogg_encoder.theora_encoder := Some create_theora liquidsoap-2.3.2/src/core/ogg_formats/vorbis_encoder.ml000066400000000000000000000113251477303350200232610ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let create_gen enc freq m = let encoder = Configure.vendor in let p1, p2, p3 = Vorbis.Encoder.headerout_packetout ~encoder enc m in let started = ref false in let header_encoder os = Ogg.Stream.put_packet os p1; Ogg.Stream.flush_page os in let fisbone_packet os = Some (Vorbis.Skeleton.fisbone ~serialno:(Ogg.Stream.serialno os) ~samplerate:(Int64.of_int freq) ()) in let stream_start os = Ogg.Stream.put_packet os p2; Ogg.Stream.put_packet os p3; Ogg_muxer.flush_pages os in let data_encoder data os _ = if not !started then started := true; let b, ofs, len = (data.Ogg_muxer.data, data.Ogg_muxer.offset, data.Ogg_muxer.length) in Vorbis.Encoder.encode_buffer_float enc os b ofs len in let empty_data () = Array.make (Lazy.force Frame.audio_channels) (Array.make 1 0.) in let end_of_page p = let granulepos = Ogg.Page.granulepos p in if granulepos < Int64.zero then Ogg_muxer.Unknown else Ogg_muxer.Time (Int64.to_float granulepos /. float freq) in let end_of_stream os = (* Assert that at least some data was encoded.. *) if not !started then ( let b = empty_data () in Vorbis.Encoder.encode_buffer_float enc os b 0 (Array.length b.(0))); Vorbis.Encoder.end_of_stream enc os; [] in { Ogg_muxer.header_encoder; fisbone_packet; stream_start; data_encoder = Ogg_muxer.Audio_encoder data_encoder; end_of_page; end_of_stream; } (** Rates are given in bits per seconds, i.e. 128000 instead of 128.. *) let create_abr ~channels ~samplerate ~min_rate ~max_rate ~average_rate ~metadata () = let min_rate, max_rate, average_rate = (1000 * min_rate, 1000 * max_rate, 1000 * average_rate) in let enc = Vorbis.Encoder.create channels samplerate max_rate average_rate min_rate in create_gen enc samplerate metadata let create_cbr ~channels ~samplerate ~bitrate ~metadata () = create_abr ~channels ~samplerate ~min_rate:bitrate ~max_rate:bitrate ~average_rate:bitrate ~metadata () let create ~channels ~samplerate ~quality ~metadata () = let enc = Vorbis.Encoder.create_vbr channels samplerate quality in create_gen enc samplerate metadata let create_vorbis = function | Ogg_format.Vorbis vorbis -> let channels = vorbis.Vorbis_format.channels in let samplerate = Lazy.force vorbis.Vorbis_format.samplerate in let reset ogg_enc m = let m = Frame.Metadata.Export.to_metadata m in let metas = Hashtbl.create 0 in Frame.Metadata.iter (fun k v -> Hashtbl.replace metas k v) m; let metadata = Vorbis.tags metas () in let enc = (* For ABR, a value of -1 means unset.. *) let f x = match x with Some x -> x | None -> -1 in match vorbis.Vorbis_format.mode with | Vorbis_format.ABR (min_rate, average_rate, max_rate) -> let min_rate, average_rate, max_rate = (f min_rate, f average_rate, f max_rate) in create_abr ~channels ~samplerate ~max_rate ~average_rate ~min_rate ~metadata () | Vorbis_format.CBR bitrate -> create_cbr ~channels ~samplerate ~bitrate ~metadata () | Vorbis_format.VBR quality -> create ~channels ~samplerate ~quality ~metadata () in Ogg_muxer.register_track ?fill:vorbis.Vorbis_format.fill ogg_enc enc in let src_freq = float (Frame.audio_of_seconds 1.) in let dst_freq = float samplerate in let encode = Ogg_encoder.encode_audio ~channels ~dst_freq ~src_freq () in { Ogg_encoder.reset; encode; id = None } | _ -> assert false let () = Hashtbl.replace Ogg_encoder.audio_encoders "vorbis" create_vorbis liquidsoap-2.3.2/src/core/operators/000077500000000000000000000000001477303350200174315ustar00rootroot00000000000000liquidsoap-2.3.2/src/core/operators/accelerate.ml000066400000000000000000000073411477303350200220600ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Source class accelerate ~ratio ~randomize source_val = let source = Lang.to_source source_val in object (self) inherit operator ~name:"accelerate" [] inherit Child_support.base ~check_self_sync:true [source_val] method self_sync = source#self_sync method fallible = source#fallible method seek_source = source#seek_source method remaining = let rem = source#remaining in if rem = -1 then rem else int_of_float (float rem *. ratio ()) method abort_track = source#abort_track method private can_generate_frame = source#is_ready (** Filled ticks. *) val mutable filled = 0 (** Skipped ticks. *) val mutable skipped = 0 method private must_drop = let ratio = ratio () in if ratio <= 1. then false else ( (* How much we filled compared to what we should have. *) let d = float filled -. (float (filled + skipped) /. ratio) in let d = Frame.seconds_of_main (truncate d) in let rnd = randomize () in if rnd = 0. then d > 0. else ( let a = d /. rnd in (* Scaled logistic function: 0. when a is very negative, 1. when a is very positive. *) let l = (tanh (a *. 2.) +. 1.) /. 2. in Random.float 1. > 1. -. l)) method private generate_frame = let pos = ref 1 in (* Drop frames if we are late. *) (* TODO: we could also duplicate if we are in advance. *) while !pos > 0 && self#must_drop && source#is_ready do self#on_child_tick (fun () -> if source#is_ready then pos := Frame.position source#get_frame); skipped <- skipped + !pos done; let buf = ref self#empty_frame in if source#is_ready then ( self#on_child_tick (fun () -> if source#is_ready then buf := source#get_frame); filled <- filled + Frame.position !buf); !buf end let _ = let return_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator "accelerate" [ ( "ratio", Lang.getter_t Lang.float_t, Some (Lang.float 2.), Some "A value higher than 1 means speeding up." ); ( "randomize", Lang.getter_t Lang.float_t, Some (Lang.float 1.), Some "Randomization (0 means no randomization)." ); ("", Lang.source_t return_t, None, None); ] ~flags:[`Experimental; `Extra] ~return_t ~category:`Audio ~descr: "Accelerate a stream by dropping frames. This is useful for testing \ scripts." (fun p -> let f v = List.assoc v p in let src = f "" in let ratio = Lang.to_float_getter (f "ratio") in let randomize = Lang.to_float_getter (f "randomize") in new accelerate ~ratio ~randomize src) liquidsoap-2.3.2/src/core/operators/add.ml000066400000000000000000000412371477303350200205220ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (** Play multiple sources at the same time, and perform weighted mix *) let max_remaining a b = if b = -1 || a = -1 then -1 else max a b type 'a field = { position : int; weight : 'a; field : Frame.field } type ('a, 'b) track = { mutable fields : 'a field list; data : 'b } class virtual base ~name tracks = let sources = List.map (fun { data } -> data) tracks in let self_sync = Clock_base.self_sync sources in let infallible = List.exists (fun s -> not s#fallible) sources in object (self) inherit Source.operator ~name sources method fallible = not infallible method self_sync = self_sync ~source:self () method remaining = let f cur pos = match (cur, pos) with | -1, -1 -> -1 | x, -1 | -1, x -> x | x, y -> max_remaining x y in List.fold_left f (-1) (List.map (fun s -> s#remaining) (List.filter (fun (s : Source.source) -> s#is_ready) sources)) method abort_track = List.iter (fun s -> s#abort_track) sources method private can_generate_frame = List.exists (fun s -> s#is_ready) sources method private sources_ready = List.filter (fun s -> s#is_ready) sources method private tracks_ready = List.filter (fun { data = s } -> s#is_ready) tracks method private generate_frames = List.fold_left (fun frames { fields; data = source } -> let fields = List.map (fun ({ weight } as field) -> { field with weight = weight () }) fields in let data = if source#is_ready then Some (source, source#get_frame) else None in { fields; data } :: frames) [] tracks method private frames_position frames = Option.value ~default:0 (List.fold_left (fun pos { data } -> match (pos, data) with | _, None -> pos | None, Some (_, frame) -> Some (Frame.position frame) | Some p, Some (_, frame) -> Some (max p (Frame.position frame))) None frames) method seek_source = match sources with [s] -> s#seek_source | _ -> (self :> Source.source) (* For backward compatibility: set metadata from the first track effectively summed. *) method private set_metadata buf = match List.fold_left (fun cur { fields; data = source } -> List.fold_left (fun cur { position; _ } -> match cur with | None -> Some (source, position) | Some (_, pos) when position < pos -> Some (source, position) | _ -> cur) cur fields) None self#tracks_ready with | None -> buf | Some (source, _) -> let metadata = Content.Metadata.get_data (Frame.Fields.find Frame.Fields.metadata source#get_frame) in Frame.add_all_metadata buf metadata end (** Add/mix several sources together. If [renorm], renormalize the PCM channels. *) class audio_add ~renorm ~power ~field tracks = object (self) inherit base ~name:"audio.add" tracks method private generate_frame = let renorm = renorm () in let power = power () in let frames = self#generate_frames in let total_weight = List.fold_left (fun total_weight { fields } -> let source_weight = List.fold_left (fun source_weight { weight } -> let weight = if power then weight *. weight else weight in source_weight +. weight) 0. fields in total_weight +. source_weight) 0. frames in let total_weight = if power then sqrt total_weight else total_weight in let pos = self#frames_position frames in let buf = Frame.create ~length:pos self#content_type in let pcm = Content.Audio.get_data (Frame.get buf field) in Audio.clear pcm 0 (Audio.length pcm); List.iter (fun { data; fields } -> match data with | None -> () | Some (_, frame) -> List.iter (fun { field; weight } -> let track_pcm = Content.Audio.get_data (Frame.get frame field) in let c = if renorm then weight /. total_weight else weight in let audio_len = Audio.length track_pcm in if c <> 1. then Audio.amplify c track_pcm 0 audio_len; Audio.add pcm 0 track_pcm 0 audio_len) fields) frames; let buf = Frame.Fields.add field (Content.Audio.lift_data pcm) buf in self#set_metadata buf end class video_add ~field ~add tracks = object (self) inherit base ~name:"video.add" tracks method private generate_frame = let frames = self#generate_frames in let length = self#frames_position frames in let frames = List.fold_left (fun frames { data; fields } -> match data with | None -> frames | Some (source, frame) -> frames @ List.map (fun { position; field } -> ( position, source#last_image field, Content.Video.get_data (Frame.get frame field) )) fields) [] frames in let frames = List.sort (fun (p, _, _) (p', _, _) -> Stdlib.compare p p') frames in let create, frames = match frames with | [] -> ( (fun ~pos:_ ~width ~height () -> Video.Canvas.Image.create width height), [] ) | (_, last_image, data) :: rest -> ( (fun ~pos ~width:_ ~height:_ () -> self#nearest_image ~pos ~last_image data), rest ) in let buf = self#generate_video ~field ~create length in let data = List.map (fun (pos, img) -> ( pos, List.fold_left (fun img (rank, last_image, data) -> add rank (self#nearest_image ~pos ~last_image data) img) img frames )) buf.Content.Video.data in let frame = Frame.set_data (Frame.create ~length Frame.Fields.empty) field Content.Video.lift_data { buf with Content.Video.data } in self#set_metadata frame end let get_tracks ~mk_weight p = let track_values = Lang.to_list (List.assoc "" p) in List.fold_left (fun (tracks, position) v -> let field, s = Lang.to_track v in let weight = mk_weight v in let field = { position; weight; field } in let track = match List.find_opt (fun { data = source } -> s == source) tracks with | Some track -> track.fields <- track.fields @ [field]; tracks | None -> tracks @ [{ data = s; fields = [field] }] in (track, position + 1)) ([], 0) track_values let add_proto = [ ( "normalize", Lang.getter_t Lang.bool_t, Some (Lang.bool true), Some "Divide by the sum of weights of ready sources (or by the number of \ ready sources if weights are not specified)." ); ( "power", Lang.getter_t Lang.bool_t, Some (Lang.bool false), Some "Perform constant-power normalization." ); ] let add_audio_tracks p = let tracks, _ = get_tracks ~mk_weight:(fun v -> try Lang.to_float_getter (Liquidsoap_lang.Value.invoke v "weight") with _ -> fun () -> 1.) p in let renorm = Lang.to_bool_getter (List.assoc "normalize" p) in let power = List.assoc "power" p |> Lang.to_bool_getter in let field = Frame.Fields.audio in (field, new audio_add ~renorm ~power ~field tracks) let _ = let frame_t = Format_type.audio () in let track_t = Type.meth ~optional:true "weight" ([], Lang.getter_t Lang.float_t) frame_t in Lang.add_track_operator ~base:Modules.track_audio "add" ~category:`Audio ~descr:"Mix audio tracks with optional normalization." (("", Lang.list_t track_t, None, None) :: add_proto) ~return_t:frame_t add_audio_tracks let add_video_tracks p = let tracks, _ = get_tracks ~mk_weight:(fun _ () -> ()) p in let add _ = Video.Canvas.Image.add in let field = Frame.Fields.video in (field, new video_add ~add ~field tracks) let _ = let frame_t = Format_type.video () in Lang.add_track_operator ~base:Modules.track_video "add" ~category:`Video ~descr:"Merge video tracks." [("", Lang.list_t frame_t, None, None)] ~return_t:frame_t add_video_tracks let tile_pos n = let vert l x y x' y' = if l = 0 then [||] else ( let dx = (x' - x) / l in let x = ref (x - dx) in Array.init l (fun _ -> x := !x + dx; (!x, y, dx, y' - y))) in let x' = Lazy.force Frame.video_width in let y' = Lazy.force Frame.video_height in let horiz m n = Array.append (vert m 0 0 x' (y' / 2)) (vert n 0 (y' / 2) x' y') in horiz (n / 2) (n - (n / 2)) let _ = let frame_t = Format_type.video () in Lang.add_track_operator ~base:Modules.track_video "tile" ~category:`Video ~descr:"Tile video tracks." [ ( "proportional", Lang.bool_t, Some (Lang.bool true), Some "Scale preserving the proportions." ); ("", Lang.list_t frame_t, None, None); ] ~return_t:frame_t (fun p -> let tracks, total_tracks = get_tracks ~mk_weight:(fun _ () -> ()) p in let proportional = Lang.to_bool (List.assoc "proportional" p) in let tp = tile_pos total_tracks in let scale = Video_converter.scaler () in let add n tmp buf = let x, y, w, h = tp.(n) in let x, y, w, h = if proportional then ( let sw, sh = (Video.Canvas.Image.width buf, Video.Canvas.Image.height buf) in if w * sh < sw * h then ( let h' = sh * w / sw in (x, y + ((h - h') / 2), w, h')) else ( let w' = sw * h / sh in (x + ((w - w') / 2), y, w', h))) else (x, y, w, h) in let tmp = Video.Canvas.Image.render tmp in let tmp' = Video.Image.create w h in scale tmp tmp'; let tmp' = Video.Canvas.Image.make ~x ~y ~width:(-1) ~height:(-1) tmp' in Video.Canvas.Image.add tmp' buf in let field = Frame.Fields.video in (field, new video_add ~field ~add tracks)) let _ = let frame_t = Lang.internal_tracks_t () in Lang.add_operator "add" ~category:`Audio (("", Lang.list_t (Lang.source_t frame_t), None, None) :: ( "weights", Lang.(list_t (getter_t float_t)), Some (Lang.list []), Some "Relative weight of the sources in the sum. The empty list stands \ for the homogeneous distribution. These are used as amplification \ coefficients if we are not normalizing." ) :: add_proto) ~descr: "Mix sources, with optional normalization. Only relay metadata from the \ first available source. Track marks are dropped from all sources." ~return_t:frame_t (fun p -> let sources_val = List.assoc "" p in let sources = List.map Lang.to_source (Lang.to_list sources_val) in if sources = [] then (new Debug_sources.fail "add" :> Source.source) else ( let weights = Lang.to_list (List.assoc "weights" p) in let content_type = Frame.Fields.bindings (List.hd sources)#content_type in let audio_tracks, video_tracks = List.fold_left (fun (audio_tracks, video_tracks) (field, format) -> if Content_audio.is_format format then ( let tracks = List.mapi (fun pos s -> (pos, s)) sources in ((field, tracks, fun s -> s) :: audio_tracks, video_tracks)) else if Content_pcm_s16.is_format format then ( let from_s s = (Track_map.( new track_map ~name:"track.decode.audio.pcm_s16" ~field ~fn:from_pcm_s16 s) :> Source.source) in let to_s s = (Track_map.( new track_map ~name:"track.encode.audio.pcm_s16" ~field ~fn:to_pcm_s16 s) :> Source.source) in let tracks = List.mapi (fun pos s -> (pos, from_s s)) sources in ((field, tracks, to_s) :: audio_tracks, video_tracks)) else if Content_pcm_f32.is_format format then ( let from_s s = (Track_map.( new track_map ~name:"track.decode.audio.pcm_s16" ~field ~fn:from_pcm_f32 s) :> Source.source) in let to_s s = (Track_map.( new track_map ~name:"track.encode.audio.pcm_s16" ~field ~fn:to_pcm_f32 s) :> Source.source) in let tracks = List.mapi (fun pos s -> (pos, from_s s)) sources in ((field, tracks, to_s) :: audio_tracks, video_tracks)) else if Content_video.is_format format then (audio_tracks, (field, sources) :: video_tracks) else if Content_timed.Metadata.is_format format || Content_timed.Track_marks.is_format format then (audio_tracks, video_tracks) else raise (Error.Invalid_value (sources_val, "Invalid source type"))) ([], []) content_type in let audio_tracks = List.map (fun (field, sources, to_source) -> let tracks = List.map (fun (pos, s) -> let track = Lang.track (field, s) in try let weight = List.nth weights pos in Lang.meth track [("weight", weight)] with _ -> track) sources in let p = [ ("normalize", List.assoc "normalize" p); ("power", List.assoc "power" p); ("", Lang.list tracks); ] in let _, track = add_audio_tracks p in (field, to_source track)) audio_tracks in let video_tracks = List.map (fun (field, sources) -> let tracks = Lang.list (List.map (fun s -> Lang.track (field, s)) sources) in let p = [("", tracks)] in let _, track = add_video_tracks p in (field, track)) video_tracks in let tracks = audio_tracks @ video_tracks in let tracks = (Frame.Fields.metadata, snd (List.hd tracks)) :: tracks in let track_sources = List.map (fun (_, s) -> s) tracks in let tracks = List.map (fun (field, track) -> let track = Lang.track (field, track) in (Frame.Fields.string_of_field field, track)) tracks in let p = [("", Lang.meth Lang.unit tracks)] in let s = (Muxer.muxer_operator p :> Source.source) in (* Make sure all track positions are the same as the top-level source. *) List.iter (fun t -> Unifier.(t#stack_unifier <-- s#stack_unifier)) track_sources; s)) liquidsoap-2.3.2/src/core/operators/amplify.ml000066400000000000000000000112321477303350200214230ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Source let parse_db s = try Scanf.sscanf s " %f dB" Audio.lin_of_dB with _ -> float_of_string s class amplify ~field ~override_field (source : source) coeff = object (self) inherit operator ~name:"track.audio.amplify" [source] val mutable override = None method fallible = source#fallible method private can_generate_frame = source#is_ready method remaining = source#remaining method abort_track = source#abort_track method seek_source = source#seek_source method self_sync = source#self_sync method private amplify k c offset len = match Content.format c with | f when Content.Audio.is_format f -> let data = Content.Audio.get_data c in Audio.amplify k data offset len | f when Content_pcm_s16.is_format f -> let data = Content_pcm_s16.get_data c in Content_pcm_s16.amplify (Array.map (fun c -> Bigarray.Array1.sub c offset len) data) k | f when Content_pcm_f32.is_format f -> let data = Content_pcm_f32.get_data c in Content_pcm_f32.amplify (Array.map (fun c -> Bigarray.Array1.sub c offset len) data) k | _ -> assert false method private process buf = let k = match override with Some o -> o | None -> coeff () in if k <> 1. then ( let content = Frame.get buf field in self#amplify k content 0 (Frame.audio_of_main (Content.length content)); Frame.set buf field content) else buf method private set_override buf = match Option.map (fun f -> f ()) override_field with | Some f -> if override <> None then self#log#info "End of the current overriding."; override <- None; List.iter (fun (_, m) -> try let k = parse_db (Frame.Metadata.find f m) in self#log#info "Overriding amplification: %f." k; override <- Some k with _ -> ()) (Frame.get_all_metadata buf) | _ -> () method private generate_frame = let buf = source#get_mutable_frame field in match self#split_frame buf with | buf, None -> self#process buf | buf, Some new_track -> let buf = self#process buf in self#set_override new_track; Frame.append buf (self#process new_track) end let _ = let frame_t = Lang.pcm_audio_t () in Lang.add_track_operator ~base:Modules.track_audio "amplify" [ ( "override", Lang.getter_t (Lang.nullable_t Lang.string_t), Some (Lang.string "liq_amplify"), Some "Specify the name of a metadata field that, when present and \ well-formed, overrides the amplification factor for the current \ track. Well-formed values are floats in decimal notation (e.g. \ `0.7`) which are taken as normal/linear multiplicative factors; \ values can be passed in decibels with the suffix `dB` (e.g. `-8.2 \ dB`, but the spaces do not matter). Defaults to \ `settings.amplify.metadata`. Set to `null` to disable." ); ("", Lang.getter_t Lang.float_t, None, Some "Multiplicative factor."); ("", frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr:"Multiply the amplitude of the signal." (fun p -> let c = Lang.to_float_getter (Lang.assoc "" 1 p) in let field, s = Lang.to_track (Lang.assoc "" 2 p) in let override_field = Lang.to_valued_option Lang.to_string_getter (Lang.assoc "override" 1 p) in (field, new amplify ~field ~override_field s c)) liquidsoap-2.3.2/src/core/operators/available.ml000066400000000000000000000063011477303350200217030ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Source class available ~track_sensitive ~override p (source : source) = object (self) inherit operator ~name:"source.available" [source] method fallible = true method remaining = source#remaining method abort_track = source#abort_track method seek_source = source#seek_source method self_sync = source#self_sync val mutable ready = None method private ready = match ready with | None -> let r = p () in ready <- Some r; r | Some r -> r method private can_generate_frame = if not (track_sensitive () && self#ready) then ready <- Some (p ()); self#ready && (override || source#is_ready) method private generate_frame = let frame = source#get_frame in match self#split_frame frame with | buf, None -> buf | buf, Some _ -> if track_sensitive () then ready <- Some (p ()); if self#ready then frame else buf end let _ = let return_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator ~base:Muxer.source "available" [ ( "track_sensitive", Lang.getter_t Lang.bool_t, Some (Lang.bool false), Some "Change availability only on end of tracks." ); ( "override", Lang.bool_t, Some (Lang.bool false), Some "Don't take availability of original source in account (this can be \ dangerous and should be avoided)." ); ( "", Lang.source_t return_t, None, Some "Source of which the availability should be changed." ); ( "", Lang.getter_t Lang.bool_t, None, Some "Predicate indicating whether the source should be available or not." ); ] ~category:`Track ~descr:"Change the availability of a source depending on a predicate." ~return_t (fun p -> let track_sensitive = List.assoc "track_sensitive" p |> Lang.to_bool_getter in let override = List.assoc "override" p |> Lang.to_bool in let s = Lang.assoc "" 1 p |> Lang.to_source in let pred = Lang.assoc "" 2 p |> Lang.to_bool_getter in new available ~track_sensitive ~override pred s) liquidsoap-2.3.2/src/core/operators/biquad_filter.ml000066400000000000000000000273471477303350200226120ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Source class biquad (source : source) filter_type freq q gain = let samplerate = float (Frame.audio_of_seconds 1.) in object (self) inherit operator ~name:"biquad_filter" [source] val mutable p0 = 0. val mutable p1 = 0. val mutable p2 = 0. val mutable q1 = 0. val mutable q2 = 0. val mutable x1 = [||] val mutable x2 = [||] val mutable y0 = [||] val mutable y1 = [||] val mutable y2 = [||] (* Last frequency used to initialize parameters. Used to detect when we should re-compute coefficients. *) val mutable last_freq = 0. val mutable last_q = 0. val mutable last_gain = 0. (* Digital filter based on "Cookbook formulae for audio EQ biquad filter coefficients" by Robert Bristow-Johnson . URL: http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt *) method private init = let chans = self#audio_channels in if Array.length x1 <> chans then ( x1 <- Array.make chans 0.; x2 <- Array.make chans 0.; y0 <- Array.make chans 0.; y1 <- Array.make chans 0.; y2 <- Array.make chans 0.); let freq = freq () in let q = q () in let gain = gain () in if last_freq <> freq || last_q <> q || last_gain <> gain then ( last_freq <- freq; last_q <- q; last_gain <- gain; let w0 = 2. *. Float.pi *. freq /. samplerate in let sin_w0 = sin w0 in let cos_w0 = cos w0 in let alpha = sin_w0 /. (2. *. q) in let b0, b1, b2, a0, a1, a2 = match filter_type with | `Low_pass -> let b1 = 1. -. cos w0 in let b0 = b1 /. 2. in (b0, b1, b0, 1. +. alpha, -2. *. cos_w0, 1. -. alpha) | `High_pass -> let b1 = 1. +. cos_w0 in let b0 = b1 /. 2. in let b1 = -.b1 in (b0, b1, b0, 1. +. alpha, -2. *. cos_w0, 1. -. alpha) | `Band_pass -> let b0 = sin_w0 /. 2. in (b0, 0., -.b0, 1. +. alpha, -2. *. cos_w0, 1. -. alpha) | `Notch -> let b1 = -2. *. cos_w0 in (1., b1, 1., 1. +. alpha, b1, 1. -. alpha) | `All_pass -> let b0 = 1. -. alpha in let b1 = -2. *. cos_w0 in let b2 = 1. +. alpha in (b0, b1, b2, b2, b1, b0) | `Peaking -> let a = if gain = 0. then 1. else 10. ** (gain /. 40.) in let ama = alpha *. a in let ada = alpha /. a in let b1 = -2. *. cos_w0 in (1. +. ama, b1, 1. -. ama, 1. +. ada, b1, 1. -. ada) | `Low_shelf -> let a = if gain = 0. then 1. else 10. ** (gain /. 40.) in let s = 2. *. sqrt a *. alpha in ( a *. (a +. 1. -. ((a -. 1.) *. cos_w0) +. s), 2. *. a *. (a -. 1. -. ((a +. 1.) *. cos_w0)), a *. (a +. 1. -. ((a -. 1.) *. cos_w0) -. s), a +. 1. +. ((a -. 1.) *. cos_w0) +. s, (-2. *. (a -. 1.)) +. ((a +. 1.) *. cos_w0), a +. 1. +. ((a -. 1.) *. cos_w0) -. s ) | `High_shelf -> let a = if gain = 0. then 1. else 10. ** (gain /. 40.) in let s = 2. *. sqrt a *. alpha in ( a *. (a +. 1. +. ((a -. 1.) *. cos_w0) +. s), -2. *. a *. (a -. 1. +. ((a +. 1.) *. cos_w0)), a *. (a +. 1. +. ((a -. 1.) *. cos_w0) -. s), a +. 1. -. ((a -. 1.) *. cos_w0) +. s, (2. *. (a -. 1.)) -. ((a +. 1.) *. cos_w0), a +. 1. -. ((a -. 1.) *. cos_w0) -. s ) in p0 <- b0 /. a0; p1 <- b1 /. a0; p2 <- b2 /. a0; q1 <- a1 /. a0; q2 <- a2 /. a0) method fallible = source#fallible method remaining = source#remaining method seek_source = source#seek_source method self_sync = source#self_sync method private can_generate_frame = source#is_ready method abort_track = source#abort_track initializer self#on_wake_up (fun () -> self#init) method private generate_frame = let data = source#get_mutable_content Frame.Fields.audio in let buf = Content.Audio.get_data data in let position = source#frame_audio_position in self#init; for c = 0 to self#audio_channels - 1 do let buf = buf.(c) in for i = 0 to position - 1 do let x0 = buf.(i) in let y0 = (p0 *. x0) +. (p1 *. x1.(c)) +. (p2 *. x2.(c)) -. (q1 *. y1.(c)) -. (q2 *. y2.(c)) in buf.(i) <- y0; x2.(c) <- x1.(c); x1.(c) <- x0; y2.(c) <- y1.(c); y1.(c) <- y0 done done; source#set_frame_data Frame.Fields.audio Content.Audio.lift_data buf end let filter_iir_eq = Lang.add_module ~base:Iir_filter.filter_iir "eq" let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator ~base:filter_iir_eq "lowshelf" [ ("frequency", Lang.getter_t Lang.float_t, None, Some "Corner frequency"); ( "slope", Lang.getter_t Lang.float_t, Some (Lang.float 1.), Some "Shelf slope (dB/octave)" ); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr:"Low shelf biquad filter." (fun p -> let f v = List.assoc v p in let freq, param, src = ( Lang.to_float_getter (f "frequency"), Lang.to_float_getter (f "slope"), Lang.to_source (f "") ) in (new biquad src `Low_shelf freq param (fun () -> 0.) :> Source.source)) let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator ~base:filter_iir_eq "highshelf" [ ("frequency", Lang.getter_t Lang.float_t, None, Some "Center frequency"); ( "slope", Lang.getter_t Lang.float_t, Some (Lang.float 1.), Some "Shelf slope (in dB/octave)" ); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr:"High shelf biquad filter." (fun p -> let f v = List.assoc v p in let freq, param, src = ( Lang.to_float_getter (f "frequency"), Lang.to_float_getter (f "slope"), Lang.to_source (f "") ) in (new biquad src `High_shelf freq param (fun () -> 0.) :> Source.source)) let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator ~base:filter_iir_eq "low" [ ("frequency", Lang.getter_t Lang.float_t, None, Some "Corner frequency"); ("q", Lang.getter_t Lang.float_t, Some (Lang.float 1.), Some "Q"); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr:"Low-pass biquad filter." (fun p -> let f v = List.assoc v p in let freq, param, src = ( Lang.to_float_getter (f "frequency"), Lang.to_float_getter (f "q"), Lang.to_source (f "") ) in (new biquad src `Low_pass freq param (fun () -> 0.) :> Source.source)) let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator ~base:filter_iir_eq "high" [ ("frequency", Lang.getter_t Lang.float_t, None, Some "Corner frequency"); ("q", Lang.getter_t Lang.float_t, Some (Lang.float 1.), Some "Q"); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr:"High-pass biquad filter." (fun p -> let f v = List.assoc v p in let freq, param, src = ( Lang.to_float_getter (f "frequency"), Lang.to_float_getter (f "q"), Lang.to_source (f "") ) in (new biquad src `High_pass freq param (fun () -> 0.) :> Source.source)) let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator ~base:filter_iir_eq "bandpass" [ ("frequency", Lang.getter_t Lang.float_t, None, Some "Center frequency"); ("q", Lang.getter_t Lang.float_t, Some (Lang.float 1.), Some "Q"); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr:"Band-pass biquad filter." (fun p -> let f v = List.assoc v p in let freq, param, src = ( Lang.to_float_getter (f "frequency"), Lang.to_float_getter (f "q"), Lang.to_source (f "") ) in (new biquad src `Band_pass freq param (fun () -> 0.) :> Source.source)) let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator ~base:filter_iir_eq "allpass" [ ("frequency", Lang.getter_t Lang.float_t, None, Some "Center frequency"); ( "bandwidth", Lang.getter_t Lang.float_t, Some (Lang.float (1. /. 3.)), Some "Bandwidth (in octaves)" ); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr:"All-pass biquad filter." (fun p -> let f v = List.assoc v p in let freq, param, src = ( Lang.to_float_getter (f "frequency"), Lang.to_float_getter (f "bandwidth"), Lang.to_source (f "") ) in (new biquad src `All_pass freq param (fun () -> 0.) :> Source.source)) let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator ~base:filter_iir_eq "notch" [ ("frequency", Lang.getter_t Lang.float_t, None, Some "Center frequency"); ("q", Lang.getter_t Lang.float_t, Some (Lang.float 1.), Some "Q"); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr:"Band-pass biquad filter." (fun p -> let f v = List.assoc v p in let freq, param, src = ( Lang.to_float_getter (f "frequency"), Lang.to_float_getter (f "q"), Lang.to_source (f "") ) in (new biquad src `Notch freq param (fun () -> 0.) :> Source.source)) let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator ~base:filter_iir_eq "peak" [ ("frequency", Lang.getter_t Lang.float_t, None, Some "Center frequency"); ("q", Lang.getter_t Lang.float_t, Some (Lang.float 1.), Some "Q"); ( "gain", Lang.getter_t Lang.float_t, Some (Lang.float 1.), Some "Gain (in dB)" ); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr:"Peak EQ biquad filter." (fun p -> let f v = List.assoc v p in let freq, param, gain, src = ( Lang.to_float_getter (f "frequency"), Lang.to_float_getter (f "q"), Lang.to_float_getter (f "gain"), Lang.to_source (f "") ) in (new biquad src `Peaking freq param gain :> Source.source)) liquidsoap-2.3.2/src/core/operators/chord.ml000066400000000000000000000110571477303350200210660ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Source let chan = 0 let note_of_string = function | "-" -> -1 (* mute *) | "A" -> 69 | "A#" | "Bb" -> 70 | "B" | "Cb" -> 71 | "C" | "B#" -> 72 | "C#" | "Db" -> 73 | "D" -> 74 | "D#" | "Eb" -> 75 | "E" | "Fb" -> 76 | "F" | "E#" -> 77 | "F#" | "Gb" -> 78 | "G" -> 79 | "G#" | "Ab" -> 80 | _ -> assert false let note_of_string s = note_of_string s - 12 class chord metadata_name (source : source) = object (self) inherit operator ~name:"chord" [source] method fallible = source#fallible method remaining = source#remaining method private can_generate_frame = source#is_ready method abort_track = source#abort_track method seek_source = source#seek_source method self_sync = source#self_sync val mutable notes_on = [] method private generate_frame = let buf = source#get_mutable_content Frame.Fields.midi in let m = Content.Midi.get_data buf in let meta = Frame.get_all_metadata source#get_frame in let chords = let ans = ref [] in List.iter (fun (t, m) -> match Frame.Metadata.find_opt metadata_name m with | None -> () | Some c -> ( try let sub = Re.Pcre.exec ~rex: (Re.Pcre.regexp "^([A-G-](?:b|#)?)(|M|m|M7|m7|dim)$") c in let n = Re.Pcre.get_substring sub 1 in let n = note_of_string n in let m = Re.Pcre.get_substring sub 2 in ans := (t, n, m) :: !ans with Not_found -> self#log#important "Could not parse chord '%s'." c)) meta; List.rev !ans in let play t n = List.iter (fun n -> MIDI.insert m.(chan) (t, MIDI.Note_on (n, 1.))) n; notes_on <- n @ notes_on in let mute t = List.iter (fun n -> MIDI.insert m.(chan) (t, MIDI.Note_off (n, 1.))) notes_on; notes_on <- [] in List.iter (fun (t, c, m) -> (* time, base, mode *) mute t; (* Negative base note means mute. *) if c >= 0 then ( match m with | "" | "M" -> (* major *) play t [c; c + 4; c + 7] | "m" -> (* minor *) play t [c; c + 3; c + 7] | "7" -> play t [c; c + 4; c + 7; c + 10] | "M7" -> play t [c; c + 4; c + 7; c + 11] | "m7" -> play t [c; c + 3; c + 7; c + 10] | "dim" -> play t [c; c + 3; c + 6] | m -> self#log#debug "Unknown mode: %s\n%!" m)) chords; source#set_frame_data Frame.Fields.midi Content.Midi.lift_data m end let _ = (* TODO: is this really the type we want to give to it? *) let in_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in let out_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~midi:(Format_type.midi_n 1) ()) in Lang.add_operator ~base:Modules.midi "chord" [ ( "metadata", Lang.string_t, Some (Lang.string "chord"), Some "Name of the metadata containing the chords." ); ("", Lang.source_t in_t, None, None); ] ~return_t:out_t ~category:`MIDI ~descr:"Generate a chord." (fun p -> let f v = List.assoc v p in let src = Lang.to_source (f "") in let metadata = Lang.to_string (f "metadata") in (new chord metadata src :> Source.source)) liquidsoap-2.3.2/src/core/operators/clip.ml000066400000000000000000000042041477303350200207120ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Source class clip ~field (source : source) = object inherit operator ~name:"clip" [source] method fallible = source#fallible method remaining = source#remaining method seek_source = source#seek_source method private can_generate_frame = source#is_ready method abort_track = source#abort_track method self_sync = source#self_sync method private generate_frame = let c = source#get_mutable_content field in let b = Content.Audio.get_data c in let position = source#frame_audio_position in Audio.clip b 0 position; source#set_frame_data field Content.Audio.lift_data b end let _ = let frame_t = Format_type.audio () in Lang.add_track_operator ~base:Modules.track_audio "clip" [("", frame_t, None, None)] ~return_t:frame_t ~category:`Audio ~descr: "Clip samples, i.e. ensure that all values are between -1 and 1: values \ lower than -1 become -1 and values higher than 1 become 1. `nan` values \ become `0.`" (fun p -> let f v = List.assoc v p in let field, src = Lang.to_track (f "") in (field, new clip ~field src)) liquidsoap-2.3.2/src/core/operators/comb.ml000066400000000000000000000057151477303350200207130ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Source (* See http://en.wikipedia.org/wiki/Comb_filter *) class comb ~field (source : source) delay feedback = let past_len = Frame.audio_of_seconds delay in object (self) inherit operator ~name:"comb" [source] method fallible = source#fallible method remaining = source#remaining method seek_source = source#seek_source method self_sync = source#self_sync method private can_generate_frame = source#is_ready method abort_track = source#abort_track val mutable past = Audio.make 0 0 0. initializer self#on_wake_up (fun () -> past <- Audio.make self#audio_channels past_len 0.) val mutable past_pos = 0 method private generate_frame = let b = Content.Audio.get_data (source#get_mutable_content field) in let position = source#frame_audio_position in let feedback = feedback () in for i = 0 to position - 1 do for c = 0 to Array.length b - 1 do let oldin = b.(c).(i) in b.(c).(i) <- b.(c).(i) +. (past.(c).(past_pos) *. feedback); past.(c).(past_pos) <- oldin done; past_pos <- (past_pos + 1) mod past_len done; source#set_frame_data field Content.Audio.lift_data b end let _ = let frame_t = Format_type.audio () in Lang.add_track_operator ~base:Modules.track_audio "comb" [ ("delay", Lang.float_t, Some (Lang.float 0.001), Some "Delay in seconds."); ( "feedback", Lang.getter_t Lang.float_t, Some (Lang.float (-6.)), Some "Feedback coefficient in dB." ); ("", frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr:"Comb filter." (fun p -> let f v = List.assoc v p in let duration, feedback, (field, src) = ( Lang.to_float (f "delay"), Lang.to_float_getter (f "feedback"), Lang.to_track (f "") ) in ( field, new comb ~field src duration (fun () -> Audio.lin_of_dB (feedback ())) )) liquidsoap-2.3.2/src/core/operators/compand.ml000066400000000000000000000046111477303350200214060ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Source class compand ~field (source : source) mu = object (self) inherit operator ~name:"compand" [source] method fallible = source#fallible method remaining = source#remaining method seek_source = source#seek_source method self_sync = source#self_sync method private can_generate_frame = source#is_ready method abort_track = source#abort_track method private generate_frame = let pos = source#frame_audio_position in let b = Content.Audio.get_data (source#get_mutable_content field) in for c = 0 to self#audio_channels - 1 do let b_c = b.(c) in for i = 0 to pos - 1 do (* Cf. http://en.wikipedia.org/wiki/Mu-law *) let sign = if b_c.(i) < 0. then -1. else 1. in b_c.(i) <- sign *. log (1. +. (mu *. Utils.abs_float b_c.(i))) /. log (1. +. mu) done done; source#set_frame_data field Content.Audio.lift_data b end let _ = let frame_t = Format_type.audio () in Lang.add_track_operator ~base:Modules.track_audio "compand" [ ("mu", Lang.float_t, Some (Lang.float 1.), None); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr:"Compand the signal." (fun p -> let f v = List.assoc v p in let mu, (field, src) = (Lang.to_float (f "mu"), Lang.to_track (f "")) in (field, new compand ~field src mu)) liquidsoap-2.3.2/src/core/operators/compress.ml000066400000000000000000000246711477303350200216300ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (* Some vague inspiration drew from http://c4dm.eecs.qmul.ac.uk/audioengineering/compressors/documents/Reiss-Tutorialondynamicrangecompression.pdf https://github.com/nwjs/chromium.src/blob/df7f8c8582b9a78c806a7fa1e9d3f3ba51f7a698/third_party/WebKit/Source/platform/audio/DynamicsCompressorKernel.cpp and https://github.com/velipso/sndfilter/blob/master/src/compressor.c *) open Mm open Source class compress ~attack ~release ~threshold ~ratio ~knee ~track_sensitive ~pre_gain ~make_up_gain ~lookahead ~window ~wet ~field (source : source) = let lookahead () = Frame.audio_of_seconds (lookahead ()) in object (self) inherit operator ~name:"compress" [source] val mutable effect_ = None method fallible = source#fallible method remaining = source#remaining method seek_source = source#seek_source method self_sync = source#self_sync method private can_generate_frame = source#is_ready method abort_track = source#abort_track (* Current gain in dB. *) val mutable gain = 0. method gain = gain (* Position in ringbuffer. *) val mutable ringbuffer_pos = 0 val mutable ringbuffer = [||] (* Averaged mean of squares. *) val mutable ms = 0. method rms = sqrt ms (* Make sure that the ringbuffer can hold this much. *) method prepare n = if n > 0 && (Array.length ringbuffer = 0 || Audio.Mono.length ringbuffer.(0) <> n) then ringbuffer <- Audio.create self#audio_channels n method private reset = gain <- 0.; ms <- 0. method private compress frame = let pos = AFrame.position frame in let buf = Content.Audio.get_data (Frame.get frame field) in let chans = Array.length buf in let samplerate = float (Lazy.force Frame.audio_rate) in let threshold = threshold () in let knee = knee () in let ratio = ratio () in let attack = attack () in let attack_coef = 1. -. exp (-1. /. (attack *. samplerate)) in let release = release () in let release_coef = 1. -. exp (-1. /. (release *. samplerate)) in let lookahead = lookahead () in let pre_gain = pre_gain () in let pre_gain_lin = Audio.lin_of_dB pre_gain in let make_up_gain = make_up_gain () in let window = window () in let window_coef = 1. -. exp (-1. /. (window *. samplerate)) in let wet = wet () in self#prepare lookahead; for i = 0 to pos - 1 do (* Apply pre_gain. *) if pre_gain <> 0. then for c = 0 to chans - 1 do buf.(c).(i) <- buf.(c).(i) *. pre_gain_lin done; (* Compute input. *) let x = if window = 0. then ( (* Peak mode: maximum absolute value over chans. *) let x = ref 0. in for c = 0 to chans - 1 do let old = if lookahead = 0 then buf.(c).(i) else ( let old = ringbuffer.(c).(ringbuffer_pos) in ringbuffer.(c).(ringbuffer_pos) <- buf.(c).(i); old) in x := max !x (Utils.abs_float old) done; if lookahead > 0 then ringbuffer_pos <- (ringbuffer_pos + 1) mod lookahead; let x = !x in ms <- x *. x; x) else ( (* Smoothed RMS mode. *) let x = ref 0. in for c = 0 to chans - 1 do let old = if lookahead = 0 then buf.(c).(i) else ( let old = ringbuffer.(c).(ringbuffer_pos) in ringbuffer.(c).(ringbuffer_pos) <- buf.(c).(i); old) in x := !x +. (old *. old) done; if lookahead > 0 then ringbuffer_pos <- (ringbuffer_pos + 1) mod lookahead; ms <- ms +. (window_coef *. ((!x /. float chans) -. ms)); sqrt ms) in (* From now on, we work in the dB domain, which gives better fidelity than the linear domain. *) let x = max (-80.) (Audio.dB_of_lin x) in (* Shape input. *) let x' = let x' = if x <= threshold -. (knee /. 2.) then x else if x < threshold +. (knee /. 2.) then ( (* Second order interpolation for the knee. *) let a = x -. threshold +. (knee /. 2.) in x +. (((1. /. ratio) -. 1.) *. a *. a /. (2. *. knee))) else threshold +. ((x -. threshold) /. ratio) in x' in (* if x >= threshold then Printf.printf "%f => %f (%f)\tratio: %f\n%!" x x' (threshold +. (x -. threshold) /. ratio) ratio; *) (* Target gain (dB). *) let target = x' -. x in (* if x >= threshold then Printf.printf "gain: %f\ttarget: %f (%f -> %f)\n%!" gain target x x'; *) (* if gain > target then Printf.printf "Attack (%f -> %f)\tcoef: %f\n%!" gain target attack_coef; *) if gain > target then (* Attack. *) gain <- gain +. (attack_coef *. (target -. gain)) else (* Release *) gain <- gain +. (release_coef *. (target -. gain)); (* Finally apply gain. *) let gain = Audio.lin_of_dB (gain +. make_up_gain) in for c = 0 to chans - 1 do buf.(c).(i) <- buf.(c).(i) *. (1. -. wet +. (wet *. gain)) done done; Frame.set_data frame field Content.Audio.lift_data buf method private generate_frame = match self#split_frame (source#get_mutable_frame field) with | frame, None -> self#compress frame | frame, Some new_track -> let frame = self#compress frame in if track_sensitive then self#reset; Frame.append frame (self#compress new_track) end let audio_compress = let return_t = Format_type.audio () in Lang.add_track_operator ~base:Modules.track_audio "compress" [ ( "attack", Lang.getter_t Lang.float_t, Some (Lang.float 50.), Some "Attack time (ms)." ); ( "release", Lang.getter_t Lang.float_t, Some (Lang.float 400.), Some "Release time (ms)." ); ( "lookahead", Lang.getter_t Lang.float_t, Some (Lang.float 0.), Some "Lookahead (ms)." ); ( "threshold", Lang.getter_t Lang.float_t, Some (Lang.float (-10.)), Some "Threshold level (dB)." ); ( "track_sensitive", Lang.bool_t, Some (Lang.bool false), Some "Reset on every track." ); ( "knee", Lang.getter_t Lang.float_t, Some (Lang.float 1.), Some "Knee width (dB)." ); ( "pre_gain", Lang.getter_t Lang.float_t, Some (Lang.float 0.), Some "Pre-amplification (dB)." ); ( "gain", Lang.getter_t Lang.float_t, Some (Lang.float 0.), Some "Post-amplification (dB)." ); ( "ratio", Lang.getter_t Lang.float_t, Some (Lang.float 2.), Some "Gain reduction ratio (reduction is ratio:1). Must be at least 1." ); ( "window", Lang.getter_t Lang.float_t, Some (Lang.float 0.), Some "RMS window length (second). `0.` means peak mode." ); ( "wet", Lang.getter_t Lang.float_t, Some (Lang.float 1.), Some "How much of input sound to output (between 0 and 1, 0 means only \ original sound, 1 means only compressed sound)." ); ("", return_t, None, None); ] ~return_t ~category:`Audio ~descr:"Compress the signal." ~meth: [ ( "gain", ([], Lang.fun_t [] Lang.float_t), "Gain (dB).", fun s -> Lang.val_fun [] (fun _ -> Lang.float s#gain) ); ( "rms", ([], Lang.fun_t [] Lang.float_t), "RMS or peak power (linear).", fun s -> Lang.val_fun [] (fun _ -> Lang.float s#rms) ); ] (fun p -> let attack = List.assoc "attack" p |> Lang.to_float_getter in let attack () = attack () /. 1000. in let release = List.assoc "release" p |> Lang.to_float_getter in let release () = release () /. 1000. in let lookahead = List.assoc "lookahead" p |> Lang.to_float_getter in let lookahead () = lookahead () /. 1000. in let threshold = List.assoc "threshold" p |> Lang.to_float_getter in let track_sensitive = List.assoc "track_sensitive" p |> Lang.to_bool in let ratio = let pos = Lang.pos p in match List.assoc "ratio" p with | Liquidsoap_lang.Value.Float { value = f } -> if f < 1. then Runtime_error.raise ~pos ~message:"Ratio must be at least 1!" "eval"; fun () -> f | v -> let f = Lang.to_float_getter v in fun () -> let v = f () in if v < 1. then Runtime_error.raise ~pos ~message:"Ratio must be at least 1!" "eval"; v in let knee = List.assoc "knee" p |> Lang.to_float_getter in let pre_gain = List.assoc "pre_gain" p |> Lang.to_float_getter in let make_up_gain = List.assoc "gain" p |> Lang.to_float_getter in let window = List.assoc "window" p |> Lang.to_float_getter in let wet = List.assoc "wet" p |> Lang.to_float_getter in let field, s = List.assoc "" p |> Track.of_value in ( field, new compress ~attack ~release ~lookahead ~ratio ~knee ~threshold ~track_sensitive ~pre_gain ~make_up_gain ~window ~wet ~field s )) liquidsoap-2.3.2/src/core/operators/compress_exp.ml000066400000000000000000000046071477303350200225010ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Source class compress ~field (source : source) mu = object (self) inherit operator ~name:"compress" [source] method fallible = source#fallible method remaining = source#remaining method private can_generate_frame = source#is_ready method abort_track = source#abort_track method seek_source = source#seek_source method self_sync = source#self_sync method private generate_frame = let b = Content.Audio.get_data (source#get_mutable_content field) in for c = 0 to self#audio_channels - 1 do let b_c = b.(c) in for i = 0 to source#frame_audio_position - 1 do let x = b_c.(i) in let sign = if x < 0. then -1. else 1. in b_c.(i) <- sign *. (1. -. ((1. -. Utils.abs_float x) ** mu)) done done; source#set_frame_data field Content.Audio.lift_data b end let _ = let return_t = Format_type.audio () in Lang.add_track_operator ~base:Compress.audio_compress "exponential" ~category:`Audio ~descr:"Exponential compressor." [ ( "mu", Lang.float_t, Some (Lang.float 2.), Some "Exponential compression factor, typically greater than 1." ); ("", return_t, None, None); ] ~return_t (fun p -> let f v = List.assoc v p in let mu, (field, src) = (Lang.to_float (f "mu"), Track.of_value (f "")) in (field, new compress ~field src mu)) liquidsoap-2.3.2/src/core/operators/cross.ml000066400000000000000000000711311477303350200211170ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Source let conf = Dtools.Conf.void ~p:(Configure.conf#plug "crossfade") "Crossfade settings" let conf_assume_autocue = Dtools.Conf.bool ~p:(conf#plug "assume_autocue") ~d:false "Assume autocue when all 4 cue in/out and fade in/out metadata override \ are present." class consumer ~clock buffer = object (self) inherit Source.source ~clock ~name:"cross.buffer" () method fallible = true method private can_generate_frame = 0 < Generator.length buffer method private generate_frame = Generator.slice buffer (Lazy.force Frame.size) method abort_track = Generator.clear buffer method self_sync = (`Static, None) method seek_source = (self :> Source.source) method remaining = Generator.length buffer end (** [rms_width] is in samples. [cross_length] is in ticks (like #remaining estimations) and must be at least one frame. *) class cross val_source ~end_duration_getter ~override_end_duration ~override_duration ~start_duration_getter ~override_start_duration ~override_max_start_duration ~persist_override ~rms_width ~assume_autocue transition = let s = Lang.to_source val_source in let original_end_duration_getter = end_duration_getter in let original_start_duration_getter = start_duration_getter in object (self) inherit source ~name:"cross" () inherit generate_from_multiple_sources ~merge:(fun () -> false) ~track_sensitive:(fun () -> false) () inherit Child_support.base ~check_self_sync:true [val_source] initializer Typing.(s#frame_type <: self#frame_type) method fallible = true (* This is complicated. crossfade should never be used with [self_sync] * sources but we do not have a static way of knowing it at the moment. * Going with the same choice as above for now. *) method self_sync = s#self_sync val mutable end_duration_getter = end_duration_getter val mutable start_duration_getter = start_duration_getter val mutable max_start_duration = None val mutable end_main_duration = 0 val mutable max_start_main_duration = None val mutable start_main_duration = 0 method end_duration = Frame.seconds_of_main end_main_duration method start_duration = Frame.seconds_of_main start_main_duration method set_end_main_duration = let end_duration = end_duration_getter () in let _end_main_duration = Frame.main_of_seconds end_duration in let frame_size = Lazy.force Frame.size in end_main_duration <- (if _end_main_duration < 0 then ( self#log#important "Cannot set crossfade end duration to negative value %f!" end_duration; frame_size (* Accept zero as simplify disabled crossfade. Set to frame_size. *)) else if _end_main_duration = 0 then frame_size (* For any non-zero too short value, warn the user. *) else if _end_main_duration < frame_size then ( self#log#important "Cannot set crossfade end duration to less than the frame size!"; frame_size) else _end_main_duration) method set_start_main_duration = let start_duration = start_duration_getter () in let _start_main_duration = Frame.main_of_seconds start_duration in let frame_size = Lazy.force Frame.size in start_main_duration <- (if _start_main_duration < 0 then ( self#log#important "Cannot set crossfade start duration to negative value %f!" start_duration; frame_size (* Accept zero as simplify disabled crossfade. Set to frame_size. *)) else if _start_main_duration = 0 then frame_size (* For any non-zero too short value, warn the user. *) else if _start_main_duration < frame_size then ( self#log#important "Cannot set crossfade start duration to less than the frame size!"; frame_size) else _start_main_duration); max_start_main_duration <- (match max_start_duration with | None -> None | Some max_start_duration -> let _max_start_main_duration = Frame.main_of_seconds max_start_duration in if _max_start_main_duration < 0 then ( self#log#important "Cannot set crossfade max start duration to negative value \ %f!" max_start_duration; None) else if _max_start_main_duration < frame_size then ( self#log#important "Cannot set crossfade max start duration to less than the \ frame size!"; None) else Some _max_start_main_duration) method reset_duration = end_duration_getter <- original_end_duration_getter; start_duration_getter <- original_start_duration_getter; max_start_duration <- None; self#set_end_main_duration; self#set_start_main_duration initializer self#reset_duration (* We need to store the end of a track, and compute the power of the signal * before the end of track. For doing so we need to remember a sliding window * of samples, maintain the sum of squares, and the number of samples in that * sum. The sliding window is necessary because of possibly inaccurate * remaining time estimaton. *) val mutable gen_before = Generator.create Frame.Fields.empty val mutable rms_before = 0. val mutable rmsi_before = 0 val mutable mem_before = Array.make rms_width 0. val mutable mem_i = 0 val mutable before_metadata = None (* Same for the new track. No need for a sliding window here. *) val mutable gen_after = Generator.create Frame.Fields.empty val mutable rms_after = 0. val mutable rmsi_after = 0 val mutable after_metadata = None method private autocue_enabled mode = let metadata, set_metadata = match mode with | `Before -> (before_metadata, fun m -> before_metadata <- Some m) | `After -> (after_metadata, fun m -> after_metadata <- Some m) in match metadata with | Some h -> ( let has_metadata = List.for_all (fun v -> Frame.Metadata.mem v h) ["liq_cue_in"; "liq_cue_out"; "liq_fade_in"; "liq_fade_out"] in let has_marker = Frame.Metadata.mem "liq_autocue" h in match (has_marker, has_metadata, assume_autocue) with | true, true, _ -> true | true, false, _ -> self#log#critical "`\"liq_autocue\"` metadata is present but some of the cue \ in/out and fade in/out metadata are missing!"; false | false, true, true -> self#log#info "Assuming autocue"; set_metadata (Frame.Metadata.add "liq_autocue" "assumed" h); true | _ -> false) | None -> false method private reset_analysis = gen_before <- Generator.create self#content_type; gen_after <- Generator.create self#content_type; rms_before <- 0.; rmsi_before <- 0; mem_i <- 0; Array.iteri (fun i _ -> mem_before.(i) <- 0.) mem_before; rms_after <- 0.; rmsi_after <- 0; before_metadata <- after_metadata; after_metadata <- None (* The played source. We _need_ exclusive control on that source, * since we are going to pull data from it at a higher rate around * track limits. *) val source = s method private prepare_source s = let s = (s :> source) in s#wake_up initializer self#on_wake_up (fun () -> source#wake_up; self#reset_analysis) val mutable status : [ `Idle | `Before of Source.source | `After of Source.source ] = `Idle method private child_get ~is_first source = let frame = ref self#empty_frame in self#on_child_tick (fun () -> if source#is_ready then frame := source#get_partial_frame (fun f -> match self#split_frame f with | buf, Some _ when Frame.position buf = 0 && is_first -> f | buf, _ -> buf)); !frame method private process_override_metadata m = (match Frame.Metadata.find_opt override_duration m with | None -> () | Some v -> ( try self#log#info "Overriding crossfade start and end duration from metadata %s" override_duration; let l = float_of_string v in end_duration_getter <- (fun () -> l); start_duration_getter <- (fun () -> l); self#set_end_main_duration; self#set_start_main_duration with _ -> ())); (match Frame.Metadata.find_opt override_end_duration m with | None -> () | Some v -> ( try self#log#info "Overriding crossfade end duration from metadata %s" override_end_duration; let l = float_of_string v in end_duration_getter <- (fun () -> l); self#set_end_main_duration with _ -> ())); (match Frame.Metadata.find_opt override_end_duration m with | None -> () | Some v -> ( try self#log#info "Overriding crossfade end duration from metadata %s" override_end_duration; let l = float_of_string v in end_duration_getter <- (fun () -> l); self#set_end_main_duration with _ -> ())); (match Frame.Metadata.find_opt override_max_start_duration m with | None -> () | Some v -> ( try self#log#info "Overriding crossfade max start duration from metadata %s" override_max_start_duration; let l = float_of_string v in max_start_duration <- Some l with _ -> ())); match Frame.Metadata.find_opt override_start_duration m with | None -> () | Some v -> ( try self#log#info "Overriding crossfade start duration from metadata %s" override_start_duration; let l = float_of_string v in start_duration_getter <- (fun () -> l); self#set_start_main_duration with _ -> ()) initializer self#on_metadata self#process_override_metadata method private append mode buf_frame = let l = Frame.get_all_metadata buf_frame in List.iter (fun (_, m) -> self#process_override_metadata m) l; (match (List.rev l, mode) with | (_, m) :: _, `Before -> before_metadata <- Some m | (_, m) :: _, `After -> after_metadata <- Some m | _ -> ()); match mode with | `Before -> Generator.append gen_before buf_frame | `After -> Generator.append gen_after buf_frame method private prepare_before = self#log#info "Buffering end of track..."; let before = new consumer ~clock:source#clock gen_before in Typing.(before#frame_type <: self#frame_type); self#prepare_source before; status <- `Before (before :> Source.source); self#buffer_before ~is_first:true (); if before#is_ready then Some before else None method private get_source ~reselect () = let reselect = match reselect with `Force -> `Ok | _ -> reselect in match status with | `Idle when source#is_ready -> self#prepare_before | `Idle -> None | `Before _ -> ( self#buffer_before ~is_first:false (); match status with | `Idle -> assert false | `Before before_source when self#can_reselect ~reselect before_source -> Some before_source | `Before _ -> None | `After after_source -> Some after_source) | `After after_source when self#can_reselect ~reselect after_source -> Some after_source | `After _ -> self#prepare_before method private buffer_before ~is_first () = if Generator.length gen_before < end_main_duration && source#is_ready then ( let buf_frame = self#child_get ~is_first source in self#append `Before buf_frame; (* Analyze them *) let pcm = AFrame.pcm buf_frame in let len = Audio.length pcm in let squares = Audio.squares pcm 0 len in rms_before <- rms_before -. mem_before.(mem_i) +. squares; mem_before.(mem_i) <- squares; mem_i <- (mem_i + len) mod rms_width; rmsi_before <- min rms_width (rmsi_before + len); (* Should we buffer more or are we done ? *) if Frame.is_partial buf_frame then ( if not persist_override then self#reset_duration; self#analyze_after) else self#buffer_before ~is_first:false ()) method private expected_start_duration = let max_start_main_duration = Option.value ~default:start_main_duration max_start_main_duration in if start_main_duration < Generator.length gen_before then min (Generator.length gen_before) max_start_main_duration else start_main_duration (* Analyze the beginning of a new track. *) method private analyze_after = let rec f ~is_first () = let expected_start_duration = self#expected_start_duration in if Generator.length gen_after < expected_start_duration && source#is_ready then ( let buf_frame = self#child_get ~is_first source in self#append `After buf_frame; if Generator.length gen_after <= rms_width then ( let pcm = AFrame.pcm buf_frame in let len = Audio.length pcm in let squares = Audio.squares pcm 0 len in rms_after <- rms_after +. squares; rmsi_after <- rmsi_after + len); if Frame.is_partial buf_frame then ( if not persist_override then self#reset_duration; self#log#critical "End of track reached while buffering next track data, crossfade \ duration is longer than the track's duration. Make sure to \ adjust the crossfade duration to avoid issues."; self#create_after) else f ~is_first:false ()) else self#create_after in f ~is_first:true () method private append_before_metadata lbl value = before_metadata <- Some (Frame.Metadata.add lbl value (Option.value ~default:Frame.Metadata.empty before_metadata)) method private append_after_metadata lbl value = after_metadata <- Some (Frame.Metadata.add lbl value (Option.value ~default:Frame.Metadata.empty after_metadata)) method private autocue_adjustements ~before_autocue ~after_autocue ~buffered_before ~buffered_after ~buffered () = let before_metadata = Option.value ~default:Frame.Metadata.empty before_metadata in let extra_cross_duration = buffered_before - buffered_after in if after_autocue then ( if before_autocue && 0 < extra_cross_duration then ( let new_cross_duration = buffered_before - extra_cross_duration in Generator.keep gen_before new_cross_duration; let new_cross_duration = Frame.seconds_of_main new_cross_duration in let extra_cross_duration = Frame.seconds_of_main extra_cross_duration in self#log#info "Shortening ending track by %.2f to match the starting track's \ buffer." extra_cross_duration; let fade_out = float_of_string (Frame.Metadata.find "liq_fade_out" before_metadata) in let fade_out = min new_cross_duration fade_out in let fade_out_delay = max (new_cross_duration -. fade_out) 0. in self#append_before_metadata "liq_fade_out" (string_of_float fade_out); self#append_before_metadata "liq_fade_out_delay" (string_of_float fade_out_delay)); (try let cross_duration = Frame.seconds_of_main buffered in let cue_out = float_of_string (Frame.Metadata.find "liq_cue_out" before_metadata) in let start_next = float_of_string (Frame.Metadata.find "liq_cross_start_next" before_metadata) in if cue_out -. start_next < cross_duration then ( self#log#info "Adding fade-in delay to match start next"; self#append_after_metadata "liq_fade_in_delay" (string_of_float (cross_duration -. cue_out +. start_next))) with _ -> ()); let fade_out_delay = try float_of_string (Frame.Metadata.find "liq_fade_out_delay" before_metadata) with _ -> 0. in if 0. < fade_out_delay then ( self#log#info "Adding %.2f fade-in delay to match the ending track's buffer" fade_out_delay; self#append_after_metadata "liq_fade_in_delay" (string_of_float fade_out_delay))) (* Sum up analysis and build the transition *) method private create_after = let before_autocue = self#autocue_enabled `Before in let after_autocue = self#autocue_enabled `After in let buffered_before = Generator.length gen_before in let buffered_after = Generator.length gen_after in let buffered = min buffered_before buffered_after in let db_after = Audio.dB_of_lin (sqrt (rms_after /. float rmsi_after /. float self#audio_channels)) in let db_before = Audio.dB_of_lin (sqrt (rms_before /. float rmsi_before /. float self#audio_channels)) in self#autocue_adjustements ~before_autocue ~after_autocue ~buffered_before ~buffered_after ~buffered (); let compound = let metadata = function None -> Frame.Metadata.empty | Some m -> m in let before_metadata = metadata before_metadata in let after_metadata = metadata after_metadata in let before_head = if (not after_autocue) && buffered < buffered_before then ( let head = Generator.slice gen_before (buffered_before - buffered) in let head_gen = Generator.create ~content:head (Generator.content_type gen_before) in let s = new consumer ~clock:source#clock head_gen in s#set_id (self#id ^ "_before_head"); Typing.(s#frame_type <: self#frame_type); Some s) else None in let before = new Insert_metadata.replay before_metadata (new consumer ~clock:source#clock gen_before) in Typing.(before#frame_type <: self#frame_type); let after_tail = if (not after_autocue) && buffered < buffered_after then ( let head = Generator.slice gen_after buffered in let head_gen = Generator.create ~content:head (Generator.content_type gen_after) in let tail_gen = gen_after in gen_after <- head_gen; let s = new consumer ~clock:source#clock tail_gen in Typing.(s#frame_type <: self#frame_type); s#set_id (self#id ^ "_after_tail"); Some s) else None in let after = new Insert_metadata.replay after_metadata (new consumer ~clock:source#clock gen_after) in Typing.(after#frame_type <: self#frame_type); before#set_id (self#id ^ "_before"); after#set_id (self#id ^ "_after"); self#log#important "Analysis: %fdB / %fdB (%.2fs / %.2fs)" db_before db_after (Frame.seconds_of_main buffered_before) (Frame.seconds_of_main buffered_after); let compound = let params = [ ( "", Lang.meth Lang.unit [ ("source", Lang.source before); ("db_level", Lang.float db_before); ("metadata", Lang.metadata before_metadata); ] ); ( "", Lang.meth Lang.unit [ ("source", Lang.source after); ("db_level", Lang.float db_after); ("metadata", Lang.metadata after_metadata); ] ); ] in Lang.to_source (Lang.apply transition params) in Typing.(compound#frame_type <: self#frame_type); let compound = match (before_head, after_tail) with | None, None -> (compound :> Source.source) | Some s, None -> (new Sequence.sequence ~merge:true [s; compound] :> Source.source) | None, Some s -> (new Sequence.sequence ~single_track:false [compound; s] :> Source.source) | Some _, Some _ -> assert false in Clock.unify ~pos:self#pos compound#clock s#clock; Typing.(compound#frame_type <: self#frame_type); compound in self#prepare_source compound; self#reset_analysis; status <- `After compound method remaining = match status with | `Idle -> source#remaining | `Before s -> ( match (s#remaining, source#remaining) with | -1, _ | _, -1 -> -1 | r, r' -> r + r') | `After s -> s#remaining method seek_source = match status with | `Idle -> source#seek_source | `Before s | `After s -> s#seek_source method abort_track = match status with | `Idle -> source#abort_track | `Before s | `After s -> source#abort_track; status <- `After s; ignore self#prepare_before end let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in let transition_arg = Lang.method_t Lang.unit_t [ ("source", ([], Lang.source_t frame_t), "Source"); ("db_level", ([], Lang.float_t), "dB level of the source."); ("metadata", ([], Lang.metadata_t), "Metadata of the source."); ] in Lang.add_operator "cross" [ ( "start_duration", Lang.nullable_t (Lang.getter_t Lang.float_t), Some Lang.null, Some "Duration (in seconds) of buffered data from the start of each track \ that is used to compute the transition between tracks." ); ( "end_duration", Lang.nullable_t (Lang.getter_t Lang.float_t), Some Lang.null, Some "Duration (in seconds) of buffered data from the end of each track \ that is used to compute the transition between tracks." ); ( "duration", Lang.getter_t Lang.float_t, Some (Lang.float 5.), Some "Duration (in seconds) of buffered data from the end and start of \ each track that is used to compute the transition between tracks." ); ( "assume_autocue", Lang.nullable_t Lang.bool_t, Some Lang.null, Some "Assume that a track has autocue enabled when all four cue in/out \ and fade in/out override metadata are present. Defaults to \ `settings.crossfade.assume_autocue` when `null`." ); ( "override_start_duration", Lang.string_t, Some (Lang.string "liq_cross_start_duration"), Some "Metadata field which, if present and containing a float, overrides \ the 'start_duration' parameter for current track." ); ( "override_max_start_duration", Lang.string_t, Some (Lang.string "liq_cross_max_start_duration"), Some "Metadata field which, if present and containing a float, informs \ the crossfade of the maximum start duration. When not present, it \ is assumed to be `0.`." ); ( "override_end_duration", Lang.string_t, Some (Lang.string "liq_cross_end_duration"), Some "Metadata field which, if present and containing a float, overrides \ the 'end_duration' parameter for current track." ); ( "override_duration", Lang.string_t, Some (Lang.string "liq_cross_duration"), Some "Metadata field which, if present and containing a float, overrides \ the 'duration' parameter for current track." ); ( "persist_override", Lang.bool_t, Some (Lang.bool false), Some "Keep duration override on track change." ); ( "width", Lang.float_t, Some (Lang.float 2.), Some "Width of the power computation window." ); ( "", Lang.fun_t [(false, "", transition_arg); (false, "", transition_arg)] (Lang.source_t frame_t), None, Some "Transition function, composing from the end of a track and the next \ track. The sources corresponding to the two tracks are decorated \ with fields indicating the power of the signal before and after the \ transition (`power`), and the metadata (`metadata`)." ); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Fade ~meth: [ ( "start_duration", Lang.([], fun_t [] float_t), "Get the current crossfade start duration.", fun s -> Lang.val_fun [] (fun _ -> Lang.float s#start_duration) ); ( "end_duration", Lang.([], fun_t [] float_t), "Get the current crossfade end duration.", fun s -> Lang.val_fun [] (fun _ -> Lang.float s#end_duration) ); ] ~descr: "Cross operator, allowing the composition of the _n_ last seconds of a \ track with the beginning of the next track, using a transition function \ depending on the relative power of the signal before and after the end \ of track." (fun p -> let assume_autocue = Lang.to_valued_option Lang.to_bool (List.assoc "assume_autocue" p) in let assume_autocue = Option.value ~default:conf_assume_autocue#get assume_autocue in let start_duration_getter = Lang.to_valued_option Lang.to_float_getter (List.assoc "start_duration" p) in let end_duration_getter = Lang.to_valued_option Lang.to_float_getter (List.assoc "end_duration" p) in let duration_getter = Lang.to_float_getter (List.assoc "duration" p) in let start_duration_getter = Option.value ~default:duration_getter start_duration_getter in let end_duration_getter = Option.value ~default:duration_getter end_duration_getter in let override_start_duration = Lang.to_string (List.assoc "override_start_duration" p) in let override_max_start_duration = Lang.to_string (List.assoc "override_max_start_duration" p) in let override_end_duration = Lang.to_string (List.assoc "override_end_duration" p) in let override_duration = Lang.to_string (List.assoc "override_duration" p) in let persist_override = Lang.to_bool (List.assoc "persist_override" p) in let rms_width = Lang.to_float (List.assoc "width" p) in let rms_width = Frame.audio_of_seconds rms_width in let transition = Lang.assoc "" 1 p in let source = Lang.assoc "" 2 p in new cross source transition ~start_duration_getter ~end_duration_getter ~rms_width ~override_start_duration ~override_max_start_duration ~override_end_duration ~override_duration ~persist_override ~assume_autocue) liquidsoap-2.3.2/src/core/operators/defer.ml000066400000000000000000000136371477303350200210620ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) external alloc_managed_int16_ba : int -> (int, Bigarray.int16_signed_elt, Bigarray.c_layout) Bigarray.Array1.t = "liquidsoap_alloc_managed_int16_ba" external cleanup_managed_int16_ba : (int, Bigarray.int16_signed_elt, Bigarray.c_layout) Bigarray.Array1.t -> unit = "liquidsoap_cleanup_managed_int16_ba" (* For this to work, we need to make sure that we are never holding onto any view on the array.. *) let alloc_managed_int16_ba len = let ba = alloc_managed_int16_ba len in Gc.finalise cleanup_managed_int16_ba ba; ba type state = { offset : int; position : int } class defer ~delay ~overhead ~field source = let overhead = match overhead with | None -> Lazy.force Frame.size | Some v -> Frame.main_of_seconds v in let delay = Frame.main_of_seconds delay in object (self) inherit Source.operator ~name:"defer" [source] method fallible = true method remaining = source#remaining method abort_track = source#abort_track method seek_source = source#seek_source method self_sync = source#self_sync val mutable state = { offset = 0; position = 0 } val mutable deferred = true val mutable generator = None val mutable data = None method private data = match data with | Some d -> d | None -> let make_chunk len = Array.init self#audio_channels (fun _ -> alloc_managed_int16_ba (Frame.audio_of_main len)) in let d = [| make_chunk delay; make_chunk overhead |] in data <- Some d; d method private generator = match generator with | Some g -> g | None -> let gen = Generator.create ~max_length:(delay + overhead) self#content_type in generator <- Some gen; gen method private buffer_data = let { offset; position } = state in let data = self#data.(position) in let chunk_len = Content_pcm_base.length data in let data_rem = chunk_len - offset in let tmp_frame = if source#is_ready then source#get_frame else self#empty_frame in let gen_len = Frame.position tmp_frame in let gen = self#generator in let buffered = Generator.length gen + offset in List.iter (fun (pos, m) -> if pos < gen_len then Generator.add_metadata ~pos:(pos + buffered) gen m) (Frame.get_all_metadata tmp_frame); List.iter (fun pos -> if pos < gen_len then Generator.add_track_mark ~pos:(pos + buffered) gen) (Frame.track_marks tmp_frame); let frame_content = Frame.get tmp_frame field in let frame_pcm = Content_pcm_s16.get_data frame_content in let blit_len = min data_rem gen_len in Content_pcm_base.blit frame_pcm 0 data offset blit_len; if offset + blit_len < chunk_len then ( assert (gen_len = blit_len); state <- { offset = offset + blit_len; position }) else ( assert (offset + data_rem = chunk_len); deferred <- false; let position = (position + 1) mod 2 in let offset = gen_len - blit_len in Generator.put gen field (Content_pcm_s16.lift_data data); Content_pcm_base.blit frame_pcm blit_len self#data.(position) 0 offset; state <- { offset; position }) val mutable should_queue = false method private queue_output = Clock.on_tick self#clock (fun () -> if source#is_ready then self#buffer_data; if should_queue then self#queue_output) initializer self#on_wake_up (fun () -> should_queue <- true; self#queue_output); self#on_sleep (fun () -> should_queue <- false) method private can_generate_frame = (not deferred) && Generator.length self#generator > 0 method private generate_frame = Generator.slice self#generator (Lazy.force Frame.size) end let _ = let frame_t = Format_type.audio ~pcm_kind:Content_pcm_s16.kind () in Lang.add_track_operator ~base:Modules.track_audio "defer" [ ("delay", Lang.float_t, None, Some "Duration of the delay, in seconds."); ( "overhead", Lang.(nullable_t float_t), Some Lang.null, Some "Duration of the delay overhead, in seconds. Defaults to frame size." ); ("", frame_t, None, Some "Track to delay."); ] ~category:`Track ~descr: "Defer an audio track by a given amount of time. Track will be available \ when the given `delay` has been fully buffered. Use this operator \ instead of `buffer` when buffering large amount of data as initial \ delay." ~return_t:frame_t (fun p -> let delay = Lang.to_float (List.assoc "delay" p) in let overhead = Lang.to_valued_option Lang.to_float (List.assoc "overhead" p) in let field, s = Lang.to_track (List.assoc "" p) in (field, new defer ~delay ~overhead ~field s)) liquidsoap-2.3.2/src/core/operators/defer_c.c000066400000000000000000000011471477303350200211670ustar00rootroot00000000000000#define CAML_INTERNALS 1 #include #include #include // Single bigarray not registered with the GC. CAMLprim value liquidsoap_alloc_managed_int16_ba(value _len) { CAMLparam0(); intnat out_size = Int_val(_len); void *data = malloc(out_size * caml_ba_element_size[CAML_BA_SINT16]); if (!data) caml_raise_out_of_memory(); CAMLreturn( caml_ba_alloc(CAML_BA_C_LAYOUT | CAML_BA_SINT16, 1, data, &out_size)); } CAMLprim value liquidsoap_cleanup_managed_int16_ba(value _ba) { CAMLparam1(_ba); free(Caml_ba_data_val(_ba)); CAMLreturn(Val_unit); } liquidsoap-2.3.2/src/core/operators/delay.ml000066400000000000000000000063451477303350200210710ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Avoids some source to play a new track before some delay elapses after the end of the previous track played. *) open Source class delay ~initial (source : source) delay = let initial_last_track, time, delay_ok = let module Time = (val Clock.time_implementation () : Liq_time.T) in let initial_last_track = if initial then Time.time () else Time.of_float 0. in let time = Time.time in let delay_ok last_track = Time.(of_float (delay ()) |<=| (time () |-| last_track)) in (initial_last_track, time, delay_ok) in object (self) inherit operator ~name:"delay" [source] val mutable last_track = initial_last_track val mutable first_track = true method fallible = true method remaining = source#remaining method abort_track = last_track <- time (); source#abort_track method seek_source = source#seek_source method self_sync = source#self_sync method private delay_ok = delay_ok last_track method private can_generate_frame = self#delay_ok && source#is_ready method private generate_frame = let frame = source#get_frame in match self#split_frame frame with | buf, Some _ when first_track && Frame.position buf = 0 -> first_track <- false; frame | buf, Some _ -> first_track <- false; last_track <- time (); buf | buf, None -> buf end let _ = let return_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator "delay" [ ( "initial", Lang.bool_t, Some (Lang.bool false), Some "Start in unavailable state, as if a track had just finished." ); ( "", Lang.getter_t Lang.float_t, None, Some "The source won't be ready less than this amount of seconds after \ any end of track" ); ("", Lang.source_t return_t, None, None); ] ~category:`Track ~descr:"Make the source unavailable for a given time between tracks." ~return_t (fun p -> let f n = Lang.assoc "" n p in let d = Lang.to_float_getter (f 1) in let s = Lang.to_source (f 2) in let initial = Lang.to_bool (List.assoc "initial" p) in new delay ~initial s d) liquidsoap-2.3.2/src/core/operators/delay_line.ml000066400000000000000000000061551477303350200220770ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Source class delay (source : source) duration = let length () = Frame.audio_of_seconds (duration ()) in object (self) inherit operator ~name:"amplify" [source] val mutable override = None method fallible = source#fallible method private can_generate_frame = source#is_ready method remaining = source#remaining method abort_track = source#abort_track method seek_source = source#seek_source method self_sync = source#self_sync (** Length of the buffer in samples. *) val mutable buffer_length = 0 (** Ringbuffer. *) val mutable buffer = [||] (** Position in the buffer. *) val mutable pos = 0 (** Make sure that the buffer has required size. *) method prepare n = if buffer_length <> n then ( buffer <- Audio.create self#audio_channels n; buffer_length <- n) initializer self#on_wake_up (fun () -> self#prepare (length ())) method private generate_frame = let buf = Content.Audio.get_data (source#get_mutable_content Frame.Fields.audio) in let position = source#frame_audio_position in let length = length () in self#prepare length; if length > 0 then for i = 0 to position - 1 do for c = 0 to self#audio_channels - 1 do let x = buf.(c).(i) in buf.(c).(i) <- buffer.(c).(pos); buffer.(c).(pos) <- x done; pos <- (pos + 1) mod length done; source#set_frame_data Frame.Fields.audio Content.Audio.lift_data buf end let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator "delay_line" [ ( "", Lang.getter_t Lang.float_t, None, Some "Duration of the delay in seconds." ); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr:"Delay the audio signal by a given amount of time." (fun p -> let duration = Lang.assoc "" 1 p |> Lang.to_float_getter in let s = Lang.assoc "" 2 p |> Lang.to_source in new delay s duration) liquidsoap-2.3.2/src/core/operators/dtmf.ml000066400000000000000000000351561477303350200207270ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Source (* See https://en.wikipedia.org/wiki/Goertzel_algorithm https://web.archive.org/web/20180628024641/http://en.dsplib.org/content/goertzel/goertzel.html https://www.ti.com/lit/pdf/spra096 *) (** DFT bands. *) module Band = struct (** A band. *) type t = { band_k : int; (** band number *) mutable band_x : float; (** intensity of the band *) band_f : float; (** frequency being detected *) band_cos : float; (** precomputed 2cos(2πk/N) *) mutable band_v : float; (** current value *) mutable band_v' : float; (** previous value *) } let frequency b = b.band_f let intensity b = b.band_x let to_string ~size ~samplerate b = let size = float size in Printf.sprintf "band %d at %.02fHz (detecting between %.02fHz and %0.02fHz)" b.band_k b.band_f (float b.band_k /. size *. samplerate) (float (b.band_k + 1) /. size *. samplerate) (** Create the band of given number. Size is the total number of bands and samplerate is the expected samplerate. *) let create ~size ~samplerate k = let size = float size in { band_k = k; band_x = 0.; band_f = float k /. size *. samplerate; band_cos = 2. *. cos (2. *. Float.pi *. float k /. size); band_v = 0.; band_v' = 0.; } (** Create band of given frequency. *) let make ~size ~samplerate f = let b = create ~size ~samplerate (Float.to_int ((f /. samplerate *. float size) +. 0.5)) in { b with band_f = f } (** Feed a band with a sample. *) let feed b x = let v = x +. (b.band_cos *. b.band_v) -. b.band_v' in b.band_v' <- b.band_v; b.band_v <- v (** Update the value of the band. This function should be called every size samples. *) let update ?(debug = false) ~alpha b = (* Square of the value for the DFT band. *) let x = (b.band_v *. b.band_v) +. (b.band_v' *. b.band_v') -. (b.band_cos *. b.band_v *. b.band_v') in let x = sqrt x in b.band_x <- ((1. -. alpha) *. b.band_x) +. (alpha *. x); (* Apparently we need to reset values, otherwise some unexpected bands get high values over time. *) b.band_v <- 0.; b.band_v' <- 0.; if debug then ( let bar x = let len = 20 in let n = Float.to_int (x *. float len /. 20000.) in let n = min len n in String.make n '=' ^ String.make (len - n) ' ' in let bar2 = bar b.band_x in let bar = bar x in Printf.printf "%02d / %.01f :\t%s %s %.01f\t%.01f\n" b.band_k b.band_f bar bar2 x b.band_x) end module Bands = struct type t = Band.t list let to_string ~size ~samplerate (bands : t) = List.map (Band.to_string ~size ~samplerate) bands |> String.concat ", " let make ~size ~samplerate freqs : t = List.map (Band.make ~size ~samplerate) freqs let update ?debug ~alpha (bands : t) = List.iter (fun b -> Band.update ?debug ~alpha b) bands; if debug = Some true then Printf.printf "%!" let feed (bands : t) x = List.iter (fun b -> Band.feed b x) bands let detect (bands : t) threshold = List.filter_map (fun b -> if Band.intensity b > threshold then Some (Band.frequency b) else None) bands end let key = let keys = [ ((697., 1209.), '1'); ((697., 1336.), '2'); ((697., 1477.), '3'); ((697., 1633.), 'A'); ((770., 1209.), '4'); ((770., 1336.), '5'); ((770., 1477.), '6'); ((770., 1633.), 'B'); ((852., 1209.), '7'); ((852., 1336.), '8'); ((852., 1477.), '9'); ((852., 1633.), 'C'); ((941., 1209.), '*'); ((941., 1336.), '0'); ((941., 1477.), '#'); ((941., 1633.), 'D'); ] in fun f -> List.assoc f keys class dtmf ~duration ~bands ~threshold ~smoothing ~debug callback (source : source) = let samplerate = float (Lazy.force Frame.audio_rate) in let nbands = bands in let size = float nbands in object (self) inherit operator ~name:"dtmf" [source] method fallible = source#fallible method remaining = source#remaining method seek_source = source#seek_source method private can_generate_frame = source#is_ready method abort_track = source#abort_track method self_sync = source#self_sync val bands = Bands.make ~size:nbands ~samplerate [697.; 770.; 852.; 941.; 1209.; 1336.; 1477.; 1633.] val mutable n = nbands val mutable state = `None method private generate_frame = let b = Content.Audio.get_data (source#get_mutable_content Frame.Fields.audio) in let position = source#frame_audio_position in let channels = self#audio_channels in let debug = debug () in let duration = duration () in let threshold = threshold () in let alpha = min 1. (size /. (samplerate *. smoothing ())) in for i = 0 to position - 1 do let x = let x = ref 0. in for c = 0 to channels - 1 do x := !x +. b.(c).(i) done; !x /. float channels in Bands.feed bands x; n <- n + 1; if n mod nbands = 0 then ( n <- n - nbands; Bands.update ~debug ~alpha bands; ((* Find relevant bands. *) let found = Bands.detect bands threshold in (* Update the state *) match found with | [f1; f2] -> ( let f = (f1, f2) in let dt = size /. samplerate in match state with | `Detected (f', t) when f' = f -> let t = t +. dt in if t < duration then state <- `Detected (f, t) else ( (try let k = String.make 1 (key f) in (* Printf.printf "Found %s\n%!" k; *) ignore (Lang.apply callback [("", Lang.string k)]) with Not_found -> () (* Printf.printf "Unknown combination (%.01f, %.01f)...\n%!" (fst f) (snd f) *)); state <- `Signaled f) | `Signaled f' when f' = f -> () | _ -> state <- `Detected (f, dt)) | _ -> state <- `None); if debug then ( Printf.printf "\n"; (match state with | `None -> Printf.printf "No key detected.\n" | `Detected (f, t) -> let k = try String.make 1 (key f) with Not_found -> "???" in Printf.printf "Detected key %s for %.03f seconds.\n" k t | `Signaled f -> let k = try String.make 1 (key f) with Not_found -> "???" in Printf.printf "Signaled key %s.\n" k); print_newline ())) done; source#set_frame_data Frame.Fields.audio Content.Audio.lift_data b end let dtmf = Lang.add_module "dtmf" let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:dtmf "detect" [ ( "duration", Lang.getter_t Lang.float_t, Some (Lang.float 0.05), Some "Duration for detecting a tone." ); ( "bands", Lang.int_t, Some (Lang.int 1024), Some "Number of frequency bands." ); ( "threshold", Lang.getter_t Lang.float_t, Some (Lang.float 50.), Some "Threshold for detecting a band." ); ( "smoothing", Lang.getter_t Lang.float_t, Some (Lang.float 0.01), Some "Smoothing time (in seconds) for band indensity (the higher, the \ less sensitive we are to local variations, but the more time we \ take to detect a band)." ); ( "debug", Lang.getter_t Lang.bool_t, Some (Lang.bool false), Some "Show internal values on standard output in order to fine-tune \ parameters: band number, band frequency, detected intensity and \ smoothed intensity." ); ( "", Lang.source_t frame_t, None, Some "Source on which DTMF tones should be detected." ); ( "", Lang.fun_t [(false, "", Lang.string_t)] Lang.unit_t, None, Some "Function called with detected key as argument." ); ] ~return_t:frame_t ~category:`Audio ~descr:"Detect DTMF tones." (fun p -> let duration = List.assoc "duration" p |> Lang.to_float_getter in let bands = List.assoc "bands" p |> Lang.to_int in let threshold = List.assoc "threshold" p |> Lang.to_float_getter in let smoothing = List.assoc "smoothing" p |> Lang.to_float_getter in let debug = List.assoc "debug" p |> Lang.to_bool_getter in let s = Lang.assoc "" 1 p |> Lang.to_source in let callback = Lang.assoc "" 2 p in (new dtmf ~duration ~bands ~threshold ~smoothing ~debug callback s :> Source.source)) class detect ~duration ~bands ~threshold ~smoothing ~debug ~frequencies callback (source : source) = let samplerate = float (Lazy.force Frame.audio_rate) in let nbands = bands in let size = float nbands in object (self) inherit operator ~name:"dtmf.detect" [source] method fallible = source#fallible method remaining = source#remaining method seek_source = source#seek_source method private can_generate_frame = source#is_ready method abort_track = source#abort_track method self_sync = source#self_sync val bands = Bands.make ~size:nbands ~samplerate frequencies val mutable n = nbands val mutable detected = [] val mutable signaled = [] initializer self#log#info "Listening on the following bands: %s" (Bands.to_string ~size:nbands ~samplerate bands) method private generate_frame = let b = Content.Audio.get_data (Frame.get source#get_frame Frame.Fields.audio) in let position = source#frame_audio_position in let channels = self#audio_channels in let debug = debug () in let duration = duration () in let threshold = threshold () in let alpha = min 1. (size /. (samplerate *. smoothing ())) in for i = 0 to position - 1 do let x = let x = ref 0. in for c = 0 to channels - 1 do x := !x +. b.(c).(i) done; !x /. float channels in Bands.feed bands x; n <- n + 1; if n mod nbands = 0 then ( n <- n - nbands; Bands.update ~debug ~alpha bands; let found = Bands.detect bands threshold in (* Forget about non-detected bands. *) detected <- List.filter (fun (f, _) -> List.mem f found) detected; signaled <- List.filter (fun f -> List.mem f found) signaled; (* Consider not already signaled bands. *) let found = List.filter (fun f -> not (List.mem f signaled)) found in let dt = size /. samplerate in List.iter (fun f -> let t = try List.assoc f detected with Not_found -> let t = ref 0. in detected <- (f, t) :: detected; t in t := !t +. dt; if !t >= duration then ( ignore (Lang.apply callback [("", Lang.float f)]); detected <- List.remove_assoc f detected; signaled <- f :: signaled)) found) done; source#set_frame_data Frame.Fields.audio Content.Audio.lift_data b end let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:Audio_gen.sine "detect" [ ( "duration", Lang.getter_t Lang.float_t, Some (Lang.float 0.5), Some "Duration for detecting a tone." ); ( "bands", Lang.int_t, Some (Lang.int 1024), Some "Number of frequency bands." ); ( "threshold", Lang.getter_t Lang.float_t, Some (Lang.float 50.), Some "Threshold for detecting a band." ); ( "smoothing", Lang.getter_t Lang.float_t, Some (Lang.float 0.01), Some "Smoothing time (in seconds) for band indensity (the higher, the \ less sensitive we are to local variations, but the more time we \ take to detect a band)." ); ( "debug", Lang.getter_t Lang.bool_t, Some (Lang.bool false), Some "Show internal values on standard output in order to fine-tune \ parameters: band number, band frequency, detected intensity and \ smoothed intensity." ); ("", Lang.list_t Lang.float_t, None, Some "List of frequencies to detect."); ( "", Lang.source_t frame_t, None, Some "Source on which sines should be detected." ); ( "", Lang.fun_t [(false, "", Lang.float_t)] Lang.unit_t, None, Some "Function called with detected frequency as argument." ); ] ~return_t:frame_t ~category:`Audio ~descr:"Detect sine waves." (fun p -> let duration = List.assoc "duration" p |> Lang.to_float_getter in let bands = List.assoc "bands" p |> Lang.to_int in let threshold = List.assoc "threshold" p |> Lang.to_float_getter in let smoothing = List.assoc "smoothing" p |> Lang.to_float_getter in let debug = List.assoc "debug" p |> Lang.to_bool_getter in let frequencies = Lang.assoc "" 1 p |> Lang.to_list |> List.map Lang.to_float in let s = Lang.assoc "" 2 p |> Lang.to_source in let callback = Lang.assoc "" 3 p in (new detect ~duration ~bands ~threshold ~smoothing ~debug ~frequencies callback s :> Source.source)) liquidsoap-2.3.2/src/core/operators/dyn_op.ml000066400000000000000000000157761477303350200212730ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) class dyn ~init ~track_sensitive ~infallible ~self_sync ~merge next_fn = object (self) inherit Source.source ~name:"source.dynamic" () inherit Source.generate_from_multiple_sources ~merge ~track_sensitive () method fallible = not infallible val mutable activation = [] val current_source : Source.source option Atomic.t = Atomic.make init method current_source = Atomic.get current_source method private no_source = if infallible then Lang.raise_error ~pos:[] ~message: (Printf.sprintf "Infallible source.dynamic %s was not able to prepare a source \ in time! Make sure to either define infallible sources in the \ source's dynamic function or mark the source as fallible.." self#id) "failure"; None method prepare s = Typing.(s#frame_type <: self#frame_type); Clock.unify ~pos:self#pos s#clock self#clock; s#wake_up method private exchange s = match self#current_source with | Some s' when s == s' -> Some s | _ -> self#log#info "Switching to source %s" s#id; self#prepare s; Atomic.set current_source (Some s); if s#is_ready then Some s else self#no_source method private get_next reselect = self#mutexify (fun () -> let s = Lang.apply next_fn [] |> Lang.to_option |> Option.map Lang.to_source in match (s, self#current_source) with | None, Some s when self#can_reselect ~reselect:(match reselect with `Force -> `Ok | v -> v) s -> Some s | Some s, Some s' when s == s' -> if self#can_reselect ~reselect:(match reselect with `Force -> `Ok | v -> v) s then Some s else self#no_source | Some s, _ -> self#exchange s | _ -> self#no_source) () method private get_source ~reselect () = match (self#current_source, reselect) with | None, _ | _, `Force | Some _, `After_position _ -> self#get_next reselect | Some s, _ when self#can_reselect ~reselect s -> Some s | _ -> self#get_next reselect initializer self#on_wake_up (fun () -> Lang.iter_sources (fun s -> Typing.(s#frame_type <: self#frame_type)) next_fn; ignore (self#get_source ~reselect:`Force ())); self#on_sleep (fun () -> match Atomic.exchange current_source None with | Some s -> s#sleep | None -> ()) method remaining = match self#current_source with Some s -> s#remaining | None -> -1 method abort_track = match self#current_source with Some s -> s#abort_track | None -> () method seek_source = match self#current_source with | Some s -> s#seek_source | None -> (self :> Source.source) method self_sync = match self_sync with | Some v -> (`Static, self#source_sync v) | None -> ( `Dynamic, match self#current_source with | Some s -> snd s#self_sync | None -> None ) end let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator ~base:Muxer.source "dynamic" [ ( "init", Lang.nullable_t (Lang.source_t frame_t), Some Lang.null, Some "Initial value for the source" ); ( "track_sensitive", Lang.getter_t Lang.bool_t, Some (Lang.bool false), Some "Whether the source should only be updated on track change." ); ( "infallible", Lang.bool_t, Some (Lang.bool false), Some "Whether the source is infallible or not (be careful when setting \ this, it will not be checked by the typing system)." ); ( "self_sync", Lang.nullable_t Lang.bool_t, Some Lang.null, Some "For the source's `self_sync` property." ); ( "merge", Lang.getter_t Lang.bool_t, Some (Lang.bool false), Some "Set or return `true` to merge subsequent tracks." ); ( "", Lang.fun_t [] (Lang.nullable_t (Lang.source_t frame_t)), None, Some "Function returning the source to be used, `null` means keep current \ source." ); ] ~return_t:frame_t ~descr: "Dynamically change the underlying source: it can either be changed by \ the function given as argument, which returns the source to be played, \ or by calling the `set` method." ~category:`Track ~meth: [ ( "current_source", ([], Lang.fun_t [] (Lang.nullable_t (Lang.source_t frame_t))), "Return the source currently selected.", fun s -> Lang.val_fun [] (fun _ -> match s#current_source with | None -> Lang.null | Some s -> Lang.source s) ); ( "prepare", ([], Lang.fun_t [(false, "", Lang.source_t frame_t)] Lang.unit_t), "Prepare a source that will be returned later.", fun s -> Lang.val_fun [("", "x", None)] (fun p -> s#prepare (List.assoc "x" p |> Lang.to_source); Lang.unit) ); ] (fun p -> let init = List.assoc "init" p |> Lang.to_option |> Option.map Lang.to_source in let track_sensitive = List.assoc "track_sensitive" p |> Lang.to_getter in let track_sensitive () = Lang.to_bool (track_sensitive ()) in let infallible = List.assoc "infallible" p |> Lang.to_bool in let merge = Lang.to_getter (List.assoc "merge" p) in let merge () = Lang.to_bool (merge ()) in let self_sync = Lang.to_valued_option Lang.to_bool (List.assoc "self_sync" p) in let next = List.assoc "" p in new dyn ~init ~track_sensitive ~infallible ~merge ~self_sync next) liquidsoap-2.3.2/src/core/operators/echo.ml000066400000000000000000000064711477303350200207110ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Source class echo (source : source) delay feedback ping_pong = object (self) inherit operator ~name:"echo" [source] method fallible = source#fallible method remaining = source#remaining method seek_source = source#seek_source method self_sync = source#self_sync method private can_generate_frame = source#is_ready method abort_track = source#abort_track val mutable effect_ = None initializer self#on_wake_up (fun () -> effect_ <- Some (Audio.Effect.delay self#audio_channels (Lazy.force Frame.audio_rate) ~ping_pong (delay ()) (feedback ()))) val mutable past_pos = 0 method private generate_frame = let b = Content.Audio.get_data (source#get_mutable_content Frame.Fields.audio) in let position = source#frame_audio_position in let effect_ = Option.get effect_ in effect_#set_delay (delay ()); effect_#set_feedback (feedback ()); effect_#process b 0 position; source#set_frame_data Frame.Fields.audio Content.Audio.lift_data b end let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator "echo" [ ( "delay", Lang.getter_t Lang.float_t, Some (Lang.float 0.5), Some "Delay in seconds." ); ( "feedback", Lang.getter_t Lang.float_t, Some (Lang.float (-6.)), Some "Feedback coefficient in dB (negative)." ); ( "ping_pong", Lang.bool_t, Some (Lang.bool false), Some "Use ping-pong delay." ); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr:"Add echo." (fun p -> let f v = List.assoc v p in let duration, feedback, pp, src = ( Lang.to_float_getter (f "delay"), Lang.to_float_getter (f "feedback"), Lang.to_bool (f "ping_pong"), Lang.to_source (f "") ) in let feedback = (* Check the initial value, wrap the getter with a converter. *) if feedback () > 0. then raise (Error.Invalid_value (f "feedback", "feedback should be negative")); fun () -> Audio.lin_of_dB (feedback ()) in new echo src duration feedback pp) liquidsoap-2.3.2/src/core/operators/filter.ml000066400000000000000000000120621477303350200212510ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Source type mode = Low_pass | High_pass | Band_pass | Notch class filter (source : source) freq q wet mode = let rate = float (Lazy.force Frame.audio_rate) in object (self) inherit operator ~name:"filter" [source] method fallible = source#fallible method remaining = source#remaining method seek_source = source#seek_source method private can_generate_frame = source#is_ready method abort_track = source#abort_track method self_sync = source#self_sync val mutable low = [||] val mutable high = [||] val mutable band = [||] val mutable notch = [||] initializer self#on_wake_up (fun () -> let channels = self#audio_channels in low <- Array.make channels 0.; high <- Array.make channels 0.; band <- Array.make channels 0.; notch <- Array.make channels 0.) (* State vartiable filter, see http://www.musicdsp.org/archive.php?classid=3#23 TODO: the problem with this filter is that it only handles freq <= rate/4, we have to find something better. See http://www.musicdsp.org/showArchiveComment.php?ArchiveID=23 Maybe should we implement Chamberlin's version instead, which handles freq <= rate/2. See http://www.musicdsp.org/archive.php?classid=3#142 *) method private generate_frame = let c = source#get_mutable_content Frame.Fields.audio in let b = Content.Audio.get_data c in let position = source#frame_audio_position in let freq = freq () in let q = q () in let wet = wet () in let f = 2. *. sin (Float.pi *. freq /. rate) in for c = 0 to Array.length b - 1 do let b_c = b.(c) in for i = 0 to position - 1 do low.(c) <- low.(c) +. (f *. band.(c)); high.(c) <- (q *. b_c.(i)) -. low.(c) -. (q *. band.(c)); band.(c) <- (f *. high.(c)) +. band.(c); notch.(c) <- high.(c) +. low.(c); b_c.(i) <- (wet *. match mode with | Low_pass -> low.(c) | High_pass -> high.(c) | Band_pass -> band.(c) | Notch -> notch.(c)) +. ((1. -. wet) *. b_c.(i)) done done; source#set_frame_data Frame.Fields.audio Content.Audio.lift_data b end let filter = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator "filter" [ ( "freq", Lang.getter_t Lang.float_t, None, Some "Characteristic frequency of the filter." ); ("q", Lang.getter_t Lang.float_t, Some (Lang.float 1.), None); ( "mode", Lang.string_t, None, Some "Available modes are 'low' (for low-pass filter), 'high' (for \ high-pass filter), 'band' (for band-pass filter) and 'notch' (for \ notch / band-stop / band-rejection filter)." ); ( "wetness", Lang.getter_t Lang.float_t, Some (Lang.float 1.), Some "How much of the original signal should be added (1. means only \ filtered and 0. means only original signal)." ); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr: "Perform several kinds of filtering on the signal. Only frequencies \ below the sampling rate / 4 (generally 10 kHz) are handled well for the \ `freq` parameter." (fun p -> let f v = List.assoc v p in let freq, q, wet, mode, src = ( Lang.to_float_getter (f "freq"), Lang.to_float_getter (f "q"), Lang.to_float_getter (f "wetness"), f "mode", Lang.to_source (f "") ) in let mode = match Lang.to_string mode with | "low" -> Low_pass | "high" -> High_pass | "band" -> Band_pass | "notch" -> Notch | _ -> raise (Error.Invalid_value (mode, "valid values are low|high|band|notch")) in (new filter src freq q wet mode :> Source.source)) liquidsoap-2.3.2/src/core/operators/filter_rc.ml000066400000000000000000000104071477303350200217360ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Source type mode = Low_pass | High_pass class filter (source : source) freq wet mode = let rate = float (Lazy.force Frame.audio_rate) in let dt = 1. /. rate in object (self) inherit operator ~name:"filter.rc" [source] method fallible = source#fallible method remaining = source#remaining method seek_source = source#seek_source method self_sync = source#self_sync method private can_generate_frame = source#is_ready method abort_track = source#abort_track val mutable prev = [||] val mutable prev_in = [||] initializer self#on_wake_up (fun () -> prev <- Array.make self#audio_channels 0.; prev_in <- Array.make self#audio_channels 0.) method private generate_frame = let b = Content.Audio.get_data (source#get_mutable_content Frame.Fields.audio) in let position = source#frame_audio_position in let rc = 1. /. freq () in let alpha = match mode with | Low_pass -> dt /. (rc +. dt) | High_pass -> rc /. (rc +. dt) in let alpha' = 1. -. alpha in let wet = wet () in let wet' = 1. -. wet in (match mode with | Low_pass -> let alpha = dt /. (rc +. dt) in for c = 0 to Array.length b - 1 do let b_c = b.(c) in for i = 0 to position - 1 do prev.(c) <- (alpha *. b_c.(i)) +. (alpha' *. prev.(c)); b_c.(i) <- (wet *. prev.(c)) +. (wet' *. b_c.(i)) done done | High_pass -> let alpha = dt /. (rc +. dt) in for c = 0 to Array.length b - 1 do let b_c = b.(c) in for i = 0 to position - 1 do prev.(c) <- alpha *. (prev.(c) +. b_c.(i) -. prev_in.(c)); prev_in.(c) <- b_c.(i); b_c.(i) <- (wet *. prev.(c)) +. (wet' *. b_c.(i)) done done); source#set_frame_data Frame.Fields.audio Content.Audio.lift_data b end let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:Filter.filter "rc" [ ("frequency", Lang.getter_t Lang.float_t, None, Some "Cutoff frequency."); ( "mode", Lang.string_t, None, Some "Available modes are 'low' (for low-pass filter), 'high' (for \ high-pass filter)." ); ( "wetness", Lang.getter_t Lang.float_t, Some (Lang.float 1.), Some "How much of the original signal should be added (1. means only \ filtered and 0. means only original signal)." ); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr:"First-order filter (RC filter)." (fun p -> let f v = List.assoc v p in let freq, wet, mode, src = ( Lang.to_float_getter (f "frequency"), Lang.to_float_getter (f "wetness"), f "mode", Lang.to_source (f "") ) in let mode = match Lang.to_string mode with | "low" -> Low_pass | "high" -> High_pass | _ -> raise (Error.Invalid_value (mode, "valid values are low|high")) in (new filter src freq wet mode :> Source.source)) liquidsoap-2.3.2/src/core/operators/fir_filter.ml000066400000000000000000000141011477303350200221050ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Source open Complex class fir (source : source) freq beta numcoeffs = object (self) inherit operator ~name:"fir_filter" [source] (* Needed to compute RC *) val f1 = (1. -. beta) *. (freq /. float_of_int (Frame.audio_of_seconds 1.)) val f2 = (1. +. beta) *. (freq /. float_of_int (Frame.audio_of_seconds 1.)) val tau = 0.5 /. (freq /. float_of_int (Frame.audio_of_seconds 1.)) (* Misc *) val mutable nzeros = numcoeffs - 1 val mutable gain = 0. val mutable xv = [||] initializer self#on_wake_up (fun () -> xv <- Array.make_matrix self#audio_channels numcoeffs 0.) (* Coefficients *) val mutable xcoeffs = Array.make numcoeffs 0. val mutable circle = [||] val mutable temp = Array.make 2048 { re = 0.; im = 0. } initializer self#log#info "Init: alpha=%+.013f beta=%+.013f F1=%+.013f F2=%+.013f tau=%+.013f \ zeros=%d." (freq /. float_of_int (Frame.audio_of_seconds 1.)) beta f1 f2 tau nzeros; (* Init circle *) let circle = let rec mkcircle n = if n < 0 then [||] else ( let theta = Float.pi *. float_of_int n /. 1024. in Array.append (mkcircle (n - 1)) [| { re = cos theta; im = sin theta } |]) in mkcircle 1024 in (* Compute vec *) let vec = Array.make 2048 { re = 0.; im = 0. } in let c n = let f = float_of_int n /. 2048. in match (f <= f1, f <= f2) with | true, _ -> 1. | false, true -> 0.5 *. (1. +. cos (Float.pi *. tau /. beta *. (f -. f1))) | false, false -> 0. in for i = 0 to 1024 do vec.(i) <- { re = c i *. tau; im = 0. } done; for i = 1 to 1024 do vec.(2048 - i) <- vec.(i) done; (* FFT *) let ( +~ ), ( -~ ), ( *~ ) = (Complex.add, Complex.sub, Complex.mul) in let rec fft t d s n = if n > 1 then ( let h = n / 2 in for i = 0 to h - 1 do !t.(s + i) <- !d.(s + (2 * i)); (* even *) !t.(s + h + i) <- !d.(s + (2 * i) + 1) (* odd *) done; fft d t s h; fft d t (s + h) h; let a = 2048 / n in for i = 0 to h - 1 do let wkt = circle.(i * a) *~ !t.(s + h + i) in !d.(s + i) <- !t.(s + i) +~ wkt; !d.(s + h + i) <- !t.(s + i) -~ wkt done) in fft (ref temp) (ref vec) 0 2048; (* inverse fft *) let h = (numcoeffs - 1) / 2 in xcoeffs <- Array.mapi (fun i _ -> vec.((2048 - h + i) mod 2048).re /. 2048.) xcoeffs; self#log#info "Xcoeffs: %s" (String.concat "\n" (Array.to_list (Array.mapi (fun i a -> Printf.sprintf "%d: %+.013f." i a) xcoeffs))); gain <- Array.fold_left ( +. ) 0. xcoeffs; self#log#info "Gain: %+.013f." gain; self#log#info "Init done." (* Digital filter based on mkfilter/mkshape/gencode by A.J. Fisher *) method fallible = source#fallible method remaining = source#remaining method private can_generate_frame = source#is_ready method seek_source = source#seek_source method abort_track = source#abort_track method self_sync = source#self_sync method private generate_frame = let b = Content.Audio.get_data (source#get_mutable_content Frame.Fields.audio) in let shift a = for i = 0 to Array.length a - 2 do a.(i) <- a.(i + 1) done in let fold_left2 f init a1 a2 = let l = min (Array.length a1) (Array.length a2) in let result = ref init in for i = 0 to l - 1 do result := f !result a1.(i) a2.(i) done; !result in let addtimes a b c = a +. (b *. c) in for c = 0 to 1 do for i = 0 to source#frame_audio_position - 1 do shift xv.(c); xv.(c).(nzeros) <- b.(c).(i) /. gain; b.(c).(i) <- fold_left2 addtimes 0. xcoeffs xv.(c) done done; source#set_frame_data Frame.Fields.audio Content.Audio.lift_data b end let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:Filter.filter "fir" [ ( "frequency", Lang.float_t, None, Some "Corner frequency in Hz (frequency at which the response is 0.5, \ that is -6 dB)." ); ("beta", Lang.float_t, None, Some "Beta should range between 0 and 1."); ("coeffs", Lang.int_t, Some (Lang.int 255), Some "Number of coefficients"); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr:"Low-pass FIR filter." (fun p -> let f v = List.assoc v p in let freq, beta, num, src = ( Lang.to_float (f "frequency"), Lang.to_float (f "beta"), Lang.to_int (f "coeffs"), Lang.to_source (f "") ) in (new fir src freq beta num :> Source.source)) liquidsoap-2.3.2/src/core/operators/flanger.ml000066400000000000000000000075171477303350200214130ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Source let pi = acos (-1.) class flanger (source : source) delay freq feedback phase = let past_len = Frame.audio_of_seconds delay in object (self) inherit operator ~name:"flanger" [source] method fallible = source#fallible method remaining = source#remaining method seek_source = source#seek_source method self_sync = source#self_sync method private can_generate_frame = source#is_ready method abort_track = source#abort_track val mutable past = Audio.make 0 0 0. initializer self#on_wake_up (fun () -> past <- Audio.make self#audio_channels past_len 0.) val mutable past_pos = 0 val mutable omega = 0. method private generate_frame = let feedback = feedback () in let b = Content.Audio.get_data (source#get_mutable_content Frame.Fields.audio) in let position = source#frame_audio_position in let d_omega = 2. *. pi *. freq () /. float (Frame.audio_of_seconds 1.) in for i = 0 to position - 1 do for c = 0 to Array.length b - 1 do let delay = (past_pos + past_len + Frame.audio_of_seconds (delay *. (1. -. cos (omega +. (float c *. phase ()))) /. 2.)) mod past_len in past.(c).(past_pos) <- b.(c).(i); b.(c).(i) <- (b.(c).(i) +. (past.(c).(delay) *. feedback)) /. (1. +. feedback) done; omega <- omega +. d_omega; while omega > 2. *. pi do omega <- omega -. (2. *. pi) done; past_pos <- (past_pos + 1) mod past_len done; source#set_frame_data Frame.Fields.audio Content.Audio.lift_data b end let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator "flanger" [ ("delay", Lang.float_t, Some (Lang.float 0.001), Some "Delay in seconds."); ( "freq", Lang.getter_t Lang.float_t, Some (Lang.float 0.5), Some "Frequency in Hz." ); ( "feedback", Lang.getter_t Lang.float_t, Some (Lang.float 0.), Some "Feedback coefficient in dB." ); ( "phase", Lang.getter_t Lang.float_t, Some (Lang.float 1.), Some "Phase difference between channels in radians." ); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr:"Flanger effect." (fun p -> let f v = List.assoc v p in let duration, freq, feedback, phase, src = ( Lang.to_float (f "delay"), Lang.to_float_getter (f "freq"), Lang.to_float_getter (f "feedback"), Lang.to_float_getter (f "phase"), Lang.to_source (f "") ) in let feedback () = Audio.lin_of_dB (feedback ()) in (new flanger src duration freq feedback phase :> Source.source)) liquidsoap-2.3.2/src/core/operators/frei0r_op.ml000066400000000000000000000320121477303350200216460ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Source open Extralib let video_frei0r = Lang.add_module ~base:Modules.video "frei0r" type t = Float | Int | Bool let log = Log.make ["frei0r"] let frei0r_enable = try let venv = Unix.getenv "LIQ_FREI0R" in venv = "1" || venv = "true" with Not_found -> true let plugin_dirs = try let path = Unix.getenv "LIQ_FREI0R_PATH" in String.split_on_char ':' path with Not_found -> Frei0r.default_paths class frei0r_filter ~name bgra instance params (source : source) = let fps = Lazy.force Frame.video_rate in let dt = 1. /. float fps in object (self) inherit operator ~name:("frei0r." ^ name) [source] method fallible = source#fallible method remaining = source#remaining method seek_source = source#seek_source method private can_generate_frame = source#is_ready method self_sync = source#self_sync method abort_track = source#abort_track val mutable t = 0. method private render img = let img = Video.Canvas.Image.render img in let img = Image.YUV420.to_RGBA32 img in if bgra then Image.RGBA32.swap_rb img; let src = Image.RGBA32.data (Image.RGBA32.copy img) in let dst = Image.RGBA32.data img in Frei0r.update1 instance t src dst; if bgra then Image.RGBA32.swap_rb img; t <- t +. dt; Video.Canvas.Image.make (Image.YUV420.of_RGBA32 img) method private generate_frame = let buf = Content.Video.get_data (source#get_mutable_content Frame.Fields.video) in params (); source#set_frame_data Frame.Fields.video Content.Video.lift_data { buf with Content.Video.data = List.map (fun (pos, img) -> (pos, self#render img)) buf.Content.Video.data; } end class frei0r_mixer ~name bgra instance params (source : source) source2 = let fps = Lazy.force Frame.video_rate in let dt = 1. /. float fps in let self_sync = Clock_base.self_sync [source; source2] in object (self) inherit operator ~name:("frei0r." ^ name) [source; source2] method seek_source = (self :> Source.source) method fallible = source#fallible && source2#fallible method remaining = match (source#remaining, source2#remaining) with | -1, x | x, -1 -> x | x, y -> min x y method private can_generate_frame = source#is_ready && source2#is_ready method self_sync = self_sync ~source:self () method abort_track = source#abort_track; source2#abort_track val mutable t = 0. method private generate_frame = let length = min source#frame_position source2#frame_position in let c = Frame.get (source#get_partial_frame (fun f -> Frame.slice f length)) Frame.Fields.video in let c' = Frame.get (source2#get_partial_frame (fun f -> Frame.slice f length)) Frame.Fields.video in let rgb = Content.Video.get_data c in let rgb = self#generate_video ~field:Frame.Fields.video ~create:(fun ~pos ~width:_ ~height:_ () -> self#nearest_image ~pos ~last_image:(source#last_image Frame.Fields.video) rgb) length in let rgb' = Content.Video.get_data c' in params (); (* Mix content where the two streams are available. * We could cut one stream when the other is too short, * and/or attempt to get some more data in the buffers... * each solution has its downsides and it'll rarely matter * because there's usually only one image per video frame. *) let data = List.map (fun (pos, img) -> let img = Video.Canvas.Image.render img in let img = Image.YUV420.to_RGBA32 img in let img' = self#nearest_image ~pos ~last_image:(source2#last_image Frame.Fields.video) rgb' in let img' = Video.Canvas.Image.render img' in let img' = Image.YUV420.to_RGBA32 img' in if bgra then Image.RGBA32.swap_rb img; if bgra then Image.RGBA32.swap_rb img'; let src = Image.RGBA32.data (Image.RGBA32.copy img) in let src' = Image.RGBA32.data img' in let dst = Image.RGBA32.data img in Frei0r.update2 instance t src src' dst; if bgra then Image.RGBA32.swap_rb img; let img = Image.YUV420.of_RGBA32 img in t <- t +. dt; (pos, Video.Canvas.Image.make img)) rgb.Content.Video.data in source#set_frame_data Frame.Fields.video Content.Video.lift_data { rgb with Content.Video.data } end class frei0r_source ~name bgra instance params = let fps = Lazy.force Frame.video_rate in let dt = 1. /. float fps in object (self) inherit source ~name:("frei0r." ^ name) () method seek_source = (self :> Source.source) method fallible = false method private can_generate_frame = true method self_sync = (`Static, None) val mutable must_fail = false method abort_track = must_fail <- true method remaining = if must_fail then 0 else -1 val mutable t = 0. method private render_image img = let img = Video.Canvas.Image.render img in let img = Image.YUV420.to_RGBA32 img in let dst = Image.RGBA32.data img in Frei0r.update0 instance t dst; if bgra then Image.RGBA32.swap_rb img; let img = Image.YUV420.of_RGBA32 img in t <- t +. dt; Video.Canvas.Image.make img method private generate_frame = if must_fail then ( must_fail <- false; self#end_of_track) else ( params (); let length = Lazy.force Frame.size in let buf = Frame.create ~length self#content_type in let rgb = self#generate_video ~field:Frame.Fields.video length in let data = List.map (fun (pos, img) -> (pos, self#render_image img)) rgb.Content.Video.data in Frame.set_data buf Frame.Fields.video Content.Video.lift_data { rgb with Content.Video.data }) end (** Make a list of parameters. *) let params plugin info = let liq_params = List.init info.Frei0r.num_params (fun i -> try let info = Frei0r.param_info plugin i in let name = Utils.normalize_parameter_string info.Frei0r.param_name in let t = match info.Frei0r.param_type with | Frei0r.Bool -> Lang.bool_t | Frei0r.Double -> Lang.getter_t Lang.float_t | Frei0r.Color -> Lang.int_t | Frei0r.Position -> Lang.product_t Lang.float_t Lang.float_t | Frei0r.String -> Lang.string_t in Some ( name, Lang.nullable_t t, Some Lang.null, Some (info.Frei0r.param_explanation ^ ".") ) with Exit -> None) in let liq_params = List.filter_map id liq_params in (* Initialize parameters and produce function to update float getters. *) let params instance p = let on_changed x0 = let x0 = ref x0 in fun f x -> if x <> !x0 then f x; x0 := x in let f v = List.assoc v p in let act = List.init info.Frei0r.num_params (fun i -> try let info = Frei0r.param_info plugin i in let name = Utils.normalize_parameter_string info.Frei0r.param_name in let v = Lang.to_option (f name) in match (v, info.Frei0r.param_type) with | None, _ -> None | Some v, Frei0r.Bool -> Frei0r.set_param_bool instance i (Lang.to_bool v); None | Some v, Frei0r.Double -> let x = Lang.to_float_getter v in let x0 = x () in let f x = Frei0r.set_param_float instance i x in let oc = on_changed x0 in f x0; Some (fun () -> oc f (x ())) | Some v, Frei0r.Color -> let c = Lang.to_int v in let r = (c lsr 16) land 0xff in let g = (c lsr 8) land 0xff in let b = c land 0xff in let r = float r /. 255. in let g = float g /. 255. in let b = float b /. 255. in Frei0r.set_param_color instance i (r, g, b); None | Some v, Frei0r.Position -> let x, y = Lang.to_product v in let x = Lang.to_float x in let y = Lang.to_float y in Frei0r.set_param_position instance i (x, y); None | Some v, Frei0r.String -> Frei0r.set_param_string instance i (Lang.to_string v); None with Not_found -> None) in let act = List.filter_map id act in fun () -> List.iter (fun f -> f ()) act in (liq_params, params) exception Unhandled_number_of_inputs exception Blacklisted let register_plugin fname = let plugin = Frei0r.load fname in let info = Frei0r.info plugin in let name = Utils.normalize_parameter_string info.Frei0r.name in if List.mem name ["curves"; (* Bad characters in doc. *) "keyspillm0pup" (* idem *)] then raise Blacklisted; let bgra = info.Frei0r.color_model = Frei0r.BGRA8888 in let inputs, _ = match info.Frei0r.plugin_type with | Frei0r.Filter -> (1, 1) | Frei0r.Source -> (0, 1) | Frei0r.Mixer2 -> (2, 1) | Frei0r.Mixer3 -> (3, 1) in if inputs > 2 then raise Unhandled_number_of_inputs; let return_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~video:(Format_type.video ()) ()) in let liq_params, params = params plugin info in let liq_params = let inputs = List.init inputs (fun _ -> ("", Lang.source_t return_t, None, None)) in liq_params @ inputs in let explanation = let e = info.Frei0r.explanation in let e = String.capitalize_ascii e in let e = String.concat "(at)" (String.split_on_char '@' e) in if e = "" then e else if e.[String.length e - 1] = '.' then String.sub e 0 (String.length e - 1) else e in let author = let a = info.Frei0r.author in String.concat "(at)" (String.split_on_char '@' a) in let descr = Printf.sprintf "%s (by %s)." explanation author in ignore (Lang.add_operator ~base:video_frei0r name liq_params ~return_t ~category:`Video ~flags:[`Extra] ~descr (fun p -> let instance = let width = Lazy.force Frame.video_width in let height = Lazy.force Frame.video_height in Frei0r.create plugin width height in let f v = List.assoc v p in let params = params instance p in if inputs = 1 then ( let source = Lang.to_source (f "") in new frei0r_filter ~name bgra instance params source) else if inputs = 2 then ( let source = Lang.to_source (f "") in let source' = Lang.to_source (Lang.assoc "" 2 p) in new frei0r_mixer ~name bgra instance params source source') else if inputs = 0 then new frei0r_source ~name bgra instance params else assert false)) let register_plugin plugin = try register_plugin plugin with | Unhandled_number_of_inputs -> () | Blacklisted -> () | e -> Printf.eprintf "Failed to register plugin %s: %s\n%!" plugin (Printexc.to_string e) let register_plugins () = let add plugins_dir = try let dir = Unix.opendir plugins_dir in try while true do let f = Unix.readdir dir in if f <> "." && f <> ".." then register_plugin (plugins_dir ^ "/" ^ f) done with End_of_file -> Unix.closedir dir with Unix.Unix_error (e, _, _) -> log#info "Error while loading directory %s: %s" plugins_dir (Unix.error_message e) in List.iter add plugin_dirs let () = Lifecycle.on_load ~name:"frei0r plugin registration" (fun () -> if frei0r_enable then Startup.time "Frei0r plugin registration" register_plugins) liquidsoap-2.3.2/src/core/operators/gate.ml000066400000000000000000000134771477303350200207170ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Source class gate ~threshold ~attack ~release ~hold ~range ~window (source : source) = object (self) inherit operator ~name:"gate" [source] method fallible = source#fallible method remaining = source#remaining method seek_source = source#seek_source method private can_generate_frame = source#is_ready method abort_track = source#abort_track method self_sync = source#self_sync (* Position of the gate between 0. and 1. *) val mutable gate = 1. method gate = gate (* Smoothed peak. *) val mutable peak = 0. (* Current state. *) val mutable state = `Open (* Time remaining before closing. *) val mutable hold_delay = int_of_float (hold () *. float (Lazy.force Frame.audio_rate)) method private generate_frame = let buf = Content.Audio.get_data (source#get_mutable_content Frame.Fields.audio) in let position = self#frame_audio_position in let chans = self#audio_channels in let samplerate = float (Lazy.force Frame.audio_rate) in let attack = attack () in let attack_rate = 1. /. (attack *. samplerate) in let release = release () in let release_rate = 1. /. (release *. samplerate) in let threshold = threshold () in let threshold_lin = Audio.lin_of_dB threshold in let window_coef = 1. -. exp (-1. /. (window () *. samplerate)) in let range = range () in let hold = int_of_float (hold () *. samplerate) in for i = 0 to position - 1 do let x = let x = ref 0. in for c = 0 to chans - 1 do x := max !x (Utils.abs_float buf.(c).(i)) done; peak <- peak +. (window_coef *. (!x -. peak)); peak in (match state with | `Closed -> if x > threshold_lin then state <- `Opening | `Opening -> gate <- gate +. attack_rate; if gate >= 1. then ( gate <- 1.; hold_delay <- hold; state <- `Open) | `Open -> if x < threshold_lin then if hold_delay <= 0 then state <- `Closing else hold_delay <- hold_delay - 1 else hold_delay <- hold | `Closing -> gate <- gate -. release_rate; if x >= threshold_lin then state <- `Opening else if gate <= 0. then ( gate <- 0.; state <- `Closed)); let gain = Audio.lin_of_dB (range *. (1. -. gate)) in for c = 0 to chans - 1 do buf.(c).(i) <- buf.(c).(i) *. gain done done; source#set_frame_data Frame.Fields.audio Content.Audio.lift_data buf end let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator "gate" [ ( "attack", Lang.getter_t Lang.float_t, Some (Lang.float 10.), Some "Time to fully open the gate (ms)." ); ( "release", Lang.getter_t Lang.float_t, Some (Lang.float 2000.), Some "Time to fully close the gate (ms)." ); ( "threshold", Lang.getter_t Lang.float_t, Some (Lang.float (-30.)), Some "Threshold at which the gate will open (dB)." ); ( "hold", Lang.getter_t Lang.float_t, Some (Lang.float 1000.), Some "Minimum amount of time the gate stays open (ms)." ); ( "range", Lang.getter_t Lang.float_t, Some (Lang.float (-30.)), Some "Difference between closed and open level (dB)." ); ( "window", Lang.getter_t Lang.float_t, Some (Lang.float 1.), Some "Duration for computing peak (ms)." ); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr: "Reduce the volume when the stream is silent (typically in order to \ avoid low intensity noise)." ~meth: [ ( "gate", ([], Lang.fun_t [] Lang.float_t), "Position of the gate (0. means closed, 1. means open).", fun s -> Lang.val_fun [] (fun _ -> Lang.float s#gate) ); ] (fun p -> let threshold = List.assoc "threshold" p |> Lang.to_float_getter in let attack = List.assoc "attack" p |> Lang.to_float_getter in let attack () = attack () /. 1000. in let release = List.assoc "release" p |> Lang.to_float_getter in let release () = release () /. 1000. in let hold = List.assoc "hold" p |> Lang.to_float_getter in let hold () = hold () /. 1000. in let range = List.assoc "range" p |> Lang.to_float_getter in let window = List.assoc "window" p |> Lang.to_float_getter in let window () = window () /. 1000. in let src = List.assoc "" p |> Lang.to_source in new gate ~threshold ~attack ~release ~hold ~range ~window src) liquidsoap-2.3.2/src/core/operators/iir_filter.ml000066400000000000000000000565511477303350200221270ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Source open Complex type filter_type = Band_stop | Band_pass | High_pass | Low_pass | All_pass type filter_family = Butterworth | Resonator class iir (source : source) filter_family filter_type order freq1 freq2 qfactor = let rate = float (Frame.audio_of_seconds 1.) in object (self) inherit operator ~name:"iir_filter" [source] (* Params *) val raw_alpha1 = freq1 /. rate val raw_alpha2 = freq2 /. rate val warped_alpha1 = tan (Float.pi *. freq1 /. rate) /. Float.pi val warped_alpha2 = tan (Float.pi *. freq2 /. rate) /. Float.pi val mutable gain = 0. (* Used for computation *) val mutable polemask = 0 val mutable splane_poles = [||] val mutable splane_numpoles = 0 val mutable splane_zeros = [||] val mutable splane_numzeros = 0 val mutable zplane_poles = [||] val mutable zplane_numpoles = 0 val mutable zplane_zeros = [||] val mutable zplane_numzeros = 0 val mutable topcoeffs = [||] val mutable botcoeffs = [||] val mutable dc_gain = { re = 0.; im = 0. } val mutable fc_gain = { re = 0.; im = 0. } val mutable hf_gain = { re = 0.; im = 0. } (* Coefficients *) val mutable xcoeffs = [||] val mutable ycoeffs = [||] (* I/O shift registries *) val mutable xv = [||] val mutable yv = [||] initializer self#on_wake_up (fun () -> self#initialize) method private initialize = self#log#info "Initializing..."; self#log#info "Alpha 1: %.013f (warped: %.013f)" raw_alpha1 warped_alpha1; self#log#info "Alpha 2: %.013f (warped: %.013f)" raw_alpha2 warped_alpha2; self#log#info "Q: %.013f" qfactor; let channels = self#audio_channels in let cor a = { re = a; im = 0. } and ( +~ ), ( -~ ), ( *~ ), ( /~ ) = (Complex.add, Complex.sub, Complex.mul, Complex.div) in let multin w n c = (* multiply factor (z-w) into coeffs *) let w = cor 0. -~ w in for i = 0 to n - 1 do c.(n - i) <- (w *~ c.(n - i)) +~ c.(n - i - 1) done; c.(0) <- w *~ c.(0) in let expand z n = let c = Array.append [| { re = 1.; im = 0. } |] (Array.make n { re = 0.; im = 0. }) in for i = 0 to n - 1 do multin z.(i) n c done; (* check that computed coeffs of z^k are all real *) for i = 0 to n do if Utils.abs_float c.(i).im > 1e-10 then (* coeff of z^i is not real; poles/zeros are not complex conj. *) assert false done; c in let eval c z = (* evaluate polynomial in z, substituting for z *) Array.fold_right (fun a b -> Complex.add (Complex.mul b z) a) c { re = 0.; im = 0. } in let evaluate t b z = (* evaluate response, substituting for z *) Complex.div (eval t z) (eval b z) in begin match filter_family with | Butterworth -> (* Compute S-plane poles (Butterworth) *) self#log#info "This is a Butterworth filter."; let choosepole z = if z.re < 0. then ( self#log#info "z = %+.013f %+.013f i." z.re z.im; if polemask mod 2 == 0 then ( splane_poles <- Array.append splane_poles [| z |]; splane_numpoles <- splane_numpoles + 1); polemask <- polemask lsl 1) in self#log#info "Theta:"; for i = 0 to (2 * order) - 1 do let theta = match order mod 2 with | 1 -> float_of_int i *. Float.pi /. float_of_int order | 0 -> (float_of_int i +. 0.5) *. Float.pi /. float_of_int order | _ -> assert false in self#log#info "%d: %+.013f --> %+.013f %+.013f i." i theta (cos theta) (sin theta); choosepole { re = cos theta; im = sin theta } done; (* Normalize *) let w1 = cor (2. *. Float.pi *. warped_alpha1) in let w2 = cor (2. *. Float.pi *. warped_alpha2) in begin match filter_type with | Band_stop -> (* Band-stop filter *) self#log#info "This is a band-stop filter."; let w0 = sqrt (w1 *~ w2) and bw = w2 -~ w1 in for i = 0 to splane_numpoles - 1 do let hba = cor 0.5 *~ (bw /~ splane_poles.(i)) in let t = sqrt (cor 1. -~ Complex.pow (w0 /~ hba) (cor 2.)) in splane_poles.(i) <- hba *~ (cor 1. +~ t); splane_poles <- Array.append splane_poles [| hba *~ (cor 1. -~ t) |] done; for _ = 0 to splane_numpoles - 1 do (* also N zeros at (0,0) *) splane_zeros <- Array.append splane_zeros [| { re = 0.; im = w0.re }; { re = 0.; im = -.w0.re }; |] done; splane_numpoles <- splane_numpoles * 2; splane_numzeros <- splane_numpoles; xv <- Array.make_matrix channels ((order * 2) + 1) 0.; yv <- Array.make_matrix channels ((order * 2) + 1) 0. | Band_pass -> (* Band-pass filter *) self#log#info "This is a band-pass filter."; let w0 = sqrt (w1 *~ w2) and bw = w2 -~ w1 in for i = 0 to splane_numpoles - 1 do let hba = cor 0.5 *~ (splane_poles.(i) *~ bw) in let t = sqrt (cor 1. -~ Complex.pow (w0 /~ hba) (cor 2.)) in splane_poles.(i) <- hba *~ (cor 1. +~ t); splane_poles <- Array.append splane_poles [| hba *~ (cor 1. -~ t) |] done; for _ = 0 to splane_numpoles - 1 do (* also N zeros at (0,0) *) splane_zeros <- Array.append splane_zeros [| cor 0. |] done; splane_numzeros <- splane_numpoles; splane_numpoles <- splane_numpoles * 2; xv <- Array.make_matrix channels ((order * 2) + 1) 0.; yv <- Array.make_matrix channels ((order * 2) + 1) 0. | High_pass -> (* Hi-pass filter *) self#log#info "This is a hi-pass filter."; for i = 0 to splane_numpoles - 1 do splane_poles.(i) <- w1 /~ splane_poles.(i) done; for _ = 0 to splane_numpoles - 1 do (* also N zeros at (0,0) *) splane_zeros <- Array.append splane_zeros [| cor 0. |] done; splane_numzeros <- splane_numpoles; xv <- Array.make_matrix channels (order + 1) 0.; yv <- Array.make_matrix channels (order + 1) 0. | Low_pass -> (* Lo-pass filter *) self#log#info "This is a lo-pass filter."; for i = 0 to splane_numpoles - 1 do splane_poles.(i) <- splane_poles.(i) *~ w1 done; splane_numzeros <- 0; xv <- Array.make_matrix channels (order + 1) 0.; yv <- Array.make_matrix channels (order + 1) 0. | _ -> assert false end; (* Compute Z-plane zeros & poles using bilinear transform *) self#log#info "S-Plane zeros:"; self#log#info "%s" (String.concat "\n" (Array.to_list (Array.mapi (fun i a -> Printf.sprintf "%d: %+.013f %+.013f i." i a.re a.im) splane_zeros))); self#log#info "S-Plane poles:"; self#log#info "%s" (String.concat "\n" (Array.to_list (Array.mapi (fun i a -> Printf.sprintf "%d: %+.013f %+.013f i." i a.re a.im) splane_poles))); zplane_numpoles <- splane_numpoles; zplane_numzeros <- splane_numzeros; let blt a = Complex.div (Complex.add { re = 2.; im = 0. } a) (Complex.sub { re = 2.; im = 0. } a) in for i = 0 to zplane_numpoles - 1 do zplane_poles <- Array.append zplane_poles [| blt splane_poles.(i) |] done; for i = 0 to zplane_numzeros - 1 do zplane_zeros <- Array.append zplane_zeros [| blt splane_zeros.(i) |] done; while zplane_numzeros < zplane_numpoles do zplane_zeros <- Array.append zplane_zeros [| { re = -1.0; im = 0. } |]; zplane_numzeros <- zplane_numzeros + 1 done; self#log#info "Z-Plane zeros:"; self#log#info "%s" (String.concat "\n" (Array.to_list (Array.mapi (fun i a -> Printf.sprintf "%d: %+.013f %+.013f i." i a.re a.im) zplane_zeros))); self#log#info "Z-Plane poles:"; self#log#info "%s" (String.concat "\n" (Array.to_list (Array.mapi (fun i a -> Printf.sprintf "%d: %+.013f %+.013f i." i a.re a.im) zplane_poles))) | Resonator -> ( (* Compute Z-plane zeros and poles (Resonator). * Let's assume we're creating a bandpass filter, * we'll transform later if needed. *) self#log#info "This is a Resonator filter."; zplane_numpoles <- 2; zplane_numzeros <- 2; zplane_zeros <- [| cor 1.; cor (-1.) |]; (* where we want the peak to be *) let theta = 2. *. Float.pi *. raw_alpha1 in if qfactor == infinity then ( self#log#info "Infinite Q factor!"; (* oscillator *) let zp = { re = cos theta; im = sin theta } in zplane_poles <- [| zp; Complex.conj zp |]) else ( (* must iterate to find exact pole positions *) topcoeffs <- expand zplane_zeros zplane_numzeros; let r = exp (cor (-.theta /. (2. *. qfactor))) and thm = ref theta and th1 = ref 0. and th2 = ref Float.pi and cvg = ref false in for _ = 0 to 50 do let zp = r *~ { re = cos !thm; im = sin !thm } in zplane_poles <- [| zp; Complex.conj zp |]; botcoeffs <- expand zplane_poles zplane_numpoles; let g = evaluate topcoeffs botcoeffs { re = cos theta; im = sin theta } in let phi = g.im /. g.re in (* approx to atan2 *) if phi > 0. then th2 := !thm else th1 := !thm; if Utils.abs_float phi < 1e-10 then cvg := true; thm := 0.5 *. (!th1 +. !th2) done; (* if we failed to converge ... *) assert !cvg); xv <- Array.make_matrix channels zplane_numzeros 0.; yv <- Array.make_matrix channels zplane_numpoles 0.; (* Do we need to transform to Bandstop or Allpass? *) match filter_type with | Band_stop -> (* Band-stop filter *) self#log#info "This is a band-stop filter."; (* compute Z-plane pole & zero positions for bandstop resonator (notch filter) *) (* place zeros exactly *) let zp = { re = cos theta; im = sin theta } in zplane_poles <- [| zp; Complex.conj zp |] | All_pass -> (* All-pass filter *) self#log#info "This is an all-pass filter."; (* compute Z-plane pole & zero positions for allpass resonator *) zplane_zeros.(0) <- zplane_poles.(0) /~ sqrt (cor (Complex.norm zplane_poles.(0))); zplane_zeros.(1) <- zplane_poles.(1) /~ sqrt (cor (Complex.norm zplane_poles.(1))) | Band_pass -> () | _ -> assert false) end; (* Now expand the polynomials *) self#log#info "Expanding polynomials..."; topcoeffs <- expand zplane_zeros zplane_numzeros; botcoeffs <- expand zplane_poles zplane_numpoles; self#log#info "Top coeffs:"; self#log#info "%s" (String.concat "\n" (Array.to_list (Array.mapi (fun i a -> Printf.sprintf "%d: %+.013f %+.013f i." i a.re a.im) topcoeffs))); self#log#info "Bottom coeffs:"; self#log#info "%s" (String.concat "\n" (Array.to_list (Array.mapi (fun i a -> Printf.sprintf "%d: %+.013f %+.013f i." i a.re a.im) botcoeffs))); (* Gain *) dc_gain <- evaluate topcoeffs botcoeffs { re = 1.; im = 0. }; let theta = 2. *. Float.pi *. 0.5 *. (raw_alpha1 +. raw_alpha2) (* jwt for centre freq. *) in fc_gain <- evaluate topcoeffs botcoeffs (Complex.exp { re = 0.; im = theta }); hf_gain <- evaluate topcoeffs botcoeffs { re = -1.; im = 0. }; gain <- begin match filter_type with | Band_stop -> Complex.norm (sqrt (dc_gain *~ hf_gain)) | Band_pass | All_pass -> Complex.norm fc_gain | High_pass -> Complex.norm hf_gain | Low_pass -> Complex.norm dc_gain end; self#log#info "Gains:"; self#log#info "DC=%+.013f Centre=%+.013f HF=%+.013f Final=%+.013f." (Complex.norm dc_gain) (Complex.norm fc_gain) (Complex.norm hf_gain) gain; (* X-coeffs *) for i = 0 to zplane_numzeros do xcoeffs <- Array.append xcoeffs [| topcoeffs.(i).re /. botcoeffs.(zplane_numpoles).re |] done; self#log#info "Xcoeffs:"; self#log#info "%s" (String.concat "\n" (Array.to_list (Array.mapi (fun i a -> Printf.sprintf "%d: %+.013f." i a) xcoeffs))); (* Y-coeffs *) for i = 0 to zplane_numpoles do ycoeffs <- Array.append ycoeffs [| 0. -. (botcoeffs.(i).re /. botcoeffs.(zplane_numpoles).re) |] done; self#log#info "Ycoeffs:"; self#log#info "%s" (String.concat "\n" (Array.to_list (Array.mapi (fun i a -> Printf.sprintf "%d: %+.013f." i a) ycoeffs))); self#log#info "Initialization done." (* Digital filter based on mkfilter/mkshape/gencode by A.J. Fisher *) method fallible = source#fallible method remaining = source#remaining method seek_source = source#seek_source method self_sync = source#self_sync method private can_generate_frame = source#is_ready method abort_track = source#abort_track val mutable v_offs = 0 method private generate_frame = let c = source#get_mutable_content Frame.Fields.audio in let b = Content.Audio.get_data c in let v_len = Array.length xv.(0) in let coeffs_len = Array.length xcoeffs in let fold_left2 init v coeffs l = let l = min (Array.length v) (min l (Array.length coeffs)) in let result = ref init in for i = 0 to l - 1 do result := !result +. (v.((i + v_offs) mod v_len) *. coeffs.(i)) done; !result in for c = 0 to self#audio_channels - 1 do let xvc = xv.(c) in let yvc = yv.(c) in let bc = b.(c) in for i = 0 to source#frame_audio_position - 1 do v_offs <- (v_offs + 1) mod v_len; xvc.((coeffs_len - 1 + v_offs) mod v_len) <- bc.(i) /. gain; let insert = fold_left2 0. xvc xcoeffs 110 (* TODO: why 110? *) +. fold_left2 0. yvc ycoeffs (coeffs_len - 1) in yvc.((coeffs_len - 1 + v_offs) mod v_len) <- insert; bc.(i) <- insert done done; source#set_frame_data Frame.Fields.audio Content.Audio.lift_data b end let filter_iir = Lang.add_module ~base:Filter.filter "iir" let filter_iir_butterworth = Lang.add_module ~base:filter_iir "butterworth" let filter_iir_resonator = Lang.add_module ~base:filter_iir "resonator" let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:filter_iir_butterworth "high" [ ("frequency", Lang.float_t, None, Some "Corner frequency"); ("order", Lang.int_t, Some (Lang.int 4), Some "Filter order"); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr:"IIR filter" (fun p -> let f v = List.assoc v p in let freq, order, src = ( Lang.to_float (f "frequency"), Lang.to_int (f "order"), Lang.to_source (f "") ) in new iir src Butterworth High_pass order freq 0. 0.) let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:filter_iir_butterworth "low" [ ("frequency", Lang.float_t, None, Some "Corner frequency"); ("order", Lang.int_t, Some (Lang.int 4), Some "Filter order"); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr:"IIR filter" (fun p -> let f v = List.assoc v p in let freq, order, src = ( Lang.to_float (f "frequency"), Lang.to_int (f "order"), Lang.to_source (f "") ) in new iir src Butterworth Low_pass order freq 0. 0.) let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:filter_iir_butterworth "bandpass" [ ("frequency1", Lang.float_t, None, Some "First corner frequency"); ("frequency2", Lang.float_t, None, Some "Second corner frequency"); ("order", Lang.int_t, Some (Lang.int 4), Some "Filter order"); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr:"IIR filter" (fun p -> let f v = List.assoc v p in let freq1, freq2, order, src = ( Lang.to_float (f "frequency1"), Lang.to_float (f "frequency2"), Lang.to_int (f "order"), Lang.to_source (f "") ) in new iir src Butterworth Band_pass order freq1 freq2 0.) let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:filter_iir_butterworth "bandstop" [ ("frequency1", Lang.float_t, None, Some "First corner frequency"); ("frequency2", Lang.float_t, None, Some "Second corner frequency"); ("order", Lang.int_t, Some (Lang.int 4), Some "Filter order"); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr:"IIR filter" (fun p -> let f v = List.assoc v p in let freq1, freq2, order, src = ( Lang.to_float (f "frequency1"), Lang.to_float (f "frequency2"), Lang.to_int (f "order"), Lang.to_source (f "") ) in new iir src Butterworth Band_stop order freq1 freq2 0.) let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:filter_iir_resonator "bandpass" [ ("frequency", Lang.float_t, None, Some "Corner frequency"); ("q", Lang.float_t, Some (Lang.float 60.), Some "Quality factor"); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr:"IIR filter" (fun p -> let f v = List.assoc v p in let freq, q, src = ( Lang.to_float (f "frequency"), Lang.to_float (f "q"), Lang.to_source (f "") ) in new iir src Resonator Band_pass 0 freq 0. q) let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:filter_iir_resonator "bandstop" [ ("frequency", Lang.float_t, None, Some "Corner frequency"); ("q", Lang.float_t, Some (Lang.float 60.), Some "Quality factor"); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr:"IIR filter" (fun p -> let f v = List.assoc v p in let freq, q, src = ( Lang.to_float (f "frequency"), Lang.to_float (f "q"), Lang.to_source (f "") ) in new iir src Resonator Band_pass 0 freq 0. q) let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:filter_iir_resonator "allpass" [ ("frequency", Lang.float_t, None, Some "Corner frequency"); ("q", Lang.float_t, Some (Lang.float 60.), Some "Quality factor"); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr:"IIR filter" (fun p -> let f v = List.assoc v p in let freq, q, src = ( Lang.to_float (f "frequency"), Lang.to_float (f "q"), Lang.to_source (f "") ) in new iir src Resonator Band_pass 0 freq 0. q) liquidsoap-2.3.2/src/core/operators/insert_metadata.ml000066400000000000000000000102371477303350200231320ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Source exception Error (* This is an internal operator. *) class insert_metadata source = object inherit operator ~name:"insert_metadata" [source] method fallible = source#fallible method private can_generate_frame = source#is_ready method remaining = source#remaining method seek_source = source#seek_source method abort_track = source#abort_track method self_sync = source#self_sync val metadata = Atomic.make None method insert_metadata ~new_track m = Atomic.set metadata (Some (new_track, m)) method private generate_frame = let buf = source#get_frame in match Atomic.exchange metadata None with | Some (new_track, m) -> let m = Frame.Metadata.append (Option.value ~default:Frame.Metadata.empty (Frame.get_metadata buf 0)) m in let buf = Frame.add_metadata buf 0 m in if new_track then Frame.(add_track_mark (drop_track_marks buf)) 0 else buf | None -> buf end let _ = let return_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator "insert_metadata" ~category:`Track ~meth: [ ( "insert_metadata", ( [], Lang.fun_t [(true, "new_track", Lang.bool_t); (false, "", Lang.metadata_t)] Lang.unit_t ), "Insert metadata in the source. The `new_track` parameter indicates \ whether a track boundary should also be inserted (by default, no \ track is inserted).", fun s -> Lang.val_fun [ ("new_track", "new_track", Some (Lang.bool false)); ("", "", None); ] (fun p -> let m = Lang.to_metadata (List.assoc "" p) in let new_track = Lang.to_bool (List.assoc "new_track" p) in s#insert_metadata ~new_track m; Lang.unit) ); ] ~return_t ~descr: "Dynamically insert metadata in a stream. Returns the source decorated \ with a method `insert_metadata` which is a function of type \ `(?new_track,metadata)->unit`, used to insert metadata in the source. \ This function also inserts a new track with the given metadata if \ passed `new_track=true`." [("", Lang.source_t return_t, None, None)] (fun p -> let s = Lang.to_source (List.assoc "" p) in let s = new insert_metadata s in s) (** Insert metadata at the beginning if none is set. Currently used by the switch classes. *) class replay meta src = object inherit operator ~name:"replay_metadata" [src] val mutable first = true method fallible = src#fallible method private can_generate_frame = src#is_ready method abort_track = src#abort_track method remaining = src#remaining method self_sync = src#self_sync method seek_source = src#seek_source method private generate_frame = let buf = src#get_frame in if first then ( first <- false; if Frame.get_all_metadata buf = [] then Frame.add_metadata buf 0 meta else buf) else buf end liquidsoap-2.3.2/src/core/operators/ladspa_op.ml000066400000000000000000000357161477303350200217410ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Source open Ladspa module Cache = Liquidsoap_lang.Cache module String = struct include String (** Division for strings. *) let residual s t = let m = String.length s in let n = String.length t in if m >= n && String.sub s 0 n = t then String.sub s n (m - n) else raise Not_found end let ladspa = Lang.add_module "ladspa" type t = Float | Int | Bool let log = Log.make ["LADSPA extension"] let ladspa_enabled = try let venv = Unix.getenv "LIQ_LADSPA" in venv = "1" || venv = "true" with Not_found -> true let ladspa_dirs = try String.split_on_char ':' (Unix.getenv "LIQ_LADSPA_DIRS") with Not_found -> ["/usr/lib64/ladspa"; "/usr/lib/ladspa"; "/usr/local/lib/ladspa"] let port_t d p = if Descriptor.port_is_boolean d p then Bool else if Descriptor.port_is_integer d p then Int else Float class virtual base source = object inherit operator ~name:"ladspa" [source] method fallible = source#fallible method remaining = source#remaining method seek_source = source#seek_source method private can_generate_frame = source#is_ready method self_sync = source#self_sync method abort_track = source#abort_track end class virtual base_nosource = object (self) inherit source ~name:"ladspa" () method seek_source = (self :> Source.source) method fallible = false method private can_generate_frame = true method self_sync = (`Static, None) val mutable must_fail = false method abort_track = must_fail <- true method remaining = -1 end let instantiate d samplerate = let ans = Descriptor.instantiate d samplerate in (* Connect output control ports (which we don't use) to some dummy buffer in order to avoid segfaults. *) for i = 0 to Descriptor.port_count d - 1 do if Descriptor.port_is_control d i && not (Descriptor.port_is_input d i) then ( let c = Bigarray.Array1.create Bigarray.float32 Bigarray.c_layout 1 in Descriptor.connect_port ans i c) done; ans (* A plugin is created for each channel. *) class ladspa_mono (source : source) plugin descr input output params = object (self) inherit base source val mutable inst = None initializer self#on_wake_up (fun () -> let p = Plugin.load plugin in let d = Descriptor.descriptor p descr in let i = Array.init (Content.Audio.channels_of_format (Option.get (Frame.Fields.find_opt Frame.Fields.audio self#content_type))) (fun _ -> instantiate d (Lazy.force Frame.audio_rate)) in Array.iter Descriptor.activate i; inst <- Some i) method private generate_frame = let b = Content.Audio.get_data (source#get_mutable_content Frame.Fields.audio) in let len = source#frame_audio_position in let inst = Option.get inst in for c = 0 to Array.length b - 1 do let buf = Audio.Mono.to_ba b.(c) 0 len in Descriptor.connect_port inst.(c) input buf; Descriptor.connect_port inst.(c) output buf; List.iter (fun (p, v) -> Descriptor.set_control_port inst.(c) p (v ())) params; Descriptor.run inst.(c) len; Audio.Mono.copy_from_ba buf b.(c) 0 len done; source#set_frame_data Frame.Fields.audio Content.Audio.lift_data b end class ladspa (source : source) plugin descr inputs outputs params = object inherit base source val inst = let p = Plugin.load plugin in let d = Descriptor.descriptor p descr in instantiate d (Lazy.force Frame.audio_rate) initializer Descriptor.activate inst method private generate_frame = let b = Content.Audio.get_data (source#get_mutable_content Frame.Fields.audio) in let len = source#frame_audio_position in let ba = Audio.to_ba b 0 len in List.iter (fun (p, v) -> Descriptor.set_control_port inst p (v ())) params; if Array.length inputs = Array.length outputs then ( (* The simple case: number of channels does not get changed. *) for c = 0 to Array.length b - 1 do Descriptor.connect_port inst inputs.(c) ba.(c); Descriptor.connect_port inst outputs.(c) ba.(c) done; Descriptor.run inst len; Audio.copy_from_ba ba b 0 len) else ( (* We have to change channels. *) for c = 0 to Array.length b - 1 do Descriptor.connect_port inst inputs.(c) ba.(c) done; let dba = Audio.to_ba b 0 len in for c = 0 to Array.length b - 1 do Descriptor.connect_port inst outputs.(c) dba.(c) done; Descriptor.run inst len; Audio.copy_from_ba dba b 0 len); source#set_frame_data Frame.Fields.audio Content.Audio.lift_data b end class ladspa_nosource plugin descr outputs params = object (self) inherit base_nosource val inst = let p = Plugin.load plugin in let d = Descriptor.descriptor p descr in instantiate d (Lazy.force Frame.audio_rate) initializer Descriptor.activate inst method private generate_frame = if must_fail then ( must_fail <- false; self#end_of_track) else ( let length = Lazy.force Frame.size in let buf = Frame.create ~length self#content_type in let b = Content.Audio.get_data (Frame.get buf Frame.Fields.audio) in List.iter (fun (p, v) -> Descriptor.set_control_port inst p (v ())) params; let alen = Frame.audio_of_main length in let ba = Audio.to_ba b 0 alen in for c = 0 to Array.length b - 1 do Descriptor.connect_port inst outputs.(c) ba.(c) done; Descriptor.run inst alen; Audio.copy_from_ba ba b 0 alen; Frame.set_data buf Frame.Fields.audio Content.Audio.lift_data b) end (* List the indexes of control ports. *) let get_control_ports d = let ports = Descriptor.port_count d in let ans = ref [] in for i = 0 to ports - 1 do if Descriptor.port_is_control d i && Descriptor.port_is_input d i then ans := i :: !ans done; List.rev !ans type port = { port_index : int; port_name : string; port_type : t; port_default : float option; port_min : float option; port_max : float option; } (** When creating operator for LADSPA plugins, we don't know yet at which samplerate Liquidsoap will operate. But the default values and bounds for LADSPA parameters might depend on the samplerate. Lacking a better solution, we use the following default samplerate, potentially creating a mismatch between the doc and the actual behavior. *) let default_samplerate = 44100 let get_control_ports d = List.map (fun p -> { port_index = p; port_name = Utils.normalize_parameter_string (Descriptor.port_name d p); port_type = port_t d p; port_default = Descriptor.port_get_default d ~samplerate:default_samplerate p; port_min = Descriptor.port_get_min d ~samplerate:default_samplerate p; port_max = Descriptor.port_get_max d ~samplerate:default_samplerate p; }) (get_control_ports d) type plugin = { plugin_file : string; plugin_descriptor : int; plugin_inputs : int array; plugin_outputs : int array; plugin_controls : port list; plugin_name : string; plugin_label : string; plugin_maker : string; } (** Get input and output ports. *) let get_audio_ports d = let i = ref [] in let o = ref [] in let ports = Descriptor.port_count d in for n = 0 to ports - 1 do if Descriptor.port_is_audio d n then if Descriptor.port_is_input d n then i := n :: !i else o := n :: !o done; (Array.of_list (List.rev !i), Array.of_list (List.rev !o)) let load_descriptor fname descr d = let plugin_inputs, plugin_outputs = get_audio_ports d in { plugin_file = fname; plugin_descriptor = descr; plugin_inputs; plugin_outputs; plugin_controls = get_control_ports d; plugin_name = Descriptor.name d; plugin_label = Utils.normalize_parameter_string (Descriptor.label d); plugin_maker = Descriptor.maker d; } (* Make a parameter for each control port. Returns the liquidsoap parameters and the parameters for the plugin. *) let params_of_controls control_ports = let liq_params = List.map (fun p -> let t = p.port_type in ( p.port_name, (match t with | Float -> Lang.getter_t Lang.float_t | Int -> Lang.getter_t Lang.int_t | Bool -> Lang.getter_t Lang.bool_t), (match p.port_default with | Some f -> Some (match t with | Float -> Lang.float f | Int -> Lang.int (int_of_float f) | Bool -> Lang.bool (f > 0.)) | None -> None), let bounds = let min = p.port_min in let max = p.port_max in if (min, max) = (None, None) then "" else ( let bounds = ref " (" in begin match min with | Some f -> ( match t with | Float -> bounds := Printf.sprintf "%s%.6g <= " !bounds f | Int -> bounds := Printf.sprintf "%s%d <= " !bounds (int_of_float (ceil f)) | Bool -> ()) | None -> () end; bounds := !bounds ^ "`" ^ p.port_name ^ "`"; begin match max with | Some f -> ( match t with | Float -> bounds := Printf.sprintf "%s <= %.6g" !bounds f | Int -> bounds := Printf.sprintf "%s <= %d" !bounds (int_of_float f) | Bool -> ()) | None -> () end; !bounds ^ ")") in Some (p.port_name ^ bounds ^ ".") )) control_ports in let params l = List.map (fun p -> ( p.port_index, let v = List.assoc p.port_name l in match p.port_type with | Float -> Lang.to_float_getter v | Int -> let f = Lang.to_int_getter v in fun () -> float_of_int (f ()) | Bool -> let f = Lang.to_bool_getter v in fun () -> if f () then 1. else 0. )) control_ports in (liq_params, params) let register_descr d = let ni = Array.length d.plugin_inputs in let no = Array.length d.plugin_outputs in let mono = ni = 1 && no = 1 in let liq_params, params = params_of_controls d.plugin_controls in let input_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in let liq_params = liq_params @ if ni = 0 then [] else [("", Lang.source_t input_t, None, None)] in let maker = d.plugin_maker in let maker = String.concat "(at)" (String.split_on_char '@' maker) in let descr = Printf.sprintf "%s by %s." d.plugin_name maker in let return_t = if mono then input_t else Frame_type.set_field input_t Frame.Fields.audio (Format_type.audio_n no) in let label = d.plugin_label in let label = try "lsp_" ^ String.residual label "http:_lsp_plugin_plugins_ladspa_" with Not_found -> label in ignore (Lang.add_operator ~base:ladspa label liq_params ~return_t ~category:`Audio ~flags:[`Extra] ~descr (fun p -> let f v = List.assoc v p in let source = try Some (Lang.to_source (f "")) with Not_found -> None in let params = params p in if ni = 0 then new ladspa_nosource d.plugin_file d.plugin_descriptor d.plugin_outputs params else if mono then (new ladspa_mono (Option.get source) d.plugin_file d.plugin_descriptor d.plugin_inputs.(0) d.plugin_outputs.(0) params :> Source.source) else (new ladspa (Option.get source) d.plugin_file d.plugin_descriptor d.plugin_inputs d.plugin_outputs params :> Source.source))) let register_descr d = (* We do not register plugins without outputs for now. *) try ignore (Audio_converter.Channel_layout.layout_of_channels (Array.length d.plugin_inputs)); ignore (Audio_converter.Channel_layout.layout_of_channels (Array.length d.plugin_outputs)); if d.plugin_outputs <> [||] then register_descr d with Audio_converter.Channel_layout.Unsupported -> log#info "Could not register LADSPA plugin %s: unhandled number of channels." d.plugin_file let register_plugin cache pname = try let p = Plugin.load pname in let descr = Descriptor.descriptors p in Array.iteri (fun n d -> let key = pname ^ string_of_int n in let d = Cache.Table.get cache key (fun () -> load_descriptor pname n d) in register_descr d) descr; Plugin.unload p with Plugin.Not_a_plugin -> () let register_plugins () = let cache = (Cache.Table.load ~dirtype:`System ~name:"LADSPA plugins" "ladspa-plugins" : plugin Cache.Table.t) in let add plugins_dir = try let dir = Unix.opendir plugins_dir in try while true do let f = Unix.readdir dir in if f <> "." && f <> ".." then register_plugin cache (plugins_dir ^ "/" ^ f) done with End_of_file -> Unix.closedir dir with Unix.Unix_error (e, _, _) -> log#info "Error while loading directory %s: %s" plugins_dir (Unix.error_message e) in List.iter add ladspa_dirs; Cache.Table.store ~dirtype:`System cache let () = Lifecycle.on_load ~name:"ladspa plugin registration" (fun () -> if !Startup.register_external_plugins && ladspa_enabled then Startup.time "LADSPA plugins registration" register_plugins) liquidsoap-2.3.2/src/core/operators/lilv_op.ml000066400000000000000000000336111477303350200214330ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Source open Lilv module Cache = Liquidsoap_lang.Cache let lv2 = Lang.add_module "lv2" let log = Log.make ["Lilv LV2"] let lilv_enabled = try let venv = Unix.getenv "LIQ_LILV" in venv = "1" || venv = "true" with Not_found -> true class virtual base source = object inherit operator ~name:"lilv" [source] method fallible = source#fallible method remaining = source#remaining method seek_source = source#seek_source method private can_generate_frame = source#is_ready method self_sync = source#self_sync method abort_track = source#abort_track end class virtual base_nosource = object (self) inherit source ~name:"lilv" () method seek_source = (self :> Source.source) method fallible = false method private can_generate_frame = true val mutable must_fail = false method abort_track = must_fail <- true method remaining = -1 end let constant_data len x = let data = Bigarray.Array1.create Bigarray.Float32 Bigarray.c_layout len in Bigarray.Array1.fill data x; data (** A mono LV2 plugin: a plugin is created for each channel. *) class lilv_mono (source : source) plugin input output params = object (self) inherit base source val mutable inst = None initializer self#on_wake_up (fun () -> let i = Array.init (Content.Audio.channels_of_format (Option.get (Frame.Fields.find_opt Frame.Fields.audio self#content_type))) (fun _ -> Plugin.instantiate plugin (float_of_int (Lazy.force Frame.audio_rate))) in Array.iter Plugin.Instance.activate i; inst <- Some i) method private generate_frame = let b = Content.Audio.get_data (source#get_mutable_content Frame.Fields.audio) in let len = source#frame_audio_position in let chans = Array.length b in let inst = Option.get inst in let ba = Audio.to_ba b 0 len in for c = 0 to chans - 1 do Plugin.Instance.connect_port_float inst.(c) input ba.(c); Plugin.Instance.connect_port_float inst.(c) output ba.(c); List.iter (fun (p, v) -> Plugin.Instance.connect_port_float inst.(c) p (constant_data len (v ()))) params; Plugin.Instance.run inst.(c) len; Audio.copy_from_ba ba b 0 len done; source#set_frame_data Frame.Fields.audio Content.Audio.lift_data b end class lilv (source : source) plugin inputs outputs params = object inherit base source val inst = Plugin.instantiate plugin (float_of_int (Lazy.force Frame.audio_rate)) initializer Plugin.Instance.activate inst method private generate_frame = let b = Content.Audio.get_data (source#get_mutable_content Frame.Fields.audio) in let len = source#frame_audio_position in List.iter (fun (p, v) -> let data = Bigarray.Array1.create Bigarray.Float32 Bigarray.c_layout len in Bigarray.Array1.fill data (v ()); Plugin.Instance.connect_port_float inst p data) params; let ba = Audio.to_ba b 0 len in if Array.length inputs = Array.length outputs then ( let chans = Array.length b in (* The simple case: number of channels does not get changed. *) for c = 0 to chans - 1 do Plugin.Instance.connect_port_float inst inputs.(c) ba.(c); Plugin.Instance.connect_port_float inst outputs.(c) ba.(c) done; Plugin.Instance.run inst len; Audio.copy_from_ba ba b 0 len) else ( (* We have to change channels. *) let dba = Audio.to_ba b 0 len in for c = 0 to Array.length b - 1 do Plugin.Instance.connect_port_float inst inputs.(c) ba.(c) done; let output_chans = Array.length b in for c = 0 to output_chans - 1 do Plugin.Instance.connect_port_float inst outputs.(c) dba.(c) done; Plugin.Instance.run inst len; Audio.copy_from_ba dba b 0 len); source#set_frame_data Frame.Fields.audio Content.Audio.lift_data b end (** An LV2 plugin without audio input. *) class lilv_nosource plugin outputs params = object (self) inherit base_nosource method self_sync = (`Static, None) val inst = Plugin.instantiate plugin (float_of_int (Lazy.force Frame.audio_rate)) initializer Plugin.Instance.activate inst method private generate_frame = if must_fail then ( must_fail <- false; self#end_of_track) else ( let length = Lazy.force Frame.size in let buf = Frame.create ~length self#content_type in let b = Content.Audio.get_data (Frame.get buf Frame.Fields.audio) in let chans = Array.length b in let alen = Frame.audio_of_main length in let ba = Audio.to_ba b 0 alen in List.iter (fun (p, v) -> Plugin.Instance.connect_port_float inst p (constant_data alen (v ()))) params; for c = 0 to chans - 1 do Plugin.Instance.connect_port_float inst outputs.(c) ba.(c) done; Plugin.Instance.run inst alen; Audio.copy_from_ba ba b 0 alen; Frame.set_data buf Frame.Fields.audio Content.Audio.lift_data b) end (** An LV2 plugin without audio output (e.g. to observe the stream). The input stream is returned. *) class lilv_noout source plugin inputs params = object inherit base source val inst = Plugin.instantiate plugin (float_of_int (Lazy.force Frame.audio_rate)) initializer Plugin.Instance.activate inst method private generate_frame = let buf = source#get_frame in let b = Content.Audio.get_data (Frame.get buf Frame.Fields.audio) in let chans = Array.length b in let alen = source#frame_audio_position in let ba = Audio.to_ba b 0 alen in List.iter (fun (p, v) -> Plugin.Instance.connect_port_float inst p (constant_data alen (v ()))) params; for c = 0 to chans - 1 do Plugin.Instance.connect_port_float inst inputs.(c) ba.(c) done; Plugin.Instance.run inst alen; buf end (* List the indexes of control ports. *) let get_control_ports p = let ports = Plugin.num_ports p in let ans = ref [] in for i = 0 to ports - 1 do let port = Plugin.port_by_index p i in if Port.is_control port && Port.is_input port then ans := i :: !ans done; List.rev !ans type port_type = Float (* TODO: handle types *) let port_type _ = Float type port = { port_index : int; port_symbol : string; port_name : string; port_type : port_type; port_default_float : float option; port_min_float : float option; port_max_float : float option; } let get_control_ports plugin = let control_ports = get_control_ports plugin in (* TODO: handle other types *) let control_ports = List.filter (fun p -> port_type p = Float) control_ports in List.map (fun i -> let p = Plugin.port_by_index plugin i in { port_index = i; port_symbol = Port.symbol p; port_name = Port.name p; port_type = port_type p; port_default_float = Port.default_float p; port_min_float = Port.min_float p; port_max_float = Port.max_float p; }) control_ports (** Get input and output ports. *) let get_audio_ports p = let i = ref [] in let o = ref [] in let ports = Plugin.num_ports p in for n = 0 to ports - 1 do let port = Plugin.port_by_index p n in if Port.is_audio port then if Port.is_input port then i := n :: !i else o := n :: !o done; (Array.of_list (List.rev !i), Array.of_list (List.rev !o)) type plugin = { plugin_uri : string; plugin_name : string; plugin_inputs : int array; plugin_outputs : int array; plugin_controls : port list; (** control ports *) plugin_maker : string; plugin_class_label : string; } let load_plugin plugin = let plugin_inputs, plugin_outputs = get_audio_ports plugin in let plugin_controls = get_control_ports plugin in let maker = Plugin.author_name plugin in let maker_homepage = Plugin.author_homepage plugin in let maker = if maker_homepage = "" then maker else Printf.sprintf "[%s](%s)" maker maker_homepage in let plugin_maker = if maker = "" then "" else " by " ^ maker in let plugin_class_label = Plugin.Class.label (Plugin.plugin_class plugin) in { plugin_uri = Plugin.uri plugin; plugin_name = Plugin.name plugin; plugin_inputs; plugin_outputs; plugin_controls; plugin_maker; plugin_class_label; } (* Make a parameter for each control port. Returns the liquidsoap parameters and the parameters for the plugin. *) let params_of_controls control_ports = let liq_params = List.map (fun p -> let t = p.port_type in ( p.port_symbol, (match t with Float -> Lang.getter_t Lang.float_t), (match p.port_default_float with | Some f -> Some (match t with Float -> Lang.float f) | None -> None), let bounds = let min = p.port_min_float in let max = p.port_max_float in if (min, max) = (None, None) then "" else ( let bounds = ref " (" in begin match min with | Some f -> ( match t with | Float -> bounds := Printf.sprintf "%s%.6g <= " !bounds f) | None -> () end; bounds := !bounds ^ "`" ^ p.port_symbol ^ "`"; begin match max with | Some f -> ( match t with | Float -> bounds := Printf.sprintf "%s <= %.6g" !bounds f) | None -> () end; !bounds ^ ")") in Some (p.port_name ^ bounds ^ ".") )) control_ports in let params l = List.map (fun p -> ( p.port_index, let v = List.assoc p.port_symbol l in match port_type p with Float -> Lang.to_float_getter v )) control_ports in (liq_params, params) let register_plugin plugin p = let ni = Array.length p.plugin_inputs in let no = Array.length p.plugin_outputs in (* Ensure that we support the number of channels. *) ignore (Audio_converter.Channel_layout.layout_of_channels ni); ignore (Audio_converter.Channel_layout.layout_of_channels no); let mono = ni = 1 && no = 1 in let input_t = Lang.frame_t Lang.unit_t (Frame.Fields.make ~audio:(Format_type.audio_n ni) ()) in let liq_params, params = params_of_controls p.plugin_controls in let liq_params = liq_params @ if ni = 0 then [] else [("", Lang.source_t input_t, None, None)] in let descr = p.plugin_name ^ p.plugin_maker ^ "." in let descr = descr ^ " This is in class " ^ p.plugin_class_label ^ "." in let descr = descr ^ " See <" ^ p.plugin_uri ^ ">." in let return_t = Lang.frame_t Lang.unit_t (Frame.Fields.make ~audio:(Format_type.audio_n no) ()) in ignore (Lang.add_operator ~base:lv2 (Utils.normalize_parameter_string p.plugin_name) liq_params ~return_t ~category:`Audio ~flags:[`Extra] ~descr (fun l -> let f v = List.assoc v l in let source = try Some (Lang.to_source (f "")) with Not_found -> None in let params = params l in if ni = 0 then new lilv_nosource plugin p.plugin_outputs params else if no = 0 then (* TODO: can we really use such a type? *) (new lilv_noout (Option.get source) plugin p.plugin_inputs params :> Source.source) else if mono then (new lilv_mono (Option.get source) plugin p.plugin_inputs.(0) p.plugin_outputs.(0) params :> Source.source) else (new lilv (Option.get source) plugin p.plugin_inputs p.plugin_outputs params :> Source.source))) let register_plugin cache plugin = (* Only the uri computation is fast. Try to retrieve other parameters from the cache. *) let uri = Plugin.uri plugin in let p = Cache.Table.get cache uri (fun () -> load_plugin plugin) in try register_plugin plugin p with Audio_converter.Channel_layout.Unsupported -> log#info "Could not register Lilv plugin %s: unhandled number of channels." p.plugin_name let register_plugins () = let cache = (Cache.Table.load ~dirtype:`System ~name:"lilv plugins" "lilv-plugins" : plugin Cache.Table.t) in let world = World.create () in World.load_all world; Plugins.iter (register_plugin cache) (World.plugins world); Cache.Table.store ~dirtype:`System cache let () = Lifecycle.on_load ~name:"lilv plugin registration" (fun () -> if !Startup.register_external_plugins && lilv_enabled then Startup.time "Lilv plugins registration" register_plugins) liquidsoap-2.3.2/src/core/operators/lufs.ml000066400000000000000000000223171477303350200207410ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Source open Extralib module List = struct include List let mean l = List.fold_left ( +. ) 0. l /. float_of_int (List.length l) end (** Second order IIR filter. *) module IIR = struct type stage type t = int * stage * stage external create : channels:int -> a1:float -> a2:float -> b0:float -> b1:float -> b2:float -> stage = "liquidsoap_lufs_create_bytecode" "liquidsoap_lufs_create_native" (** Create and IIR filter. The coefficients are given for a samplerate of 48 kHz and are adjusted for required target samplerate. *) let create ~channels ~samplerate ~a1 ~a2 ~b0 ~b1 ~b2 = if samplerate = 48000. then create ~channels ~a1 ~a2 ~b0 ~b1 ~b2 else ( (* The coefficients of the specification are given for a 48 kHz samplerate, this computes the values for other samplerates. This is "strongly inspired" of https://github.com/klangfreund/LUFSMeter/ *) let k_q = (2. -. (2. *. a2)) /. (a2 -. a1 +. 1.) in let k = sqrt ((a1 +. a2 +. 1.) /. (a2 -. a1 +. 1.)) in let q = k /. k_q in let atan_k = atan k in let vb = (b0 -. b2) /. (1. -. a2) in let vh = (b0 -. b1 +. b2) /. (a2 -. a1 +. 1.) in let vl = (b0 +. b1 +. b2) /. (a1 +. a2 +. 1.) in let k = tan (atan_k *. 48000. /. samplerate) in let a = 1. /. (1. +. (k /. q) +. (k *. k)) in let b0 = (vh +. (vb *. k /. q) +. (vl *. k *. k)) *. a in let b1 = 2. *. ((vl *. k *. k) -. vh) *. a in let b2 = (vh -. (vb *. k /. q) +. (vl *. k *. k)) *. a in let a1 = 2. *. ((k *. k) -. 1.) *. a in let a2 = (1. -. (k /. q) +. (k *. k)) *. a in create ~channels ~a1 ~a2 ~b0 ~b1 ~b2) let create ~channels ~samplerate = let stage1 = create ~a1:(-1.69065929318241) ~a2:0.73248077421585 ~b0:1.53512485958697 ~b1:(-2.69169618940638) ~b2:1.19839281085285 ~channels ~samplerate in let stage2 = create ~a1:(-1.99004745483398) ~a2:0.99007225036621 ~b0:1. ~b1:(-2.) ~b2:1. ~channels ~samplerate in (channels, stage1, stage2) external process : stage1:stage -> stage2:stage -> float array array -> float = "liquidsoap_lufs_process" let process (channels, stage1, stage2) samples = assert (Array.length samples = channels); process ~stage1 ~stage2 samples end (** Compute the loudness from the mean of squares. *) let loudness z = -0.691 +. (10. *. log10 z) let energy z = Float.pow 10. ((z +. 0.691) /. 10.) let min_lufs = -70. module LufsIntegratedHistogram = struct let granularity = 100. type histogram_entry = { mutable count : int; loudness : float; energy : float; } type histogram = { mutable pending : float list; mutable entries : (int * histogram_entry) list; mutable threshold : float; mutable threshold_count : int; } let pos loudness = int_of_float ((loudness -. min_lufs) *. granularity) let create () = { pending = []; entries = []; threshold = 0.; threshold_count = 0 } let get_entry h pos = try List.assoc pos h.entries with Not_found -> let loudness = (float pos /. granularity) +. min_lufs in let entry = { count = 0; loudness; energy = energy loudness } in h.entries <- (pos, entry) :: h.entries; entry let append h v = let blocks = v :: h.pending in if List.length blocks = 4 then ( h.pending <- []; let power = List.mean blocks in let loudness = loudness power in if min_lufs <= loudness then ( let entry = get_entry h (pos loudness) in entry.count <- entry.count + 1; h.threshold <- h.threshold +. power; h.threshold_count <- h.threshold_count + 1)) else h.pending <- blocks let compute { entries; threshold; threshold_count } = let threshold = loudness (threshold /. float threshold_count) -. 10. in let power, total = List.fold_left (fun (power, total) (_, { count; loudness; energy }) -> if threshold <= loudness then (power +. (energy *. float count), total + count) else (power, total)) (0., 0) entries in loudness (power /. float total) end class lufs window source = object (self) inherit operator [source] ~name:"lufs" method fallible = source#fallible method private can_generate_frame = source#is_ready method remaining = source#remaining method seek_source = source#seek_source method abort_track = source#abort_track method self_sync = source#self_sync (** Last 100ms blocks. *) val mutable ms_blocks = [] val mutable len_100ms = 0 val mutable iir = None method private iir = match iir with | None -> let channels = self#audio_channels in let samplerate = self#samplerate in let h = IIR.create ~channels ~samplerate in iir <- Some h; h | Some v -> v initializer self#on_wake_up (fun () -> len_100ms <- Frame.main_of_seconds 0.1) val mutable lufs_integrated = LufsIntegratedHistogram.create () method lufs_integrated = LufsIntegratedHistogram.compute lufs_integrated method private add_integrated_block v = LufsIntegratedHistogram.append lufs_integrated v method private reset_lufs_integrated = iir <- None; lufs_integrated <- LufsIntegratedHistogram.create () (** Compute LUFS. *) method lufs = (* Compute ms of overlapping 400ms blocks. *) let blocks = let rec aux b' b'' b''' = function | b :: l -> ((b +. b' +. b'' +. b''') /. 4.) :: aux b b' b'' l | [] -> [] in let aux = function b :: b' :: b'' :: l -> aux b b' b'' l | _ -> [] in aux ms_blocks in (* Blocks over absolute threshold. *) let absolute = List.filter (fun z -> loudness z > min_lufs) blocks in (* Relative threshold. *) let threshold = loudness (List.mean absolute) -. 10. in (* Blocks over relative threshold. *) let relative = List.filter (fun z -> loudness z > threshold) blocks in loudness (List.mean relative) (** Momentary LUFS. *) method lufs_momentary = loudness (List.mean (List.prefix 4 ms_blocks)) method private process_frame frame = Generator.append self#buffer frame; while len_100ms < Generator.length self#buffer do let frame = Generator.slice self#buffer len_100ms in if Frame.has_track_marks frame then self#reset_lufs_integrated; let buf = AFrame.pcm frame in let power = IIR.process self#iir buf in self#add_integrated_block power; ms_blocks <- power :: ms_blocks done; (* Keep only a limited (by the window) number of blocks. *) ms_blocks <- List.prefix (int_of_float (window () /. 0.1)) ms_blocks method private generate_frame = let frame = source#get_frame in self#process_frame frame; frame end let _ = let return_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator "lufs" ~category:`Visualization ~meth: [ ( "lufs", ([], Lang.fun_t [] Lang.float_t), "Current value for the LUFS (short-term value computed over the \ duration specified by the `window` parameter).", fun s -> Lang.val_fun [] (fun _ -> Lang.float s#lufs) ); ( "lufs_integrated", ([], Lang.fun_t [] Lang.float_t), "Average LUFS value over the current track.", fun s -> Lang.val_fun [] (fun _ -> Lang.float s#lufs_integrated) ); ( "lufs_momentary", ([], Lang.fun_t [] Lang.float_t), "Momentary LUFS (over a 400ms window).", fun s -> Lang.val_fun [] (fun _ -> Lang.float s#lufs_momentary) ); ] ~return_t ~descr: "Compute current LUFS of the source according to the EBU R128 standard. \ It returns the source with a method to compute the current value." [ ( "window", Lang.getter_t Lang.float_t, Some (Lang.float 3.), Some "Duration of the window (in seconds) used to compute the LUFS." ); ("", Lang.source_t return_t, None, None); ] (fun p -> let f v = List.assoc v p in let src = Lang.to_source (f "") in let window = Lang.to_float_getter (f "window") in new lufs window src) liquidsoap-2.3.2/src/core/operators/lufs_c.c000066400000000000000000000056571477303350200210650ustar00rootroot00000000000000#include #include #include #include #include #include #include #define MAX_CHANNELS 12 typedef struct iir { uint8_t channels; double x1[MAX_CHANNELS]; double x2[MAX_CHANNELS]; double y1[MAX_CHANNELS]; double y2[MAX_CHANNELS]; double a1; double a2; double b0; double b1; double b2; } iir_t; #define IIR_val(v) (*(iir_t **)Data_custom_val(v)) static void finalize_iir(value v) { iir_t *iir = IIR_val(v); free(iir); } static struct custom_operations iir_ops = { "liquidsoap_iir", finalize_iir, custom_compare_default, custom_hash_default, custom_serialize_default, custom_deserialize_default}; CAMLprim value liquidsoap_lufs_create_native(value _channels, value _a1, value _a2, value _b0, value _b1, value _b2) { CAMLparam5(_a1, _a2, _b0, _b1, _b2); CAMLlocal1(ans); int channels = Int_val(_channels); if (channels > MAX_CHANNELS) caml_failwith("LUFS: too many channels! Maximum channels is 12."); iir_t *iir = calloc(sizeof(iir_t), 1); if (!iir) caml_raise_out_of_memory(); iir->channels = channels; iir->a1 = Double_val(_a1); iir->a2 = Double_val(_a2); iir->b0 = Double_val(_b0); iir->b1 = Double_val(_b1); iir->b2 = Double_val(_b2); ans = caml_alloc_custom(&iir_ops, sizeof(iir_t *), 0, 1); IIR_val(ans) = iir; CAMLreturn(ans); } CAMLprim value liquidsoap_lufs_create_bytecode(value *argv, int argn) { return liquidsoap_lufs_create_native(argv[0], argv[1], argv[2], argv[3], argv[4], argv[5]); } static inline void liquidsoap_lufs_process_stage(iir_t *iir, double *x, double *y) { int i, c; size_t buf_len = iir->channels * sizeof(double); for (i = 0; i < iir->channels; i++) y[i] = iir->b0 * x[i] + iir->b1 * iir->x1[i] + iir->b2 * iir->x2[i] - iir->a1 * iir->y1[i] - iir->a2 * iir->y2[i]; for (c = 0; c < iir->channels; c++) { iir->x2[c] = iir->x1[c]; iir->x1[c] = x[c]; iir->y2[c] = iir->y1[c]; iir->y1[c] = y[c]; } } CAMLprim value liquidsoap_lufs_process(value _stage1, value _stage2, value _x, value _ret) { CAMLparam4(_stage1, _stage2, _x, _ret); double tmp1[MAX_CHANNELS], tmp2[MAX_CHANNELS]; double power = 0; int samples = Wosize_val(Field(_x, 0)) / Double_wosize; iir_t *stage1 = IIR_val(_stage1); iir_t *stage2 = IIR_val(_stage2); int i, c; for (i = 0; i < samples; i++) { for (c = 0; c < stage1->channels; c++) tmp1[c] = Double_field(Field(_x, c), i); liquidsoap_lufs_process_stage(stage1, tmp1, tmp2); liquidsoap_lufs_process_stage(stage2, tmp2, tmp1); for (c = 0; c < stage1->channels; c++) power += tmp1[c] * tmp1[c]; } CAMLreturn(caml_copy_double(power / samples)); } liquidsoap-2.3.2/src/core/operators/map_metadata.ml000066400000000000000000000107361477303350200224070ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Source class map_metadata source rewrite_f insert_missing update strip = object (self) inherit operator ~name:"metadata.map" [source] initializer Typing.(self#frame_type <: Lang.unit_t) method fallible = source#fallible method private can_generate_frame = source#is_ready method remaining = source#remaining method abort_track = source#abort_track method seek_source = source#seek_source method self_sync = source#self_sync method private rewrite m = let m' = Lang.apply rewrite_f [("", Lang.metadata m)] in let replace_val m v = let x, y = Lang.to_product v in let x = Lang.to_string x and y = Lang.to_string y in if not strip then Frame.Metadata.add x y m else if y <> "" then Frame.Metadata.add x y m else Frame.Metadata.remove x m in let m = if not update then Frame.Metadata.empty else m in List.fold_left replace_val m (Lang.to_list m') method private process buf = List.fold_left (fun buf (t, m) -> let m = self#rewrite m in if strip && Frame.Metadata.is_empty m then Frame.free_metadata buf t else Frame.add_metadata buf t m) buf (Frame.get_all_metadata buf) method private generate_frame = match self#split_frame source#get_frame with | frame, None -> self#process frame | frame, Some new_track -> let frame = self#process frame in let new_track = self#process new_track in Frame.append frame (match (insert_missing, Frame.get_metadata new_track 0) with | false, _ -> new_track | true, None -> self#log#important "Inserting missing metadata."; Frame.add_metadata new_track 0 (self#rewrite Frame.Metadata.empty) | true, Some _ -> new_track) end let register = let return_t = Format_type.metadata in Lang.add_track_operator ~base:Modules.track_metadata "map" [ ( "", Lang.fun_t [ (false, "", Lang.list_t (Lang.product_t Lang.string_t Lang.string_t)); ] (Lang.list_t (Lang.product_t Lang.string_t Lang.string_t)), None, Some "A function that returns new metadata." ); ( "update", Lang.bool_t, Some (Lang.bool true), Some "Update metadata. If false, existing metadata are cleared and only \ returned values are set as new metadata." ); ( "strip", Lang.bool_t, Some (Lang.bool false), Some "Completely remove empty metadata. Operates on both empty values and \ empty metadata chunk." ); ( "insert_missing", Lang.bool_t, Some (Lang.bool true), Some "Treat track beginnings without metadata as having empty ones. The \ operational order is: create empty if needed, map and strip if \ enabled." ); ("", return_t, None, None); ] ~category:`Track ~descr:"Rewrite metadata on the fly using a function." ~return_t (fun p -> let field, source = Lang.to_track (Lang.assoc "" 2 p) in assert (field = Frame.Fields.metadata); let f = Lang.assoc "" 1 p in let update = Lang.to_bool (List.assoc "update" p) in let strip = Lang.to_bool (List.assoc "strip" p) in let missing = Lang.to_bool (List.assoc "insert_missing" p) in (field, new map_metadata source f missing update strip)) liquidsoap-2.3.2/src/core/operators/map_op.ml000066400000000000000000000044701477303350200212430ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Source class map ~field source f = object inherit operator ~name:"audio.map" [source] method fallible = source#fallible method remaining = source#remaining method seek_source = source#seek_source method self_sync = source#self_sync method private can_generate_frame = source#is_ready method abort_track = source#abort_track method private generate_frame = let b = Content.Audio.get_data (source#get_mutable_content field) in for i = 0 to source#frame_audio_position - 1 do for c = 0 to Array.length b - 1 do b.(c).(i) <- f b.(c).(i) done done; source#set_frame_data field Content.Audio.lift_data b end let to_fun_float f x = Lang.to_float (Lang.apply f [("", Lang.float x)]) let _ = let frame_t = Format_type.audio () in Lang.add_track_operator ~base:Modules.track_audio "map" [ ("", Lang.fun_t [(false, "", Lang.float_t)] Lang.float_t, None, None); ("", frame_t, None, None); ] ~return_t:frame_t ~descr:"Map a function to all audio samples. This is SLOW!" ~category:`Audio ~flags:[`Experimental] (* It works well but is probably useless. *) (fun p -> let f = to_fun_float (Lang.assoc "" 1 p) in let field, src = Lang.to_track (Lang.assoc "" 2 p) in (field, new map ~field src f)) liquidsoap-2.3.2/src/core/operators/max_duration.ml000066400000000000000000000070001477303350200224520ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** This one is a bit tricky as we want to make sure that the underlying source is cleaned up when it's done pulling. Used in switch-based transitions to avoid infinite stack of sources. *) class max_duration ~override_meta ~duration source = object (self) inherit Source.operator ~name:"max_duration" [] initializer Clock.unify ~pos:self#pos self#clock source#clock val mutable remaining = duration val mutable s : Source.source = source method self_sync = source#self_sync method fallible = true method private can_generate_frame = remaining > 0 && s#is_ready method abort_track = s#abort_track method remaining = match (remaining, s#remaining) with | 0, _ -> 0 | _, -1 -> -1 | rem, rem' -> min rem rem' method! seek len = source#seek_source#seek (min remaining len) method seek_source = source#seek_source method private check_for_override buf = List.iter (fun (_, m) -> Frame.Metadata.iter (fun lbl v -> if lbl = override_meta then ( try let v = float_of_string v in remaining <- Frame.main_of_seconds v; self#log#info "Overriding remaining value: %.02f." v with _ -> self#log#important "Invalid remaining override value: %s." v)) m) (Frame.get_all_metadata buf) method private generate_frame = let buf = s#get_frame in self#check_for_override buf; let pos = Frame.position buf in let len = min remaining pos in remaining <- remaining - len; if remaining <= 0 then s <- Debug_sources.empty (); if len < pos then Frame.slice buf len else buf end let _ = let return_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator "max_duration" [ ( "override", Lang.string_t, Some (Lang.string "liq_remaining"), Some "Metadata field which, if present and containing a float, overrides \ the remaining play time." ); ("", Lang.float_t, None, Some "Maximum duration"); ("", Lang.source_t return_t, None, None); ] ~category:`Track ~descr:"Limit source duration" ~return_t (fun p -> let override_meta = Lang.to_string (List.assoc "override" p) in let duration = Frame.main_of_seconds (Lang.to_float (Lang.assoc "" 1 p)) in let s = Lang.to_source (Lang.assoc "" 2 p) in new max_duration ~override_meta ~duration s) liquidsoap-2.3.2/src/core/operators/midi_routing.ml000066400000000000000000000064161477303350200224630ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Source class virtual base ~name (source : source) = object inherit operator ~name [source] method fallible = source#fallible method remaining = source#remaining method seek_source = source#seek_source method self_sync = source#self_sync method private can_generate_frame = source#is_ready method abort_track = source#abort_track end class merge (source : source) out = object inherit base source ~name:"midi.merge_all" method private generate_frame = let m = Content.Midi.get_data (source#get_mutable_content Frame.Fields.midi) in for c = 0 to Array.length m - 1 do MIDI.merge m.(out) m.(c); if c <> out then MIDI.clear_all m.(c) done; source#set_frame_data Frame.Fields.midi Content.Midi.lift_data m end class remove (source : source) t = object inherit base source ~name:"midi.remove" method private generate_frame = let m = Content.Midi.get_data (source#get_mutable_content Frame.Fields.midi) in List.iter (fun c -> if c < Array.length m then MIDI.clear_all m.(c)) t; source#set_frame_data Frame.Fields.midi Content.Midi.lift_data m end let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~midi:(Format_type.midi ()) ()) in Lang.add_operator ~base:Modules.midi "merge_all" [ ("track_out", Lang.int_t, Some (Lang.int 0), Some "Destination track."); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`MIDI ~descr:"Merge all MIDI tracks in one." (fun p -> let f v = List.assoc v p in let out = Lang.to_int (f "track_out") in let src = Lang.to_source (f "") in new merge src out) let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~midi:(Format_type.midi ()) ()) in Lang.add_operator ~base:Modules.midi "remove" [ ("", Lang.list_t Lang.int_t, None, Some "Tracks to remove."); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`MIDI ~descr:"Remove MIDI tracks." (fun p -> (* let f v = List.assoc v p in *) let t = List.map Lang.to_int (Lang.to_list (Lang.assoc "" 1 p)) in let src = Lang.to_source (Lang.assoc "" 2 p) in new remove src t) liquidsoap-2.3.2/src/core/operators/ms_stereo.ml000066400000000000000000000114501477303350200217640ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Source type mode = Encode | Decode class msstereo ~field (source : source) mode width = object inherit operator ~name:"stereo.ms.encode" [source] method fallible = source#fallible method private can_generate_frame = source#is_ready method remaining = source#remaining method seek_source = source#seek_source method self_sync = source#self_sync method abort_track = source#abort_track method private generate_frame = let buffer = Content.Audio.get_data (source#get_mutable_content field) in for i = 0 to source#frame_audio_position - 1 do match mode with | Encode -> let left = buffer.(0).(i) and right = buffer.(1).(i) in buffer.(0).(i) <- 0.5 *. (left +. right); (* mid *) buffer.(1).(i) <- 0.5 *. (left -. right) (* side *) | Decode -> let mid = buffer.(0).(i) and side = buffer.(1).(i) in buffer.(0).(i) <- mid +. (side *. width); (* left *) buffer.(1).(i) <- mid -. (side *. width) (* right *) done; source#set_frame_data field Content.Audio.lift_data buffer end let stereo_ms = Lang.add_module ~base:Stereo.stereo "ms" let _ = let return_t = Format_type.audio_stereo () in Lang.add_track_operator ~base:stereo_ms "encode" [("", return_t, None, None)] ~return_t ~category:`Audio ~descr:"Encode left+right stereo to mid+side stereo (M/S)." (fun p -> let field, s = Lang.to_track (Lang.assoc "" 1 p) in (field, new msstereo ~field s Encode 0.)) let _ = let return_t = Format_type.audio_stereo () in Lang.add_track_operator ~base:stereo_ms "decode" [ ( "width", Lang.float_t, Some (Lang.float 1.), Some "Width of the stereo field." ); ("", return_t, None, None); ] ~return_t ~category:`Audio ~descr:"Decode mid+side stereo (M/S) to left+right stereo." (fun p -> let field, s = Lang.to_track (Lang.assoc "" 1 p) in let w = Lang.to_float (Lang.assoc "width" 1 p) in (field, new msstereo ~field s Decode w)) class spatializer ~field ~width (source : source) = object inherit operator ~name:"stereo.width" [source] method fallible = source#fallible method private can_generate_frame = source#is_ready method remaining = source#remaining method seek_source = source#seek_source method self_sync = source#self_sync method abort_track = source#abort_track method private generate_frame = let position = source#frame_audio_position in let buf = Content.Audio.get_data (source#get_mutable_content field) in let width = width () in let width = (width +. 1.) /. 2. in let a = let w = width in let w' = 1. -. width in w /. sqrt ((w *. w) +. (w' *. w')) in for i = 0 to position - 1 do let left = buf.(0).(i) in let right = buf.(1).(i) in let mid = (left +. right) /. 2. in let side = (left -. right) /. 2. in buf.(0).(i) <- ((1. -. a) *. mid) -. (a *. side); buf.(1).(i) <- ((1. -. a) *. mid) +. (a *. side) done; source#set_frame_data field Content.Audio.lift_data buf end let _ = let return_t = Format_type.audio_stereo () in Lang.add_track_operator ~base:Stereo.stereo "width" [ ( "", Lang.getter_t Lang.float_t, Some (Lang.float 0.), Some "Width of the signal (-1: mono, 0.: original, 1.: wide stereo)." ); ("", return_t, None, None); ] ~return_t ~category:`Audio ~descr:"Spacializer which allows controlling the width of the signal." (fun p -> let width = Lang.assoc "" 1 p |> Lang.to_float_getter in let field, s = Lang.assoc "" 2 p |> Lang.to_track in (field, new spatializer ~field ~width s)) liquidsoap-2.3.2/src/core/operators/muxer.ml000066400000000000000000000226131477303350200211270ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) type field = { target_field : Frame.field; source_field : Frame.field } type track = { mutable fields : field list; source : Source.source } class muxer ~pos ~base tracks = let sources = List.fold_left (fun sources { source } -> if List.memq source sources then sources else source :: sources) (match base with Some s -> [Source_tracks.source s] | None -> []) tracks in let fallible = List.exists (fun s -> s#fallible) sources in let self_sync = Clock_base.self_sync sources in object (self) (* Pass duplicated list to operator to make sure caching is properly enabled. *) inherit Source.operator ~name:"source" sources method self_sync = self_sync ~source:self () method fallible = fallible method abort_track = List.iter (fun s -> s#abort_track) sources method private sources_ready = List.for_all (fun s -> s#is_ready) sources method private can_generate_frame = self#sources_ready method! seek len = let s = self#seek_source in if s == (self :> Source.source) then ( self#log#info "Operator is muxing from multiple sources and cannot seek without \ risking losing content synchronization!"; len) else s#seek len method seek_source = match List.fold_left (fun sources source -> let source = source#seek_source in if List.memq source sources then sources else source :: sources) [] sources with | [s] -> s#seek_source | _ -> (self :> Source.source) method remaining = List.fold_left (fun r s -> if r = -1 then s#remaining else min r s#remaining) (-1) (List.filter (fun (s : Source.source) -> s#is_ready) sources) val mutable muxed_tracks = None method private tracks = match muxed_tracks with | Some s -> s | None -> let base = match base with | Some source_tracks -> let fields = List.map (fun source_field -> { source_field; target_field = source_field }) (Source_tracks.fields source_tracks) in [{ source = Source_tracks.source source_tracks; fields }] | None -> [] in let tracks = match ( base, List.partition (fun { source = s } -> List.exists (fun { source = s' } -> s == s') base) tracks ) with | _, ([], _) -> base @ tracks | [{ fields = f }], ([({ fields = f' } as p)], tracks) -> { p with fields = f' @ List.filter (fun { target_field = t } -> List.exists (fun { target_field = t' } -> t = t') f') f; } :: tracks | _ -> assert false in if List.for_all (fun { fields } -> List.for_all (fun { target_field } -> target_field = Frame.Fields.metadata || target_field = Frame.Fields.track_marks) fields) tracks then Runtime_error.raise ~pos ~message: "source muxer needs at least one track with content that is \ not metadata or track_marks!" "invalid"; muxed_tracks <- Some tracks; tracks method generate_frame = let length = Lazy.force Frame.size in let pos, frame = List.fold_left (fun (pos, frame) { fields; source } -> let buf = source#get_frame in ( min pos (Frame.position buf), List.fold_left (fun frame { source_field; target_field } -> let c = Frame.get buf source_field in Frame.set frame target_field c) frame fields )) (length, Frame.create ~length Frame.Fields.empty) self#tracks in Frame.slice frame pos end let muxer_operator p = let base, tracks = match List.assoc "" p with | Liquidsoap_lang.Value.Custom { methods } as v when Source_tracks.is_value v -> (Some v, methods) | v -> (None, Liquidsoap_lang.Value.methods v) in let tracks = List.fold_left (fun tracks (label, t) -> let source_field, s = Lang.to_track t in let target_field = Frame.Fields.register label in let field = { source_field; target_field } in match List.find_opt (fun { source } -> source == s) tracks with | Some track -> track.fields <- field :: track.fields; tracks | None -> { source = s; fields = [field] } :: tracks) [] (Liquidsoap_lang.Methods.bindings tracks) in let s = new muxer ~pos:(try Lang.pos p with _ -> []) ~base tracks in let target_fields = List.fold_left (fun target_fields { source; fields } -> let source_fields, target_fields = List.fold_left (fun (source_fields, target_fields) { source_field; target_field } -> match source_field with | f when f = Frame.Fields.metadata -> (source_fields, target_fields) | f when f = Frame.Fields.track_marks -> (source_fields, target_fields) | _ -> let source_field_t = Lang.univ_t () in ( Frame.Fields.add source_field source_field_t source_fields, Frame.Fields.add target_field source_field_t target_fields )) (Frame.Fields.empty, target_fields) fields in let source_frame_t = Lang.frame_t (Lang.univ_t ()) source_fields in Typing.(source#frame_type <: source_frame_t); target_fields) Frame.Fields.empty tracks in Typing.( s#frame_type <: Lang.frame_t (match base with | Some s -> (Source_tracks.source s)#frame_type | None -> Lang.univ_t ()) target_fields); s let source = let frame_t = Lang.univ_t ~constraints:[Format_type.muxed_tracks] () in let tracks_t = Type.meth ~optional:true "track_marks" ([], Format_type.track_marks) (Type.meth ~optional:true "metadata" ([], Format_type.metadata) frame_t) in Lang.add_operator "source" ~category:`Input ~descr:"Create a source that muxes the given tracks." ~return_t:frame_t [("", tracks_t, None, Some "Tracks to mux")] muxer_operator let _ = let track_t = Lang.univ_t ~constraints:[Format_type.track] () in let return_t = Format_type.track_marks in Lang.add_track_operator ~base:Modules.track "track_marks" ~category:`Track ~descr:"Return the track marks associated with the given track" ~return_t [("", track_t, None, None)] (fun p -> let _, s = Lang.to_track (List.assoc "" p) in (Frame.Fields.track_marks, s)) let track_metadata = let track_t = Lang.univ_t ~constraints:[Format_type.track] () in let return_t = Format_type.metadata in Lang.add_track_operator ~base:Modules.track "metadata" ~category:`Track ~descr:"Return the metadata associated with the given track" ~return_t [("", track_t, None, None)] (fun p -> let _, s = Lang.to_track (List.assoc "" p) in (Frame.Fields.metadata, s)) let _ = let frame_t = Lang.univ_t () in let return_t = Lang_source.source_tracks_t frame_t in let arguments = [("", Lang.source_t ~methods:false frame_t, None, None)] in Lang.add_builtin ~base:source "tracks" ~category:(`Source `Track) ~descr:"Return the tracks of a given source." arguments return_t (fun env -> let return_t, env = Lang_source.check_arguments ~return_t ~env arguments in let return_t = Type.filter_meths return_t (fun { Type.meth } -> meth <> "metadata" && meth <> "track_marks") in let source_val = List.assoc "" env in let s = Lang.to_source source_val in Typing.(s#frame_type <: return_t); Source_tracks.to_value s) liquidsoap-2.3.2/src/core/operators/noblank.ml000066400000000000000000000271001477303350200214070ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Source (** Below, lengths are in audio samples, thresholds in RMS (in [0.;1.]). *) class virtual base ~start_blank ~track_sensitive ~max_blank ~min_noise ~threshold = object (self) (** State can be either - `Noise l: the source is considered to be emitting, but it has been silent for l samples; - `Blank l: the source is considered to be silent, but it has been noisy for l samples. *) val state = Atomic.make (if start_blank then `Blank 0 else `Noise 0) val dB_levels = Atomic.make None method dB_levels = Atomic.get dB_levels method virtual private log : Log.t method is_blank = match Atomic.get state with `Blank _ -> true | _ -> false method private string_of_state = function `Blank _ -> "blank" | `Noise _ -> "no blank" method private set_state s = begin match (Atomic.get state, s) with | `Blank _, `Noise _ | `Noise _, `Blank _ -> self#log#info "Setting state to %s" (self#string_of_state s) | _ -> () end; Atomic.set state s (** This method should be called after the frame [s] has been filled, where [p0] is the position in [s] before filling. *) method private check_blank s = if Frame.track_marks s <> [] then ( if (* Don't bother analyzing the end of this track, jump to the new state. *) track_sensitive () then self#set_state (`Noise 0)) else ( let len = AFrame.position s in let rms = AFrame.rms s 0 len in Atomic.set dB_levels (Some rms); let threshold = threshold () in let noise = Array.fold_left (fun noise r -> noise || r > threshold) false rms in match Atomic.get state with | `Noise blank_len -> if noise then (if blank_len <> 0 then self#set_state (`Noise 0)) else ( let blank_len = blank_len + len in if blank_len <= max_blank () then self#set_state (`Noise blank_len) else self#set_state (`Blank 0)) | `Blank noise_len -> if noise then ( let noise_len = noise_len + len in if noise_len < min_noise () then self#set_state (`Blank noise_len) else self#set_state (`Noise 0)) else if noise_len <> 0 then self#set_state (`Blank 0)) end class detect ~start_blank ~max_blank ~min_noise ~threshold ~track_sensitive ~on_blank ~on_noise source = object (self) inherit operator ~name:"blank.detect" [source] inherit base ~track_sensitive ~start_blank ~max_blank ~min_noise ~threshold method fallible = source#fallible method private can_generate_frame = source#is_ready method abort_track = source#abort_track method remaining = source#remaining method seek_source = source#seek_source method self_sync = source#self_sync method private generate_frame = let buf = source#get_frame in let was_blank = self#is_blank in let is_blank = self#check_blank buf; self#is_blank in (match (was_blank, is_blank) with | true, false -> ignore (Lang.apply on_noise []) | false, true -> ignore (Lang.apply on_blank []) | _ -> ()); buf end class strip ~start_blank ~max_blank ~min_noise ~threshold ~track_sensitive source = object (self) (* Stripping is easy: - declare yourself as unavailable when the source is silent - keep pulling data from the source during those times. *) inherit active_operator ~name:"blank.strip" [source] inherit base ~track_sensitive ~start_blank ~max_blank ~min_noise ~threshold method fallible = true method private can_generate_frame = (* This needs to be computed at all times as it makes sure that the source is ready to be ready from in [#output]. *) let is_source_ready = source#is_ready in (not self#is_blank) && is_source_ready method remaining = if self#is_blank then 0 else source#remaining method seek_source = if self#is_blank then (self :> Source.source) else source#seek_source method abort_track = source#abort_track method self_sync = source#self_sync method private generate_frame = source#get_frame method private output = if source#is_ready then self#check_blank source#get_frame method reset = () end class eat ~track_sensitive ~at_beginning ~start_blank ~max_blank ~min_noise ~threshold source_val = let source = Lang.to_source source_val in object (self) (* Eating blank is trickier than stripping. *) inherit operator ~name:"blank.eat" [] inherit base ~track_sensitive ~start_blank ~max_blank ~min_noise ~threshold inherit Child_support.base ~check_self_sync:true [source_val] (** We strip when the source is silent, but only at the beginning of tracks if [at_beginning] is passed. *) val mutable stripping = false val mutable beginning = true method fallible = true method private can_generate_frame = source#is_ready method remaining = source#remaining method seek_source = source#seek_source method abort_track = source#abort_track method self_sync = source#self_sync method private generate_frame = let first = ref true in let frame = ref self#empty_frame in while source#is_ready && (!first || stripping) do first := false; self#on_child_tick (fun () -> if source#is_ready then frame := source#get_frame); let frame = !frame in if track_sensitive () && Frame.track_marks frame <> [] then ( stripping <- false; beginning <- true); let was_blank = self#is_blank in let is_blank = self#check_blank frame; self#is_blank in match (was_blank, is_blank) with | false, true -> if beginning || not at_beginning then stripping <- true | true, false -> stripping <- false; beginning <- false | _ -> () done; !frame end let proto frame_t = [ ( "threshold", Lang.getter_t Lang.float_t, Some (Lang.float (-40.)), Some "Power in decibels under which the stream is considered silent." ); ( "start_blank", Lang.bool_t, Some (Lang.bool false), Some "Start assuming we have blank." ); ( "max_blank", Lang.getter_t Lang.float_t, Some (Lang.float 20.), Some "Maximum duration of silence allowed, in seconds." ); ( "min_noise", Lang.getter_t Lang.float_t, Some (Lang.float 0.), Some "Minimum duration of noise required to end silence, in seconds." ); ( "track_sensitive", Lang.getter_t Lang.bool_t, Some (Lang.bool true), Some "Reset blank counter at each track." ); ("", Lang.source_t frame_t, None, None); ] let extract p = let f v = List.assoc v p in let s = f "" in let start_blank = Lang.to_bool (f "start_blank") in let max_blank = let l = Lang.to_float_getter (f "max_blank") in fun () -> Frame.audio_of_seconds (l ()) in let min_noise = let l = Lang.to_float_getter (f "min_noise") in fun () -> Frame.audio_of_seconds (l ()) in let threshold = let v = f "threshold" in let t = Lang.to_float_getter v in fun () -> let t = t () in if t > 0. then raise (Error.Invalid_value (v, "threshold should be negative")); Audio.lin_of_dB t in let ts = Lang.to_bool_getter (f "track_sensitive") in (start_blank, max_blank, min_noise, threshold, ts, s) let meth () = [ ( "dB_levels", ([], Lang.fun_t [] (Lang.nullable_t (Lang.list_t Lang.float_t))), "Return the detected dB level for each channel.", fun s -> Lang.val_fun [] (fun _ -> match s#dB_levels with | None -> Lang.null | Some lvl -> Lang.list Array.( to_list (map (fun v -> Lang.float (Audio.dB_of_lin v)) lvl))) ); ( "is_blank", ([], Lang.fun_t [] Lang.bool_t), "Indicate whether blank was detected.", fun s -> Lang.val_fun [] (fun _ -> Lang.bool s#is_blank) ); ] let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:Blank.blank "detect" ~return_t:frame_t ~category:`Track ~meth:(meth ()) ~descr:"Calls a given handler when detecting a blank." (( "", Lang.fun_t [] Lang.unit_t, None, Some "Handler called when blank is detected." ) :: ( "on_noise", Lang.fun_t [] Lang.unit_t, Some (Lang.val_cst_fun [] Lang.unit), Some "Handler called when noise is detected." ) :: proto frame_t) (fun p -> let on_blank = Lang.assoc "" 1 p in let on_noise = Lang.assoc "on_noise" 1 p in let p = List.remove_assoc "" p in let start_blank, max_blank, min_noise, threshold, track_sensitive, s = extract p in new detect ~start_blank ~max_blank ~min_noise ~threshold ~track_sensitive ~on_blank ~on_noise (Lang.to_source s)) let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:Blank.blank "strip" ~return_t:frame_t ~meth:(meth ()) ~category:`Track ~descr: "Make the source unavailable when it is streaming blank. This is an \ active operator, meaning that the source used in this operator will be \ consumed continuously, even when it is not actively used." (proto frame_t) (fun p -> let start_blank, max_blank, min_noise, threshold, track_sensitive, s = extract p in new strip ~track_sensitive ~start_blank ~max_blank ~min_noise ~threshold (Lang.to_source s)) let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:Blank.blank "eat" ~return_t:frame_t ~category:`Track ~meth:(meth ()) ~descr: "Eat blanks, i.e., drop the contents of the stream until it is not blank \ anymore." (( "at_beginning", Lang.bool_t, Some (Lang.bool false), Some "Only eat at the beginning of a track." ) :: proto frame_t) (fun p -> let at_beginning = Lang.to_bool (List.assoc "at_beginning" p) in let start_blank, max_blank, min_noise, threshold, track_sensitive, s = extract p in new eat ~at_beginning ~track_sensitive ~start_blank ~max_blank ~min_noise ~threshold s) liquidsoap-2.3.2/src/core/operators/normalize.ml000066400000000000000000000156061477303350200217730ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Source class normalize ~track_sensitive (source : source) (* RMS target. *) rmst (* Number of samples for computing rms. *) window (* Spring coefficient when the sound is going louder. *) kup (* Spring coefficient when the sound is going less loud. *) kdown threshold gmin gmax = let rmsi = Frame.audio_of_seconds window in object (self) inherit operator ~name:"normalize" [source] (** Current squares of RMS. *) val mutable rms = 0. (** Current number of samples used to compute [rmsl] and [rmsr]. *) val mutable rmsc = 0 (** Volume coefficient. *) val mutable v = 1. (** Previous volume coefficient. *) val mutable vold = 1. method gain = v (** Last fully computed rms. *) val mutable last_rms = 0. method rms = last_rms method init = rms <- 0.; rmsc <- 0; v <- 1.; vold <- 1. initializer self#on_wake_up (fun () -> self#init) method fallible = source#fallible method remaining = source#remaining method seek_source = source#seek_source method self_sync = source#self_sync method private can_generate_frame = source#is_ready method abort_track = source#abort_track method private normalize buf = let b = Content.Audio.get_data (Frame.get buf Frame.Fields.audio) in let rmst = rmst () in let kup = kup () in let kdown = kdown () in let threshold = threshold () in let gmin = gmin () in let gmax = gmax () in for i = 0 to source#frame_audio_position - 1 do for c = 0 to self#audio_channels - 1 do let bc = b.(c) in let x = bc.(i) in rms <- rms +. (x *. x); bc.(i) <- x *. ((float rmsc *. vold) +. (float (rmsi - rmsc) *. v)) /. float rmsi done; rmsc <- rmsc + 1; if rmsc >= rmsi then ( let r = sqrt (rms /. float_of_int (rmsi * self#audio_channels)) in last_rms <- r; if r > threshold then if r *. v > rmst then v <- v +. (kdown *. ((rmst /. r) -. v)) else v <- v +. (kup *. ((rmst /. r) -. v)); vold <- v; v <- max gmin (min gmax v); rms <- 0.; rmsc <- 0) done; Frame.set_data buf Frame.Fields.audio Content.Audio.lift_data b method private generate_frame = match self#split_frame (source#get_mutable_frame Frame.Fields.audio) with | buf, None -> self#normalize buf | buf, Some new_track -> let buf = self#normalize buf in if track_sensitive then self#init; Frame.append buf (self#normalize new_track) end let normalize = Lang.add_module "normalize" let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:normalize "old" [ ( "target", Lang.getter_t Lang.float_t, Some (Lang.float (-13.)), Some "Desired RMS (dB)." ); ( "window", Lang.float_t, Some (Lang.float 0.1), Some "Duration of the window used to compute the current RMS power \ (second)." ); ( "k_up", Lang.getter_t Lang.float_t, Some (Lang.float 0.005), Some "Coefficient when the power must go up (between 0 and 1, slowest to \ fastest)." ); ( "k_down", Lang.getter_t Lang.float_t, Some (Lang.float 0.1), Some "Coefficient when the power must go down (between 0 and 1, slowest \ to fastest)." ); ( "threshold", Lang.getter_t Lang.float_t, Some (Lang.float (-40.)), Some "Minimal RMS for activaing gain control (dB)." ); ( "gain_min", Lang.getter_t Lang.float_t, Some (Lang.float (-6.)), Some "Minimal gain value (dB)." ); ( "gain_max", Lang.getter_t Lang.float_t, Some (Lang.float 6.), Some "Maximal gain value (dB)." ); ( "track_sensitive", Lang.bool_t, Some (Lang.bool true), Some "Reset values on every track." ); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr: "Normalize the signal. Dynamic normalization of the signal is sometimes \ the only option, and can make a listening experience much nicer. \ However, its dynamic aspect implies some limitations which can go as \ far as creating saturation in some extreme cases. If possible, consider \ using some track-based normalization techniques such as those based on \ replay gain. See the documentation for more details. This is the \ implementation provided in Liquidsoap < 2.0. A new, better and more \ customizable one is now given in `normalize`." ~meth: [ ( "gain", ([], Lang.fun_t [] Lang.float_t), "Current amplification coefficient.", fun s -> Lang.val_fun [] (fun _ -> Lang.float s#gain) ); ( "rms", ([], Lang.fun_t [] Lang.float_t), "Current RMS.", fun s -> Lang.val_fun [] (fun _ -> Lang.float s#rms) ); ] (fun p -> let f v = List.assoc v p in let target, window, kup, kdown, threshold, gmin, gmax, src = ( Lang.to_float_getter (f "target"), Lang.to_float (f "window"), Lang.to_float_getter (f "k_up"), Lang.to_float_getter (f "k_down"), Lang.to_float_getter (f "threshold"), Lang.to_float_getter (f "gain_min"), Lang.to_float_getter (f "gain_max"), Lang.to_source (f "") ) in let track_sensitive = Lang.to_bool (f "track_sensitive") in new normalize ~track_sensitive src (fun () -> Audio.lin_of_dB (target ())) window kup kdown (fun () -> Audio.lin_of_dB (threshold ())) (fun () -> Audio.lin_of_dB (gmin ())) (fun () -> Audio.lin_of_dB (gmax ()))) liquidsoap-2.3.2/src/core/operators/on_end.ml000066400000000000000000000067501477303350200212350ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) class on_end ~delay f s = object (self) inherit Source.operator ~name:"on_end" [s] inherit Latest_metadata.source val mutable executed = false val mutable started = false method fallible = s#fallible method private can_generate_frame = s#is_ready method remaining = s#remaining method abort_track = s#abort_track method seek_source = s#seek_source method self_sync = s#self_sync method private on_new_metadata = () method private on_end rem = if not executed then ignore (Lang.apply f [("", Lang.float rem); ("", Lang.metadata latest_metadata)]); executed <- true method private generate_frame = let rem = Frame.seconds_of_main s#remaining in let frame = s#get_frame in let has_started = started in started <- true; match self#split_frame frame with | buf, None -> self#save_latest_metadata buf; if 0. <= rem && rem <= delay () then self#on_end rem; buf | buf, Some new_track -> if has_started && not executed then ( self#log#important "New track occurred before the expected delay was reached!"; self#on_end (Frame.seconds_of_main (Frame.position buf))); self#clear_latest_metadata; self#save_latest_metadata new_track; executed <- false; frame end let _ = let return_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator ~base:Muxer.source "on_end" [ ( "delay", Lang.getter_t Lang.float_t, Some (Lang.float 5.), Some "Execute handler when remaining time is less or equal to this value." ); ("", Lang.source_t return_t, None, None); ( "", Lang.fun_t [(false, "", Lang.float_t); (false, "", Lang.metadata_t)] Lang.unit_t, None, Some "Function to execute. First argument is the remaining time, second \ is the latest metadata. That function should be fast because it is \ executed in the main streaming thread." ); ] ~category:`Track ~descr: "Call a given handler when there is less than a given amount of time \ remaining before then end of track." ~return_t (fun p -> let delay = Lang.to_float_getter (List.assoc "delay" p) in let s = Lang.assoc "" 1 p |> Lang.to_source in let f = Lang.assoc "" 2 p in new on_end ~delay f s) liquidsoap-2.3.2/src/core/operators/on_frame.ml000066400000000000000000000075421477303350200215610ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) class on_frame ~before f s = object inherit Source.operator ~name:"on_frame" [s] method fallible = s#fallible method private can_generate_frame = s#is_ready method abort_track = s#abort_track method remaining = s#remaining method seek_source = s#seek_source method self_sync = s#self_sync method private generate_frame = if before then ignore (Lang.apply f []); let ret = s#get_frame in if not before then ignore (Lang.apply f []); ret end let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator ~base:Muxer.source "on_frame" [ ( "before", Lang.bool_t, Some (Lang.bool true), Some "Execute the callback before computing the next frame." ); ("", Lang.source_t frame_t, None, None); ( "", Lang.fun_t [] Lang.unit_t, None, Some "Function called on every frame. It should be fast because it is \ executed in the main streaming thread." ); ] ~category:`Track ~descr:"Call a given handler on every frame." ~return_t:frame_t (fun p -> let before = List.assoc "before" p |> Lang.to_bool in let s = Lang.assoc "" 1 p |> Lang.to_source in let f = Lang.assoc "" 2 p in new on_frame ~before f s) (** Operations on frames. *) class frame_op ~name f default s = object inherit Source.operator ~name [s] method fallible = s#fallible method private can_generate_frame = s#is_ready method abort_track = s#abort_track method remaining = s#remaining method seek_source = s#seek_source method self_sync = s#self_sync val mutable value = default method value : Lang.value = value method private generate_frame = let buf = s#get_frame in let pos = Frame.position buf in value <- f buf 0 pos; buf end let source_frame = Lang.add_module ~base:Muxer.source "frame" let op name descr f_t f default = let frame_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in ignore (Lang.add_operator ~base:source_frame name [("", Lang.source_t frame_t, None, None)] ~category:`Track ~descr ~return_t:(Lang.method_t frame_t [("frame_" ^ name, ([], f_t), descr)]) ~meth:[("frame_" ^ name, ([], f_t), descr, fun s -> s#value)] (fun p -> let s = List.assoc "" p |> Lang.to_source in new frame_op ~name f default s)) let () = op "duration" "Compute the duration of the last frame." Lang.float_t (fun _ _ len -> Lang.float (Frame.seconds_of_main len)) (Lang.float 0.); op "rms" "Compute the rms of the last frame." Lang.float_t (fun buf off len -> let rms = Mm.Audio.Analyze.rms (AFrame.pcm buf) (Frame.audio_of_main off) (Frame.audio_of_main len) in let rms = Array.fold_left max 0. rms in Lang.float rms) (Lang.float 0.) liquidsoap-2.3.2/src/core/operators/on_metadata.ml000066400000000000000000000045641477303350200222500ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2019 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) class on_metadata f s = object (self) inherit Source.operator ~name:"on_metadata" [s] method fallible = s#fallible method private can_generate_frame = s#is_ready method abort_track = s#abort_track method remaining = s#remaining method seek_source = s#seek_source method self_sync = s#self_sync method private generate_frame = let buf = s#get_frame in List.iter (fun (p, m) -> self#log#debug "on_metadata: got metadata at position %d: calling handler..." p; ignore (Lang.apply f [("", Lang.metadata m)])) (Frame.get_all_metadata buf); buf end let _ = let return_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator ~base:Muxer.source "on_metadata" [ ("", Lang.source_t return_t, None, None); ( "", Lang.fun_t [ (false, "", Lang.list_t (Lang.product_t Lang.string_t Lang.string_t)); ] Lang.unit_t, None, Some "Function called on every metadata packet in the stream. It should \ be fast because it is executed in the main streaming thread." ); ] ~category:`Track ~descr:"Call a given handler on metadata packets." ~return_t (fun p -> let s = Lang.assoc "" 1 p |> Lang.to_source in let f = Lang.assoc "" 2 p in new on_metadata f s) liquidsoap-2.3.2/src/core/operators/on_offset.ml000066400000000000000000000077631477303350200217620ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) exception Invalid_override of string let ( -- ) = Int64.sub let ( ++ ) = Int64.add let ticks_of_offset offset = Int64.of_float (offset *. float (Lazy.force Frame.main_rate)) class on_offset ~force ~offset f s = object (self) inherit Source.operator ~name:"on_offset" [s] inherit Latest_metadata.source method fallible = s#fallible method private can_generate_frame = s#is_ready method remaining = s#remaining method abort_track = s#abort_track method seek_source = s#seek_source method self_sync = s#self_sync val mutable elapsed = 0L method offset = ticks_of_offset (offset ()) val mutable executed = false method private execute = self#log#info "Executing on_offset callback."; let pos = Int64.to_float elapsed /. float (Lazy.force Frame.main_rate) in ignore (Lang.apply f [("", Lang.float pos); ("", Lang.metadata latest_metadata)]); executed <- true method private on_new_metadata = () method private on_frame buf = self#save_latest_metadata buf; let new_pos = Int64.of_int (Frame.position buf) in elapsed <- elapsed ++ new_pos; if (not executed) && self#offset <= elapsed then self#execute method private generate_frame = let buf = s#get_frame in match self#split_frame buf with | frame, None -> self#on_frame frame; frame | frame, Some new_frame -> self#on_frame frame; if force && not executed then self#execute; executed <- false; self#clear_latest_metadata; elapsed <- Int64.of_int (Frame.position new_frame); buf end let _ = let return_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator "on_offset" [ ( "offset", Lang.getter_t Lang.float_t, None, Some "Execute handler when position in track is equal or more than to \ this value." ); ( "force", Lang.bool_t, Some (Lang.bool false), Some "Force execution of callback if track ends before 'offset' position \ has been reached." ); ( "", Lang.fun_t [(false, "", Lang.float_t); (false, "", Lang.metadata_t)] Lang.unit_t, None, Some "Function to execute. First argument is the actual position within \ the current track, second is the latest metadata. That function \ should be fast because it is executed in the main streaming thread." ); ("", Lang.source_t return_t, None, None); ] ~category:`Track ~descr: "Call a given handler when position in track is equal or more than a \ given amount of time." ~return_t (fun p -> let offset = List.assoc "offset" p |> Lang.to_float_getter in let force = List.assoc "force" p |> Lang.to_bool in let f = Lang.assoc "" 1 p in let s = Lang.to_source (Lang.assoc "" 2 p) in new on_offset ~offset ~force f s) liquidsoap-2.3.2/src/core/operators/on_track.ml000066400000000000000000000047721477303350200215750ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2019 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) class on_track f s = object inherit Source.operator ~name:"on_track" [s] method fallible = s#fallible method private can_generate_frame = s#is_ready method abort_track = s#abort_track method remaining = s#remaining method seek_source = s#seek_source method self_sync = s#self_sync method private generate_frame = let buf = s#get_frame in if Frame.track_marks buf <> [] then begin let m = match s#last_metadata with | None -> Lang.list [] | Some m -> Lang.metadata m in ignore (Lang.apply f [("", m)]) end; buf end let _ = let return_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator ~base:Muxer.source "on_track" [ ("", Lang.source_t return_t, None, None); ( "", Lang.fun_t [ (false, "", Lang.list_t (Lang.product_t Lang.string_t Lang.string_t)); ] Lang.unit_t, None, Some "Function called on every beginning of track in the stream, with the \ corresponding metadata as argument. If there is no metadata at the \ beginning of track, the empty list is passed. That function should \ be fast because it is executed in the main streaming thread." ); ] ~category:`Track ~descr:"Call a given handler on new tracks." ~return_t (fun p -> let s = Lang.assoc "" 1 p |> Lang.to_source in let f = Lang.assoc "" 2 p in new on_track f s) liquidsoap-2.3.2/src/core/operators/pan.ml000066400000000000000000000053511477303350200205450ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Source class pan ~field (source : source) phi phi_0 = object inherit operator ~name:"pan" [source] method fallible = source#fallible method private can_generate_frame = source#is_ready method remaining = source#remaining method seek_source = source#seek_source method abort_track = source#abort_track method self_sync = source#self_sync method private generate_frame = let buffer = Content.Audio.get_data (source#get_mutable_content field) in (* Degrees to radians + half field. *) let phi_0 = phi_0 () *. Float.pi /. 360. in (* Map -1 / 1 to radians. *) let phi = phi () *. phi_0 in let gain_left = (tan phi_0 -. tan phi) /. 2. in let gain_right = (tan phi_0 +. tan phi) /. 2. in let len = source#frame_audio_position in Audio.Mono.amplify gain_left buffer.(0) 0 len; Audio.Mono.amplify gain_right buffer.(1) 0 len; source#set_frame_data field Content.Audio.lift_data buffer end let _ = let track_t = Format_type.audio_stereo () in Lang.add_track_operator ~base:Stereo.stereo "pan" [ ( "field", Lang.getter_t Lang.float_t, Some (Lang.float 90.), Some "Field width in degrees (between 0 and 90)." ); ( "", Lang.getter_t Lang.float_t, None, Some "Pan value. Should be between `-1` (left side) and `1` (right side)." ); ("", track_t, None, None); ] ~return_t:track_t ~category:`Audio ~descr:"Pan a stereo sound." (fun p -> let phi_0 = Lang.to_float_getter (List.assoc "field" p) in let phi = Lang.to_float_getter (Lang.assoc "" 1 p) in let field, s = Lang.to_track (Lang.assoc "" 2 p) in (field, new pan ~field s phi phi_0)) liquidsoap-2.3.2/src/core/operators/pipe.ml000066400000000000000000000303201477303350200207160ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Source type next_stop = [ `Metadata of Frame.metadata | `Break_and_metadata of Frame.metadata | `Break | `Sleep | `Nothing ] type chunk = { sbuf : Bytes.t; next : next_stop; mutable ofs : int; mutable len : int; } class pipe ~replay_delay ~data_len ~process ~bufferize ~max ~restart ~restart_on_error source_val = (* We need a temporary log until the source has an id *) let log_ref = ref (fun _ -> ()) in let log x = !log_ref x in let log_error = ref (fun _ -> ()) in let abg_max_len = Frame.audio_of_seconds max in let replay_delay = Frame.audio_of_seconds replay_delay in let resampler = Decoder_utils.samplerate_converter () in let len = match data_len with x when x < 0 -> None | l -> Some l in let mutex = Mutex.create () in let replay_pending = ref [] in let next_stop = ref `Nothing in let header_read = ref false in let bytes = Bytes.create Utils.pagesize in let source = Lang.to_source source_val in object (self) inherit source ~name:"pipe" () (* We are expecting real-rate with a couple of hickups.. *) inherit Child_support.base ~check_self_sync:false [source_val] inherit! Generated.source ~empty_on_abort:false ~bufferize () val mutable samplesize = 16 val mutable samplerate = Frame.audio_of_seconds 1. (* Filled in by wake_up. *) val mutable converter = fun _ _ _ -> assert false method! self_sync = source#self_sync method private header = Bytes.unsafe_of_string (Wav_aiff.wav_header ~channels:self#audio_channels ~sample_rate:samplerate ?len ~sample_size:16 ()) method private on_start push = Process_handler.really_write self#header push; `Continue method private on_stdout pull = if not !header_read then ( let wav = Wav_aiff.read_header Wav_aiff.callback_ops pull in header_read := true; Mutex_utils.mutexify mutex (fun () -> if Wav_aiff.channels wav <> self#audio_channels then failwith "Invalid channels from pipe process!"; samplesize <- Wav_aiff.sample_size wav; samplerate <- Wav_aiff.sample_rate wav; converter <- Decoder_utils.from_iff ~format:`Wav ~channels:self#audio_channels ~samplesize) (); `Reschedule `Non_blocking) else ( let len = pull bytes 0 Utils.pagesize in let data = converter bytes 0 len in let data, ofs, len = resampler ~samplerate data 0 (Audio.length data) in let buffered = Generator.length self#buffer in let duration = Frame.main_of_audio len in let offset = Frame.main_of_audio ofs in Generator.put self#buffer Frame.Fields.audio (Content.Audio.lift_data ~offset ~length:duration data); let to_replay = Mutex_utils.mutexify mutex (fun () -> let pending = !replay_pending in let to_replay, pending = List.fold_left (fun ((pos, b), cur) (pos', b') -> if pos' + len > replay_delay then ( if pos > 0 then log "Cannot replay multiple element at once.. Picking up \ the most recent"; if pos > 0 && pos < pos' then ((pos, b), cur) else ((pos', b'), cur)) else ((pos, b), (pos' + len, b') :: cur)) ((-1, `Nothing), []) pending in replay_pending := pending; to_replay) () in begin match to_replay with | -1, _ -> () | _, `Break_and_metadata m -> Generator.add_metadata self#buffer m; Generator.add_track_mark self#buffer | _, `Metadata m -> Generator.add_metadata self#buffer m | _, `Break -> Generator.add_track_mark self#buffer | _ -> () end; if abg_max_len < buffered + len then `Delay (Frame.seconds_of_audio (buffered + len - abg_max_len)) else `Continue) val mutable handler = None val to_write = Queue.create () method fallible = true method private get_handler = match handler with Some h -> h | None -> raise Process_handler.Finished method private child_get = let frame = ref self#empty_frame in self#on_child_tick (fun () -> if source#is_ready then frame := source#get_frame); !frame method private get_to_write = if source#is_ready then ( let frame = self#child_get in let buf = AFrame.pcm frame in let blen = Audio.length buf in let slen_of_len len = 2 * len * Array.length buf in let slen = slen_of_len blen in let sbuf = Bytes.create slen in Audio.S16LE.of_audio buf 0 sbuf 0 blen; let metadata = List.sort (fun (pos, _) (pos', _) -> compare pos pos') (Frame.get_all_metadata frame) in let track_mark = match Frame.track_marks frame with p :: _ -> Some p | [] -> None in let ofs = List.fold_left (fun ofs (pos, m) -> let pos = slen_of_len pos in let len = pos - ofs in let next = if track_mark = Some pos then `Break_and_metadata m else `Metadata m in Queue.push { sbuf; next; ofs; len } to_write; pos) 0 metadata in if ofs < slen then ( let len = slen - ofs in let next = if track_mark <> None then `Break else `Nothing in Queue.push { sbuf; next; ofs; len } to_write)) method private on_stdin pusher = if Queue.is_empty to_write then self#get_to_write; try let ({ sbuf; next; ofs; len } as chunk) = Queue.peek to_write in (* Select documentation: large write may still block.. *) let wlen = min Utils.pagesize len in let ret = pusher sbuf ofs wlen in if ret = len then ( let action = if next <> `Nothing && replay_delay >= 0 then ( Mutex_utils.mutexify mutex (fun () -> replay_pending := (0, next) :: !replay_pending) (); `Continue) else ( Mutex_utils.mutexify mutex (fun () -> next_stop := next) (); if next <> `Nothing then `Stop else `Continue) in ignore (Queue.take to_write); action) else ( chunk.ofs <- ofs + ret; chunk.len <- len - ret; `Continue) with Queue.Empty -> `Continue method private on_stderr reader = let len = reader bytes 0 Utils.pagesize in !log_error (Bytes.unsafe_to_string (Bytes.sub bytes 0 len)); `Continue method private on_stop = Mutex_utils.mutexify mutex (fun e -> let ret = !next_stop in next_stop := `Nothing; header_read := false; let should_restart = match e with | `Status s when s <> Unix.WEXITED 0 -> restart_on_error | `Exception _ -> restart_on_error | _ -> true in match (should_restart, ret) with | false, _ -> false | _, `Sleep -> false | _, `Break_and_metadata m -> Generator.add_metadata self#buffer m; Generator.add_track_mark self#buffer; true | _, `Metadata m -> Generator.add_metadata self#buffer m; true | _, `Break -> Generator.add_track_mark self#buffer; true | _, `Nothing -> restart) initializer self#on_wake_up (fun () -> source#wake_up; converter <- Decoder_utils.from_iff ~format:`Wav ~channels:self#audio_channels ~samplesize; (* Now we can create the log function *) log_ref := self#log#info "%s"; log_error := self#log#debug "%s"; handler <- Some (Process_handler.run ~on_stop:self#on_stop ~on_start:self#on_start ~on_stdout:self#on_stdout ~on_stdin:self#on_stdin ~priority:`Blocking ~on_stderr:self#on_stderr ~log process)) method! abort_track = source#abort_track method! sleep = Mutex_utils.mutexify mutex (fun () -> try next_stop := `Sleep; replay_pending := []; Process_handler.stop self#get_handler; handler <- None with Process_handler.Finished -> ()) () end let _ = let return_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator "pipe" [ ("process", Lang.string_t, None, Some "Process used to pipe data to."); ( "replay_delay", Lang.nullable_t Lang.float_t, Some Lang.null, Some "Replay track marks and metadata from the input source on the output \ after a given delay. If `null` (default) close and flush the \ process on each track and metadata to get an exact timing. This \ parameter is typically used when integrating with `stereotool`." ); ( "data_length", Lang.nullable_t Lang.int_t, Some Lang.null, Some "Length passed in the WAV data chunk. Data is streamed so no the \ consuming program should process it as it comes. Some program \ operate better when this value is set to `0`, some other when it is \ set to the maximum length allowed by the WAV specs. Use any \ negative value to set to maximum length." ); ( "buffer", Lang.float_t, Some (Lang.float 1.), Some "Duration of the pre-buffered data." ); ( "max", Lang.float_t, Some (Lang.float 10.), Some "Maximum duration of the buffered data." ); ( "restart", Lang.bool_t, Some (Lang.bool true), Some "Restart process when exited." ); ( "restart_on_error", Lang.bool_t, Some (Lang.bool true), Some "Restart process when exited with error." ); ("", Lang.source_t return_t, None, None); ] ~return_t ~category:`Audio ~descr:"Process audio signal through a given process stdin/stdout." (fun p -> let f v = List.assoc v p in let ( process, replay_delay, data_len, bufferize, max, restart, restart_on_error, src ) = ( Lang.to_string (f "process"), Lang.to_option (f "replay_delay"), Lang.to_option (f "data_length"), Lang.to_float (f "buffer"), Lang.to_float (f "max"), Lang.to_bool (f "restart"), Lang.to_bool (f "restart_on_error"), f "" ) in let replay_delay = match replay_delay with None -> -1. | Some v -> Lang.to_float v in let data_len = match data_len with None -> -1 | Some v -> Lang.to_int v in (new pipe ~replay_delay ~data_len ~bufferize ~max ~restart ~restart_on_error ~process src :> source)) liquidsoap-2.3.2/src/core/operators/pitch.ml000066400000000000000000000115031477303350200210720ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Source module Ringbuffer = Audio.Ringbuffer let average_diff delta buf ofs len = let s = ref 0. in for i = 0 to len - delta - 1 do for c = 0 to Array.length buf - 1 do s := !s +. Utils.abs_float (buf.(c).(ofs + i + delta) -. buf.(c).(ofs + i)) done done; !s /. float ((len - delta) * Array.length buf) let note_of_freq f = let x = (log (f /. 440.) /. log 2.) +. 1. in let x = if x < 0. then 100. +. x else x in let x, _ = modf x in int_of_float (x *. 12.) mod 12 let string_of_note = function | 0 -> "A" | 1 -> "A#/Bb" | 2 -> "B" | 3 -> "C" | 4 -> "C#/Db" | 5 -> "D" | 6 -> "D#/Eb" | 7 -> "E" | 8 -> "F" | 9 -> "F#/Gb" | 10 -> "G" | 11 -> "G#/Ab" | _ -> assert false class pitch every length freq_min freq_max (source : source) = (* Compute a wave length from a frequency. *) let samples_per_second = float (Frame.audio_of_seconds 1.) in let wl f = int_of_float (samples_per_second /. f) in let length = Frame.audio_of_seconds length in object (self) inherit operator ~name:"pitch" [source] val mutable ring = None method ring : Ringbuffer.t = match ring with | Some ring -> ring | None -> let r = Ringbuffer.create self#audio_channels (2 * length) in ring <- Some r; r (** Array used to get data to analyze. *) method databuf = Lazy.force (Lazy.from_fun (fun () -> Audio.create self#audio_channels length)) val mutable computations = -1 method fallible = source#fallible method remaining = source#remaining method seek_source = source#seek_source method private can_generate_frame = source#is_ready method abort_track = source#abort_track method self_sync = source#self_sync method private generate_frame = let buf = Content.Audio.get_data (source#get_mutable_content Frame.Fields.audio) in let ring = self#ring in let databuf = self#databuf in Ringbuffer.write ring buf; if Ringbuffer.read_space ring > length then Ringbuffer.read_advance ring (Ringbuffer.read_space ring - length); computations <- (computations + 1) mod every; if computations = 0 && Ringbuffer.read_space ring >= length then ( let wl_min = wl freq_max in let wl_max = wl freq_min in let d_opt = ref infinity in let wl_opt = ref 0 in Ringbuffer.read ring databuf; for l = wl_min to wl_max do let d = average_diff l databuf 0 length in if d < !d_opt then ( (* Printf.printf "d: %.02f l: %d\n%!" d l; *) d_opt := d; wl_opt := l) done; let f = samples_per_second /. float !wl_opt in let f = if f > freq_max then 0. else f in self#log#important "Found frequency: %.02f (%s)\n%!" f (string_of_note (note_of_freq f))); source#set_frame_data Frame.Fields.audio Content.Audio.lift_data buf end let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator "pitch" [ ( "length", Lang.float_t, Some (Lang.float 0.1), Some "Length in seconds of the analysis window" ); ("freq_min", Lang.float_t, Some (Lang.float 40.), Some "Minimal frequency"); ( "freq_max", Lang.float_t, Some (Lang.float 10000.), Some "Maximal frequency" ); ("", Lang.source_t frame_t, None, None); ] ~return_t:frame_t ~category:`Audio ~descr:"Compute the pitch of a sound." ~flags:[`Hidden; `Experimental] (fun p -> let f v = List.assoc v p in let length = Lang.to_float (f "length") in let freq_min = Lang.to_float (f "freq_min") in let freq_max = Lang.to_float (f "freq_max") in let src = Lang.to_source (f "") in (new pitch 10 length freq_min freq_max src :> Source.source)) liquidsoap-2.3.2/src/core/operators/replaygain_op.ml000066400000000000000000000062611477303350200226210ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Source class replaygain (source : source) = object (self) inherit operator ~name:"source.replaygain.compute" [source] val mutable override = None method fallible = source#fallible method private can_generate_frame = source#is_ready method remaining = source#remaining method abort_track = source#abort_track method seek_source = source#seek_source method self_sync = source#self_sync val mutable state = None method reset = state <- Some (Audio.Analyze.ReplayGain.create ~channels:self#audio_channels ~samplerate:(Frame.audio_of_seconds 1.)) method private state = if state = None then self#reset; Option.get state method private generate_frame = let buf = source#get_frame in Audio.Analyze.ReplayGain.process self#state (AFrame.pcm buf) 0 source#frame_audio_position; buf method peak = Audio.Analyze.ReplayGain.peak self#state method gain = Audio.Analyze.ReplayGain.gain self#state end let source_replaygain = Lang.add_module ~base:Muxer.source "replaygain" let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:source_replaygain "compute" ~meth: [ ( "reset", ([], Lang.fun_t [] Lang.unit_t), "Reset ReplayGain computation.", fun s -> Lang.val_fun [] (fun _ -> s#reset; Lang.unit) ); ( "peak", ([], Lang.fun_t [] Lang.float_t), "Peak sample.", fun s -> Lang.val_fun [] (fun _ -> Lang.float s#peak) ); ( "gain", ([], Lang.fun_t [] Lang.float_t), "Suggested gain (in dB).", fun s -> Lang.val_fun [] (fun _ -> Lang.float s#gain) ); ] [("", Lang.source_t frame_t, None, None)] ~return_t:frame_t ~category:`Audio ~descr: "Compute the ReplayGain of the source. Data is accumulated until the \ `gain` method is called, i.e. the gain is computed _after_ the source \ has been played.." (fun p -> let s = List.assoc "" p |> Lang.to_source in new replaygain s) liquidsoap-2.3.2/src/core/operators/resample.ml000066400000000000000000000103501477303350200215720ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Source class resample ~field ~ratio source = let source_val = Lang.source source in let write_frame_ref = ref (fun _ -> ()) in let consumer = new Producer_consumer.consumer ~write_frame:(fun _ frame -> !write_frame_ref frame) ~name:"stretch.consumer" ~source:source_val () in let () = Typing.(consumer#frame_type <: source#frame_type) in object (self) inherit operator ~name:"stretch" [] inherit Child_support.base ~check_self_sync:true [source_val] method self_sync = source#self_sync method fallible = source#fallible method! seek len = let glen = min (Generator.length self#buffer) len in Generator.truncate self#buffer glen; (if glen < len then source#seek (len - glen) else 0) + glen method seek_source = (self :> Source.source) method remaining = let rem = source#remaining in if rem = -1 then rem else int_of_float (float (rem + Generator.length self#buffer) *. ratio ()) method abort_track = source#abort_track method private can_generate_frame = source#is_ready val mutable converter = None initializer self#on_wake_up (fun () -> converter <- Some (Audio_converter.Samplerate.create self#audio_channels); write_frame_ref := self#write_frame) method private write_frame = function `Frame frame -> self#process_frame frame | `Flush -> () method private process_frame frame = let ratio = ratio () in let content = Content.Audio.get_data (Frame.get frame field) in let converter = Option.get converter in let pcm, offset, length = Audio_converter.Samplerate.resample converter ratio content 0 (Audio.length content) in let offset = Frame_settings.main_of_audio offset in let length = Frame_settings.main_of_audio length in Generator.put self#buffer field (Content.Audio.lift_data ~offset ~length pcm); let convert x = int_of_float (float x *. ratio) in List.iter (fun (pos, m) -> Generator.add_metadata ~pos:(convert pos) self#buffer m) (Frame.get_all_metadata frame); List.iter (fun pos -> Generator.add_track_mark ~pos:(convert pos) self#buffer) (Frame.track_marks frame) method private generate_frame = consumer#set_output_enabled true; while Generator.length self#buffer < Lazy.force Frame.size && source#is_ready do self#child_tick done; consumer#set_output_enabled false; Generator.slice self#buffer (Lazy.force Frame.size) end let _ = let return_t = Format_type.audio () in Lang.add_track_operator ~base:Modules.track_audio "stretch" (* TODO better name *) [ ( "ratio", Lang.getter_t Lang.float_t, None, Some "A value higher than 1 means slowing down." ); ("", return_t, None, None); ] ~return_t ~category:`Audio ~descr: "Slow down or accelerate an audio stream by stretching (sounds lower) or \ squeezing it (sounds higher)." (fun p -> let f v = List.assoc v p in let field, src = Lang.to_track (f "") in let ratio = Lang.to_float_getter (f "ratio") in (field, (new resample ~field ~ratio src :> Source.source))) liquidsoap-2.3.2/src/core/operators/rms_smooth.ml000066400000000000000000000060211477303350200221540ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Source class rms ~tau source = let samplerate = float (Lazy.force Frame.audio_rate) in object (self) inherit operator [source] ~name:"rms" method fallible = source#fallible method private can_generate_frame = source#is_ready method remaining = source#remaining method seek_source = source#seek_source method abort_track = source#abort_track method self_sync = source#self_sync val mutable rms = 0. method rms = sqrt rms method private generate_frame = let chans = self#audio_channels in let a = 1. -. exp (-1. /. (tau () *. samplerate)) in let frame = source#get_frame in let position = AFrame.position frame in let buf = AFrame.pcm frame in for i = 0 to position - 1 do let r = ref 0. in for c = 0 to chans - 1 do let x = buf.(c).(i) in r := !r +. (x *. x) done; let r = !r /. float chans in rms <- ((1. -. a) *. rms) +. (a *. r) done; frame end let _ = let return_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:Window_op.rms "smooth" ~category:`Visualization ~meth: [ ( "rms", ([], Lang.fun_t [] Lang.float_t), "Current value for the RMS.", fun s -> Lang.val_fun [] (fun _ -> Lang.float s#rms) ); ] ~return_t ~descr: "Compute the current RMS for the source, this varies more smoothly that \ `rms` and is updated more frequently. Returns the source with a method \ `rms`." [ ( "duration", Lang.getter_t Lang.float_t, Some (Lang.float 0.5), Some "Duration of the window in seconds (more precisely, this is the time \ constant of the low-pass filter)." ); ("", Lang.source_t return_t, None, None); ] (fun p -> let duration = List.assoc "duration" p |> Lang.to_float_getter in let src = List.assoc "" p |> Lang.to_source in new rms ~tau:duration src) liquidsoap-2.3.2/src/core/operators/sequence.ml000066400000000000000000000135431477303350200216010ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Source (** Given a list of [sources], play one track from each of the first sources, then loop on the last one. Optionally, merge tracks when advancing in the sequence. The [merge] flag will *not* merge tracks while looping on the last source -- this behavior would not be suited to the current use of [sequence] in transitions. *) class sequence ?(merge = false) ?(single_track = true) sources = let self_sync_type = Clock_base.self_sync_type sources in let seq_sources = Atomic.make sources in object (self) inherit operator ~name:"sequence" sources inherit generate_from_multiple_sources ~merge:(fun () -> merge && List.length (Atomic.get seq_sources) <> 1) ~track_sensitive:(fun () -> true) () method self_sync = ( Lazy.force self_sync_type, match sources with hd :: _ -> snd hd#self_sync | [] -> None ) method fallible = match List.rev sources with hd :: _ -> hd#fallible | [] -> true (* We have to wait until at least one source is ready. *) val mutable has_started = false method queue = Atomic.get seq_sources method private has_started = match has_started with | true -> true | false -> has_started <- List.exists (fun s -> s#is_ready) self#queue; has_started method private get_stateful_source ?(source_skipped = false) ~reselect () = match (self#has_started, self#queue) with | _, [] -> None | true, s :: [] -> if self#can_reselect ~reselect:(match reselect with `Force -> `Ok | _ -> reselect) s then Some s else None | true, s :: rest -> if self#can_reselect ~reselect: (match reselect with | `After_position _ when (not source_skipped) && single_track -> `Force | v -> v) s then Some s else ( self#log#info "Finished with %s" s#id; Atomic.set seq_sources rest; self#get_stateful_source ~source_skipped:true ~reselect:(match reselect with `Force -> `Ok | v -> v) ()) | _ -> None method private get_source ~reselect () = self#get_stateful_source ~reselect () method remaining = if merge then ( let ( + ) a b = if a < 0 || b < 0 then -1 else a + b in List.fold_left ( + ) 0 (List.map (fun s -> s#remaining) self#queue)) else (List.hd self#queue)#remaining method seek_source = match self#queue with | s :: _ -> s#seek_source | _ -> (self :> Source.source) method abort_track = if merge then ( match List.rev self#queue with | [] -> assert false | hd :: _ -> Atomic.set seq_sources [hd]); match self#queue with hd :: _ -> hd#abort_track | _ -> () end class merge_tracks source = object inherit operator ~name:"sequence" [source] method fallible = source#fallible method private can_generate_frame = source#is_ready method abort_track = source#abort_track method remaining = -1 method self_sync = source#self_sync method seek_source = source#seek_source method private generate_frame = let buf = source#get_frame in Frame.set buf Frame.Fields.track_marks (Content.make ~length:(Frame.position buf) Content_timed.Track_marks.format) end let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator "sequence" [ ( "merge", Lang.bool_t, Some (Lang.bool false), Some "Merge tracks when advancing from one source to the next one. This \ will NOT merge consecutive tracks from the last source; see \ merge_tracks() if you need that too." ); ( "single_track", Lang.bool_t, Some (Lang.bool true), Some "Advance to the new track in the sequence on new track. Set to \ `false` to play each source until it becomes unavailable." ); ("", Lang.list_t (Lang.source_t frame_t), None, None); ] ~category:`Track ~descr: "Play a sequence of sources. By default, play one track per source, \ except for the last one which is played as much as available." ~return_t:frame_t ~meth: [ ( "queue", ([], Lang.fun_t [] (Lang.list_t (Lang.source_t frame_t))), "Return the current sequence of source", fun s -> Lang.val_fun [] (fun _ -> Lang.list (List.map Lang.source s#queue)) ); ] (fun p -> new sequence ~merge:(Lang.to_bool (List.assoc "merge" p)) ~single_track:(Lang.to_bool (List.assoc "single_track" p)) (Lang.to_source_list (List.assoc "" p))) liquidsoap-2.3.2/src/core/operators/soundtouch_op.ml000066400000000000000000000107001477303350200226520ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Source class soundtouch source_val rate tempo pitch = let source = Lang.to_source source_val in let write_frame_ref = ref (fun _ -> ()) in let consumer = new Producer_consumer.consumer ~write_frame:(fun _ frame -> !write_frame_ref frame) ~name:"soundtouch.consumer" ~source:source_val () in let () = Typing.(consumer#frame_type <: source#frame_type); Typing.(source#frame_type <: consumer#frame_type) in object (self) inherit operator ~name:"soundtouch" [] inherit Child_support.base ~check_self_sync:true [source_val] val mutable st = None method fallible = source#fallible method self_sync = source#self_sync method private can_generate_frame = source#is_ready method seek_source = source#seek_source method remaining = -1 method abort_track = Generator.add_track_mark self#buffer; source#abort_track method private write_frame = function `Frame databuf -> self#process_frame databuf | `Flush -> () method process_frame databuf = let st = Option.get st in Soundtouch.set_rate st (rate ()); Soundtouch.set_tempo st (tempo ()); Soundtouch.set_pitch st (pitch ()); let db = AFrame.pcm databuf in Soundtouch.put_samples_ni st db 0 (AFrame.position databuf); let available = Soundtouch.get_available_samples st in if available > 0 then ( let buf = Audio.create self#audio_channels available in ignore (Soundtouch.get_samples_ni st buf 0 available); Generator.put self#buffer Frame.Fields.audio (Content.Audio.lift_data buf)); let gen_pos = Generator.length self#buffer in List.iter (fun pos -> Generator.add_track_mark ~pos:(pos + gen_pos) self#buffer) (Frame.track_marks databuf); List.iter (fun (pos, m) -> Generator.add_metadata ~pos:(pos + gen_pos) self#buffer m) (Frame.get_all_metadata databuf) method private generate_frame = let size = Lazy.force Frame.size in consumer#set_output_enabled true; while Generator.length self#buffer < size && source#is_ready do self#child_tick done; consumer#set_output_enabled false; Generator.slice self#buffer size initializer self#on_wake_up (fun () -> st <- Some (Soundtouch.make self#audio_channels (Lazy.force Frame.audio_rate)); self#log#important "Using soundtouch %s." (Soundtouch.get_version_string (Option.get st)); write_frame_ref := self#write_frame) end let _ = (* TODO: could we keep the video in some cases? *) let return_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator "soundtouch" [ ("rate", Lang.getter_t Lang.float_t, Some (Lang.float 1.0), None); ("tempo", Lang.getter_t Lang.float_t, Some (Lang.float 1.0), None); ("pitch", Lang.getter_t Lang.float_t, Some (Lang.float 1.0), None); ("", Lang.source_t return_t, None, None); ] ~category:`Audio ~return_t ~descr:"Change the rate, the tempo or the pitch of the sound." ~flags:[`Experimental] (fun p -> let f v = List.assoc v p in let rate = Lang.to_float_getter (f "rate") in let tempo = Lang.to_float_getter (f "tempo") in let pitch = Lang.to_float_getter (f "pitch") in let s = f "" in (new soundtouch s rate tempo pitch :> Source.source)) liquidsoap-2.3.2/src/core/operators/st_bpm.ml000066400000000000000000000055101477303350200212500ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Source class bpm (source : source) = object (self) inherit operator ~name:"bpm" [source] method fallible = source#fallible method private can_generate_frame = source#is_ready method self_sync = source#self_sync method remaining = source#remaining method seek_source = source#seek_source method abort_track = source#abort_track val mutable bpm = None initializer self#on_wake_up (fun () -> bpm <- Some (Soundtouch.BPM.make (Content.Audio.channels_of_format (Option.get (Frame.Fields.find_opt Frame.Fields.audio self#content_type))) (Lazy.force Frame.audio_rate))) method private generate_frame = let buf = Content.Audio.get_data (source#get_mutable_content Frame.Fields.audio) in let bpm = Option.get bpm in let len = source#frame_audio_position in Soundtouch.BPM.put_samples_ni bpm buf 0 len; source#set_frame_data Frame.Fields.audio Content.Audio.lift_data buf method bpm = match bpm with Some bpm -> Soundtouch.BPM.get_bpm bpm | None -> 0. end let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator "bpm" [("", Lang.source_t frame_t, None, None)] ~return_t:frame_t ~category:`Visualization ~descr: "Detect the BPM (number of beats per minute). The returned source has a \ method `bpm`, which can be called to compute it." ~meth: [ ( "bpm", ([], Lang.fun_t [] Lang.float_t), "Compute the current BPM.", fun s -> Lang.val_fun [] (fun _ -> Lang.float s#bpm) ); ] (fun p -> let s = Lang.to_source (List.assoc "" p) in new bpm s) liquidsoap-2.3.2/src/core/operators/stereotool_op.ml000066400000000000000000000171201477303350200226610ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) type config = { unlincensed_used_features : string option; valid_license : bool; latency : float; api_version : int; software_version : int; } class stereotool ~field ~handler source = object (self) inherit Source.operator ~name:"stereotool" [source] val config = Lazy.from_fun (fun () -> (* This is computed first to inject some audio data. *) let latency = Frame.seconds_of_audio (Stereotool.latency ~samplerate:(Lazy.force Frame.audio_rate) ~feed_silence:true handler) in { unlincensed_used_features = Stereotool.unlincensed_used_features handler; valid_license = Stereotool.valid_license handler; latency; api_version = Stereotool.api_version handler; software_version = Stereotool.software_version handler; }) method config = Lazy.force config initializer self#on_wake_up (fun () -> let { unlincensed_used_features; valid_license; latency; api_version; software_version; } = self#config in self#log#important "Stereotool initialized! Valid license: %b, latency: %.02fs, \ API/software version: %d/%d" valid_license latency api_version software_version; if not valid_license then self#log#severe "Using invalid license!"; match unlincensed_used_features with | None -> () | Some s -> self#log#severe "Using unlicensed features: %s" s) method fallible = source#fallible method remaining = source#remaining method seek_source = source#seek_source method private can_generate_frame = source#is_ready method abort_track = source#abort_track method self_sync = source#self_sync method private generate_frame = let b = Content.Audio.get_data (source#get_mutable_content field) in Stereotool.process ~samplerate:(Lazy.force Frame.audio_rate) handler b 0 source#frame_audio_position; source#set_frame_data field Content.Audio.lift_data b end let _ = let frame_t = Format_type.audio () in Lang.add_track_operator ~base:Modules.track_audio "stereotool" [ ( "library_file", Lang.string_t, None, Some "Path to the shared library file." ); ("license_key", Lang.nullable_t Lang.string_t, Some Lang.null, None); ( "preset", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Path to a preset file to load when initializing the operator." ); ( "load_type", Lang.string_t, Some (Lang.string "totalinit"), Some "Load type for preset. One of: \"totalinit\", \"all_settings\", \ \"audiofm\", \"audio\", \"processing\", \"repair\", \ \"repair_no_pnr\" or \"sublevel_pnr\"." ); ("", frame_t, None, None); ] ~meth: [ ( "api_version", ([], Lang.fun_t [] Lang.int_t), "API version.", fun s -> Lang.val_fun [] (fun _ -> Lang.int s#config.api_version) ); ( "software_version", ([], Lang.fun_t [] Lang.int_t), "Software version.", fun s -> Lang.val_fun [] (fun _ -> Lang.int s#config.software_version) ); ( "latency", ([], Lang.fun_t [] Lang.float_t), "Get the operator's latency.", fun s -> Lang.val_fun [] (fun _ -> Lang.float s#config.latency) ); ( "valid_license", ([], Lang.fun_t [] Lang.bool_t), "Check if the license is valid for the current settings.", fun s -> Lang.val_fun [] (fun _ -> Lang.bool s#config.valid_license) ); ( "unlincensed_used_features", ([], Lang.fun_t [] (Lang.nullable_t Lang.string_t)), "Check if the license is valid for the current settings.", fun s -> Lang.val_fun [] (fun _ -> match s#config.unlincensed_used_features with | None -> Lang.null | Some s -> Lang.string s) ); ] ~return_t:frame_t ~category:`Audio ~descr:"Process the given audio track with StereoTool." (fun p -> let library = Lang.to_string (List.assoc "library_file" p) in let license_key = Lang.to_valued_option Lang.to_string (List.assoc "license_key" p) in let license_key = if license_key = Some "" then None else license_key in let load_type_val = List.assoc "load_type" p in let load_type = match Lang.to_string load_type_val with | "totalinit" -> `Totalinit | "all_settings" -> `All_settings | "audiofm" -> `Audiofm | "audio" -> `Audio | "processing" -> `Processing | "repair" -> `Repair | "repair_no_pnr" -> `Repair_no_pnr | "sublevel_pnr" -> `Sublevel_pnr | s -> let pos = match Liquidsoap_lang.Value.pos load_type_val with | None -> Lang.pos p | Some p -> [p] in Runtime_error.raise ~pos ~message:(Printf.sprintf "Invalid load type: %S" s) "invalid" in let preset_val = List.assoc "preset" p in let preset = Lang.to_valued_option Lang.to_string preset_val in let handler = let library = Utils.check_readable ~pos:(Lang.pos p) library in try Stereotool.init ?license_key ~filename:library () with | Stereotool.Library_not_found -> Runtime_error.raise ~pos:(Lang.pos p) ~message:"Invalid stereotool library" "invalid" | Stereotool.Library_initialized f -> Runtime_error.raise ~pos:(Lang.pos p) ~message: (Printf.sprintf "Stereotool already initialized with a different library: \ %s" (Lang_string.quote_string f)) "invalid" in (match preset with | None -> () | Some filename -> let pos = match Liquidsoap_lang.Value.pos preset_val with | None -> Lang.pos p | Some p -> [p] in if not (Stereotool.load_preset ~load_type ~filename handler) then Runtime_error.raise ~pos ~message: (Printf.sprintf "Preset loading of file %S failed!" filename) "eval"); let field, src = Lang.to_track (List.assoc "" p) in (field, new stereotool ~field ~handler src)) liquidsoap-2.3.2/src/core/operators/still_frame.ml000066400000000000000000000063521477303350200222720ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Save picture extracted from the video. *) open Mm open Source open Extralib class still_frame ~name (source : source) = object (self) inherit operator ~name [source] method fallible = source#fallible method remaining = source#remaining method seek_source = source#seek_source method self_sync = source#self_sync method private can_generate_frame = source#is_ready method abort_track = source#abort_track val mutable fname = None method save (f : string) = if not (String.ends_with f ".bmp") then self#log#severe "Only BMP files are supported for now, the filename should end with \ .bmp" else fname <- Some f method private still buf = match fname with | None -> () | Some f -> ( let v = Content.Video.get_data (Frame.get buf Frame.Fields.video) in match v.Content.Video.data with | [] -> () | (_, i) :: _ -> let i = i |> Video.Canvas.Image.render |> Image.YUV420.to_RGBA32 |> Image.RGBA32.to_BMP in let oc = open_out f in output_string oc i; close_out oc; fname <- None) method private generate_frame = let buf = source#get_frame in self#still buf; buf end let _ = let return_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~video:(Format_type.video ()) ()) in Lang.add_operator ~base:Modules.video "still_frame" [("", Lang.source_t return_t, None, None)] ~return_t ~category:`Video ~descr: "Take still frames from a video source by calling the `save` method. For \ now only bitmap output is supported." ~meth: [ ( "save", ([], Lang.fun_t [(false, "", Lang.string_t)] Lang.unit_t), "Save current image, argument is the file name to save to.", fun s -> Lang.val_fun [("", "", None)] (fun p -> s#save (List.assoc "" p |> Lang.to_string); Lang.unit) ); ] (fun p -> let s = List.assoc "" p |> Lang.to_source in new still_frame ~name:"video.still_frame" s) liquidsoap-2.3.2/src/core/operators/switch.ml000066400000000000000000000335511477303350200212730ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Custom operator which selects one of its children sources either at the beginning of a track or at every frame, depending on a parametrizable predicate. A few specializations of it are defined below. *) open Source (* A transition is a value of type (source,source) -> source *) type transition = Lang.value type child = { source : source; transition : transition } (** The switch can either happen at any time in the stream (insensitive) or only at track limits (sensitive). *) type track_mode = Sensitive | Insensitive type selection = { predicate : Lang.value; child : child; effective_source : source; } let satisfied f = Lang.to_bool (Lang.apply f []) let trivially_true = function | Value.Fun { fun_body = { Term.term = `Bool true } } -> true | _ -> false let pick_selection (p, _, s) = (p, s) (** Like [List.find] but evaluates [f] on every element when [strict] is [true]. *) let find ?(strict = false) f l = let rec aux = function | x :: l -> if f x then ( if strict then List.iter (fun x -> ignore (f x)) l; x) else aux l | [] -> raise Not_found in aux l class switch ~all_predicates ~override_meta ~transition_length ~replay_meta ~track_sensitive children = let cases = List.map (fun (_, _, s) -> s) children in let sources = ref (List.map (fun c -> c.source) cases) in let failed = ref false in let () = List.iter (Lang.iter_sources ~on_imprecise:(fun () -> failed := true) (fun s -> sources := s :: !sources)) (List.map (fun c -> c.transition) cases) in let self_sync_type = if !failed then Lazy.from_val `Dynamic else Clock_base.self_sync_type !sources in object (self) inherit operator ~name:"switch" (List.map (fun x -> x.source) cases) inherit generate_from_multiple_sources ~merge:(fun () -> false) ~track_sensitive () val mutable transition_length = transition_length val mutable selected : selection option = None (* We cannot reselect the same source twice during a streaming cycle. *) val mutable excluded_sources = [] initializer self#on_before_streaming_cycle (fun () -> excluded_sources <- []) method private is_selected_generated = match selected with | None -> false | Some { child = { source }; effective_source } -> source != effective_source method private select ~reselect () = let may_select ~single s = match selected with | Some { child; effective_source } when child.source == s.source -> (not single) && self#can_reselect ~reselect effective_source | _ -> not (List.memq s excluded_sources) in try Some (pick_selection (find ~strict:all_predicates (fun (d, single, s) -> satisfied d && may_select ~single s && s.source#is_ready) children)) with Not_found -> None method fallible = not (List.exists (fun (d, single, s) -> (not s.source#fallible) && (not single) && trivially_true d) children) method get_source ~reselect () = match selected with | Some s when (track_sensitive () || satisfied s.predicate) && self#can_reselect ~reselect: (* We want to force a re-select on each new track. *) (match reselect with | `After_position _ -> `Force | v -> v) s.effective_source -> Some s.effective_source | _ -> ( begin match ( selected, self#select (* If we've returned the same source, it should be accepted now. *) ~reselect:(match reselect with `Force -> `Ok | v -> v) () ) with | None, None -> () | Some _, None -> selected <- None | None, Some (predicate, c) -> self#log#important "Switch to %s." c.source#id; let new_source = (* Force insertion of old metadata if relevant. * It can't be done in a static way: we need to start * pulling data to see if new metadata comes out, in case * the source was shared and kept streaming from somewhere * else (this is thanks to Frame.get_chunk). * A quicker hack might have been doable if there wasn't a * transition in between. *) match c.source#last_metadata with | Some m when replay_meta -> new Insert_metadata.replay m c.source | _ -> c.source in Typing.(new_source#frame_type <: self#frame_type); new_source#wake_up; selected <- Some { predicate; child = c; effective_source = new_source } | Some old_selection, Some (_, c) when old_selection.child.source == c.source -> () | old_selection, Some (predicate, c) -> let forget, old_source = match old_selection with | None -> (true, Debug_sources.empty ()) | Some old_selection -> (false, old_selection.child.source) in self#log#important "Switch to %s with%s transition." c.source#id (if forget then " forgetful" else ""); let new_source = (* Force insertion of old metadata if relevant. * It can't be done in a static way: we need to start * pulling data to see if new metadata comes out, in case * the source was shared and kept streaming from somewhere * else (this is thanks to Frame.get_chunk). * A quicker hack might have been doable if there wasn't a * transition in between. *) match c.source#last_metadata with | Some m when replay_meta -> new Insert_metadata.replay m c.source | _ -> c.source in Typing.(old_source#frame_type <: self#frame_type); Typing.(new_source#frame_type <: self#frame_type); let s = Lang.to_source (Lang.apply c.transition [ ("", Lang.source old_source); ("", Lang.source new_source); ]) in Typing.(s#frame_type <: self#frame_type); let s = match s#id with | id when id = new_source#id -> s | _ -> let s = new Max_duration.max_duration ~override_meta ~duration:transition_length s in Typing.(s#frame_type <: self#frame_type); (new Sequence.sequence ~merge:true [s; new_source] :> Source.source) in Typing.(s#frame_type <: self#frame_type); Clock.unify ~pos:self#pos s#clock self#clock; s#wake_up; selected <- Some { predicate; child = c; effective_source = s } end; match selected with | Some s when s.effective_source#is_ready -> excluded_sources <- s.child :: excluded_sources; Some s.effective_source | _ -> None) method self_sync = ( Lazy.force self_sync_type, match selected with | Some s -> snd s.effective_source#self_sync | None -> None ) method remaining = match selected with None -> 0 | Some s -> s.effective_source#remaining method abort_track = match selected with | Some s -> s.effective_source#abort_track | None -> () method seek_source = match selected with | Some s -> s.effective_source#seek_source | None -> (self :> Source.source) method selected = Option.map (fun { child } -> child.source) selected end (** Common tools for Lang bindings of switch operators *) let default_transition = Lang.eval ~cache:false ~stdlib:`Disabled ~typecheck:false "fun (_, y) -> y" let _ = let return_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in let pred_t = Lang.fun_t [] Lang.bool_t in Lang.add_operator "switch" ~category:`Track ~descr: "At the beginning of a track, select the first source whose predicate is \ true." ~meth: [ ( "selected", ([], Lang.fun_t [] (Lang.nullable_t Lang.(source_t return_t))), "Currently selected source.", fun s -> Lang.val_fun [] (fun _ -> match s#selected with | Some s -> Lang.source s | None -> Lang.null) ); ] [ ( "track_sensitive", Lang.getter_t Lang.bool_t, Some (Lang.bool true), Some "Re-select only on end of tracks." ); ( "transition_length", Lang.float_t, Some (Lang.float 5.), Some "Maximum transition duration." ); ( "override", Lang.string_t, Some (Lang.string "liq_transition_length"), Some "Metadata field which, if present and containing a float, overrides \ the `transition_length` parameter." ); ( "replay_metadata", Lang.bool_t, Some (Lang.bool true), Some "Replay the last metadata of a child when switching to it in the \ middle of a track." ); ( "all_predicates", Lang.bool_t, Some (Lang.bool false), Some "Always evaluate all predicates when re-selecting." ); (let transition_t = Lang.fun_t [ (false, "", Lang.source_t return_t); (false, "", Lang.source_t return_t); ] (Lang.source_t return_t) in ( "transitions", Lang.list_t transition_t, Some (Lang.list []), Some "Transition functions, padded with `fun (x,y) -> y` functions." )); ( "single", Lang.list_t Lang.bool_t, Some (Lang.list []), Some "Forbid the selection of a branch for two tracks in a row. The empty \ list stands for `[false,...,false]`." ); ( "", Lang.list_t (Lang.product_t pred_t (Lang.source_t return_t)), None, Some "Sources with the predicate telling when they can be played." ); ] ~return_t (fun p -> let children = List.map (fun p -> let pred, s = Lang.to_product p in (pred, Lang.to_source s)) (Lang.to_list (List.assoc "" p)) in let ts = Lang.to_bool_getter (List.assoc "track_sensitive" p) in let tr = let l = List.length children in let tr = Lang.to_list (List.assoc "transitions" p) in let ltr = List.length tr in if ltr > l then raise (Error.Invalid_value (List.assoc "transitions" p, "Too many transitions")); if ltr < l then tr @ List.init (l - ltr) (fun _ -> default_transition) else tr in let replay_meta = Lang.to_bool (List.assoc "replay_metadata" p) in let tl = Frame.main_of_seconds (Lang.to_float (List.assoc "transition_length" p)) in let override_meta = Lang.to_string (List.assoc "override" p) in let all_predicates = Lang.to_bool (List.assoc "all_predicates" p) in let singles = List.map Lang.to_bool (Lang.to_list (List.assoc "single" p)) in let singles = if singles = [] then List.init (List.length children) (fun _ -> false) else singles in let children = List.map2 (fun t (f, s) -> (f, { source = s; transition = t })) tr children in let children = try List.map2 (fun (d, s) single -> (d, single, s)) children singles with Invalid_argument s when s = "List.map2" -> raise (Error.Invalid_value ( List.assoc "single" p, "there should be exactly one flag per children" )) in new switch ~replay_meta ~override_meta ~all_predicates ~transition_length:tl ~track_sensitive:ts children) liquidsoap-2.3.2/src/core/operators/time_warp.ml000066400000000000000000000463471477303350200217700ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (** Create a buffer between two clocks. This creates an active operator in the inner clock (the action consists in filling the buffer) but it does not create or force in any way the clock that's going to animate it. We actually create two sources, to avoid the mess of having a same source belonging to one clock but being animated by another. This makes it possible to have the inner clock stop and shutdown the source that feeds the buffer, without disturbing the other clock in which the buffer-consumer will still behave OK (except obviously that the buffer will empty). *) module Buffer = struct (* The kind of value shared by a producer and a consumer. *) type control = { lock : Mutex.t; generator : Generator.t Lazy.t; mutable buffering : bool; mutable abort : bool; } let proceed control f = Mutex_utils.mutexify control.lock f () (** The source which produces data by reading the buffer. *) class producer ~id c = object (self) inherit Source.source ~name:id () method self_sync = (`Static, None) method fallible = true method remaining = proceed c (fun () -> Generator.remaining (Lazy.force c.generator)) method private can_generate_frame = proceed c (fun () -> not c.buffering) method! seek len = let len = min (Generator.length (Lazy.force c.generator)) len in Generator.truncate (Lazy.force c.generator) len; len method seek_source = (self :> Source.source) method buffer_length = Generator.length (Lazy.force c.generator) method private generate_frame = proceed c (fun () -> assert (not c.buffering); let frame = Generator.slice (Lazy.force c.generator) (Lazy.force Frame.size) in if Frame.is_partial frame && Generator.length (Lazy.force c.generator) = 0 then ( self#log#important "Buffer emptied, start buffering..."; c.buffering <- true); frame) method abort_track = proceed c (fun () -> c.abort <- true) end class consumer ~id ~autostart ~infallible ~on_start ~on_stop ~pre_buffer ~max_buffer source_val c = let prebuf = Frame.main_of_seconds pre_buffer in let maxbuf = Frame.main_of_seconds max_buffer in let source = Lang.to_source source_val in object inherit Output.output ~output_kind:id ~infallible ~register_telnet:false ~on_start ~on_stop source_val autostart method! reset = () method start = () method stop = () method self_sync = source#self_sync val source = Lang.to_source source_val method send_frame frame = proceed c (fun () -> if c.abort then ( c.abort <- false; source#abort_track); Generator.append (Lazy.force c.generator) frame; if Generator.length (Lazy.force c.generator) > prebuf then ( c.buffering <- false; if Generator.length (Lazy.force c.generator) > maxbuf then Generator.truncate (Lazy.force c.generator) (Generator.length (Lazy.force c.generator) - maxbuf))) end let create ~id ~autostart ~infallible ~on_start ~on_stop ~pre_buffer ~max_buffer source_val = let control = { generator = Lazy.from_fun (fun () -> Generator.create (Lang.to_source source_val)#content_type); lock = Mutex.create (); buffering = true; abort = false; } in let _ = new consumer ~id:(Printf.sprintf "%s.consumer" id) ~autostart ~infallible ~on_start ~on_stop source_val ~pre_buffer ~max_buffer control in new producer ~id:(Printf.sprintf "%s.producer" id) control end let buffer = let frame_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator "buffer" ([ ( "fallible", Lang.bool_t, Some (Lang.bool true), Some "Allow the child source to fail." ); ] @ List.filter (fun (lbl, _, _, _) -> lbl <> "fallible") Output.proto @ [ ( "buffer", Lang.float_t, Some (Lang.float 1.), Some "Amount of data to pre-buffer, in seconds." ); ( "max", Lang.float_t, Some (Lang.float 10.), Some "Maximum amount of buffered data, in seconds." ); ("", Lang.source_t frame_t, None, None); ]) ~return_t:frame_t ~category:`Liquidsoap ~meth: [ ( "buffer_length", ([], Lang.fun_t [] Lang.int_t), "Buffer length, in main ticks", fun s -> Lang.val_fun [] (fun _ -> Lang.int s#buffer_length) ); ] ~descr:"Create a buffer between two different clocks." (fun p -> let id = Lang.to_default_option ~default:"buffer" Lang.to_string (List.assoc "id" p) in let infallible = not (Lang.to_bool (List.assoc "fallible" p)) in let autostart = Lang.to_bool (List.assoc "start" p) in let on_start = List.assoc "on_start" p in let on_stop = List.assoc "on_stop" p in let on_start () = ignore (Lang.apply on_start []) in let on_stop () = ignore (Lang.apply on_stop []) in let s = List.assoc "" p in let pre_buffer = Lang.to_float (List.assoc "buffer" p) in let max_buffer = Lang.to_float (List.assoc "max" p) in let max_buffer = max max_buffer (pre_buffer *. 1.1) in Buffer.create ~id ~infallible ~autostart ~on_start ~on_stop ~pre_buffer ~max_buffer s) module AdaptativeBuffer = struct (** Ringbuffers where number of channels is fixed on first write. *) module RB = struct module RB = Audio.Ringbuffer type t = { size : int; (* size of the ringbuffer (in samples) *) mutable rb : RB.t option (* the ringbuffer *); } let create size = { size; rb = None } let read_space r = match r.rb with Some rb -> RB.read_space rb | None -> 0 let read r buf = RB.read (Option.get r.rb) buf let read_advance r n = RB.read_advance (Option.get r.rb) n let write_space r = match r.rb with Some rb -> RB.write_space rb | None -> r.size let rec write r buf = match r.rb with | Some rb -> RB.write rb buf | None -> r.rb <- Some (RB.create (Audio.channels buf) r.size); write r buf end (* TODO: also have track_marks and metadata as in generators. *) (** The kind of value shared by a producer and a consumer. *) type control = { lock : Mutex.t; (* this mutex must be taken before accessing any other field *) rb : RB.t; (* the ringbuffer *) mutable rb_length : float; (* average length of the ringbuffer, in samples *) mutable mg : Generator.t; mutable buffering : bool; (* when true we are buffering: filling the buffer, but not reading from it *) mutable abort : bool; (* whether we asked to abort the current track *) } let proceed control f = Mutex_utils.mutexify control.lock f () (** The source which produces data by reading the buffer. *) class producer ~pre_buffer ~averaging ~limit ~resample c = let prebuf = float (Frame.audio_of_seconds pre_buffer) in (* Nice enough approximation of the factor when the time constant is large compared to the frame duration, see https://en.wikipedia.org/wiki/Exponential_smoothing#Time_constant *) let alpha = AFrame.duration () /. averaging in object (self) inherit Source.source ~name:"buffer.adaptative.producer" () method seek_source = (self :> Source.source) method self_sync = (`Static, None) method fallible = true method remaining = proceed c (fun () -> Generator.remaining c.mg) method private can_generate_frame = proceed c (fun () -> not c.buffering) method ratio = proceed c (fun () -> c.rb_length /. prebuf) method buffer_duration = proceed c (fun () -> Frame.seconds_of_audio (RB.read_space c.rb)) method buffer_estimated_duration = proceed c (fun () -> Frame.seconds_of_audio (int_of_float c.rb_length)) val mutable converter = None initializer self#on_wake_up (fun () -> if resample then converter <- Some (Audio_converter.Samplerate.create self#audio_channels)) method private generate_frame = proceed c (fun () -> assert (not c.buffering); (* Update the average length of the ringbuffer (with a damping coefficient in order not to be too sensitive to quick local variations). We use exponential smoothing here: https://en.wikipedia.org/wiki/Exponential_smoothing *) c.rb_length <- ((1. -. alpha) *. c.rb_length) +. (alpha *. float (RB.read_space c.rb)); (* Limit estimation *) c.rb_length <- min c.rb_length (prebuf *. limit); c.rb_length <- max c.rb_length (prebuf /. limit); (* Fill dlen samples of dst using slen samples of the ringbuffer. *) let fill dst dofs dlen slen = (* TODO: when the RB is low on space we'd better not fill the whole frame *) let slen = min slen (RB.read_space c.rb) in if slen > 0 then ( match converter with | Some converter -> let src = Audio.create self#audio_channels slen in RB.read c.rb src; let ratio = float dlen /. float slen in let buf, off, len = Audio_converter.Samplerate.resample converter ratio src 0 slen in if len <> dlen then self#log#debug "Unexpected length after resampling: %d instead of %d" len dlen; let len = min len dlen in Audio.blit buf off dst dofs len; (* In case we are too short, duplicate samples. *) if len > 0 then for i = dofs + len to dofs + dlen - 1 do for c = 0 to Array.length dst - 1 do dst.(c).(i) <- dst.(c).(len - 1) done done | None -> let src = Audio.create self#audio_channels slen in RB.read c.rb src; if slen = dlen then Audio.blit src 0 dst dofs slen else (* TODO: we could do better than nearest interpolation. However, for slight adaptations the difference should not really be audible. *) for c = 0 to self#audio_channels - 1 do let srcc = src.(c) in let dstc = dst.(c) in for i = 0 to dlen - 1 do let x = srcc.(i * slen / dlen) in dstc.(i + dofs) <- x done done) in (* We scale the reading so that the buffer always approximately contains prebuf data. *) let scaling = c.rb_length /. prebuf in let scale n = int_of_float (float n *. scaling) in let unscale n = int_of_float (float n /. scaling) in let length = Lazy.force Frame.size in let frame = Frame.create ~length self#content_type in let alen = Frame.audio_of_main length in let buf = Content.Audio.get_data (Frame.get frame Frame.Fields.audio) in let salen = scale alen in fill buf 0 alen salen; let frame = Frame.set_data frame Frame.Fields.audio Content.Audio.lift_data buf in (* self#log#debug "filled %d from %d (x %f)" len ofs scaling; *) (* Fill in metadata and track mark *) let content = Generator.slice c.mg (scale length) in let frame = List.fold_left (fun frame (pos, m) -> Frame.add_metadata frame (unscale pos) m) frame (Frame.get_all_metadata content) in let frame = match Frame.track_marks content with | p :: _ -> Frame.add_track_mark frame (unscale p) | _ -> frame in (* If there is no data left, we should buffer again. *) if RB.read_space c.rb = 0 then ( self#log#important "Buffer emptied, start buffering..."; self#log#debug "Current scaling factor is x%f." scaling; Generator.clear c.mg; c.buffering <- true); frame) method abort_track = proceed c (fun () -> c.abort <- true) end class consumer ~autostart ~infallible ~on_start ~on_stop ~pre_buffer ~reset source_val c = let prebuf = Frame.audio_of_seconds pre_buffer in let source = Lang.to_source source_val in object (self) inherit Output.output ~output_kind:"buffer" ~register_telnet:false ~name:"buffer.adaptative.consumer" ~infallible ~on_start ~on_stop source_val autostart method! reset = () method start = () method stop = () method self_sync = source#self_sync val source = Lang.to_source source_val method send_frame frame = proceed c (fun () -> if c.abort then ( c.abort <- false; source#abort_track); let len = AFrame.position frame in let buf = AFrame.pcm frame in if RB.write_space c.rb < len then ( (* Not enough write space, let's drop a frame. *) self#log#important "Buffer full, dropping a frame."; RB.read_advance c.rb len; Generator.truncate c.mg (Frame.main_of_audio len)); RB.write c.rb (Audio.sub buf 0 len); Generator.append c.mg frame; if RB.read_space c.rb > prebuf then ( if c.buffering && reset then c.rb_length <- float (Frame.audio_of_seconds pre_buffer); c.buffering <- false)) end let create ~autostart ~infallible ~on_start ~on_stop ~pre_buffer ~max_buffer ~averaging ~limit ~reset ~resample source_val = let control = { lock = Mutex.create (); rb = RB.create (Frame.audio_of_seconds max_buffer); rb_length = float (Frame.audio_of_seconds pre_buffer); mg = Generator.create Frame.Fields.empty; buffering = true; abort = false; } in let _ = new consumer ~autostart ~infallible ~on_start ~on_stop source_val ~pre_buffer ~reset control in new producer ~pre_buffer ~averaging ~limit ~resample control end let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:buffer "adaptative" (Output.proto @ [ ( "buffer", Lang.float_t, Some (Lang.float 1.), Some "Amount of data to prebuffer, in seconds." ); ( "max", Lang.float_t, Some (Lang.float 10.), Some "Maximum amount of buffered data, in seconds." ); ( "averaging", Lang.float_t, Some (Lang.float 30.), Some "Length of the buffer averaging, in seconds (the time constant of \ the smoothing to be precise). The greater this is, the less \ reactive to local variations we are." ); ( "limit", Lang.float_t, Some (Lang.float 1.25), Some "Maximum acceleration or deceleration factor, ie how fast or slow \ we can be compared to realtime." ); ( "reset", Lang.bool_t, Some (Lang.bool false), Some "Reset speed estimation to 1 when the source becomes available \ again (resuming from a buffer underflow)." ); ( "resample", Lang.bool_t, Some (Lang.bool true), Some "Use proper resampling instead of simply duplicating samples." ); ("", Lang.source_t frame_t, None, None); ]) ~meth: [ ( "duration", ([], Lang.fun_t [] Lang.float_t), "Current buffer duration, in seconds.", fun s -> Lang.val_fun [] (fun _ -> Lang.float s#buffer_duration) ); ( "estimated", ([], Lang.fun_t [] Lang.float_t), "Current smoothed buffer duration, in seconds.", fun s -> Lang.val_fun [] (fun _ -> Lang.float s#buffer_estimated_duration) ); ( "ratio", ([], Lang.fun_t [] Lang.float_t), "Get the current scaling ratio.", fun s -> Lang.val_fun [] (fun _ -> Lang.float s#ratio) ); ] ~return_t:frame_t ~category:`Liquidsoap ~descr: "Create a buffer between two different clocks. The speed of the output \ is adapted so that no buffer underrun or overrun occurs. This wonderful \ behavior has a cost: the pitch of the sound might be changed a little." ~flags:[`Experimental] (fun p -> let infallible = not (Lang.to_bool (List.assoc "fallible" p)) in let autostart = Lang.to_bool (List.assoc "start" p) in let on_start = List.assoc "on_start" p in let on_stop = List.assoc "on_stop" p in let on_start () = ignore (Lang.apply on_start []) in let on_stop () = ignore (Lang.apply on_stop []) in let s = List.assoc "" p in let pre_buffer = Lang.to_float (List.assoc "buffer" p) in let max_buffer = Lang.to_float (List.assoc "max" p) in let averaging = Lang.to_float (List.assoc "averaging" p) in let limit = Lang.to_float (List.assoc "limit" p) in let limit = if limit < 1. then 1. /. limit else limit in let reset = Lang.to_bool (List.assoc "reset" p) in let resample = List.assoc "resample" p |> Lang.to_bool in let max_buffer = max max_buffer (pre_buffer *. 1.1) in AdaptativeBuffer.create ~infallible ~autostart ~on_start ~on_stop ~pre_buffer ~max_buffer ~averaging ~limit ~reset ~resample s) liquidsoap-2.3.2/src/core/operators/track/000077500000000000000000000000001477303350200205355ustar00rootroot00000000000000liquidsoap-2.3.2/src/core/operators/track/merge_metadata.ml000066400000000000000000000060131477303350200240260ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) class merge_metadata tracks = let sources = List.map snd tracks in let self_sync = Clock_base.self_sync sources in object (self) inherit Source.operator ~name:"track.metadata.merge" sources initializer Typing.(self#frame_type <: Lang.unit_t) method self_sync = self_sync ~source:self () method fallible = false method abort_track = List.iter (fun s -> s#abort_track) sources method private ready_sources = List.filter (fun s -> s#is_ready) sources method private can_generate_frame = self#ready_sources <> [] method seek_source = match self#ready_sources with | s :: [] -> s | _ -> (self :> Source.source) method remaining = match self#ready_sources with s :: [] -> s#remaining | _ -> -1 method private generate_frame = match self#ready_sources with | [] -> assert false | s :: rest -> List.fold_left (fun frame source -> let l = Frame.get_all_metadata source#get_frame in let l = List.fold_left (fun l (pos, m) -> ( pos, Frame.Metadata.append (Option.value ~default:Frame.Metadata.empty (Frame.get_metadata frame pos)) m ) :: l) [] l in Frame.add_all_metadata frame l) s#get_frame rest end let _ = let metadata_t = Format_type.metadata in Lang.add_track_operator ~base:Muxer.track_metadata "merge" ~category:`Track ~descr: "Merge metadata from all given tracks. If two sources have metadata with \ the same label at the same time, the one from the last source in the \ list takes precedence." ~return_t:metadata_t [("", Lang.list_t metadata_t, None, None)] (fun p -> let tracks = List.map Lang.to_track (Lang.to_list (List.assoc "" p)) in (Frame.Fields.metadata, new merge_metadata tracks)) liquidsoap-2.3.2/src/core/operators/track_map.ml000066400000000000000000000106171477303350200217310ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Source class track_map ~name ~field ~fn s = object inherit operator ~name [s] method fallible = s#fallible method remaining = s#remaining method abort_track = s#abort_track method seek_source = s#seek_source method self_sync = s#self_sync method private can_generate_frame = s#is_ready method private generate_frame = let buf = s#get_frame in Frame.set buf field (fn (Frame.get buf field)) end let to_pcm_s16 c = Content_pcm_s16.(lift_data (from_audio (Content.Audio.get_data c))) let _ = let content_t = Lang.univ_t () in let input_t = Type.( make (Custom (Format_type.kind_handler (Content_audio.kind, content_t)))) in let output_t = Type.( make (Custom (Format_type.kind_handler (Content_pcm_s16.kind, content_t)))) in Lang.add_track_operator ~base:Modules.track_encode_audio "pcm_s16" [("", input_t, None, None)] ~category:`Audio ~descr:"Encode an audio track using PCM signed 16 bit integers." ~return_t:output_t (fun p -> let field, s = Lang.to_track (Lang.assoc "" 1 p) in ( field, new track_map ~name:"track.encode.audio.pcm_s16" ~field ~fn:to_pcm_s16 s )) let from_pcm_s16 c = Content.Audio.lift_data Content_pcm_s16.(to_audio (get_data c)) let _ = let content_t = Lang.univ_t () in let input_t = Type.( make (Custom (Format_type.kind_handler (Content_pcm_s16.kind, content_t)))) in let output_t = Type.( make (Custom (Format_type.kind_handler (Content_audio.kind, content_t)))) in Lang.add_track_operator ~base:Modules.track_decode_audio "pcm_s16" [("", input_t, None, None)] ~category:`Audio ~descr:"Decode an audio track using PCM signed 16 bit integers." ~return_t:output_t (fun p -> let field, s = Lang.to_track (Lang.assoc "" 1 p) in ( field, new track_map ~name:"track.decode.audio.pcm_s16" ~field ~fn:from_pcm_s16 s )) let to_pcm_f32 c = Content_pcm_f32.(lift_data (from_audio (Content.Audio.get_data c))) let _ = let content_t = Lang.univ_t () in let input_t = Type.( make (Custom (Format_type.kind_handler (Content_audio.kind, content_t)))) in let output_t = Type.( make (Custom (Format_type.kind_handler (Content_pcm_f32.kind, content_t)))) in Lang.add_track_operator ~base:Modules.track_encode_audio "pcm_f32" [("", input_t, None, None)] ~category:`Audio ~descr:"Encode an audio track using PCM signed 16 bit integers." ~return_t:output_t (fun p -> let field, s = Lang.to_track (Lang.assoc "" 1 p) in ( field, new track_map ~name:"track.encode.audio.pcm_f32" ~field ~fn:to_pcm_f32 s )) let from_pcm_f32 c = Content.Audio.lift_data Content_pcm_f32.(to_audio (get_data c)) let _ = let content_t = Lang.univ_t () in let input_t = Type.( make (Custom (Format_type.kind_handler (Content_pcm_f32.kind, content_t)))) in let output_t = Type.( make (Custom (Format_type.kind_handler (Content_audio.kind, content_t)))) in Lang.add_track_operator ~base:Modules.track_decode_audio "pcm_f32" [("", input_t, None, None)] ~category:`Audio ~descr:"Decode an audio track using PCM signed 16 bit integers." ~return_t:output_t (fun p -> let field, s = Lang.to_track (Lang.assoc "" 1 p) in ( field, new track_map ~name:"track.decode.audio.pcm_f32" ~field ~fn:from_pcm_f32 s )) liquidsoap-2.3.2/src/core/operators/video_effects.ml000066400000000000000000000636361477303350200226060ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Source let log = Log.make ["video"] let cached_effect effect_ = let cache = ref None in fun args -> match !cache with | Some (old_args, result) when old_args = args -> result | _ -> let result = effect_ args in cache := Some (args, result); result let rgb_of_int c = let c = if c < 0 || c > 0xffffff then ( log#important "color 0x%x is greater than maximum assignable value 0xffffff" c; c land 0xffffff) else c in Image.RGB8.Color.of_int c let yuv_of_int c = Image.Pixel.yuv_of_rgb (rgb_of_int c) let proto_color = [ ( "color", Lang.getter_t Lang.int_t, Some (Lang.int 0), Some "Color to fill the image with (0xRRGGBB)." ); ( "alpha", Lang.getter_t Lang.float_t, Some (Lang.float 1.), Some "Transparency of the color between 0 and 1 (0 is fully transparent and \ 1 is fully opaque)." ); ] module Getter = struct type 'a t = unit -> 'a let map f x () = f (x ()) end let color_arg p = let color = List.assoc "color" p |> Lang.to_int_getter |> Getter.map yuv_of_int in let alpha = List.assoc "alpha" p |> Lang.to_float_getter |> Getter.map (fun x -> int_of_float (x *. 255.)) in (color, alpha) class virtual base ~name (source : source) f = object inherit operator ~name [source] method fallible = source#fallible method remaining = source#remaining method seek_source = source#seek_source method self_sync = source#self_sync method private can_generate_frame = source#is_ready method abort_track = source#abort_track method virtual content_type : Frame.content_type method private generate_frame = let c = source#get_mutable_content Frame.Fields.video in let buf = Content.Video.get_data c in let data = buf.Content.Video.data in let data = if data = [] then data else ( let positions, images = List.fold_left (fun (positions, images) (pos, img) -> (pos :: positions, img :: images)) ([], []) buf.Content.Video.data in let positions = List.rev positions in let video = Array.of_list (List.rev images) in f video 0 (List.length images); List.mapi (fun i pos -> (pos, Video.Canvas.get video i)) positions) in source#set_frame_data Frame.Fields.video Content.Video.lift_data { buf with Content.Video.data } end class effect_ ~name (source : source) effect_ = object inherit base ~name source (fun buf off len -> Video.Canvas.iter effect_ buf off len) end class effect_map ~name (source : source) effect_ = object inherit base ~name source (fun buf off len -> Video.Canvas.map effect_ buf off len) end let return_t () = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~video:(Format_type.video ()) ()) let video_alpha = Lang.add_module ~base:Modules.video "alpha" let _ = let return_t = return_t () in Lang.add_operator ~base:Modules.video "greyscale" [("", Lang.source_t return_t, None, None)] ~return_t ~category:`Video ~descr:"Convert video to greyscale." (fun p -> let f v = List.assoc v p in let src = Lang.to_source (f "") in new effect_ ~name:"video.greyscale" src Image.YUV420.Effect.greyscale) let _ = let return_t = return_t () in Lang.add_operator ~base:Modules.video "sepia" [("", Lang.source_t return_t, None, None)] ~return_t ~category:`Video ~descr:"Convert video to sepia." (fun p -> let f v = List.assoc v p in let src = Lang.to_source (f "") in new effect_ ~name:"video.sepia" src Image.YUV420.Effect.sepia) let _ = let return_t = return_t () in Lang.add_operator ~base:Modules.video "invert" [("", Lang.source_t return_t, None, None)] ~return_t ~category:`Video ~descr:"Invert video." (fun p -> let f v = List.assoc v p in let src = Lang.to_source (f "") in new effect_ ~name:"video.invert" src Image.YUV420.Effect.invert) let _ = let return_t = return_t () in Lang.add_operator ~base:Modules.video "hmirror" [("", Lang.source_t return_t, None, None)] ~return_t ~category:`Video ~descr:"Flip image horizontally." (fun p -> let f v = List.assoc v p in let src = Lang.to_source (f "") in new effect_ ~name:"video.hmirror" src Image.YUV420.hmirror) let video_opacity = let return_t = return_t () in Lang.add_operator ~base:Modules.video "opacity" [ ( "", Lang.getter_t Lang.float_t, None, Some "Coefficient to scale opacity with: from 0 (fully transparent) to 1 \ (fully opaque)." ); ("", Lang.source_t return_t, None, None); ] ~return_t ~category:`Video ~descr:"Scale opacity of video." (fun p -> let a = Lang.to_float_getter (Lang.assoc "" 1 p) in let src = Lang.to_source (Lang.assoc "" 2 p) in new effect_ ~name:"video.opacity" src (fun buf -> Image.YUV420.Effect.Alpha.scale buf (a ()))) let _ = let return_t = return_t () in Lang.add_operator ~base:video_alpha "remove" [("", Lang.source_t return_t, None, None)] ~return_t ~category:`Video ~descr:"Remove α channel." (fun p -> let src = Lang.to_source (List.assoc "" p) in new effect_ ~name:"video.alpha.remove" src (fun img -> Image.YUV420.fill_alpha img 0xff)) let _ = let return_t = return_t () in Lang.add_operator ~base:Modules.video "fill" ([("", Lang.source_t return_t, None, None)] @ proto_color) ~return_t ~category:`Video ~descr:"Fill frame with a color." (fun p -> let f v = List.assoc v p in let c, a = color_arg p in let src = Lang.to_source (f "") in new effect_ ~name:"video.fill" src (fun buf -> Image.YUV420.fill buf (c ()); Image.YUV420.fill_alpha buf (a ()))) let _ = let return_t = return_t () in Lang.add_operator ~base:Modules.video "persistence" [ ( "duration", Lang.getter_t Lang.float_t, Some (Lang.float 1.), Some "Persistence duration in seconds." ); ("", Lang.source_t return_t, None, None); ] ~return_t ~category:`Video ~descr:"Make images of the video persistent." (fun p -> let duration = List.assoc "duration" p |> Lang.to_float_getter in let src = List.assoc "" p |> Lang.to_source in let fps = Lazy.force Frame.video_rate |> float_of_int in let prev = ref (Image.YUV420.create 0 0) in new effect_ ~name:"video.persistence" src (fun buf -> let duration = duration () in if duration > 0. then ( let alpha = 1. -. (1. /. (duration *. fps)) in let alpha = int_of_float (255. *. alpha) in Image.YUV420.fill_alpha !prev alpha; Image.YUV420.add !prev buf; prev := Image.YUV420.copy buf))) let _ = let return_t = return_t () in Lang.add_operator ~base:Modules.video "add_rectangle" ([ ( "x", Lang.getter_t Lang.int_t, Some (Lang.int 0), Some "Horizontal offset." ); ( "y", Lang.getter_t Lang.int_t, Some (Lang.int 0), Some "Vertical offset." ); ("width", Lang.getter_t Lang.int_t, None, Some "Width."); ("height", Lang.getter_t Lang.int_t, None, Some "Height."); ("", Lang.source_t return_t, None, None); ] @ proto_color) ~return_t ~category:`Video ~descr:"Draw a rectangle." (fun p -> let x = List.assoc "x" p |> Lang.to_int_getter in let y = List.assoc "y" p |> Lang.to_int_getter in let width = List.assoc "width" p |> Lang.to_int_getter in let height = List.assoc "height" p |> Lang.to_int_getter in let c, a = color_arg p in let src = List.assoc "" p |> Lang.to_source in let effect_ = cached_effect (fun (width, height, color, alpha) -> let r = Image.YUV420.create width height in Image.YUV420.fill r color; Image.YUV420.fill_alpha r alpha; r) in new effect_map ~name:"video.add_rectangle" src (fun buf -> let x = x () in let y = y () in let width = width () in let height = height () in let color = c () in let alpha = a () in let r = effect_ (width, height, color, alpha) in let r = Video.Canvas.Image.make ~x ~y ~width:(-1) ~height:(-1) r in Video.Canvas.Image.add r buf)) let _ = let return_t = return_t () in Lang.add_operator ~base:video_alpha "of_color" [ ( "precision", Lang.float_t, Some (Lang.float 0.2), Some "Precision in color matching (0. means match precisely the color and \ 1. means match every color)." ); ( "color", Lang.int_t, Some (Lang.int 0), Some "Color which should be transparent (in 0xRRGGBB format)." ); ("", Lang.source_t return_t, None, None); ] ~return_t ~category:`Video ~descr:"Set a color to be transparent." (fun p -> let f v = List.assoc v p in let prec, color, src = ( Lang.to_float (f "precision"), Lang.to_int (f "color"), Lang.to_source (f "") ) in let prec = int_of_float (prec *. 255.) in let color = yuv_of_int color in new effect_ ~name:"video.alpha.of_color" src (fun buf -> Image.YUV420.alpha_of_color buf color prec)) let _ = let return_t = return_t () in Lang.add_operator ~base:video_alpha "movement" [ ( "precision", Lang.float_t, Some (Lang.float 0.2), Some "Precision when comparing pixels to those of previous image (between \ 0 and 1)." ); ("", Lang.source_t return_t, None, None); ] ~return_t ~category:`Video ~descr: "Make moving parts visible and non-moving parts transparent. A cheap way \ to have a bluescreen." (fun p -> (* let precision = List.assoc "precision" p |> Lang.to_float in *) let src = List.assoc "" p |> Lang.to_source in let prev = ref None in new effect_ ~name:"video.alpha.movement" src (fun img -> (match !prev with | None -> () | Some prev -> Image.YUV420.alpha_of_diff prev img (0xff * 2 / 10) 2); prev := Some img)) (* let () = Lang.add_operator "video.opacity.blur" [ "", Lang.source_t return_t, None, None ] ~return_t ~category:`Video ~descr:"Blur opacity of video." (fun p -> let src = Lang.to_source (Lang.assoc "" 1 p) in new effect_ src Image.YUV420.Effect.Alpha.blur) *) let _ = let return_t = return_t () in Lang.add_operator ~base:Modules.video "lomo" [("", Lang.source_t return_t, None, None)] ~return_t ~category:`Video ~descr:"Emulate the \"Lomo effect\"." (fun p -> let f v = List.assoc v p in let src = Lang.to_source (f "") in new effect_ ~name:"video.lomo" src Image.YUV420.Effect.lomo) let _ = let return_t = return_t () in Lang.add_operator ~base:Modules.video "rotate" [ ( "angle", Lang.getter_t Lang.float_t, Some (Lang.float 0.), Some "Angle in radians." ); ("", Lang.source_t return_t, None, None); ] ~return_t ~category:`Video ~descr:"Rotate video." (fun p -> let a = List.assoc "angle" p |> Lang.to_float_getter in let s = List.assoc "" p |> Lang.to_source in new effect_ ~name:"video.rotate" s (fun buf -> let x = Image.YUV420.width buf / 2 in let y = Image.YUV420.height buf / 2 in Image.YUV420.rotate (Image.YUV420.copy buf) x y (a ()) buf)) let _ = let return_t = return_t () in Lang.add_operator ~base:Modules.video "resize" [ ( "width", Lang.nullable_t (Lang.getter_t Lang.int_t), Some Lang.null, Some "Target width (`null` means original width)." ); ( "height", Lang.nullable_t (Lang.getter_t Lang.int_t), Some Lang.null, Some "Target height (`null` means original height)." ); ( "proportional", Lang.bool_t, Some (Lang.bool true), Some "Keep original proportions." ); ("x", Lang.getter_t Lang.int_t, Some (Lang.int 0), Some "x offset."); ("y", Lang.getter_t Lang.int_t, Some (Lang.int 0), Some "y offset."); ("", Lang.source_t return_t, None, None); ] ~return_t ~category:`Video ~descr:"Resize and translate video." (fun p -> let f v = List.assoc v p in let src = Lang.to_source (f "") in let width = Lang.to_valued_option Lang.to_int_getter (f "width") in let height = Lang.to_valued_option Lang.to_int_getter (f "height") in let proportional = Lang.to_bool (f "proportional") in let ox = Lang.to_int_getter (f "x") in let oy = Lang.to_int_getter (f "y") in let scaler = Video_converter.scaler () in new effect_map ~name:"video.resize" src (fun buf -> let owidth = Video.Canvas.Image.width buf in let oheight = Video.Canvas.Image.height buf in let width = match width with None -> owidth | Some w -> w () in let height = match height with None -> oheight | Some h -> h () in let width, height = if width >= 0 && height >= 0 then (width, height) else if (* Negative values mean proportional scale. *) width < 0 && height < 0 then (owidth, oheight) else if width < 0 then (owidth * height / oheight, height) else if height < 0 then (width, oheight * width / owidth) else assert false in buf |> Video.Canvas.Image.resize ~scaler ~proportional width height |> Video.Canvas.Image.translate (ox ()) (oy ()))) let _ = let return_t = return_t () in Lang.add_operator ~base:video_opacity "box" [ ("width", Lang.getter_t Lang.int_t, None, Some "Box width."); ("height", Lang.getter_t Lang.int_t, None, Some "Box height."); ("x", Lang.getter_t Lang.int_t, Some (Lang.int 0), Some "x offset."); ("y", Lang.getter_t Lang.int_t, Some (Lang.int 0), Some "y offset."); ("alpha", Lang.getter_t Lang.float_t, None, Some "alpha value."); ("", Lang.source_t return_t, None, None); ] ~return_t ~category:`Video ~descr:"Set alpha value on a given box inside the image." (fun p -> let f v = List.assoc v p in let src = Lang.to_source (f "") in let width = Lang.to_int_getter (f "width") in let height = Lang.to_int_getter (f "height") in let ox = Lang.to_int_getter (f "x") in let oy = Lang.to_int_getter (f "y") in let alpha = Lang.to_float_getter (f "alpha") in new effect_ ~name:"video.opacity.box" src (fun buf -> Image.YUV420.box_alpha buf (ox ()) (oy ()) (width ()) (height ()) (alpha ()))) let _ = let return_t = return_t () in Lang.add_operator ~base:Modules.video "translate" [ ("x", Lang.getter_t Lang.int_t, Some (Lang.int 0), Some "x offset."); ("y", Lang.getter_t Lang.int_t, Some (Lang.int 0), Some "y offset."); ("", Lang.source_t return_t, None, None); ] ~return_t ~category:`Video ~descr:"Translate video." (fun p -> let f v = List.assoc v p in let src = f "" |> Lang.to_source in let dx = f "x" |> Lang.to_int_getter in let dy = f "y" |> Lang.to_int_getter in new effect_map ~name:"video.translate" src (fun buf -> Video.Canvas.Image.translate (dx ()) (dy ()) buf)) let _ = let return_t = return_t () in Lang.add_operator ~base:Modules.video "scale" [ ( "scale", Lang.getter_t Lang.float_t, Some (Lang.float 1.), Some "Scaling coefficient in both directions." ); ( "xscale", Lang.getter_t Lang.float_t, Some (Lang.float 1.), Some "x scaling." ); ( "yscale", Lang.getter_t Lang.float_t, Some (Lang.float 1.), Some "y scaling." ); ("x", Lang.getter_t Lang.int_t, Some (Lang.int 0), Some "x offset."); ("y", Lang.getter_t Lang.int_t, Some (Lang.int 0), Some "y offset."); ("", Lang.source_t return_t, None, None); ] ~return_t ~category:`Video ~descr:"Scale and translate video." (fun p -> let f v = List.assoc v p in let src = Lang.to_source (f "") in let c, cx, cy, ox, oy = ( Lang.to_float_getter (f "scale"), Lang.to_float_getter (f "xscale"), Lang.to_float_getter (f "yscale"), Lang.to_int_getter (f "x"), Lang.to_int_getter (f "y") ) in new effect_map ~name:"video.scale" src (fun buf -> let c = c () in let cx = c *. cx () in let cy = c *. cy () in let d = 1080 in let cx = int_of_float ((cx *. float d) +. 0.5) in let cy = int_of_float ((cy *. float d) +. 0.5) in let scaler = Video_converter.scaler () in let buf = Video.Canvas.Image.scale ~scaler (cx, d) (cy, d) buf in Video.Canvas.Image.translate (ox ()) (oy ()) buf)) let _ = let return_t = return_t () in Lang.add_operator ~base:Modules.video "add_line" ([ ( "", Lang.getter_t (Lang.product_t Lang.int_t Lang.int_t), None, Some "Start point." ); ( "", Lang.getter_t (Lang.product_t Lang.int_t Lang.int_t), None, Some "End point." ); ("", Lang.source_t return_t, None, None); ] @ proto_color) ~return_t ~category:`Video ~descr:"Draw a line on the video." (fun param -> let to_point_getter v = let v = Lang.to_getter v in fun () -> let x, y = v () |> Lang.to_product in (Lang.to_int x, Lang.to_int y) in let p = Lang.assoc "" 1 param |> to_point_getter in let q = Lang.assoc "" 2 param |> to_point_getter in let s = Lang.assoc "" 3 param |> Lang.to_source in let c, a = color_arg param in let effect_ = cached_effect (fun (r, g, b, a) -> Video.Canvas.Image.Draw.line (r, g, b, a) (p ()) (q ())) in new effect_map ~name:"video.add_line" s (fun buf -> let r, g, b = c () in let a = a () in let line = effect_ (r, g, b, a) in Video.Canvas.Image.add line buf)) let _ = let return_t = return_t () in Lang.add_operator ~base:Modules.video "render" [ ( "transparent", Lang.bool_t, Some (Lang.bool true), Some "Make uncovered portions of the image transparent (they are black by \ default)." ); ("", Lang.source_t return_t, None, None); ] ~return_t ~category:`Video ~descr:"Render the video by computing the result of its canvas images." (fun p -> let transparent = List.assoc "transparent" p |> Lang.to_bool in let s = List.assoc "" p |> Lang.to_source in new effect_map ~name:"video.render" s (fun buf -> Video.Canvas.Image.rendered ~transparent buf)) let _ = let return_t = return_t () in Lang.add_operator ~base:Modules.video "viewport" [ ("x", Lang.int_t, Some (Lang.int 0), Some "Horizontal offset."); ("y", Lang.int_t, Some (Lang.int 0), Some "Vertical offset."); ( "width", Lang.nullable_t Lang.int_t, Some Lang.null, Some "Width (default is frame width)." ); ( "height", Lang.nullable_t Lang.int_t, Some Lang.null, Some "height (default is frame height)." ); ("", Lang.source_t return_t, None, None); ] ~return_t ~category:`Video ~descr:"Set the viewport for the current video." (fun p -> let x = List.assoc "x" p |> Lang.to_int in let y = List.assoc "y" p |> Lang.to_int in let width = List.assoc "width" p |> Lang.to_option |> Option.map Lang.to_int in let height = List.assoc "height" p |> Lang.to_option |> Option.map Lang.to_int in let s = List.assoc "" p |> Lang.to_source in let width = match width with | Some width -> width | None -> Lazy.force Frame.video_width in let height = match height with | Some height -> height | None -> Lazy.force Frame.video_height in new effect_map ~name:"video.viewport" s (fun buf -> Video.Canvas.Image.viewport ~x ~y width height buf)) let _ = let return_t = return_t () in Lang.add_operator ~base:Modules.video "crop" [("", Lang.source_t return_t, None, None)] ~return_t ~category:`Video ~descr:"Make the viewport of the current video match its bounding box." (fun p -> let s = List.assoc "" p |> Lang.to_source in new effect_map ~name:"video.crop" s (fun buf -> let (x, y), (w, h) = Video.Canvas.Image.bounding_box buf in Video.Canvas.Image.viewport ~x ~y w h buf)) let _ = let return_t = return_t () in Lang.add_operator ~base:Modules.video "align" [ ("left", Lang.bool_t, Some (Lang.bool false), Some "Align left."); ("right", Lang.bool_t, Some (Lang.bool false), Some "Align right."); ("top", Lang.bool_t, Some (Lang.bool false), Some "Align top."); ("bottom", Lang.bool_t, Some (Lang.bool false), Some "Align bottom."); ("", Lang.source_t return_t, None, None); ] ~return_t ~category:`Video ~descr:"Translate the video so that it is aligned on boundaries." (fun p -> let f x = List.assoc x p |> Lang.to_bool in let left = f "left" in let right = f "right" in let top = f "top" in let bottom = f "bottom" in let s = List.assoc "" p |> Lang.to_source in new effect_map ~name:"video.align" s (fun buf -> let (x, y), (w, h) = Video.Canvas.Image.bounding_box buf in let dx = if left then -x else if right then Video.Canvas.Image.width buf - w else 0 in let dy = if top then -y else if bottom then Video.Canvas.Image.height buf - h else 0 in Video.Canvas.Image.translate dx dy buf)) let _ = let return_t = return_t () in let buf = ref None in let int ?(default = 0) f = Option.map f !buf |> Option.value ~default |> Lang.int in Lang.add_operator ~base:Modules.video "info" [("", Lang.source_t return_t, None, None)] ~meth: [ ( "width", ([], Lang.fun_t [] Lang.int_t), "Width of video.", fun _ -> Lang.val_fun [] (fun _ -> int Video.Canvas.Image.width) ); ( "height", ([], Lang.fun_t [] Lang.int_t), "Height of video.", fun _ -> Lang.val_fun [] (fun _ -> int Video.Canvas.Image.height) ); ( "planes", ([], Lang.fun_t [] Lang.int_t), "Number of planes in a video frame.", fun _ -> Lang.val_fun [] (fun _ -> int Video.Canvas.Image.planes) ); ( "size", ([], Lang.fun_t [] Lang.int_t), "Size of a video frame (in bytes).", fun _ -> Lang.val_fun [] (fun _ -> int Video.Canvas.Image.size) ); ] ~return_t ~category:`Video ~descr: "Compute various information about the video (dimension, size, etc.). \ Those are accessible through the methods attached to the source." (fun p -> let s = List.assoc "" p |> Lang.to_source in new effect_map ~name:"video.info" s (fun b -> buf := Some b; b)) let _ = let return_t = return_t () in Lang.add_operator ~base:video_alpha "to_y" [("", Lang.source_t return_t, None, None)] ~return_t ~category:`Video ~descr: "Convert the α channel to Y channel, thus converting opaque \ (resp. transparent) pixels to bright (resp. dark) ones. This is useful \ to observe the α channel." (fun p -> let s = List.assoc "" p |> Lang.to_source in new effect_ ~name:"video.alpha.to_y" s Image.YUV420.alpha_to_y) let _ = let return_t = return_t () in let x = ref 0 in let y = ref 0 in let width = ref 0 in let height = ref 0 in Lang.add_operator ~base:Modules.video "bounding_box" [("", Lang.source_t return_t, None, None)] ~meth: [ ( "x", ([], Lang.fun_t [] Lang.int_t), "x offset of video.", fun _ -> Lang.val_fun [] (fun _ -> Lang.int !x) ); ( "y", ([], Lang.fun_t [] Lang.int_t), "y offset of video.", fun _ -> Lang.val_fun [] (fun _ -> Lang.int !y) ); ( "width", ([], Lang.fun_t [] Lang.int_t), "Width of video.", fun _ -> Lang.val_fun [] (fun _ -> Lang.int !width) ); ( "height", ([], Lang.fun_t [] Lang.int_t), "Height of video.", fun _ -> Lang.val_fun [] (fun _ -> Lang.int !height) ); ] ~return_t ~category:`Video ~descr: "Retrieve the origin (methods `x` / `y`) and the dimensions (methods \ `width` / `height`) of the bounding box of the video." (fun p -> let s = List.assoc "" p |> Lang.to_source in new effect_map ~name:"video.bounding_box" s (fun buf -> let (x', y'), (w, h) = Video.Canvas.Image.bounding_box buf in x := x'; y := y'; width := w; height := h; buf)) liquidsoap-2.3.2/src/core/operators/video_fade.ml000066400000000000000000000263331477303350200220570ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Source let video_fade = Lang.add_module ~base:Modules.video "fade" (** Fade-in at the beginning of every track. The [duration] is in seconds. *) class fade_in ?(meta = "liq_video_fade_in") duration fader fadefun source = object (self) inherit operator ~name:"video.fade.in" [source] method fallible = source#fallible method private can_generate_frame = source#is_ready method abort_track = source#abort_track method remaining = source#remaining method self_sync = source#self_sync method seek_source = source#seek_source val mutable state = `Idle method private process frame = let fade, fadefun, duration, position = match state with | `Idle -> let duration = match Frame.get_metadata frame 0 with | None -> duration | Some m -> ( match Frame.Metadata.find_opt meta m with | Some d -> ( try float_of_string d with _ -> duration) | None -> duration) in let fade = fader (Frame.video_of_seconds duration) in let duration = Frame.main_of_seconds duration in let fadefun = fadefun () in let v = (fade, fadefun, duration, 0) in state <- `Play v; v | `Play v -> v in if position < duration then ( let buf = Content.Video.get_data (Content.copy (Frame.get frame Frame.Fields.video)) in let data = List.mapi (fun i (pos, img) -> let m = fade (Frame.video_of_main position + i) in ignore (fadefun img m); (pos, img)) buf.Content.Video.data in state <- `Play (fade, fadefun, duration, position + Frame.position frame); Frame.set_data frame Frame.Fields.video Content.Video.lift_data { buf with Content.Video.data }) else frame method private generate_frame = match self#split_frame source#get_frame with | frame, None -> self#process frame | end_track, Some begin_track -> let end_track = self#process end_track in state <- `Idle; Frame.append end_track (self#process begin_track) end (** Fade-out after every frame. *) class fade_out ?(meta = "liq_video_fade_out") duration fader fadefun source = object (self) inherit operator ~name:"video.fade.out" [source] method fallible = source#fallible method abort_track = source#abort_track method self_sync = source#self_sync method seek_source = source#seek_source (* Fade-out length (in video frames) for the current track. * The value is set at the beginning of every track, depending on metadata. *) val mutable cur_length = None method remaining = source#remaining method private can_generate_frame = source#is_ready method private process_frame ~remaining frame = (* In main ticks: [length] of the fade. *) let fade, fadefun, length = match cur_length with | Some (f, g, l) -> (f, g, l) | None -> (* Set the length at the beginning of a track *) let duration = match Frame.get_metadata frame 0 with | None -> duration | Some m -> ( match Frame.Metadata.find_opt meta m with | None -> duration | Some d -> ( try float_of_string d with _ -> duration)) in let l = Frame.main_of_seconds duration in let f = fader (Frame.video_of_seconds duration) in let g = fadefun () in cur_length <- Some (f, g, l); (f, g, l) in if remaining < length then ( let content = Content.copy (Frame.get frame Frame.Fields.video) in let buf = Content.Video.get_data content in let data = List.mapi (fun i (pos, img) -> let m = fade (Frame.video_of_main remaining - i) in (* TODO @smimram *) ignore (fadefun img m); (pos, img)) buf.Content.Video.data in Frame.set_data frame Frame.Fields.video Content.Video.lift_data { buf with Content.Video.data }) else frame method private generate_frame = match self#split_frame source#get_frame with | frame, None -> self#process_frame ~remaining:source#remaining frame | end_track, Some begin_track -> let end_track = self#process_frame ~remaining:0 end_track in cur_length <- None; Frame.append end_track (self#process_frame ~remaining:source#remaining begin_track) end (** Lang interface *) (* TODO: share more with fade.ml *) let proto frame_t = [ ( "duration", Lang.float_t, Some (Lang.float 3.), Some "Duration of the fading. This value can be set on a per-file basis \ using the metadata field passed as override." ); ( "transition", Lang.string_t, Some (Lang.string "fade"), Some "Kind of transition \ (fade|slide_left|slide_right|slide_up|slide_down|grow|disc|random)." ); ( "type", Lang.string_t, Some (Lang.string "lin"), Some "Fader shape (lin|sin|log|exp): linear, sinusoidal, logarithmic or \ exponential." ); ("", Lang.source_t frame_t, None, None); ] let rec transition_of_string p transition = let translate img dx dy = Video.Canvas.Image.translate dx dy img in let ifm n a = int_of_float (float_of_int n *. a) in match transition with | "fade" -> fun () img t -> Video.Canvas.Image.iter (fun img -> Image.YUV420.fill_alpha img (ifm 256 t)) img | "slide_left" -> fun () buf t -> translate buf (ifm (Video.Canvas.Image.width buf) (t -. 1.)) 0 | "slide_right" -> fun () buf t -> translate buf (ifm (Video.Canvas.Image.width buf) (1. -. t)) 0 | "slide_up" -> fun () buf t -> translate buf 0 (ifm (Video.Canvas.Image.height buf) (1. -. t)) | "slide_down" -> fun () buf t -> translate buf 0 (ifm (Video.Canvas.Image.height buf) (t -. 1.)) | "grow" -> fun () img t -> let w = Video.Canvas.Image.width img in let h = Video.Canvas.Image.height img in let w' = ifm w t in let h' = ifm h t in let img = Video.Canvas.Image.render img in let out = Video.Image.create w' h' in Image.YUV420.scale img out; let x = (w - w') / 2 in let y = (h - h') / 2 in Video.Canvas.Image.create w h |> Video.Canvas.Image.translate x y | "disc" -> fun () buf t -> let w = Video.Canvas.Image.width buf in let h = Video.Canvas.Image.height buf in let r_max = int_of_float (sqrt (float_of_int ((w * w) + (h * h)))) / 2 in Video.Canvas.Image.iter (fun buf -> Image.YUV420.disk_alpha buf (w / 2) (h / 2) (ifm r_max t)) buf | "random" -> let trans = [| "slide_left"; "slide_right"; "slide_up"; "slide_down"; "fade"; "grow"; "disc"; |] in fun () -> let f = transition_of_string p trans.(Random.int (Array.length trans)) () in f | _ -> raise (Error.Invalid_value (List.assoc "transition" p, "Invalid transition kind")) let extract p = ( Lang.to_float (List.assoc "duration" p), (let mode = List.assoc "type" p in let f = (* A few typical shapes.. * In theory, any mapping from [0:1] to [0:1] is OK, * preferably monotonic and one-to-one. *) match Lang.to_string mode with | "lin" -> fun x -> x | "log" -> let curve = 10. in let m = log (1. +. curve) in fun x -> log (1. +. (x *. 10.)) /. m | "exp" -> let curve = 2. in let m = exp curve -. 1. in fun x -> (exp (curve *. x) -. 1.) /. m | "sin" -> let pi = acos (-1.) in fun x -> (1. +. sin ((x -. 0.5) *. pi)) /. 2. | _ -> let msg = "The 'type' parameter should be 'lin','sin','log' or 'exp'!" in raise (Error.Invalid_value (mode, msg)) in fun l -> let l = float l in fun i -> let i = float i /. l in f (max 0. (min 1. i))), (let transition = Lang.to_string (List.assoc "transition" p) in transition_of_string p transition), Lang.to_source (List.assoc "" p) ) let override_doc = Some "Metadata field which, if present and containing a float, overrides the \ 'duration' parameter for current track." let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~video:(Format_type.video ()) ()) in Lang.add_operator ~base:video_fade "in" (( "override", Lang.string_t, Some (Lang.string "liq_video_fade_in"), override_doc ) :: proto frame_t) ~return_t:frame_t ~category:`Video ~descr: "Fade the beginning of tracks. Metadata 'liq_video_fade_in' can be used \ to set the duration for a specific track (float in seconds)." (fun p -> let d, f, t, s = extract p in let meta = Lang.to_string (List.assoc "override" p) in new fade_in ~meta d f t s) let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~video:(Format_type.video ()) ()) in Lang.add_operator ~base:video_fade "out" (( "override", Lang.string_t, Some (Lang.string "liq_video_fade_out"), override_doc ) :: proto frame_t) ~return_t:frame_t ~category:`Video ~descr: "Fade the end of tracks. Metadata 'liq_video_fade_out' can be used to \ set the duration for a specific track (float in seconds)." (fun p -> let d, f, t, s = extract p in let meta = Lang.to_string (List.assoc "override" p) in new fade_out ~meta d f t s) liquidsoap-2.3.2/src/core/operators/window_op.ml000066400000000000000000000123171477303350200217740ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Source type mode = RMS | Peak class window mode duration source = object (self) inherit operator [source] ~name:(match mode with RMS -> "rms" | Peak -> "peak") method fallible = source#fallible method private can_generate_frame = source#is_ready method remaining = source#remaining method seek_source = source#seek_source method abort_track = source#abort_track method self_sync = source#self_sync (** Accumulator (e.g. sum of squares). *) val mutable acc = [||] (** Duration of the accumulated data (in samples). *) val mutable acc_dur = 0 (** Last computed value (rms or peak). *) val mutable value = [||] initializer self#on_wake_up (fun () -> let channels = self#audio_channels in acc <- Array.make channels 0.; value <- Array.make channels 0.) val m = Mutex.create () method value = Mutex_utils.mutexify m (fun () -> value) () method private generate_frame = let frame = source#get_frame in let duration = duration () in if duration > 0. then ( let duration = Frame.audio_of_seconds duration in let position = AFrame.position frame in let buf = AFrame.pcm frame in for i = 0 to position - 1 do for c = 0 to self#audio_channels - 1 do let x = buf.(c).(i) in match mode with | RMS -> acc.(c) <- acc.(c) +. (x *. x) | Peak -> acc.(c) <- max acc.(c) (Utils.abs_float x) done; acc_dur <- acc_dur + 1; if acc_dur >= duration then ( let dur = float acc_dur in let value' = Array.init self#audio_channels (fun i -> match mode with | RMS -> let v = sqrt (acc.(i) /. dur) in acc.(i) <- 0.; v | Peak -> let v = acc.(i) in acc.(i) <- 0.; v) in acc_dur <- 0; Mutex_utils.mutexify m (fun () -> value <- value') ()) done); frame end let declare ?base mode name frame_t fun_ret_t f_ans = let meth, doc = match mode with | RMS -> ("rms", "RMS volume") | Peak -> ("peak", "peak volume") in let return_t = frame_t () in Lang.add_operator ?base name ~category:`Audio ~meth: [ ( meth, ([], Lang.fun_t [] fun_ret_t), "Current value for the " ^ doc ^ ".", fun s -> Lang.val_fun [] (fun _ -> f_ans s#value) ); ] ~return_t ~descr: ("Get current " ^ doc ^ " of the source. Returns the source with a method `" ^ meth ^ "` to compute the current " ^ doc ^ " of the source, with `0.0 <= " ^ doc ^ " <= 1.0`.") [ ( "duration", Lang.getter_t Lang.float_t, Some (Lang.float 0.5), Some "Duration of the window (in seconds). A value <= 0, means that \ computation should not be performed." ); ("", Lang.source_t return_t, None, None); ] (fun p -> let f v = List.assoc v p in let src = Lang.to_source (f "") in let duration = Lang.to_float_getter (f "duration") in new window mode duration src) let rms, peak = let mean value = let x = Array.fold_left ( +. ) 0. value in let x = x /. float (Array.length value) in Lang.float x in let stereo value = Lang.product (Lang.float value.(0)) (Lang.float value.(1)) in let pcm_t () = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in let stereo_t () = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio_stereo ()) ()) in let rms = declare RMS "rms" pcm_t Lang.float_t mean in let peak = declare Peak "peak" pcm_t Lang.float_t mean in let declare mode suffix frame_t fun_ret_t f_ans = let base = match mode with RMS -> rms | Peak -> peak in ignore (declare ~base mode suffix frame_t fun_ret_t f_ans) in declare RMS "stereo" stereo_t (Lang.product_t Lang.float_t Lang.float_t) stereo; declare Peak "stereo" stereo_t (Lang.product_t Lang.float_t Lang.float_t) stereo; (rms, peak) liquidsoap-2.3.2/src/core/outputs/000077500000000000000000000000001477303350200171365ustar00rootroot00000000000000liquidsoap-2.3.2/src/core/outputs/ao_out.ml000066400000000000000000000113331477303350200207570ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Output using ao lib. *) open Ao module SyncSource = Clock.MkSyncSource (struct type t = unit let to_string _ = "ao" end) let sync_source = SyncSource.make () class output ~self_sync ~driver ~register_telnet ~infallible ~on_start ~on_stop ~options ?channels_matrix source start = let samples_per_second = Lazy.force Frame.audio_rate in let bytes_per_sample = 2 in object (self) inherit Output.output ~register_telnet ~infallible ~on_start ~on_stop ~name:"ao" ~output_kind:"output.ao" source start val mutable device = None method self_sync = if self_sync then (`Dynamic, if device <> None then Some sync_source else None) else (`Static, None) method get_device = match device with | Some d -> d | None -> let driver = if driver = "" then get_default_driver () else find_driver driver in let dev = self#log#important "Opening %s (%d channels)..." driver.Ao.name self#audio_channels; open_live ~driver ~options ?channels_matrix ~rate:samples_per_second ~bits:(bytes_per_sample * 8) ~channels:self#audio_channels () in device <- Some dev; dev method start = ignore self#get_device method stop = match device with | Some d -> Ao.close d; device <- None | None -> () method push_block data = let dev = self#get_device in play dev (Bytes.unsafe_to_string data) method send_frame frame = play self#get_device (AFrame.s16le frame) method! reset = () end let _ = let return_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:Modules.output "ao" (Output.proto @ [ ( "self_sync", Lang.bool_t, Some (Lang.bool true), Some "Use the dedicated AO clock." ); ( "driver", Lang.string_t, Some (Lang.string ""), Some "Driver to be used, \"\" for AO's default." ); ( "channels_matrix", Lang.string_t, Some (Lang.string ""), Some "Output channels matrix, \"\" for AO's default." ); ( "options", Lang.metadata_t, Some (Lang.list []), Some "List of parameters, depends on the driver." ); ("", Lang.source_t return_t, None, None); ]) ~category:`Output ~meth:Output.meth ~descr:"Output stream to local sound card using libao." ~return_t (fun p -> let self_sync = Lang.to_bool (List.assoc "self_sync" p) in let driver = Lang.to_string (List.assoc "driver" p) in let options = List.map (fun x -> let a, b = Lang.to_product x in (Lang.to_string a, Lang.to_string b)) (Lang.to_list (List.assoc "options" p)) in let channels_matrix = Lang.to_string (List.assoc "channels_matrix" p) in let channels_matrix = if channels_matrix = "" then None else Some channels_matrix in let infallible = not (Lang.to_bool (List.assoc "fallible" p)) in let register_telnet = Lang.to_bool (List.assoc "register_telnet" p) in let start = Lang.to_bool (List.assoc "start" p) in let on_start = let f = List.assoc "on_start" p in fun () -> ignore (Lang.apply f []) in let on_stop = let f = List.assoc "on_stop" p in fun () -> ignore (Lang.apply f []) in let source = List.assoc "" p in (new output ~self_sync ~driver ~infallible ~register_telnet ~on_start ~on_stop ?channels_matrix ~options source start :> Output.output)) liquidsoap-2.3.2/src/core/outputs/bjack_out.ml000066400000000000000000000106341477303350200214350ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Output using ocaml-jack. *) let bytes_per_sample = 2 class output ~self_sync ~infallible ~register_telnet ~on_stop ~on_start ~server source = let samples_per_frame = AFrame.size () in let seconds_per_frame = Frame.seconds_of_audio samples_per_frame in let samples_per_second = Lazy.force Frame.audio_rate in object (self) inherit Output.output ~infallible ~register_telnet ~on_stop ~on_start ~name:"output.jack" ~output_kind:"output.jack" source true val mutable device = None method self_sync = if self_sync then (`Dynamic, if device <> None then Some Bjack_in.sync_source else None) else (`Static, None) method get_device = match device with | None -> (* Wait for things to settle *) Thread.delay (5. *. seconds_per_frame); let server_name = match server with "" -> None | s -> Some s in let dev = try Bjack.open_t ~rate:samples_per_second ~bits_per_sample:(bytes_per_sample * 8) ~input_channels:0 ~output_channels:self#audio_channels ~flags:[] ?server_name ~ringbuffer_size:(samples_per_frame * bytes_per_sample) ~client_name:self#id () with Bjack.Open -> failwith "Could not open JACK device: is the server running?" in Bjack.set_all_volume dev 100; device <- Some dev; dev | Some d -> d method start = ignore self#get_device method stop = match device with | Some d -> Bjack.close d; device <- None | None -> () method send_frame frame = let dev = self#get_device in let data = AFrame.s16le frame in let len = String.length data in let remaining = ref (len - Bjack.write dev data) in while !remaining > 0 do Thread.delay (seconds_per_frame /. 2.); let tmp = Str.string_after data (len - !remaining) in let written = Bjack.write dev tmp in remaining := !remaining - written done method! reset = () end let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:Modules.output "jack" (Output.proto @ [ ( "self_sync", Lang.bool_t, Some (Lang.bool true), Some "Force the use of the dedicated bjack clock." ); ( "server", Lang.string_t, Some (Lang.string ""), Some "Jack server to connect to." ); ("", Lang.source_t frame_t, None, None); ]) ~return_t:frame_t ~category:`Output ~meth:Output.meth ~descr:"Output stream to jack." (fun p -> let source = List.assoc "" p in let self_sync = Lang.to_bool (List.assoc "self_sync" p) in let server = Lang.to_string (List.assoc "server" p) in let infallible = not (Lang.to_bool (List.assoc "fallible" p)) in let register_telnet = Lang.to_bool (List.assoc "register_telnet" p) in let on_start = let f = List.assoc "on_start" p in fun () -> ignore (Lang.apply f []) in let on_stop = let f = List.assoc "on_stop" p in fun () -> ignore (Lang.apply f []) in (new output ~self_sync ~infallible ~register_telnet ~on_start ~on_stop ~server source :> Output.output)) liquidsoap-2.3.2/src/core/outputs/graphics_out.ml000066400000000000000000000056541477303350200221710ustar00rootroot00000000000000(***************************************************************************** Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm class output ~infallible ~register_telnet ~autostart ~on_start ~on_stop source = object (self) inherit Output.output ~name:"graphics" ~output_kind:"output.graphics" ~infallible ~register_telnet ~on_start ~on_stop source autostart val mutable sleep = false method stop = sleep <- true method start = let width, height = self#video_dimensions in Graphics.open_graph ""; Graphics.set_window_title "Liquidsoap"; Graphics.resize_window width height; sleep <- false method send_frame buf = match (VFrame.data buf).Content.Video.data with | [] -> () | (_, img) :: _ -> let width, height = self#video_dimensions in let img = img |> Video.Canvas.Image.viewport width height |> Video.Canvas.Image.render ~transparent:false |> Image.YUV420.to_int_image |> Graphics.make_image in Graphics.draw_image img 0 0 method! reset = () end let _ = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~video:(Format_type.video ()) ()) in Lang.add_operator ~base:Modules.output "graphics" (Output.proto @ [("", Lang.source_t frame_t, None, None)]) ~return_t:frame_t ~category:`Output ~meth:Output.meth ~descr:"Display video stream using the Graphics library." (fun p -> let autostart = Lang.to_bool (List.assoc "start" p) in let infallible = not (Lang.to_bool (List.assoc "fallible" p)) in let register_telnet = Lang.to_bool (List.assoc "register_telnet" p) in let on_start = let f = List.assoc "on_start" p in fun () -> ignore (Lang.apply f []) in let on_stop = let f = List.assoc "on_stop" p in fun () -> ignore (Lang.apply f []) in let source = List.assoc "" p in (new output ~infallible ~register_telnet ~autostart ~on_start ~on_stop source :> Output.output)) liquidsoap-2.3.2/src/core/outputs/harbor_output.ml000066400000000000000000000522311477303350200223700ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let ( let* ) = Duppy.Monad.bind module Http = Liq_http (** Output to an harbor server. *) module type T = sig include Harbor.Transport_t val source_name : string val source_description : string end module Mutex_control = struct type priority = Tutils.priority let scheduler = Tutils.scheduler let priority = `Non_blocking end module Duppy_m = Duppy.Monad.Mutex.Factory (Mutex_control) module Duppy_c = Duppy.Monad.Condition.Factory (Duppy_m) module Duppy = Harbor.Http_transport.Duppy module Icecast = struct type protocol = unit let protocol_of_icecast_protocol _ = () type content = string let format_of_content x = x type info = unit let info_of_encoder _ _ = () end module M = Icecast_utils.Icecast_v (Icecast) open M (* Max total length for ICY metadata is 255*16 Format is: "StreamTitle='%s';StreamUrl='%s'" "StreamTitle='';"; is 15 chars long, "StreamUrl='';" is 13 chars long, leaving 4052 chars remaining. Splitting those in: max title length = 3852 max url length = 200 *) let max_title = 3852 let max_url = 200 let proto frame_t = Output.proto @ Icecast_utils.base_proto frame_t @ [ ("mount", Lang.string_t, None, None); ("port", Lang.int_t, Some (Lang.int 8000), None); ( "transport", Lang.http_transport_base_t, Some (Lang.base_http_transport Http.unix_transport), Some "Http transport. Use `http.transport.ssl` or \ `http.transport.secure_transport`, when available, to enable HTTPS \ output" ); ( "user", Lang.nullable_t Lang.string_t, Some Lang.null, Some "User for client connection. You also need to setup a `password`." ); ( "password", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Password for client connection. A `user` must also be set. We check \ for this password is checked unless an `auth` function is defined, \ which is used in this case." ); ( "timeout", Lang.float_t, Some (Lang.float 30.), Some "Timeout for network operations (in seconds)." ); ( "encoding", Lang.string_t, Some (Lang.string ""), Some "Encoding used to send metadata. If empty, defaults to \"UTF-8\"" ); ("url", Lang.nullable_t Lang.string_t, Some Lang.null, None); ( "metaint", Lang.int_t, Some (Lang.int 8192), Some "Interval used to send ICY metadata" ); ( "auth", Lang.nullable_t (Lang.fun_t [ (false, "address", Lang.string_t); (false, "", Lang.string_t); (false, "", Lang.string_t); ] Lang.bool_t), Some Lang.null, Some "Authentication function. `f(~address,login,password)` returns \ `true` if the user should be granted access for this login. When \ defined, `user` and `password` arguments are not taken in account." ); ( "buffer", Lang.int_t, Some (Lang.int (5 * 65535)), Some "Maximum buffer per-client." ); ( "burst", Lang.int_t, Some (Lang.int 65534), Some "Initial burst of data sent to the client." ); ( "chunk", Lang.int_t, Some (Lang.int Utils.pagesize), Some "Send data to clients using chunks of at least this length." ); ( "on_connect", Lang.fun_t [ (false, "headers", Lang.metadata_t); (false, "uri", Lang.string_t); (false, "protocol", Lang.string_t); (false, "", Lang.string_t); ] Lang.unit_t, Some (Lang.val_cst_fun [("headers", None); ("uri", None); ("protocol", None); ("", None)] Lang.unit), Some "Callback executed when connection is established (takes headers, \ connection uri, protocol and client's IP as arguments)." ); ( "on_disconnect", Lang.fun_t [(false, "", Lang.string_t)] Lang.unit_t, Some (Lang.val_cst_fun [("", None)] Lang.unit), Some "Callback executed when connection stops (takes client's IP as \ argument)." ); ( "headers", Lang.metadata_t, Some (Lang.list []), Some "Additional headers." ); ( "dumpfile", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Dump stream to file, for debugging purpose. Disabled if null." ); ("", Lang.source_t frame_t, None, None); ] type client_state = Hello | Sending | Done type metadata = { mutable metadata : Frame.metadata option; metadata_m : Mutex.t; } type client = { buffer : Strings.Mutable.t; condition : Duppy_c.condition; condition_m : Duppy_m.mutex; mutex : Mutex.t; meta : metadata; mutable latest_meta : string; metaint : int; timeout : float; url : string option; mutable metapos : int; chunk : int; mutable state : client_state; close : unit -> unit; handler : (Tutils.priority, Harbor.reply) Duppy.Monad.Io.handler; } let add_meta c data = let mk_icy_meta meta = let meta_info = match ( Frame.Metadata.find_opt "artist" meta, Frame.Metadata.find_opt "title" meta ) with | Some a, Some t -> Some (Printf.sprintf "%s - %s" a t) | Some s, None | None, Some s -> Some s | None, None -> None in let meta = match meta_info with | Some s when String.length s > max_title -> Printf.sprintf "StreamTitle='%s...';" (String.sub s 0 (max_title - 3)) | Some s -> Printf.sprintf "StreamTitle='%s';" s | None -> "" in let meta = match c.url with | Some s when String.length s > max_url -> Printf.sprintf "%sStreamURL='%s...';" meta (String.sub s 0 (max_url - 3)) | Some s -> Printf.sprintf "%sStreamURL='%s';" meta s | None -> meta in (* Pad string to a multiple of 16 bytes. *) let len = String.length meta in let pad = (len / 16) + 1 in let ret = Bytes.make ((pad * 16) + 1) '\000' in Bytes.set ret 0 (Char.chr pad); String.blit meta 0 ret 1 len; let ret = Bytes.unsafe_to_string ret in if ret <> c.latest_meta then ( c.latest_meta <- ret; ret) else "\000" in let get_meta () = let meta = Mutex_utils.mutexify c.meta.metadata_m (fun () -> let meta = c.meta.metadata in c.meta.metadata <- None; meta) () in match meta with Some meta -> mk_icy_meta meta | None -> "\000" in let rec process cur data = let len = Strings.length data in if c.metaint <= c.metapos + len then ( let meta = get_meta () in let next_meta_pos = c.metaint - c.metapos in let before = Strings.sub data 0 next_meta_pos in let after = Strings.sub data next_meta_pos (len - next_meta_pos) in let cur = Strings.concat [cur; before; Strings.of_string meta] in c.metapos <- 0; process cur after) else ( c.metapos <- c.metapos + len; Strings.concat [cur; data]) in if c.metaint > 0 then process Strings.empty data else data let rec client_task c = let* data = Duppy.Monad.Io.exec ~priority:`Maybe_blocking c.handler (Mutex_utils.mutexify c.mutex (fun () -> let buflen = Strings.Mutable.length c.buffer in let data = if buflen > c.chunk then add_meta c (Strings.Mutable.flush c.buffer) else Strings.empty in Duppy.Monad.return data) ()) in let* () = if Strings.is_empty data then let* () = Duppy_m.lock c.condition_m in let* () = Duppy_c.wait c.condition c.condition_m in Duppy_m.unlock c.condition_m else Duppy.Monad.Io.write ?timeout:(Some c.timeout) ~priority:`Non_blocking c.handler (Strings.to_bytes data) in let* state = Duppy.Monad.Io.exec ~priority:`Maybe_blocking c.handler (let ret = Mutex_utils.mutexify c.mutex (fun () -> c.state) () in Duppy.Monad.return ret) in if state <> Done then client_task c else Duppy.Monad.return () let client_task c = Mutex_utils.mutexify c.mutex (fun () -> assert (c.state = Hello); c.state <- Sending) (); Duppy.Monad.catch (client_task c) (fun _ -> Duppy.Monad.raise ()) (** Sending encoded data to a shout-compatible server. It directly takes the Lang param list and extracts stuff from it. *) class output p = let pos = Lang.pos p in let e f v = f (List.assoc v p) in let s v = e Lang.to_string v in let on_connect = List.assoc "on_connect" p in let on_disconnect = List.assoc "on_disconnect" p in let on_connect ~headers ~protocol ~uri s = ignore (Lang.apply on_connect [ ("headers", Lang.metadata headers); ("uri", Lang.string uri); ("protocol", Lang.string protocol); ("", Lang.string s); ]) in let on_disconnect s = ignore (Lang.apply on_disconnect [("", Lang.string s)]) in let metaint = Lang.to_int (List.assoc "metaint" p) in let data = encoder_data p in let encoding = Lang.to_string (List.assoc "encoding" p) in let recode m = let out_enc = match encoding with "" -> Charset.utf8 | s -> Charset.of_string s in let f = Charset.convert ~target:out_enc in Frame.Metadata.fold (fun a b m -> Frame.Metadata.add a (f b) m) Frame.Metadata.empty m in let timeout = Lang.to_float (List.assoc "timeout" p) in let buflen = Lang.to_int (List.assoc "buffer" p) in let burst = Lang.to_int (List.assoc "burst" p) in let chunk = Lang.to_int (List.assoc "chunk" p) in let () = if chunk > buflen then raise (Error.Invalid_value (List.assoc "buffer" p, "Maximum buffering inferior to chunk length")) else (); if burst > buflen then raise (Error.Invalid_value (List.assoc "buffer" p, "Maximum buffering inferior to burst length")) else () in let source_val = Lang.assoc "" 2 p in let source = Lang.to_source source_val in let mount = s "mount" in let uri = match mount.[0] with '/' -> mount | _ -> Printf.sprintf "%c%s" '/' mount in let uri = let regexp = [%string {|^%{uri}$|}] in Liquidsoap_lang.Builtins_regexp. { descr = regexp; flags = []; regexp = Re.Pcre.regexp regexp } in let autostart = Lang.to_bool (List.assoc "start" p) in let infallible = not (Lang.to_bool (List.assoc "fallible" p)) in let register_telnet = Lang.to_bool (List.assoc "register_telnet" p) in let on_start = let f = List.assoc "on_start" p in fun () -> ignore (Lang.apply f []) in let on_stop = let f = List.assoc "on_stop" p in fun () -> ignore (Lang.apply f []) in let url = List.assoc "url" p |> Lang.to_option |> Option.map Lang.to_string in let port = e Lang.to_int "port" in let transport = e Lang.to_http_transport "transport" in let default_user = List.assoc "user" p |> Lang.to_option |> Option.map Lang.to_string in let default_password = List.assoc "password" p |> Lang.to_option |> Option.map Lang.to_string in let address_resolver s = let s = Harbor.file_descr_of_socket s in Utils.name_of_sockaddr ~rev_dns:Harbor_base.conf_revdns#get (Unix.getpeername s) in let auth_function = List.assoc "auth" p |> Lang.to_option in let login ~socket user password = let address = address_resolver socket in let user, password = let f = Charset.convert in (f user, f password) in match auth_function with | Some f -> Lang.to_bool (Lang.apply f [ ("address", Lang.string address); ("", Lang.string user); ("", Lang.string password); ]) | None -> ( match (default_user, default_password) with | _, None -> false | None, _ -> false | Some default_user, Some default_password -> user = default_user && password = default_password) in let dumpfile = Lang.to_valued_option Lang.to_string (List.assoc "dumpfile" p) in let extra_headers = List.map (fun v -> let f (x, y) = (Lang.to_string x, Lang.to_string y) in f (Lang.to_product v)) (Lang.to_list (List.assoc "headers" p)) in object (self) (** File descriptor where to dump. *) inherit [Strings.t] Output.encoded ~output_kind:"output.harbor" ~infallible ~register_telnet ~autostart ~export_cover_metadata:false ~on_start ~on_stop ~name:mount source_val val mutable dump = None val mutable encoder = None val mutable clients = Queue.create () val clients_m = Mutex.create () val duppy_c = Duppy_c.create () val duppy_m = Duppy_m.create () val mutable chunk_len = 0 val burst_data = Strings.Mutable.empty () val metadata = { metadata = None; metadata_m = Mutex.create () } method encode frame = (Option.get encoder).Encoder.encode frame method self_sync = source#self_sync method insert_metadata m = let m = Frame.Metadata.Export.to_metadata m in let m = recode m in Mutex_utils.mutexify metadata.metadata_m (fun () -> metadata.metadata <- Some m) (); (Option.get encoder).Encoder.insert_metadata (Frame.Metadata.Export.from_metadata ~cover:false m) method add_client ~protocol ~headers ~uri ~query s = let ip = (* Show port = true to catch different clients from same ip *) let fd = Harbor.file_descr_of_socket s in Utils.name_of_sockaddr ~show_port:true (Unix.getpeername fd) in let metaint, icyheader = try assert (List.assoc "Icy-MetaData" headers = "1"); (metaint, Printf.sprintf "icy-metaint: %d\r\n" metaint) with _ -> (-1, "") in let extra_headers = String.concat "" (List.map (fun (x, y) -> Printf.sprintf "%s: %s\r\n" x y) extra_headers) in let reply = Printf.sprintf "HTTP/%s 200 OK\r\nContent-type: %s\r\n%s%s\r\n" protocol data.format icyheader extra_headers in let buffer = Strings.Mutable.of_strings ((Option.get encoder).Encoder.header ()) in let close () = try Harbor.close s with _ -> () in let rec client = { buffer; condition = duppy_c; condition_m = duppy_m; metaint; meta = metadata; latest_meta = "\000"; metapos = 0; url; timeout; mutex = Mutex.create (); state = Hello; chunk; close; handler; } and handler = { Duppy.Monad.Io.scheduler = Tutils.scheduler; socket = s; data = ""; on_error = (fun e -> let bt = Printexc.get_backtrace () in let msg = match e with | Duppy.Io.Timeout -> Printf.sprintf "Timeout error for %s" ip | Duppy.Io.Io_error -> Printf.sprintf "I/O error for %s" ip | Duppy.Io.Unix (c, p, m) -> Printf.sprintf "Unix error for %s: %s" ip (Printexc.to_string (Unix.Unix_error (c, p, m))) | Duppy.Io.Unknown e -> Printf.sprintf "%s" (Printexc.to_string e) in Utils.log_exception ~log:self#log ~bt msg; self#log#info "Client %s disconnected" ip; Mutex_utils.mutexify client.mutex (fun () -> client.state <- Done; ignore (Strings.Mutable.flush client.buffer)) (); on_disconnect ip; Harbor.Close (Harbor.mk_simple "")); } in self#log#info "Serving client %s." ip; let* () = Duppy.Monad.catch (if (default_user <> None && default_password <> None) || auth_function <> None then ( let default_user = Option.value default_user ~default:"" in Duppy.Monad.Io.exec ~priority:`Maybe_blocking handler (Harbor.http_auth_check ~query ~login:(default_user, login) s headers)) else Duppy.Monad.return ()) (function | Harbor.Close s -> self#log#info "Client %s failed to authenticate!" ip; client.state <- Done; Harbor.reply s | _ -> assert false) in Duppy.Monad.Io.exec ~priority:`Maybe_blocking handler (Harbor.relayed reply (fun () -> self#log#info "Client %s connected" ip; Mutex_utils.mutexify clients_m (fun () -> Queue.push client clients) (); on_connect ~protocol ~uri ~headers:(Frame.Metadata.from_list headers) ip)) method send b = let slen = Strings.length b in if slen > 0 then ( chunk_len <- chunk_len + slen; let wake_up = if chunk_len >= chunk then ( chunk_len <- 0; true) else false in Strings.Mutable.append_strings burst_data b; Strings.Mutable.keep burst_data burst; let new_clients = Queue.create () in (match dump with | Some s -> Strings.iter (output_substring s) b | None -> ()); Mutex_utils.mutexify clients_m (fun () -> Queue.iter (fun c -> let start = Mutex_utils.mutexify c.mutex (fun () -> match c.state with | Hello -> Strings.Mutable.append c.buffer burst_data; Queue.push c new_clients; true | Sending -> let buf = Strings.Mutable.length c.buffer in if buf + slen > buflen then Strings.Mutable.drop c.buffer (min buf slen); Strings.Mutable.append_strings c.buffer b; Queue.push c new_clients; false | Done -> false) () in if start then Duppy.Monad.run ~return:c.close ~raise:c.close (client_task c) else ()) clients; if wake_up && Queue.length new_clients > 0 then Duppy.Monad.run ~return:(fun () -> ()) ~raise:(fun () -> ()) (Duppy_c.broadcast duppy_c) else (); clients <- new_clients) ()) else () method start = assert (encoder = None); let enc = data.factory self#id in encoder <- Some (enc Frame.Metadata.Export.empty); let handler ~protocol ~meth:_ ~data:_ ~headers ~query ~socket uri = self#add_client ~protocol ~headers ~uri ~query socket in Harbor.add_http_handler ~pos ~transport ~port ~verb:`Get ~uri handler; match dumpfile with Some f -> dump <- Some (open_out_bin f) | None -> () method stop = ignore ((Option.get encoder).Encoder.stop ()); encoder <- None; Harbor.remove_http_handler ~port ~verb:`Get ~uri (); let new_clients = Queue.create () in Mutex_utils.mutexify clients_m (fun () -> Queue.iter (fun c -> Mutex_utils.mutexify c.mutex (fun () -> c.state <- Done; Duppy.Monad.run ~return:(fun () -> ()) ~raise:(fun () -> ()) (Duppy_c.broadcast duppy_c)) ()) clients; clients <- new_clients) (); match dump with Some f -> close_out f | None -> () method! reset = self#stop; self#start end let _ = let return_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator ~category:`Output ~descr:"Encode and output the stream using the harbor server." ~meth:Output.meth ~base:Modules.output "harbor" (proto return_t) ~return_t (fun p -> (new output p :> Output.output)) liquidsoap-2.3.2/src/core/outputs/hls_output.ml000066400000000000000000001265211477303350200217050ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** HLS output. *) exception Invalid_state let default_id3_version = 3 let log = Log.make ["hls"; "output"] let default_name = Lang.eval ~cache:false ~typecheck:false ~stdlib:`Disabled {|fun (metadata) -> "#{metadata.stream_name}_#{metadata.position}.#{metadata.extname}"|} let hls_proto frame_t = let main_playlist_writer_t = Lang.fun_t [ (false, "extra_tags", Lang.list_t Lang.string_t); (false, "prefix", Lang.string_t); (false, "version", Lang.int_t); ( false, "", Lang.list_t (Lang.optional_method_t (Lang.method_t Lang.string_t [ ("bandwidth", ([], Lang.int_t), "Stream bandwidth"); ("codecs", ([], Lang.string_t), "Stream codecs"); ]) [ ( "video_size", ([], Lang.product_t Lang.int_t Lang.int_t), "Stream video size" ); ]) ); ] (Lang.nullable_t Lang.string_t) in let segment_name_t = Lang.fun_t [ ( false, "", Lang.record_t [ ("position", Lang.int_t); ("extname", Lang.string_t); ("duration", Lang.float_t); ("ticks", Lang.int_t); ("stream_name", Lang.string_t); ] ); ] Lang.string_t in let stream_info_t = Lang.product_t Lang.string_t (Lang.optional_method_t (Lang.format_t frame_t) [ ("bandwidth", ([], Lang.int_t), "Bandwidth"); ("codecs", ([], Lang.string_t), "Codec"); ("extname", ([], Lang.string_t), "Filename extension"); ( "id3", ([], Lang.bool_t), "Set to `false` to disable ID3 tags. Tags are enabled by default \ whenever allowed." ); ( "id3_version", ([], Lang.int_t), "Version for ID3 tag. Default version: " ^ string_of_int default_id3_version ); ( "replay_id3", ([], Lang.bool_t), "Replay ID3 data on each segment to make sure new listeners \ always start with fresh value. Enabled by default." ); ("extra_tags", ([], Lang.list_t Lang.string_t), "Extra tags"); ( "video_size", ([], Lang.product_t Lang.int_t Lang.int_t), "Video size" ); ]) in Output.proto @ [ ( "playlist", Lang.string_t, Some (Lang.string "stream.m3u8"), Some "Playlist name (m3u8 extension is recommended)." ); ( "extra_tags", Lang.list_t Lang.string_t, Some (Lang.list []), Some "Extra tags to insert into the main playlist." ); ( "prefix", Lang.string_t, Some (Lang.string ""), Some "Prefix for each files in playlists." ); ( "main_playlist_writer", Lang.nullable_t main_playlist_writer_t, Some Lang.null, Some "Main playlist writer. Main playlist writing is disabled when `null` \ or when returning `null`." ); ( "segment_duration", Lang.float_t, Some (Lang.float 10.), Some "Segment duration (in seconds)." ); ( "segment_name", segment_name_t, Some default_name, Some "Segment name. Default: `fun (metadata) -> \ \"#{metadata.stream_name}_#{metadata.position}.#{metadata.extname}\"`" ); ( "segments_overhead", Lang.nullable_t Lang.int_t, Some (Lang.int 5), Some "Number of segments to keep after they have been featured in the \ live playlist. Set to `null` to disable." ); ( "segments", Lang.int_t, Some (Lang.int 10), Some "Number of segments per playlist." ); ( "perm", Lang.int_t, Some (Lang.octal_int 0o666), Some "Permission of the created files, up to umask." ); ( "dir_perm", Lang.int_t, Some (Lang.octal_int 0o777), Some "Permission of the directories if some have to be created, up to \ umask." ); ( "temp_dir", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Temporary directory used for writing files. This should be in the \ same partition or device as the final directory to guarantee atomic \ file operations. Use the same directory as the HLS files if `null`." ); ( "on_file_change", Lang.fun_t [(false, "state", Lang.string_t); (false, "", Lang.string_t)] Lang.unit_t, Some (Lang.val_cst_fun [("state", None); ("", None)] Lang.unit), Some "Callback executed when a file changes. `state` is one of: \ `\"created\"`, `\"updated\"` or `\"deleted\"`, second argument is \ file path. Typical use: sync file with a CDN" ); ( "persist_at", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Location of the configuration file used to restart the output. \ Relative paths are assumed to be with regard to the directory for \ generated file." ); ( "strict_persist", Lang.bool_t, Some (Lang.bool false), Some "Fail if an invalid saved state exists." ); ("", Lang.string_t, None, Some "Directory for generated files."); ( "", Lang.list_t stream_info_t, None, Some "List of specifications for each stream: (name, format)." ); ("", Lang.source_t frame_t, None, None); ] type atomic_out_channel = < output_string : string -> unit ; output_substring : string -> int -> int -> unit ; position : int ; truncate : int -> unit ; saved_filename : string option ; read : int -> int -> string ; close : unit > type segment = { id : int; discontinuous : bool; current_discontinuity : int; segment_extra_tags : string list; mutable init_filename : string option; mutable filename : string option; mutable out_channel : atomic_out_channel option; (* Segment length in main ticks. *) mutable len : int; mutable last_segmentable_position : (int * int) option; } (* We used to encode optional entries with null but it's more future-proof to use undefined. These routines abstract it away. *) let json_optional lbl f = function None -> [] | Some v -> [(lbl, f v)] let parse_json_optional lbl f l = match List.assoc_opt lbl l with | Some `Null | None -> None | Some v -> Some (f v) let parse_json lbl f l = match List.assoc_opt lbl l with Some v -> f v | None -> raise Invalid_state let parse_json_int lbl l = parse_json lbl (function `Int i -> i | _ -> raise Invalid_state) l let parse_json_bool lbl l = parse_json lbl (function `Bool b -> b | _ -> raise Invalid_state) l let parse_json_string lbl l = parse_json lbl (function `String s -> s | _ -> raise Invalid_state) l let json_of_segment { id; discontinuous; current_discontinuity; init_filename; filename; segment_extra_tags; len; last_segmentable_position; } = `Assoc ([ ("id", `Int id); ("discontinuous", `Bool discontinuous); ("current_discontinuity", `Int current_discontinuity); ] @ json_optional "init_filename" (fun s -> `String s) init_filename @ json_optional "filename" (fun s -> `String s) filename @ [ ("extra_tags", `Tuple (List.map (fun s -> `String s) segment_extra_tags)); ("len", `Int len); ] @ json_optional "last_segmentable_position" (fun (len, offset) -> `Tuple [`Int len; `Int offset]) last_segmentable_position) let segment_of_json = function | `Assoc l -> let id = parse_json_int "id" l in let discontinuous = parse_json_bool "discontinuous" l in let current_discontinuity = parse_json_int "current_discontinuity" l in let segment_extra_tags = parse_json "extra_tags" (function | `Tuple l -> List.map (function `String s -> s | _ -> raise Invalid_state) l | _ -> raise Invalid_state) l in let len = parse_json_int "len" l in let init_filename = parse_json_optional "init_filename" (function `String s -> s | _ -> raise Invalid_state) l in let filename = parse_json_optional "filename" (function `String s -> s | _ -> raise Invalid_state) l in let last_segmentable_position = parse_json_optional "last_segmentable_position" (function | `Tuple [`Int len; `Int offset] -> (len, offset) | _ -> raise Invalid_state) l in { id; discontinuous; current_discontinuity; init_filename; len; segment_extra_tags; filename; out_channel = None; last_segmentable_position; } | _ -> raise Invalid_state type segments = segment list ref let push_segment segment segments = segments := !segments @ [segment] let remove_segment segments = match !segments with | [] -> assert false | s :: l -> segments := l; s type init_state = [ `Todo | `No_init | `Has_init of string ] type metadata = [ `None | `Sent of Frame.Metadata.Export.t | `Todo of Frame.Metadata.Export.t ] let pending_metadata = function `Todo _ -> true | _ -> false (** A stream in the HLS (which typically contains many, with different qualities). *) type stream = { name : string; format : Encoder.format; encoder : Encoder.encoder; video_size : (int * int) option Lazy.t; bandwidth : int Lazy.t; codecs : string Lazy.t; (** codecs (see RFC 6381) *) extname : string; id3_enabled : bool; replay_id3 : bool; stream_extra_tags : string list; mutable pending_extra_tags : string list Atomic.t; mutable metadata : metadata; mutable init_state : init_state; mutable init_position : int; mutable position : int; mutable current_segment : segment option; mutable discontinuity_count : int; } type hls_state = [ `Idle | `Started | `Stopped | `Restarted | `Streaming ] open Extralib let ( ^^ ) = Filename.concat type file_state = [ `Created | `Updated | `Deleted ] let string_of_file_state = function | `Created -> "created" | `Updated -> "updated" | `Deleted -> "deleted" class hls_output p = let on_start = let f = List.assoc "on_start" p in fun () -> ignore (Lang.apply f []) in let on_stop = let f = List.assoc "on_stop" p in fun () -> ignore (Lang.apply f []) in let on_file_change = let f = List.assoc "on_file_change" p in fun ~state filename -> ignore (Lang.apply f [ ("state", Lang.string (string_of_file_state state)); ("", Lang.string filename); ]) in let autostart = Lang.to_bool (List.assoc "start" p) in let infallible = not (Lang.to_bool (List.assoc "fallible" p)) in let register_telnet = Lang.to_bool (List.assoc "register_telnet" p) in let prefix = Lang.to_string (List.assoc "prefix" p) in let main_playlist_writer = Option.map (fun fn ~extra_tags ~version ~prefix streams -> let extra_tags = Lang.list (List.map Lang.string extra_tags) in let version = Lang.int version in let prefix = Lang.string prefix in let streams = Lang.list (List.map (fun stream -> Lang.meth (Lang.string stream.name) [ ("bandwidth", Lang.int (Lazy.force stream.bandwidth)); ("codecs", Lang.string (Lazy.force stream.codecs)); ( "video_size", match Lazy.force stream.video_size with | None -> Lang.null | Some (w, h) -> Lang.product (Lang.int w) (Lang.int h) ); ]) streams) in Lang.to_valued_option Lang.to_string (Lang.apply fn [ ("extra_tags", extra_tags); ("prefix", prefix); ("version", version); ("", streams); ])) (Lang.to_option (List.assoc "main_playlist_writer" p)) in let directory_val = Lang.assoc "" 1 p in let hls_directory = Lang_string.home_unrelate (Lang.to_string directory_val) in let perms = Lang.to_int (List.assoc "perm" p) in let dir_perm = Lang.to_int (List.assoc "dir_perm" p) in let temp_dir = Lang.to_valued_option Lang.to_string (List.assoc "temp_dir" p) in let () = if (not (Sys.file_exists hls_directory)) || not (Sys.is_directory hls_directory) then ( try Utils.mkdir ~perm:dir_perm hls_directory with _ -> raise (Error.Invalid_value (directory_val, "Could not create or open output directory!"))) in let persist_at = Option.map (fun filename -> let filename = Lang.to_string filename in let filename = if Filename.is_relative filename then Filename.concat hls_directory filename else filename in let dir = Filename.dirname filename in (try Utils.mkdir ~perm:dir_perm dir with exn -> raise (Error.Invalid_value ( List.assoc "persist_at" p, Printf.sprintf "Error while creating directory %s for persisting state: %s" (Lang_string.quote_string dir) (Printexc.to_string exn) ))); filename) (Lang.to_option (List.assoc "persist_at" p)) in let strict_persist = Lang.to_bool (List.assoc "strict_persist" p) in (* better choice? *) let segment_duration = Lang.to_float (List.assoc "segment_duration" p) in let segment_ticks = Frame.main_of_seconds segment_duration / Lazy.force Frame.size in let segment_main_duration = segment_ticks * Lazy.force Frame.size in let segment_duration = Frame.seconds_of_main segment_main_duration in let segment_name = Lang.to_fun (List.assoc "segment_name" p) in let segment_name ~position ~extname ~duration ~ticks sname = Lang.to_string (segment_name [ ( "", Lang.record [ ("position", Lang.int position); ("extname", Lang.string extname); ("duration", Lang.float duration); ("ticks", Lang.int ticks); ("stream_name", Lang.string sname); ] ); ]) in let streams = let streams = Lang.assoc "" 2 p in let l = Lang.to_list streams in if l = [] then raise (Error.Invalid_value (streams, "The list of streams cannot be empty")); l in let mk_streams, streams = let f s = let name, fmt_val = Lang.to_product s in let stream_info, fmt = Lang.split_meths fmt_val in let name = Lang.to_string name in let format = Lang.to_format fmt in let encoder_factory = try Encoder.get_factory format with Not_found -> raise (Error.Invalid_value (fmt_val, "Unsupported format")) in let encoder = encoder_factory ~hls:true ~pos:(Value.pos fmt_val) name Frame.Metadata.Export.empty in let bandwidth = Lazy.from_fun (fun () -> try Lang.to_int (List.assoc "bandwidth" stream_info) with Not_found -> ( match Encoder.(encoder.hls.bitrate ()) with | Some b -> b + (b / 10) | None -> ( try Encoder.bitrate format with Not_found -> raise (Error.Invalid_value ( fmt, Printf.sprintf "Bandwidth for stream %S cannot be inferred \ from codec, please specify it with: \ `%%encoder(...).{bandwidth = , ...}`" name ))))) in let codecs = Lazy.from_fun (fun () -> try Lang.to_string (List.assoc "codecs" stream_info) with Not_found -> ( match Encoder.(encoder.hls.codec_attrs ()) with | Some attrs -> attrs | None -> ( try Encoder.iso_base_file_media_file_format format with Not_found -> raise (Error.Invalid_value ( fmt, Printf.sprintf "Stream info for stream %S cannot be inferred \ from codec, please specify it with: \ `%%encoder(...).{codecs = \"...\", ...}`" name ))))) in let extname = try Lang.to_string (List.assoc "extname" stream_info) with Not_found -> ( try Encoder.extension format with Not_found -> raise (Error.Invalid_value ( fmt, Printf.sprintf "File extension for stream %S cannot be inferred from \ codec, please specify it with: `%%encoder(...).{extname \ = \"...\", ...}`" name ))) in let extname = if extname = "mp4" then "m4s" else extname in let video_size = Lazy.from_fun (fun () -> try let w, h = Lang.to_product (List.assoc "video_size" stream_info) in Some (Lang.to_int w, Lang.to_int h) with Not_found -> ( match Encoder.(encoder.hls.video_size ()) with | Some s -> Some s | None -> Encoder.video_size format)) in let id3_enabled = match Lang.to_bool (List.assoc "id3" stream_info) with | id3_enabled -> let id3_version = try Some (Lang.to_int (List.assoc "id3_version" stream_info)) with Not_found -> None in encoder.hls.init ~id3_enabled ?id3_version () | exception Not_found -> encoder.hls.init () in let replay_id3 = match Lang.to_bool (List.assoc "replay_id3" stream_info) with | b -> b | exception Not_found -> true in let stream_extra_tags = match List.map (fun s -> String.trim (Lang.to_string s)) (Lang.to_list (List.assoc "extra_tags" stream_info)) with | l -> l | exception Not_found -> [] in { name; format; encoder; bandwidth; codecs; video_size; extname; id3_enabled; replay_id3; stream_extra_tags; pending_extra_tags = Atomic.make []; metadata = `None; init_state = `Todo; init_position = 0; position = 1; current_segment = None; discontinuity_count = 0; } in let mk_streams () = List.map f streams in (mk_streams, mk_streams ()) in let x_version = Lazy.from_fun (fun () -> if List.find_opt (fun s -> match s.current_segment with | Some { init_filename = Some _ } -> true | _ -> false) streams <> None then 7 else 3) in let source_val = Lang.assoc "" 3 p in let source = Lang.to_source source_val in let main_playlist_filename = Lang.to_string (List.assoc "playlist" p) in let main_playlist_extra_tags = List.map (fun s -> String.trim (Lang.to_string s)) (Lang.to_list (List.assoc "extra_tags" p)) in let segments_per_playlist = Lang.to_int (List.assoc "segments" p) in let segments_overhead = Lang.to_valued_option Lang.to_int (List.assoc "segments_overhead" p) in let max_segments = Option.map (fun segments_overhead -> segments_per_playlist + segments_overhead) segments_overhead in object (self) inherit [(int * Strings.t option * Strings.t) list] Output.encoded ~infallible ~register_telnet ~on_start ~on_stop ~autostart ~export_cover_metadata:false ~output_kind:"output.file" ~name:main_playlist_filename source_val (** Available segments *) val mutable segments = List.map (fun { name } -> (name, ref [])) streams val mutable streams = streams method streams = streams val mutable current_position = (0, 0) val mutable state : hls_state = `Idle method self_sync = source#self_sync method private toggle_state event = match (event, state) with | `Restart, _ | `Resumed, _ | `Start, `Stopped -> state <- `Restarted | `Stop, _ -> state <- `Stopped | `Start, _ -> state <- `Started | `Streaming, _ -> state <- `Streaming method private open_out filename = let temp_dir = Option.value ~default:hls_directory temp_dir in let tmp_file = Filename.temp_file ~temp_dir "liq" "tmp" in Unix.chmod tmp_file perms; let fd = Unix.openfile tmp_file [Unix.O_RDWR; Unix.O_CREAT; Unix.O_TRUNC; Unix.O_CLOEXEC] perms in object method output_string s = Tutils.write_all fd (Bytes.unsafe_of_string s) method output_substring s ofs len = Tutils.write_all fd (Bytes.sub (Bytes.unsafe_of_string s) ofs len) method position = Unix.lseek fd 0 Unix.SEEK_CUR method truncate = Unix.ftruncate fd method read ofs len = Unix.fsync fd; assert (ofs = Unix.lseek fd ofs Unix.SEEK_SET); let b = Bytes.create len in let rec f n = if n < len then ( let r = Unix.read fd b n (len - n) in if r <> 0 then f (n + r) else n) else n in Bytes.sub_string b 0 (f 0) val mutable saved_filename = None method saved_filename = saved_filename method close = (try Unix.close fd with _ -> ()); Fun.protect ~finally:(fun () -> try Sys.remove tmp_file with _ -> ()) (fun () -> let fname = Filename.concat hls_directory (filename ()) in saved_filename <- Some fname; let state = if Sys.file_exists fname then `Updated else `Created in (try Unix.rename tmp_file fname with Unix.Unix_error (Unix.EXDEV, _, _) -> self#log#important "Rename failed! Directory for temporary files appears to be \ on a different file system. Please set it to the same one \ using `temp_dir` argument to guarantee atomic file \ operations!"; Utils.copy ~mode:[Open_creat; Open_trunc; Open_binary] ~perms tmp_file fname; Sys.remove tmp_file); on_file_change ~state fname) end method private unlink filename = self#log#debug "Cleaning up %s.." filename; on_file_change ~state:`Deleted filename; try Unix.unlink filename with Unix.Unix_error (e, _, _) -> self#log#important "Could not remove file %s: %s" filename (Unix.error_message e) method private unlink_segment = function { filename = Some filename } -> self#unlink filename | _ -> () method private close_segment = function | { current_segment = Some ({ out_channel = Some oc } as segment) } as s -> oc#close; segment.filename <- oc#saved_filename; segment.out_channel <- None; let segments = List.assoc s.name segments in push_segment segment segments; if match max_segments with | None -> false | Some max_segments -> List.length !segments >= max_segments then ( let segment = remove_segment segments in self#unlink_segment segment; match segment.init_filename with | None -> () | Some filename -> if Sys.file_exists filename && not (List.exists (fun s -> s.init_filename = segment.init_filename) !segments) then self#unlink filename); s.current_segment <- None; if state <> `Stopped then ( self#write_playlist s; self#write_main_playlist) | _ -> () method private open_segment s = self#log#debug "Opening segment %d for stream %s." s.position s.name; let discontinuous, current_discontinuity = if state = `Restarted then (true, s.discontinuity_count + 1) else (false, s.discontinuity_count) in state <- `Started; let segment_extra_tags = Atomic.exchange s.pending_extra_tags [] in let segment = { id = s.position; discontinuous; current_discontinuity; len = 0; segment_extra_tags; init_filename = (match s.init_state with `Has_init f -> Some f | _ -> None); filename = None; out_channel = None; last_segmentable_position = None; } in let { position; extname } = s in let filename () = let ticks = segment.len in let duration = Frame.seconds_of_main ticks in segment_name ~position ~extname ~duration ~ticks s.name in let out_channel = self#open_out filename in Strings.iter out_channel#output_substring (s.encoder.Encoder.header ()); segment.out_channel <- Some out_channel; s.current_segment <- Some segment; s.discontinuity_count <- current_discontinuity; s.position <- s.position + 1; if s.id3_enabled then ( let m = match s.metadata with | `Todo m -> s.metadata <- `Sent m; Frame.Metadata.Export.to_list m | `Sent m when s.replay_id3 -> Frame.Metadata.Export.to_list m | `Sent _ | `None -> [] in let frame_position, sample_position = current_position in match s.encoder.hls.insert_id3 ~frame_position ~sample_position m with | None -> () | Some s -> out_channel#output_string s) method reopen_segment ~position:(len, offset) = function | { current_segment = Some ({ len = current_len; out_channel = Some oc } as current_segment); } as s -> let rem = oc#read offset (oc#position - offset) in current_segment.len <- len; oc#truncate offset; self#close_segment s; self#open_segment s; let segment = Option.get s.current_segment in let oc = Option.get segment.out_channel in oc#output_string rem; segment.len <- current_len - len | _ -> assert false method private cleanup_streams = List.iter (fun (_, s) -> List.iter (fun s -> self#unlink_segment s) !s) segments; List.iter (fun s -> ignore (Option.map (fun segment -> (try (Option.get segment.out_channel)#close with _ -> ()); ignore (Option.map (fun filename -> if Sys.file_exists filename then self#unlink filename) segment.init_filename); self#unlink_segment segment) s.current_segment); s.current_segment <- None) streams method private playlist_name s = s.name ^ ".m3u8" method private write_playlist s = let segments = List.filter_map (function | { filename = Some fname } as s -> Some (fname, s) | _ -> None) (List.rev !(List.assoc s.name segments)) in let segments = List.fold_left (fun cur el -> if List.length cur < segments_per_playlist then el :: cur else cur) [] segments in let discontinuity_sequence, media_sequence = match segments with | (_, { current_discontinuity; id }) :: _ -> (current_discontinuity, id - 1) | [] -> (0, 0) in let filename = self#playlist_name s in self#log#debug "Writing playlist %s.." s.name; let oc = self#open_out (fun () -> filename) in Fun.protect ~finally:(fun () -> oc#close) (fun () -> oc#output_string "#EXTM3U\r\n"; oc#output_string (Printf.sprintf "#EXT-X-TARGETDURATION:%d\r\n" (int_of_float (ceil segment_duration))); oc#output_string (Printf.sprintf "#EXT-X-VERSION:%d\r\n" (Lazy.force x_version)); oc#output_string (Printf.sprintf "#EXT-X-MEDIA-SEQUENCE:%d\r\n" media_sequence); oc#output_string (Printf.sprintf "#EXT-X-DISCONTINUITY-SEQUENCE:%d\r\n" discontinuity_sequence); List.iter (fun tag -> oc#output_string tag; oc#output_string "\r\n") s.stream_extra_tags; List.iteri (fun pos (filename, segment) -> if 0 < pos && segment.discontinuous then oc#output_string "#EXT-X-DISCONTINUITY\r\n"; if pos = 0 || segment.discontinuous then ( match segment.init_filename with | Some filename -> let filename = Printf.sprintf "%s%s" prefix (Filename.basename filename) in oc#output_string (Printf.sprintf "#EXT-X-MAP:URI=%s\r\n" (Lang_string.quote_string filename)) | _ -> ()); oc#output_string (Printf.sprintf "#EXTINF:%.03f,\r\n" (Frame.seconds_of_main segment.len)); List.iter (fun tag -> oc#output_string tag; oc#output_string "\r\n") segment.segment_extra_tags; oc#output_string (Printf.sprintf "%s%s\r\n" prefix (Filename.basename filename))) segments) val mutable main_playlist_written = false method private write_main_playlist = (match (main_playlist_writer, main_playlist_written) with | _, true -> () | None, false -> self#log#debug "`main_playlist_writer` is `null`: skipping main playlist" | Some main_playlist_writer, false -> ( let main_playlist = main_playlist_writer ~version:(Lazy.force x_version) ~extra_tags:main_playlist_extra_tags ~prefix streams in match main_playlist with | None -> self#log#debug "main_playlist_writer returned `null`: skipping main \ playlist" | Some playlist -> self#log#debug "Writing main playlist %s.." main_playlist_filename; let oc = self#open_out (fun () -> main_playlist_filename) in oc#output_string playlist; oc#close)); main_playlist_written <- true method private cleanup_playlists = List.iter (fun s -> self#unlink (self#playlist_name s)) streams; self#unlink main_playlist_filename; main_playlist_written <- false method start = (match persist_at with | Some persist_at when Sys.file_exists persist_at -> ( try self#log#info "Resuming from saved state"; self#read_state persist_at; self#toggle_state `Resumed; try Unix.unlink persist_at with _ -> () with exn when not strict_persist -> self#log#info "Failed to resume from saved state: %s" (Printexc.to_string exn); self#toggle_state `Start) | _ -> self#toggle_state `Start); List.iter self#open_segment streams; self#toggle_state `Streaming method stop = self#toggle_state `Stop; (try let data = List.map (fun s -> (0, None, s.encoder.Encoder.stop ())) streams in self#send data with _ -> ()); (match persist_at with | Some persist_at -> self#log#info "Saving state to %s.." (Lang_string.quote_string persist_at); List.iter (fun s -> self#close_segment s) streams; self#write_state persist_at | None -> self#cleanup_streams; self#cleanup_playlists); streams <- mk_streams () method! reset = self#toggle_state `Restart method private write_state persist_at = let fd = open_out_bin persist_at in let streams = `Tuple (List.map (fun { name; position; discontinuity_count; pending_extra_tags } -> `Assoc [ ("name", `String name); ("position", `Int position); ( "pending_extra_tags", `Tuple (List.map (fun s -> `String s) (Atomic.get pending_extra_tags)) ); ("discontinuity_count", `Int discontinuity_count); ]) streams) in let segments = `Assoc (List.map (fun (s, l) -> (s, `Tuple (List.map json_of_segment !l))) segments) in output_string fd (Json.to_string ~compact:false ~json5:false (`Assoc [("streams", streams); ("segments", segments)])); close_out fd method private read_state persist_at = let saved_streams, saved_segments = match Json.from_string (Utils.read_all persist_at) with | `Assoc [("streams", streams); ("segments", segments)] -> (streams, segments) | _ -> raise Invalid_state in let saved_streams = List.map (function | `Assoc [ ("name", `String name); ("position", `Int position); ("pending_extra_tags", `Tuple pending_extra_tags); ("discontinuity_count", `Int discontinuity_count); ] -> let pending_extra_tags = List.map (function `String s -> s | _ -> raise Invalid_state) pending_extra_tags in (name, position, pending_extra_tags, discontinuity_count) | _ -> raise Invalid_state) (match saved_streams with `Tuple l -> l | _ -> raise Invalid_state) in let saved_segments = match saved_segments with | `Assoc l -> List.map (function | s, `Tuple segments -> (s, ref (List.map segment_of_json segments)) | _ -> raise Invalid_state) l | _ -> raise Invalid_state in List.iter2 (fun stream (name, pos, pending_extra_tags, discontinuity_count) -> assert (name = stream.name); Atomic.set stream.pending_extra_tags pending_extra_tags; stream.discontinuity_count <- discontinuity_count; stream.init_position <- pos; stream.position <- pos + 1) streams saved_streams; segments <- saved_segments method private process_init ~init ~segment ({ extname; name; init_position } as s) = match init with | None -> s.init_state <- `No_init | Some data when not (Strings.is_empty data) -> let init_filename = segment_name ~position:init_position ~extname ~duration:0. ~ticks:0 name in let oc = self#open_out (fun () -> init_filename) in Fun.protect ~finally:(fun () -> oc#close) (fun () -> Strings.iter oc#output_substring data); segment.init_filename <- Some init_filename; s.init_state <- `Has_init init_filename | Some _ -> raise Encoder.Not_enough_data method private should_reopen ~segment ~len s = if segment.len + len > segment_main_duration then ( true, Printf.sprintf "Terminating current segment %d on stream %s to make expected \ length" segment.id s.name, true ) else if s.id3_enabled && pending_metadata s.metadata then ( true, Printf.sprintf "Terminating current segment %d on stream %s to insert new metadata" segment.id s.name, false ) else if Atomic.get s.pending_extra_tags <> [] then ( true, Printf.sprintf "Terminating current segment %d on stream %s to insert pending \ extra tags" segment.id s.name, false ) else (false, "", false) method encode frame = let len = Frame.position frame in let frame_pos, samples_pos = current_position in let frame_size = Lazy.force Frame.size in let samples_pos = samples_pos + len in current_position <- (frame_pos + (samples_pos / frame_size), samples_pos mod frame_size); List.map (fun s -> let segment = Option.get s.current_segment in let b = if s.init_state = `Todo then ( try let init, encoded = Encoder.(s.encoder.hls.init_encode frame) in self#process_init ~init ~segment s; (len, None, encoded) with Encoder.Not_enough_data -> (len, None, Strings.empty)) else ( match Encoder.(s.encoder.hls.split_encode frame) with | `Ok (flushed, encoded) -> (len, Some flushed, encoded) | `Nope encoded -> (len, None, encoded)) in let segment = Option.get s.current_segment in segment.len <- segment.len + len; b) streams method private write_pipe s (len, flushed, data) = let ({ out_channel } as segment) = Option.get s.current_segment in let oc = Option.get out_channel in (match flushed with | None -> () | Some b -> Strings.iter oc#output_substring b; segment.last_segmentable_position <- Some (segment.len, oc#position)); (match (self#should_reopen ~segment ~len s, segment.last_segmentable_position) with | (false, _, _), _ | (true, _, false), None -> () | (true, reason, _), position -> let position = match position with | None -> self#log#important "Splitting segment without a new keyframe! You might \ want to adjust your encoder's parameters to increase \ the keyframe frequency!"; (segment.len, oc#position) | Some p -> p in self#log#info "%s" reason; self#reopen_segment ~position s); let { out_channel } = Option.get s.current_segment in let oc = Option.get out_channel in Strings.iter oc#output_substring data method send b = List.iter2 self#write_pipe streams b method insert_metadata m = List.iter (fun s -> match s.metadata with | `Sent m' when Frame.Metadata.Export.equal m m' -> () | _ -> s.metadata <- `Todo m) streams end let insert_tag_t = Lang.fun_t [(false, "", Lang.string_t)] Lang.unit_t let insert_tag pending_extra_tags_list = Lang.val_fun [("", "", None)] (fun p -> let tag = String.trim (Lang.to_string (List.assoc "" p)) in List.iter (fun pending_extra_tags -> let rec append () = let tags = Atomic.get pending_extra_tags in if not (Atomic.compare_and_set pending_extra_tags tags (tag :: tags)) then append () in append ()) pending_extra_tags_list; Lang.unit) let stream_t kind = Lang.record_t [ ("name", Lang.string_t); ("encoder", Lang.format_t kind); ( "video_size", Lang.nullable_t (Lang.record_t [("width", Lang.int_t); ("height", Lang.int_t)]) ); ("bandwidth", Lang.int_t); ("codecs", Lang.string_t); ("extname", Lang.string_t); ("id3_enabled", Lang.bool_t); ("replay_id3", Lang.bool_t); ("extra_tags", Lang.list_t Lang.string_t); ("discontinuity_count", Lang.int_t); ("insert_tag", insert_tag_t); ] let value_of_stream { name; format; video_size; bandwidth; codecs; extname; id3_enabled; replay_id3; stream_extra_tags; discontinuity_count; pending_extra_tags; } = Lang.record [ ("name", Lang.string name); ("encoder", Lang_encoder.L.format format); ( "video_size", match Lazy.force video_size with | None -> Lang.null | Some (w, h) -> Lang.record [("width", Lang.int w); ("height", Lang.int h)] ); ("bandwidth", Lang.int (Lazy.force bandwidth)); ("codecs", Lang.string (Lazy.force codecs)); ("extname", Lang.string extname); ("id3_enabled", Lang.bool id3_enabled); ("replay_id3", Lang.bool replay_id3); ("extra_tags", Lang.list (List.map Lang.string stream_extra_tags)); ("discontinuity_count", Lang.int discontinuity_count); ("insert_tag", insert_tag [pending_extra_tags]); ] let _ = let return_t = Lang.univ_t () in Lang.add_operator ~base:Pipe_output.output_file "hls" (hls_proto return_t) ~return_t ~category:`Output ~meth: ([ ( "insert_tag", ([], insert_tag_t), "Insert the same tag into all the streams", fun s -> insert_tag (List.map (fun { pending_extra_tags } -> pending_extra_tags) s#streams) ); ( "streams", ([], Lang.fun_t [] (Lang.list_t (stream_t return_t))), "Output streams", fun s -> Lang.val_fun [] (fun _ -> Lang.list (List.map value_of_stream s#streams)) ); ] @ Start_stop.meth ()) ~descr: "Output the source stream to an HTTP live stream served from a local \ directory." (fun p -> new hls_output p) liquidsoap-2.3.2/src/core/outputs/icecast2.ml000066400000000000000000000607111477303350200211720ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Output to an icecast server. *) module Http = Liq_http let conf_icecast = Dtools.Conf.void ~p:(Configure.conf#plug "icecast") "Icecast configuration" let conf_prefer_address = Dtools.Conf.string ~p:(conf_icecast#plug "prefer_address") ~d:"system" "Set preference for resolving addresses. One of: `\"system\"`, `\"ipv4\"` \ or `\"ipv6\"`." let error_translator = function | Cry.Error _ as e -> Some (Cry.string_of_error e) | _ -> None let () = Printexc.register_printer error_translator type icecast_info = { quality : string option; bitrate : int option; samplerate : int option; channels : int option; } module Icecast = struct type content = Cry.content_type let format_of_content x = match x with | x when x = Icecast_utils.mpeg_mime -> Cry.mpeg | x when x = Icecast_utils.ogg_application_mime -> Cry.ogg_application | x when x = Icecast_utils.ogg_audio_mime -> Cry.ogg_audio | x when x = Icecast_utils.ogg_video_mime -> Cry.ogg_video | _ -> Cry.content_type_of_string x type info = icecast_info let info_of_encoder format encoder = match format with | Encoder.MP3 m -> let quality, bitrate = match m.Mp3_format.bitrate_control with | Mp3_format.CBR x -> (None, Some x) | Mp3_format.ABR x -> (None, x.Mp3_format.mean_bitrate) | Mp3_format.VBR x -> (Some (string_of_int (Option.get x.Mp3_format.quality)), None) in { quality; bitrate; samplerate = Some (Lazy.force m.Mp3_format.samplerate); channels = Some (if m.Mp3_format.stereo then 2 else 1); } | Encoder.Shine m -> { quality = None; bitrate = Some m.Shine_format.bitrate; samplerate = Some (Lazy.force m.Shine_format.samplerate); channels = Some m.Shine_format.channels; } | Encoder.FdkAacEnc m -> { quality = None; bitrate = Some m.Fdkaac_format.bitrate; samplerate = Some (Lazy.force m.Fdkaac_format.samplerate); channels = Some m.Fdkaac_format.channels; } | Encoder.NDI _ -> { quality = None; bitrate = None; samplerate = None; channels = None } | Encoder.External m -> { quality = None; bitrate = None; samplerate = Some (Lazy.force m.External_encoder_format.samplerate); channels = Some m.External_encoder_format.channels; } | Encoder.Flac m -> { quality = Some (string_of_int m.Flac_format.compression); bitrate = None; samplerate = Some (Lazy.force m.Flac_format.samplerate); channels = Some m.Flac_format.channels; } | Encoder.Ffmpeg m -> let bitrate = Option.map (fun v -> v / 1000) Encoder.(encoder.hls.bitrate ()) in let audio_stream = match List.assoc_opt Frame.Fields.audio m.Ffmpeg_format.streams with | Some (`Encode { options = `Audio options }) -> Some options | _ -> None in { quality = None; bitrate; samplerate = Option.map (fun stream -> Lazy.force stream.Ffmpeg_format.samplerate) audio_stream; channels = Option.map (fun stream -> stream.Ffmpeg_format.channels) audio_stream; } | Encoder.WAV m -> { quality = None; bitrate = None; samplerate = Some (Lazy.force m.Wav_format.samplerate); channels = Some m.Wav_format.channels; } | Encoder.AVI m -> { quality = None; bitrate = None; samplerate = Some (Lazy.force m.Avi_format.samplerate); channels = Some m.Avi_format.channels; } | Encoder.Ogg { Ogg_format.audio; _ } -> ( match audio with | Some (Ogg_format.Vorbis { Vorbis_format.channels = n; mode = Vorbis_format.VBR q; samplerate = s; _; }) -> { quality = Some (string_of_float q); bitrate = None; samplerate = Some (Lazy.force s); channels = Some n; } | Some (Ogg_format.Vorbis { Vorbis_format.channels = n; mode = Vorbis_format.ABR (_, b, _); samplerate = s; _; }) -> { quality = None; bitrate = b; samplerate = Some (Lazy.force s); channels = Some n; } | Some (Ogg_format.Vorbis { Vorbis_format.channels = n; mode = Vorbis_format.CBR b; samplerate = s; _; }) -> { quality = None; bitrate = Some b; samplerate = Some (Lazy.force s); channels = Some n; } | _ -> { quality = None; bitrate = None; samplerate = None; channels = None; }) end module M = Icecast_utils.Icecast_v (Icecast) open M let user_agent = Lang.product (Lang.string "User-Agent") (Lang.string Http.user_agent) let default_icy_song = Lang.eval ~cache:false ~typecheck:false ~stdlib:`Disabled {|fun (m) -> begin title = m["title"] artist = m["artist"] if artist != "" and title != "" then "#{artist} - #{title}" elsif artist != "" then artist elsif title != "" then title else null() end end|} let proto frame_t = Output.proto @ Icecast_utils.base_proto frame_t @ [ ("mount", Lang.string_t, None, Some "Source mount point."); ("icy_id", Lang.int_t, Some (Lang.int 1), Some "Shoutcast source ID."); ("name", Lang.nullable_t Lang.string_t, Some Lang.null, None); ("host", Lang.string_t, Some (Lang.string "localhost"), None); ("port", Lang.int_t, Some (Lang.int 8000), None); ( "prefer_address", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Preferred address type when resolving hostnames. One of: \ `\"system\"`, `\"ipv4\"` or `\"ipv6\"`. Defaults to \ `settings.icecast.prefer_address` when `null`." ); ( "transport", Lang.http_transport_base_t, Some (Lang.base_http_transport Http.unix_transport), Some "Http transport. Use `http.transport.ssl` or \ `http.transport.secure_transport`, when available, to enable HTTPS \ output" ); ( "connection_timeout", Lang.float_t, Some (Lang.float 5.), Some "Timeout for establishing network connections (disabled is negative)." ); ( "send_last_metadata_on_connect", Lang.bool_t, Some (Lang.bool true), Some "Send the source's last metadata when connecting to the remote \ icecast server." ); ( "timeout", Lang.float_t, Some (Lang.float 30.), Some "Timeout for network read and write." ); ( "user", Lang.nullable_t Lang.string_t, Some Lang.null, Some "User for shout source connection. Defaults to \"source\" for \ icecast connections. Useful only in special cases, like with \ per-mountpoint users." ); ("password", Lang.string_t, Some (Lang.string "hackme"), None); ( "encoding", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Encoding used to send metadata and stream info (name, genre and \ description). If null, defaults to \"UTF-8\"." ); ("genre", Lang.nullable_t Lang.string_t, Some Lang.null, None); ("protocol", Lang.string_t, Some (Lang.string "http"), None); ( "method", Lang.string_t, Some (Lang.string "source"), Some "Method to use with the 'http(s)' protocol. One of: 'source', 'put' \ or 'post'." ); ( "chunked", Lang.bool_t, Some (Lang.bool false), Some "Used chunked transfer with the 'http(s)' protocol." ); ( "send_icy_metadata", Lang.nullable_t Lang.bool_t, Some Lang.null, Some "Send new metadata using the ICY protocol. Guessed when `null`" ); ( "icy_metadata", Lang.list_t Lang.string_t, Some (Lang.list (List.map Lang.string [ "song"; "title"; "artist"; "genre"; "date"; "album"; "tracknum"; "comment"; "dj"; "next"; ])), Some "List of metadata to send with ICY metadata update" ); ( "icy_song", Lang.fun_t [(false, "", Lang.metadata_t)] (Lang.nullable_t Lang.string_t), Some default_icy_song, Some "Function used to generate the default icy \"song\" metadata. \ Metadata is not added when returning `null`. Default: `$(artist) - \ $(title)` if both are defined, otherwise `artist` or `title` if \ either is defined or `null`." ); ("url", Lang.nullable_t Lang.string_t, Some Lang.null, None); ("description", Lang.nullable_t Lang.string_t, Some Lang.null, None); ( "on_connect", Lang.fun_t [] Lang.unit_t, Some (Lang.val_cst_fun [] Lang.unit), Some "Callback executed when connection is established." ); ( "on_disconnect", Lang.fun_t [] Lang.unit_t, Some (Lang.val_cst_fun [] Lang.unit), Some "Callback executed when connection stops." ); ( "on_error", Lang.fun_t [(false, "", Lang.string_t)] Lang.float_t, Some (Lang.val_cst_fun [("", None)] (Lang.float 3.)), Some "Callback executed when an error happens. The callback receives a \ string representation of the error that occurred and returns a \ float. If returned value is positive, connection will be tried \ again after this amount of time (in seconds)." ); ("public", Lang.bool_t, Some (Lang.bool true), None); ( "headers", Lang.metadata_t, Some (Lang.list [user_agent]), Some "Additional headers." ); ( "dumpfile", Lang.nullable_t Lang.string_t, Some Lang.null, Some "Dump stream to file, for debugging purpose. Disabled if null." ); ("", Lang.source_t frame_t, None, None); ] (** Sending encoded data to a shout-compatible server. It directly takes the Lang param list and extracts stuff from it. *) class output p = let e f v = f (List.assoc v p) in let s v = e Lang.to_string v in let s_opt v = e (Lang.to_valued_option Lang.to_string) v in let on_connect = List.assoc "on_connect" p in let on_disconnect = List.assoc "on_disconnect" p in let on_error = List.assoc "on_error" p in let on_connect () = ignore (Lang.apply on_connect []) in let on_disconnect () = ignore (Lang.apply on_disconnect []) in let on_error error = let msg = Printexc.to_string error in Lang.to_float (Lang.apply on_error [("", Lang.string msg)]) in let send_last_metadata_on_connect = e Lang.to_bool "send_last_metadata_on_connect" in let data = encoder_data p in let chunked = Lang.to_bool (List.assoc "chunked" p) in let protocol = let m = let m = List.assoc "method" p in match Lang.to_string m with | "source" -> Cry.Source | "put" -> Cry.Put | "post" -> Cry.Post | _ -> raise (Error.Invalid_value (m, "Valid values are: 'source' 'put' or 'post'.")) in let v = List.assoc "protocol" p in match Lang.to_string v with | "http" -> Cry.Http m | "icy" -> Cry.Icy | _ -> raise (Error.Invalid_value (v, "Valid values are 'http' (icecast) and 'icy' (shoutcast)")) in let send_icy_metadata = match ( data.format, Lang.to_valued_option Lang.to_bool (List.assoc "send_icy_metadata" p) ) with | _, Some b -> b | format, None when format = mpeg || format = wav || format = aac || format = flac -> true | format, None when format = ogg_application || format = ogg_audio || format = ogg_video -> false | _ -> raise (Error.Invalid_value ( List.assoc "send_icy_metadata" p, "Could not guess send_icy_metadata for this format, please \ specify either true or false." )) in let icy_metadata = List.map Lang.to_string (Lang.to_list (List.assoc "icy_metadata" p)) in let icy_song = List.assoc "icy_song" p in let icy_song m = Lang.to_valued_option Lang.to_string (Lang.apply icy_song [("", m)]) in let out_enc = match s_opt "encoding" with | None | Some "" -> Charset.utf8 | Some s -> Charset.of_string s in let source_val = Lang.assoc "" 2 p in let source = Lang.to_source source_val in let icy_id = Lang.to_int (List.assoc "icy_id" p) in let mount = s "mount" in let name = match Lang.to_option (List.assoc "name" p) with | None -> mount | Some v -> Lang.to_string v in let name = Charset.convert ~target:out_enc name in let mount = match protocol with | Cry.Icy -> Cry.Icy_id icy_id | _ -> Cry.Icecast_mount mount in let autostart = Lang.to_bool (List.assoc "start" p) in let infallible = not (Lang.to_bool (List.assoc "fallible" p)) in let register_telnet = Lang.to_bool (List.assoc "register_telnet" p) in let on_start = let f = List.assoc "on_start" p in fun () -> ignore (Lang.apply f []) in let on_stop = let f = List.assoc "on_stop" p in fun () -> ignore (Lang.apply f []) in let host = s "host" in let port = e Lang.to_int "port" in let transport = e Lang.to_http_transport "transport" in let prefer_address = let v = List.assoc "prefer_address" p in match Option.value ~default:conf_prefer_address#get (Lang.to_valued_option Lang.to_string v) with | "system" -> `System_default | "ipv4" -> `Ipv4 | "ipv6" -> `Ipv6 | _ -> raise (Error.Invalid_value (v, "Valid values are: `\"system\"`, `\"ipv4\"` or `\"ipv6\"`.")) in let transport = (transport :> Cry.transport) in let transport = object method name = transport#name method protocol = transport#protocol method default_port = transport#default_port method connect ?bind_address ?timeout ?prefer = transport#connect ?bind_address ?timeout ~prefer:(Option.value ~default:prefer_address prefer) end in let user = match (protocol, s_opt "user") with | Cry.Http _, None -> "source" | _, user -> Option.value ~default:"" user in let password = s "password" in let genre = Option.map (fun s -> Charset.convert ~target:out_enc s) (s_opt "genre") in let url = s_opt "url" in let timeout = e Lang.to_float "timeout" in let connection_timeout = let v = e Lang.to_float "connection_timeout" in if v > 0. then Some v else None in let dumpfile = s_opt "dumpfile" in let description = Option.map (fun s -> Charset.convert ~target:out_enc s) (s_opt "description") in let public = e Lang.to_bool "public" in let headers = List.map (fun v -> let f (x, y) = (Lang.to_string x, Lang.to_string y) in f (Lang.to_product v)) (Lang.to_list (List.assoc "headers" p)) in let connection = Cry.create ~timeout ~transport ?connection_timeout () in object (self) inherit [Strings.t] Output.encoded ~output_kind:"output.icecast" ~infallible ~register_telnet ~autostart ~export_cover_metadata:false ~on_start ~on_stop ~name source_val (** In this operator, we don't exactly follow the start/stop mechanism of Output.encoded because we want to control in a more subtle way the connection/disconnection with icecast. So we have specific icecast_start/stop procedures that only deal with the shout connection. And the global output_start/stop also deal with the encoder. As a result, if shout gets disconnected, encoding will keep going, and the sending will keep being attempted, which will at some point trigger a restart. *) (** Time after which we should attempt to connect. *) val mutable restart_time = 0. (** File descriptor where to dump. *) val mutable dump = None val mutable encoder = None method self_sync = source#self_sync method encode frame = (* We assume here that there always is an encoder available when the source is connected. *) match (Cry.get_status connection, encoder) with | Cry.Connected _, Some enc -> enc.Encoder.encode frame | _ -> Strings.empty method insert_metadata m = (* Update metadata using ICY if told to.. *) if send_icy_metadata then ( let f = Charset.convert ~target:out_enc in let icy_meta = Hashtbl.create 10 in let m = Frame.Metadata.Export.to_metadata m in Frame.Metadata.iter (fun lbl v -> if List.mem lbl icy_metadata then Hashtbl.replace icy_meta lbl (f v)) m; if not (Hashtbl.mem icy_meta "song") then ( match icy_song (Lang.metadata m) with | None -> () | Some v -> Hashtbl.replace icy_meta "song" (f v)); (* Do nothing if shout connection isn't available *) match Cry.get_status connection with | Cry.Connected _ -> ( try Cry.update_metadata ~charset:(Charset.to_string out_enc) connection icy_meta with e -> let bt = Printexc.get_backtrace () in Utils.log_exception ~log:self#log ~bt (Printf.sprintf "Metadata update may have failed with error: %s" (Printexc.to_string e))) | Cry.Disconnected -> ()) else ( (* Encoder is not always present.. *) match encoder with | Some encoder -> encoder.Encoder.insert_metadata m | None -> ()) method icecast_send b = try if Cry.get_status connection = Cry.Disconnected then self#icecast_start; Strings.iter (fun s offset length -> Cry.send connection ~offset ~length s) b; match dump with | Some s -> Strings.iter (output_substring s) b | None -> () with e -> let bt = Printexc.get_raw_backtrace () in self#log#severe "Error while sending data: %s!" (Printexc.to_string e); let delay = on_error e in if delay >= 0. then ( (* Ask for a restart after [restart_time]. *) (try self#icecast_stop with _ -> ()); restart_time <- Unix.gettimeofday () +. delay; self#log#important "Will try to reconnect in %.02f seconds." delay) else Printexc.raise_with_backtrace e bt method send b = match Cry.get_status connection with | Cry.Disconnected when Unix.time () > restart_time -> self#icecast_send b | Cry.Connected _ -> self#icecast_send b | _ -> () (* We lazily start connection when data is available to send. *) method start = () method stop = self#icecast_stop method icecast_start = assert (encoder = None); let enc = data.factory self#id Frame.Metadata.Export.empty in encoder <- Some enc; assert (Cry.get_status connection = Cry.Disconnected); begin match dumpfile with | Some f -> dump <- Some (open_out_bin f) | None -> () end; let display_mount = match mount with | Cry.Icy_id id -> Printf.sprintf "sid#%d" id | Cry.Icecast_mount mount -> mount in self#log#important "Connecting mount %s for %s@%s..." display_mount user host; let audio_info = Hashtbl.create 10 in let f x y z = match x with Some q -> Hashtbl.replace audio_info y (z q) | None -> () in let info = data.info enc in f info.bitrate "bitrate" string_of_int; f info.quality "quality" (fun x -> x); f info.samplerate "samplerate" string_of_int; f info.channels "channels" string_of_int; let user_agent = try List.assoc "User-Agent" headers with Not_found -> Printf.sprintf "liquidsoap %s" Configure.version in let handler = Cry.connection ~host ~port ~user ~password ?genre ?url ?description ~name ~public ~protocol ~mount ~chunked ~audio_info ~user_agent ~content_type:data.format () in List.iter (fun (x, y) -> (* User-Agent has already been passed to Cry.. *) if x <> "User-Agent" then Hashtbl.replace handler.Cry.headers x y) headers; try Cry.connect connection handler; self#log#important "Connection setup was successful."; (match source#last_metadata with | Some m when send_last_metadata_on_connect -> ( try self#insert_metadata (Frame.Metadata.Export.from_metadata ~cover:false m) with _ -> ()) | _ -> ()); (* Execute on_connect hook. *) on_connect () with (* In restart mode, no_connect and no_login are not fatal. The output will just try to reconnect later. *) | e -> let bt = Printexc.get_raw_backtrace () in Utils.log_exception ~log:self#log ~bt:(Printexc.raw_backtrace_to_string bt) (Printf.sprintf "Connection failed: %s" (Printexc.to_string e)); self#icecast_stop; let delay = on_error e in if delay >= 0. then ( self#log#important "Will try again in %.02f sec." delay; restart_time <- Unix.time () +. delay) else Printexc.raise_with_backtrace e bt method icecast_stop = (* In some cases it might be possible to output the remaining data, but it's not worth the trouble. *) begin try ignore ((Option.get encoder).Encoder.stop ()) with _ -> () end; encoder <- None; begin match Cry.get_status connection with | Cry.Disconnected -> () | Cry.Connected _ -> self#log#important "Closing connection..."; (try Cry.close connection with exn -> let bt = Printexc.get_backtrace () in Utils.log_exception ~log:self#log ~bt (Printf.sprintf "Error while closing connection: %s" (Printexc.to_string exn))); on_disconnect () end; match dump with Some f -> close_out f | None -> () end let _ = let return_t = Lang.univ_t () in Lang.add_operator ~base:Modules.output "icecast" ~category:`Output ~descr:"Encode and output the stream to an icecast2 or shoutcast server." ~meth:Output.meth (proto return_t) ~return_t (fun p -> (new output p :> Output.output)) liquidsoap-2.3.2/src/core/outputs/ndi_out.ml000066400000000000000000000203041477303350200211300ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Output using NDI. *) open Mm open Ndi_format module SyncSource = Clock.MkSyncSource (struct type t = unit let to_string _ = "ndi" end) let sync_source = SyncSource.make () type sender = { handler : Ndi.Send.sender; mutable position : int64 } class output ~self_sync ~register_telnet ~name ~groups ~infallible ~on_start ~on_stop ~handler ~format source start = let sample_rate = Lazy.force Frame.audio_rate in let frame_rate = Lazy.force Frame.video_rate in let video_height = Lazy.force Frame.video_height in let video_width = Lazy.force Frame.video_width in (* Timecode is in increment of 100 ns *) let timecode_base = Int64.div 10_000_000L (Int64.of_int (Lazy.force Frame.main_rate)) in let clock_audio, clock_video = match (self_sync, format.audio, format.video) with | false, _, _ -> (false, false) | true, _, true -> (false, true) | true, true, _ -> (true, false) | _ -> assert false in object (self) inherit Output.output ~register_telnet ~infallible ~on_start ~on_stop ~name:"ndi" ~output_kind:"output.ndi" source start val mutable sender = None method self_sync = if self_sync then (`Dynamic, if sender <> None then Some sync_source else None) else (`Static, None) method get_sender = match sender with | Some s -> s | None -> let handler = Ndi.Send.init ~clock_audio ~clock_video ?groups ?name handler in let s = { handler; position = 0L } in sender <- Some s; s method start = ignore self#get_sender method stop = match sender with | Some { handler } -> Ndi.Send.destroy handler; sender <- None | None -> () method private send_audio_frame ~timecode ~sender frame = let pcm = AFrame.pcm frame in let channels = Array.length pcm in let samples = Audio.length pcm in let data = Bigarray.Array1.create Bigarray.float32 Bigarray.c_layout (samples * channels) in Audio.FLTP.of_audio ~src:pcm ~src_offset:0 ~dst:data ~dst_offset:0 ~len:samples ~stride:samples; let audio_frame = { Ndi.Frame.Audio.sample_rate; channels; samples; timecode = Some timecode; data = `Fltp { Ndi.Frame.Audio.data; stride = samples * 4 }; metadata = None; timestamp = None; } in Ndi.Send.send_audio sender.handler audio_frame method private send_video_frame ~timecode ~sender frame = let buf = VFrame.data frame in List.iter (fun (pos, img) -> let img = img (* TODO: we could scale instead of aggressively changing the viewport *) |> Video.Canvas.Image.viewport video_width video_height |> Video.Canvas.Image.render ~transparent:false in let y, u, v = Image.YUV420.data img in let y_dim = Bigarray.Array1.dim y in let u_dim = Bigarray.Array1.dim u in let v_dim = Bigarray.Array1.dim v in let stride = Image.YUV420.y_stride img in let data = Bigarray.Array1.create Bigarray.int8_unsigned Bigarray.c_layout (y_dim + u_dim + v_dim) in Bigarray.Array1.blit y (Bigarray.Array1.sub data 0 y_dim); Bigarray.Array1.blit u (Bigarray.Array1.sub data y_dim u_dim); Bigarray.Array1.blit v (Bigarray.Array1.sub data (y_dim + u_dim) v_dim); let video_frame = { Ndi.Frame.Video.xres = video_width; yres = video_height; frame_rate_N = frame_rate; frame_rate_D = 1; picture_aspect_ratio = None; format = `Progressive; timecode = Some Int64.(add timecode (mul (of_int pos) timecode_base)); data = `I420 { Ndi.Frame.Video.data; stride }; metadata = None; timestamp = None; } in Ndi.Send.send_video sender.handler video_frame) buf.Content.Video.data method send_frame frame = let sender = self#get_sender in let timecode = Int64.mul sender.position timecode_base in if format.audio then self#send_audio_frame ~timecode ~sender frame; if format.video then self#send_video_frame ~timecode ~sender frame; sender.position <- Int64.add sender.position (Int64.of_int (Frame.position frame)) method! reset = () end let _ = let return_t = Lang.univ_t () in Lang.add_operator ~base:Modules.output "ndi" ~flags:[`Experimental] (Output.proto @ [ ( "self_sync", Lang.bool_t, Some (Lang.bool false), Some "Use the dedicated NDI clock." ); ( "library_file", Lang.string_t, None, Some "Path to the shared library file." ); ( "name", Lang.nullable_t Lang.string_t, Some Lang.null, Some "NDI sender name" ); ( "groups", Lang.nullable_t Lang.string_t, Some Lang.null, Some "NDI sender groups" ); ( "", Lang.format_t return_t, None, Some "Encoding format. Only the `%ndi` encoder is allowed here!" ); ("", Lang.source_t return_t, None, None); ]) ~category:`Output ~meth:Output.meth ~descr:"Output stream to NDI" ~return_t (fun p -> let self_sync = Lang.to_bool (List.assoc "self_sync" p) in let lib = Lang.to_string (List.assoc "library_file" p) in let lib = Utils.check_readable ~pos:(Lang.pos p) lib in let handler = try Ndi.init ~filename:lib () with | Ndi.Library_not_found -> Runtime_error.raise ~pos:(Lang.pos p) ~message:"Invalid ndi library" "invalid" | Ndi.Library_initialized f -> Runtime_error.raise ~pos:(Lang.pos p) ~message: (Printf.sprintf "Ndi already initialized with a different library: %s" (Lang_string.quote_string f)) "invalid" in let name = Lang.to_valued_option Lang.to_string (List.assoc "name" p) in let groups = Lang.to_valued_option Lang.to_string (List.assoc "groups" p) in let infallible = not (Lang.to_bool (List.assoc "fallible" p)) in let register_telnet = Lang.to_bool (List.assoc "register_telnet" p) in let start = Lang.to_bool (List.assoc "start" p) in let on_start = let f = List.assoc "on_start" p in fun () -> ignore (Lang.apply f []) in let on_stop = let f = List.assoc "on_stop" p in fun () -> ignore (Lang.apply f []) in let format = match Lang.to_format (Lang.assoc "" 1 p) with | NDI n -> n | _ -> Runtime_error.raise ~pos:(Lang.pos p) ~message:"Only the %ndi encoder is allowed for `output.ndi`!" "invalid" in let source = Lang.assoc "" 2 p in (new output ~self_sync ~name ~groups ~infallible ~register_telnet ~on_start ~on_stop ~handler ~format source start :> Output.output)) liquidsoap-2.3.2/src/core/outputs/output.ml000066400000000000000000000231311477303350200210300ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Custom classes for easy creation of output nodes. *) open Source let fallibility_check = ref true let proto = Start_stop.output_proto @ [ ( "register_telnet", Lang.bool_t, Some (Lang.bool true), Some "Register telnet commands for this output." ); ( "fallible", Lang.bool_t, Some (Lang.bool false), Some "Allow the child source to fail, in which case the output will be \ stopped until the source is available again." ); ] let meth = Start_stop.meth () module Queue = Queues.Queue (** Given abstract start stop and send methods, creates an output. Takes care of pulling the data out of the source, type checkings, maintains a queue of last ten metadata and setups standard Server commands, including start/stop. *) class virtual output ~output_kind ?clock ?(name = "") ~infallible ~register_telnet ~(on_start : unit -> unit) ~(on_stop : unit -> unit) val_source autostart = let source = Lang.to_source val_source in object (self) initializer (* This should be done before the active_operator initializer attaches us to a clock. *) if !fallibility_check && infallible && source#fallible then raise (Error.Invalid_value (val_source, "That source is fallible.")) initializer Typing.(source#frame_type <: self#frame_type) inherit active_operator ?clock ~name:output_kind [source] inherit Start_stop.base ~on_start ~on_stop as start_stop method virtual private start : unit method virtual private stop : unit method virtual private send_frame : Frame.t -> unit method fallible = not infallible method! source_type : source_type = `Output (self :> Source.active) method private add_on_air m = let d = Unix.gettimeofday () in let m = Frame.Metadata.add "on_air" (Request.pretty_date (Unix.localtime d)) m in Frame.Metadata.add "on_air_timestamp" (Printf.sprintf "%.02f" d) m (* Metadata stuff: keep track of what was streamed. *) val q_length = 10 val metadata_queue = Queue.create () method private add_metadata m = let m = Frame.Metadata.Export.from_metadata ~cover:false m in Queue.push metadata_queue m; if Queue.length metadata_queue > q_length then ignore (Queue.pop metadata_queue) initializer self#on_metadata self#add_metadata (* Registration of Telnet commands must be delayed because some operators change their id at initialization time. *) val mutable registered_telnet = false method private register_telnet = if register_telnet && not registered_telnet then ( registered_telnet <- true; (* Add a few more server controls *) let ns = [self#id] in Server.add ~ns "skip" (fun _ -> self#skip; "Done") ~descr:"Skip current song."; Server.add ~ns "metadata" ~descr:"Print current metadata." (fun _ -> fst (Queue.fold metadata_queue (fun m (s, i) -> let s = s ^ (if s = "" then "--- " else "\n--- ") ^ string_of_int i ^ " ---\n" ^ Frame.Metadata.to_string (Frame.Metadata.Export.to_metadata m) in (s, i - 1)) ("", Queue.length metadata_queue))); Server.add ~ns "remaining" ~descr:"Display estimated remaining time." (fun _ -> let r = source#remaining in if r < 0 then "(undef)" else ( let t = Frame.seconds_of_main r in Printf.sprintf "%.2f" t))) method private cleanup_telnet = if registered_telnet then List.iter (Server.remove ~ns:[self#id]) ["skip"; "metadata"; "remaining"]; registered_telnet <- false method private can_generate_frame = source#is_ready method remaining = source#remaining method abort_track = source#abort_track method seek_source = source#seek_source (* Operator startup *) initializer self#on_wake_up (fun () -> (* We prefer [name] as an ID over the default, but do not overwrite user-defined ID. Our ID will be used for the server interface. *) if name <> "" then self#set_id ~force:false name; self#log#debug "Clock is %s." (Clock.id self#clock); if Frame.Fields.is_empty self#content_type then failwith (Printf.sprintf "Empty content-type detected for output %s. You might want to \ use an expliciy type annotation!" self#id); if not autostart then start_stop#transition_to `Stopped; self#register_telnet); self#on_sleep (fun () -> self#cleanup_telnet; start_stop#transition_to `Stopped) (* The output process *) val mutable skip = false method private skip = skip <- true method private generate_frame = Frame.map_metadata source#get_frame (fun (pos, m) -> Some (pos, self#add_on_air m)) method output = if self#is_ready && state = `Idle then start_stop#transition_to `Started; if start_stop#state = `Started then ( let data = if self#is_ready then self#get_frame else self#end_of_track in (* Output that frame if it has some data *) if Frame.position data > 0 then self#send_frame data; if Frame.is_partial data then ( if not self#fallible then ( self#log#critical "Infallible source produced a partial frame!"; assert false); self#log#info "Source ended (no more tracks) stopping output..."; self#transition_to `Idle); if skip then ( self#log#important "Performing user-requested skip"; skip <- false; self#abort_track)) end class dummy ?clock ~infallible ~on_start ~on_stop ~autostart ~register_telnet source = let s = Lang.to_source source in object inherit output source autostart ?clock ~name:"dummy" ~output_kind:"output.dummy" ~infallible ~on_start ~on_stop ~register_telnet method! private reset = () method private start = () method private stop = () method private send_frame _ = () method self_sync = s#self_sync end let _ = let return_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator ~base:Modules.output "dummy" (proto @ [("", Lang.source_t return_t, None, None)]) ~category:`Output ~descr:"Dummy output: computes the stream, without actually using it." ~meth ~return_t (fun p -> let infallible = not (Lang.to_bool (List.assoc "fallible" p)) in let autostart = Lang.to_bool (List.assoc "start" p) in let on_start = List.assoc "on_start" p in let on_stop = List.assoc "on_stop" p in let on_start () = ignore (Lang.apply on_start []) in let on_stop () = ignore (Lang.apply on_stop []) in let register_telnet = Lang.to_bool (List.assoc "register_telnet" p) in new dummy ~on_start ~on_stop ~infallible ~autostart ~register_telnet (List.assoc "" p)) (** More concrete abstract-class, which takes care of the #send_frame method for outputs based on encoders. *) class virtual ['a] encoded ~output_kind ?clock ~name ~infallible ~on_start ~on_stop ~register_telnet ~autostart ~export_cover_metadata source = object (self) inherit output ~infallible ~on_start ~on_stop ~output_kind ?clock ~name ~register_telnet source autostart method virtual private insert_metadata : Frame.Metadata.Export.t -> unit method virtual private encode : Frame.t -> 'a method virtual private send : 'a -> unit method private send_frame frame = let rec output_chunks frame = let f start stop = let data = self#encode (Frame.sub frame start (stop - start)) in self#send data; match Frame.get_metadata frame start with | None -> () | Some m -> self#insert_metadata (Frame.Metadata.Export.from_metadata ~cover:export_cover_metadata m) in function | [] -> assert false | [i] -> assert (i = Lazy.force Frame.size || not infallible) | start :: stop :: l -> if start < stop then f start stop else assert (start = stop); output_chunks frame (stop :: l) in output_chunks frame (0 :: List.sort compare (List.map fst (Frame.get_all_metadata frame) @ [Frame.position frame])) end liquidsoap-2.3.2/src/core/outputs/output.mli000066400000000000000000000060441477303350200212050ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Custom classes for easy creation of output nodes. *) val fallibility_check : bool ref (** Parameters needed to instantiate an output. *) val proto : (string * Lang.t * Lang.value option * string option) list class virtual output : output_kind:string -> ?clock:Clock.t -> ?name:string -> infallible:bool -> register_telnet:bool -> on_start:(unit -> unit) -> on_stop:(unit -> unit) -> Lang.value -> bool -> object inherit Source.active_source method fallible : bool method remaining : int method abort_track : unit method private can_generate_frame : bool method private generate_frame : Frame.t method state : Start_stop.state method transition_to : Start_stop.state -> unit method seek_source : Source.source method output : unit method private video_dimensions : int * int method private reset : unit method virtual private send_frame : Frame.t -> unit method virtual private start : unit method virtual private stop : unit end (** Default methods on output values. *) val meth : (string * Lang.scheme * string * (output -> Lang.value)) list class virtual ['a] encoded : output_kind:string -> ?clock:Clock.t -> name:string -> infallible:bool -> on_start:(unit -> unit) -> on_stop:(unit -> unit) -> register_telnet:bool -> autostart:bool -> export_cover_metadata:bool -> Lang.value -> object inherit output method private send_frame : Frame.t -> unit method virtual private encode : Frame.t -> 'a method virtual private insert_metadata : Frame.Metadata.Export.t -> unit method virtual private send : 'a -> unit method private reset : unit method virtual private start : unit method virtual private stop : unit end class dummy : ?clock:Clock.t -> infallible:bool -> on_start:(unit -> unit) -> on_stop:(unit -> unit) -> autostart:bool -> register_telnet:bool -> Lang.value -> object inherit output method private reset : unit method private start : unit method private stop : unit method private send_frame : Frame.t -> unit method self_sync : Clock.self_sync end liquidsoap-2.3.2/src/core/outputs/pipe_output.ml000066400000000000000000000523311477303350200220510ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** base class *) let output = Modules.output let encoder_factory ?format format_val = let format = match format with Some f -> f | None -> Lang.to_format format_val in try (Encoder.get_factory format) ~hls:false ~pos:(Value.pos format_val) with Not_found -> raise (Error.Invalid_value (format_val, "Unsupported encoding format")) let base_proto = ( "export_cover_metadata", Lang.bool_t, Some (Lang.bool true), Some "Export cover metadata." ) :: Output.proto class virtual base ?clock ~source:source_val ~name p = let e f v = f (List.assoc v p) in (* Output settings *) let autostart = e Lang.to_bool "start" in let infallible = not (Lang.to_bool (List.assoc "fallible" p)) in let register_telnet = Lang.to_bool (List.assoc "register_telnet" p) in let on_start = let f = List.assoc "on_start" p in fun () -> ignore (Lang.apply f []) in let on_stop = let f = List.assoc "on_stop" p in fun () -> ignore (Lang.apply f []) in let export_cover_metadata = Lang.to_bool (List.assoc "export_cover_metadata" p) in object (self) inherit [Strings.t] Output.encoded ?clock ~infallible ~register_telnet ~on_start ~on_stop ~autostart ~export_cover_metadata ~output_kind:"output.file" ~name source_val val mutable encoder = None val mutable current_metadata = None method virtual start : unit method virtual stop : unit method virtual private encoder_factory : string -> Frame.Metadata.Export.t -> Encoder.encoder method create_encoder = let enc = self#encoder_factory self#id in let meta = match current_metadata with | Some m -> m | None -> Frame.Metadata.Export.empty in encoder <- Some (enc meta) (* Make sure to call stop on the encoder to close any open connection. *) method close_encoder = match encoder with | None -> Strings.empty | Some enc -> let flushed = try enc.Encoder.stop () with _ -> Strings.empty in encoder <- None; flushed method! reset = () method encode frame = match encoder with | None -> Strings.empty | Some encoder -> encoder.Encoder.encode frame method virtual write_pipe : string -> int -> int -> unit method send b = Strings.iter self#write_pipe b method insert_metadata m = match encoder with | None -> () | Some encoder -> encoder.Encoder.insert_metadata m end (** url output: discard encoded data, try to restart on encoding error (can be networking issues etc.) *) let url_proto frame_t = base_proto @ [ ("url", Lang.string_t, None, Some "Url to output to."); ( "restart_delay", Lang.nullable_t Lang.float_t, Some (Lang.float 2.), Some "If not `null`, restart output on errors after the given delay." ); ( "on_error", Lang.fun_t [(false, "", Lang.error_t)] Lang.unit_t, Some (Lang.val_fun [("", "", None)] (fun _ -> Lang.unit)), Some "Callback executed when an error occurs." ); ( "self_sync", Lang.bool_t, Some (Lang.bool false), Some "Should the source control its own synchronization? Set to `true` \ for output to e.g. `rtmp` output using `%ffmpeg` and etc." ); ("", Lang.format_t frame_t, None, Some "Encoding format."); ("", Lang.source_t frame_t, None, None); ] class url_output p = let url = Lang.to_string (List.assoc "url" p) in let format_val = Lang.assoc "" 1 p in let format = Lang.to_format format_val in let () = if not (Encoder.url_output format) then raise (Error.Invalid_value (format_val, "Encoding format does not support output url!")) in let format = Encoder.with_url_output format url in let source = Lang.assoc "" 2 p in let restart_delay = Lang.to_valued_option Lang.to_float (List.assoc "restart_delay" p) in let on_error = List.assoc "on_error" p in let on_error ~bt exn = let error = Lang.runtime_error_of_exception ~bt ~kind:"output" exn in ignore (Lang.apply on_error [("", Lang.error error)]) in let on_start = let f = List.assoc "on_start" p in fun () -> ignore (Lang.apply f []) in let p = List.map (fun ((lbl, _) as v) -> match lbl with | "on_start" -> ("on_start", Lang.val_fun [] (fun _ -> Lang.unit)) | _ -> v) p in let self_sync = Lang.to_bool (List.assoc "self_sync" p) in let name = "output.url" in object (self) inherit base p ~source ~name as base method private encoder_factory = encoder_factory ~format format_val val mutable restart_time = 0. method can_connect = restart_time <= Unix.gettimeofday () method on_error ~bt exn = (try ignore self#close_encoder with _ -> ()); Utils.log_exception ~log:self#log ~bt:(Printexc.raw_backtrace_to_string bt) (Printf.sprintf "Error while connecting: %s" (Printexc.to_string exn)); on_error ~bt exn; match restart_delay with | None -> Printexc.raise_with_backtrace exn bt | Some delay -> restart_time <- Unix.gettimeofday () +. delay; self#log#important "Will try again in %.02f seconds." delay method connect = match encoder with | None when self#can_connect -> ( try self#create_encoder; on_start () with exn -> let bt = Printexc.get_raw_backtrace () in self#on_error ~bt exn) | _ -> () method start = self#connect method stop = ignore self#close_encoder method! encode frame = try match encoder with | None when self#can_connect -> self#connect; base#encode frame | None -> Strings.empty | Some _ -> base#encode frame with exn -> let bt = Printexc.get_raw_backtrace () in self#on_error ~bt exn; Strings.empty method write_pipe _ _ _ = () method self_sync = (`Static, self#source_sync self_sync) end let _ = let return_t = Lang.univ_t () in Lang.add_operator ~base:output "url" (url_proto return_t) ~return_t ~category:`Output ~meth:Output.meth ~descr: "Encode and let encoder handle data output. Useful with encoder with no \ expected output or to encode to files that need full control from the \ encoder, e.g. `%ffmpeg` with `rtmp` output." (fun p -> (new url_output p :> Output.output)) (** Piped virtual class: open/close pipe, implements metadata interpolation and takes care of the various reload mechanisms. *) let default_reopen_on_error = Lang.eval ~cache:false ~stdlib:`Disabled ~typecheck:false "fun (_) -> null()" let default_reopen_on_metadata = Lang.eval ~cache:false ~stdlib:`Disabled ~typecheck:false "fun (_) -> false" let default_reopen_when = Lang.eval ~cache:false ~stdlib:`Disabled ~typecheck:false "fun () -> false" let pipe_proto frame_t arg_doc = base_proto @ [ ( "reopen_on_error", Lang.fun_t [(false, "", Lang.nullable_t Lang.error_t)] (Lang.nullable_t Lang.float_t), Some default_reopen_on_error, Some "Callback called when there is an error. Error is raised when \ returning `null`. Otherwise, the file is reopened after the \ returned value, in seconds." ); ( "reopen_on_metadata", Lang.fun_t [(false, "", Lang.metadata_t)] Lang.bool_t, Some default_reopen_on_metadata, Some "Callback called on metadata. If returned value is `true`, the file \ is reopened." ); ( "reopen_when", Lang.fun_t [] Lang.bool_t, Some default_reopen_when, Some "Callback called on each frame. If returned value is `true`, the \ file is reopened." ); ( "reopen_delay", Lang.getter_t Lang.float_t, Some (Lang.float 120.), Some "Prevent re-opening within that delay, in seconds. Only applies to \ `reopen_when`." ); ( "on_reopen", Lang.fun_t [] Lang.unit_t, Some (Lang.val_cst_fun [] Lang.unit), Some "Callback executed when the output is reopened." ); ("", Lang.format_t frame_t, None, Some "Encoding format."); ("", Lang.getter_t Lang.string_t, None, Some arg_doc); ("", Lang.source_t frame_t, None, None); ] let pipe_meth = let meth = List.map (fun (a, b, c, fn) -> (a, b, c, fun s -> fn (s :> Output.output))) Output.meth in ( "reopen", ([], Lang.fun_t [] Lang.unit_t), "Reopen the output pipe. The actual reopening happens the next time the \ output has some data to output.", fun s -> Lang.val_fun [] (fun _ -> s#need_reopen; Lang.unit) ) :: meth class virtual piped_output ?clock ~name p = let source = Lang.assoc "" 3 p in let reopen_on_error = List.assoc "reopen_on_error" p in let reopen_on_error error = let error = Lang.error error in match Lang.to_valued_option Lang.to_float (Lang.apply reopen_on_error [("", error)]) with | Some v when 0. <= v -> v | _ -> -1. in let reopen_on_metadata = List.assoc "reopen_on_metadata" p in let reopen_on_metadata m = let m = Lang.metadata m in Lang.to_bool (Lang.apply reopen_on_metadata [("", m)]) in let reopen_when = List.assoc "reopen_when" p in let reopen_when () = Lang.to_bool (Lang.apply reopen_when []) in let reopen_delay = Lang.to_float_getter (List.assoc "reopen_delay" p) in let on_reopen = List.assoc "on_reopen" p in let on_reopen () = ignore (Lang.apply on_reopen []) in object (self) inherit base ?clock ~source ~name p as base val mutable open_date = 0. val need_reopen = Atomic.make false method need_reopen = Atomic.set need_reopen true method virtual open_pipe : unit method virtual close_pipe : unit method virtual is_open : bool method interpolate ?(subst = fun x -> x) s = let current_metadata = match current_metadata with | Some m -> let m = Frame.Metadata.Export.to_metadata m in fun x -> subst (Frame.Metadata.find x m) | None -> fun _ -> raise Not_found in Utils.interpolate current_metadata s method prepare_pipe = self#open_pipe; open_date <- Unix.gettimeofday (); Atomic.set need_reopen false; self#create_encoder method cleanup_pipe = if self#is_open then ( base#send self#close_encoder; self#close_pipe) method start = self#prepare_pipe method stop = self#cleanup_pipe method reopen = self#log#important "Re-opening output pipe."; self#cleanup_pipe; self#prepare_pipe; on_reopen () method private reopen_on_error ~bt exn = let error = Lang.runtime_error_of_exception ~bt ~kind:"output" exn in match reopen_on_error error with | reopen_delay when reopen_delay < 0. -> Printexc.raise_with_backtrace exn bt | reopen_delay -> open_date <- Unix.gettimeofday () +. reopen_delay; self#log#important "Error while streaming: %s, will re-open in %.02fs" (Printexc.to_string exn) reopen_delay method! output = try base#output with exn -> let bt = Printexc.get_raw_backtrace () in (try self#cleanup_pipe with _ -> ()); self#reopen_on_error ~bt exn method! send b = if self#is_open then base#send b method! encode frame = (match self#is_open with | false when open_date <= Unix.gettimeofday () -> self#prepare_pipe | true when Atomic.get need_reopen -> if open_date <= Unix.gettimeofday () then self#reopen | true when open_date +. reopen_delay () <= Unix.gettimeofday () && reopen_when () -> self#reopen | _ -> ()); base#encode frame method! insert_metadata metadata = if reopen_on_metadata (Frame.Metadata.Export.to_metadata metadata) then self#reopen; base#insert_metadata metadata end (** Out channel virtual class: takes care of current out channel and writing to it. *) let chan_proto frame_t arg_doc = [ ( "flush", Lang.bool_t, Some (Lang.bool false), Some "Perform a flush after each write." ); ] @ pipe_proto frame_t arg_doc class virtual ['a] chan_output p = let flush = Lang.to_bool (List.assoc "flush" p) in object (self) val mutable chan : 'a option = None method virtual open_chan : 'a method virtual close_chan : 'a -> unit method virtual output_substring : 'a -> string -> int -> int -> unit method virtual flush : 'a -> unit method open_pipe = chan <- Some self#open_chan method write_pipe b ofs len = let chan = Option.get chan in try self#output_substring chan b ofs len; if flush then self#flush chan with Sys_error _ as exn -> let bt = Printexc.get_raw_backtrace () in Lang.raise_as_runtime ~bt ~kind:"system" exn method close_pipe = match chan with | None -> () | Some ch -> self#close_chan ch; chan <- None method is_open = chan <> None end (** File output *) class virtual ['a] file_output_base p = let source = Lang.to_source (Lang.assoc "" 3 p) in let filename = Lang.to_string_getter (Lang.assoc "" 2 p) in let on_close = List.assoc "on_close" p in let on_close s = Lang.to_unit (Lang.apply on_close [("", Lang.string s)]) in let perm = Lang.to_int (List.assoc "perm" p) in let dir_perm = Lang.to_int (List.assoc "dir_perm" p) in let append = Lang.to_bool (List.assoc "append" p) in object (self) val current_filename = Atomic.make None method virtual interpolate : ?subst:(string -> string) -> string -> string method private filename = let filename = filename () in let filename = Lang_string.home_unrelate filename in (* Avoid / in metas for filename.. *) let subst m = String.concat "-" (String.split_on_char '/' m) in self#interpolate ~subst filename method virtual open_out_gen : open_flag list -> int -> string -> 'a method self_sync = source#self_sync method private prepare_filename = let mode = Open_wronly :: Open_creat :: (if append then [Open_append] else [Open_trunc]) in match Atomic.get current_filename with | Some filename -> (filename, mode, perm) | None -> ( let filename = self#filename in try Utils.mkdir ~perm:dir_perm (Filename.dirname filename); Atomic.set current_filename (Some filename); (filename, mode, perm) with Sys_error _ as exn -> let bt = Printexc.get_raw_backtrace () in Lang.raise_as_runtime ~bt ~kind:"system" exn) method open_chan = try let filename, mode, perm = self#prepare_filename in self#open_out_gen mode perm filename with Sys_error _ as exn -> let bt = Printexc.get_raw_backtrace () in Lang.raise_as_runtime ~bt ~kind:"system" exn method virtual close_out : 'a -> unit method close_chan fd = try self#close_out fd; (match Atomic.get current_filename with | Some f -> self#on_close f | None -> ()); Atomic.set current_filename None with Sys_error _ as exn -> let bt = Printexc.get_raw_backtrace () in Lang.raise_as_runtime ~bt ~kind:"system" exn method private on_close = on_close end class file_output ?clock ~format_val p = object inherit piped_output ?clock ~name:"output.file" p inherit [out_channel] chan_output p inherit [out_channel] file_output_base p method encoder_factory = encoder_factory format_val method open_out_gen mode perm filename = let fd = open_out_gen mode perm filename in set_binary_mode_out fd true; fd method output_substring = output_substring method flush = flush method close_out = close_out end class file_output_using_encoder ?clock ~format_val p = let format = Lang.to_format format_val in let append = Lang.to_bool (List.assoc "append" p) in let on_close = List.assoc "on_close" p in let on_close s = Lang.to_unit (Lang.apply on_close [("", Lang.string s)]) in let p = ("append", Lang.bool true) :: List.remove_assoc "append" p in object (self) inherit piped_output ?clock ~name:"output.file" p as base inherit [unit] chan_output p inherit [unit] file_output_base p method open_out_gen mode perm filename = let fd = open_out_gen mode perm filename in close_out fd; () method encoder_factory name meta = (* Make sure the file is created with the right perms. *) let filename, mode, perm = self#prepare_filename in Atomic.set current_filename (Some filename); self#open_out_gen mode perm filename; let format = Encoder.with_file_output ~append format filename in encoder_factory ~format format_val name meta method! close_encoder = let ret = base#close_encoder in (try on_close (Option.get (Atomic.get current_filename)) with _ -> ()); Atomic.set current_filename None; ret method output_substring () _ _ _ = () method flush () = () method close_out () = () end let file_proto frame_t = [ ( "append", Lang.bool_t, Some (Lang.bool false), Some "Do not truncate but append in the file if it exists." ); ( "perm", Lang.int_t, Some (Lang.int 0o666), Some "Permission of the file if it has to be created, up to umask. You can \ and should write this number in octal notation: 0oXXX. The default \ value is however displayed in decimal (0o666 = 6×8^2 + 6×8 + 6 = \ 438)." ); ( "dir_perm", Lang.int_t, Some (Lang.int 0o777), Some "Permission of the directories if some have to be created, up to \ umask. Although you can enter values in octal notation (0oXXX) they \ will be displayed in decimal (for instance, 0o777 = 7×8^2 + 7×8 + 7 = \ 511)." ); ( "on_close", Lang.fun_t [(false, "", Lang.string_t)] Lang.unit_t, Some (Lang.val_cst_fun [("", None)] Lang.unit), Some "This function will be called for each file, after that it is finished \ and closed. The filename will be passed as argument." ); ] @ chan_proto frame_t "Filename where to output the stream." let new_file_output ?clock p = let format_val = Lang.assoc "" 1 p in let format = Lang.to_format format_val in if Encoder.file_output format then (new file_output_using_encoder ?clock ~format_val p :> piped_output) else (new file_output ?clock ~format_val p :> piped_output) let output_file = let return_t = Lang.univ_t () in Lang.add_operator ~base:output "file" (file_proto return_t) ~return_t ~category:`Output ~meth:pipe_meth ~descr:"Output the source stream to a file." (fun p -> new_file_output p) (** External output *) class external_output ?clock p = let format_val = Lang.assoc "" 1 p in let process = Lang.to_string_getter (Lang.assoc "" 2 p) in let self_sync = Lang.to_bool (List.assoc "self_sync" p) in object (self) inherit piped_output ?clock ~name:"output.external" p inherit [out_channel] chan_output p method encoder_factory = encoder_factory format_val method self_sync = (`Static, self#source_sync self_sync) method open_chan = let process = process () in let process = self#interpolate process in Unix.open_process_out process method close_chan chan = try ignore (Unix.close_process_out chan) with Sys_error _ -> () method output_substring = output_substring method flush = flush method close_out = close_out end let pipe_proto frame_t descr = ( "self_sync", Lang.bool_t, Some (Lang.bool false), Some "Set to `true` if the process is expected to control the output's \ latency. Typical example: `ffmpeg` with the `-re` command-line option." ) :: chan_proto frame_t descr let _ = let return_t = Lang.univ_t () in Lang.add_operator ~base:output "external" (pipe_proto return_t "Process to pipe data to.") ~return_t ~category:`Output ~meth:pipe_meth ~descr:"Send the stream to a process' standard input." (fun p -> (new external_output p :> piped_output)) liquidsoap-2.3.2/src/core/outputs/sdl_out.ml000066400000000000000000000121751477303350200211470ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (** Output using SDL lib. *) open Tsdl class output ~infallible ~register_telnet ~on_start ~on_stop ~autostart source_val = let () = Sdl_utils.init [Sdl.Init.video] in let source = Lang.to_source source_val in object (self) inherit Output.output ~name:"sdl" ~output_kind:"output.sdl" ~infallible ~register_telnet ~on_start ~on_stop source_val autostart val mutable fullscreen = false val mutable window = None method self_sync = source#self_sync method start = let w, h = self#video_dimensions in window <- Some (Sdl_utils.check (fun () -> Sdl.create_window "Liquidsoap" ~w ~h Sdl.Window.windowed) ()); self#log#info "Initialized SDL video surface." (** We don't care about latency. *) method! reset = () (** Stop SDL. We have to assume that there's only one SDL output anyway. *) method stop = Sdl.quit () method process_events = let e = Sdl.Event.create () in if Sdl.poll_event (Some e) then ( match Sdl.Event.(enum (get e typ)) with | `Quit -> (* Avoid an immediate restart (which would happen with autostart). But do not cancel autostart. We should perhaps have a method in the output class for that kind of thing, and try to get an uniform behavior. *) self#transition_to `Stopped; self#transition_to `Started | `Key_down -> (let k = Sdl.Event.(get e keyboard_keycode) in match k with | k when k = Sdl.K.f -> fullscreen <- not fullscreen; Sdl_utils.check (fun () -> Sdl.set_window_fullscreen (Option.get window) (if fullscreen then Sdl.Window.fullscreen else Sdl.Window.windowed)) () | k when k = Sdl.K.q -> let e = Sdl.Event.create () in Sdl.Event.(set e typ quit); assert (Sdl_utils.check Sdl.push_event e) | _ -> ()); self#process_events | _ -> self#process_events) method send_frame buf = self#process_events; let window = Option.get window in let surface = Sdl_utils.check Sdl.get_window_surface window in let width, height = self#video_dimensions in match (VFrame.data buf).Content.Video.data with | [] -> () | (_, img) :: _ -> (* We only display the first image of each frame *) let img = img |> Video.Canvas.Image.viewport width height |> Video.Canvas.Image.render ~transparent:false in Sdl_utils.Surface.of_img surface img; Sdl_utils.check Sdl.update_window_surface window end let output_sdl = let frame_t = Lang.frame_t (Lang.univ_t ()) (Frame.Fields.make ~video:(Format_type.video ()) ()) in Lang.add_operator ~base:Modules.output "sdl" (Output.proto @ [("", Lang.source_t frame_t, None, None)]) ~return_t:frame_t ~category:`Output ~meth:Output.meth ~descr:"Display a video using SDL." (fun p -> let autostart = Lang.to_bool (List.assoc "start" p) in let infallible = not (Lang.to_bool (List.assoc "fallible" p)) in let register_telnet = Lang.to_bool (List.assoc "register_telnet" p) in let on_start = let f = List.assoc "on_start" p in fun () -> ignore (Lang.apply f []) in let on_stop = let f = List.assoc "on_stop" p in fun () -> ignore (Lang.apply f []) in let source = List.assoc "" p in (new output ~infallible ~register_telnet ~autostart ~on_start ~on_stop source :> Output.output)) let _ = Lang.add_builtin ~category:(`Source `Output) ~descr:"Check whether video output is available with SDL." ~base:output_sdl "has_video" [] Lang.bool_t (fun _ -> match Sdl.init Sdl.Init.video with | Ok _ -> Lang.bool true | Error _ -> Lang.bool false) liquidsoap-2.3.2/src/core/playlist_parser.ml000066400000000000000000000100221477303350200211550ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Plug for playlist parsing, in which [src/playlists] plugins come. *) module Http = Liq_http let log = Log.make ["playlist parser"] let conf_playlist = Dtools.Conf.void ~p:(Configure.conf#plug "playlist") "Playlist formats" let conf_cue_in_metadata = Dtools.Conf.string ~p:(conf_playlist#plug "cue_in_metadata") ~d:"liq_cue_in" "Cue in metadata for playlists with track index." ~comments: [ "Some playlists format, such as CUE files specify index points to start"; "tracks playback. In this case, tracks are resolved to a annotate: \ request with"; "a cue-in metadata containing the index. If you want to make use of \ this index,"; "you should specify here what label you want for this metadata."; ] let conf_cue_out_metadata = Dtools.Conf.string ~p:(conf_playlist#plug "cue_out_metadata") ~d:"liq_cue_out" "Cue out metadata for playlists with track index." ~comments: [ "Some playlists format, such as CUE files specify index points to start"; "tracks playback. In this case, tracks are resolved to a annotate: \ request with"; "a cue-in metadata containing the index. If you want to make use of \ this index,"; "you should specify here what label you want for this metadata."; ] (** A playlist is list of metadatas,uri *) type playlist = ((string * string) list * string) list type parser = ?pwd:string -> string -> playlist (** A plugin is a boolean and a parsing function *) type plugin = { (* true if the format can be automatically detected *) strict : bool; (* The parser is expected to respect the order of the files in the playlist. *) parser : parser; } (** Parsers are given a string and return a list of metadatas,uri, if possible. *) let parsers : plugin Plug.t = Plug.create ~doc:"Method to parse playlist." "playlist formats" let get_file ?pwd file = match pwd with | Some pwd -> if Http.is_url pwd && not (Http.is_url file) then pwd ^ file else ( let f = Filename.concat pwd file in if Sys.file_exists f then f else file) | None -> file exception Exit of (string * playlist) (** Get a valid parser for [string]. The validity is not based on file type but only on success of the parser instantiation. Being based on file extension is weak, and troublesome when accessing a remote file -- that would force us to create a local temporary file with the same extension. *) let search_valid ?pwd string = try let plugins = Plug.list parsers in (* Try strict plugins first *) let compare (_, a) (_, b) = compare b.strict a.strict in let plugins = List.sort compare plugins in List.iter (fun (format, plugin) -> log#info "Trying %s parser" format; match try Some (plugin.parser ?pwd string) with _ -> None with | Some d -> raise (Exit (format, d)) | None -> ()) plugins; log#important "No format found"; raise Not_found with Exit (format, d) -> (format, d) liquidsoap-2.3.2/src/core/playlists/000077500000000000000000000000001477303350200174375ustar00rootroot00000000000000liquidsoap-2.3.2/src/core/playlists/playlist_basic.ml000066400000000000000000000103601477303350200227730ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let log = Log.make ["playlist"; "basic"] let split_lines buf = Re.Pcre.split ~rex:(Re.Pcre.regexp "[\r\n]+") buf let parse_meta = let processor = MenhirLib.Convert.Simplified.traditional2revised Liquidsoap_lang.Parser.annotate_metadata_entry in let rec f cur s = try let lexbuf = Sedlexing.Utf8.from_string s in let tokenizer = Liquidsoap_lang.Preprocessor.mk_tokenizer lexbuf in let metadata = processor tokenizer in let b = Buffer.create 10 in let rec g () = match Sedlexing.next lexbuf with | Some c -> Buffer.add_utf_8_uchar b c; g () | None -> Buffer.contents b in f (metadata :: cur) (g ()) with _ -> if cur <> [] then (List.rev cur, s) else ([], "") in f [] let parse_extinf s = try let rex = Re.Pcre.regexp "#EXTINF:(\\d*)\\s*(.*)" in let sub = Re.Pcre.exec ~rex s in let meta, song = match Re.Pcre.get_substring sub 2 with | "" -> ([], "") | s when s.[0] = ',' -> ([], String.sub s 1 (String.length s - 1)) | s -> parse_meta s in let meta = match Re.Pcre.get_substring sub 1 with | "" -> meta | duration -> ("extinf_duration", duration) :: meta in let lines = Re.Pcre.split ~rex:(Re.Pcre.regexp "\\s*-\\s*") song in meta @ match lines with | [] | [""; ""] -> [] | [""; song] -> [("song", String.trim song)] | [artist; title] -> [("artist", String.trim artist); ("title", String.trim title)] | _ when song = "" -> [] | _ -> [("song", String.trim song)] with Not_found -> [] (* This parser cannot detect the format !! *) let parse_mpegurl ?pwd string = let lines = List.filter (fun x -> x <> "") (split_lines string) in let is_info line = Re.Pcre.pmatch ~rex:(Re.Pcre.regexp "^#EXTINF") line in let skip_line line = line.[0] == '#' in let rec get_urls cur lines = match lines with | x :: y :: lines when is_info x && not (skip_line y) -> let metadata = parse_extinf x in get_urls ((metadata, Playlist_parser.get_file ?pwd y) :: cur) lines | x :: lines when not (skip_line x) -> get_urls (([], Playlist_parser.get_file ?pwd x) :: cur) lines | _ :: lines -> get_urls cur lines | [] -> List.rev cur in get_urls [] lines let parse_scpls ?pwd string = let string = Re.Pcre.substitute ~rex:(Re.Pcre.regexp "#[^\\r\\n]*[\\n\\r]+") ~subst:(fun _ -> "") string in (* Format check, raise Not_found if invalid *) ignore (Re.Pcre.exec ~rex:(Re.Pcre.regexp "^[\\r\\n\\s]*\\[playlist\\]") (String.lowercase_ascii string)); let lines = split_lines string in let urls = List.map (fun s -> try let rex = Re.Pcre.regexp ~flags:[`CASELESS] "file\\d*\\s*=\\s*(.*)\\s*" in let sub = Re.Pcre.exec ~rex s in Re.Pcre.get_substring sub 1 with Not_found -> "") lines in let urls = List.filter (fun s -> s <> "") urls in List.map (fun t -> ([], Playlist_parser.get_file ?pwd t)) urls let _ = Builtins_resolvers.add_playlist_parser ~format:"SCPLS" "scpls" parse_scpls let _ = Builtins_resolvers.add_playlist_parser ~format:"M3U" "m3u" parse_mpegurl liquidsoap-2.3.2/src/core/playlists/playlist_xml.ml000066400000000000000000000027601477303350200225170ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) let log = Log.make ["playlist"; "xml"] let tracks ?pwd s = try let recode_metas m = let f = Charset.convert in List.map (fun (a, b) -> (f a, f b)) m in List.map (fun (a, b) -> (recode_metas a, Playlist_parser.get_file ?pwd b)) (Xmlplaylist.tracks s) with Xmlplaylist.Error e -> log#debug "Parsing failed: %s" (Xmlplaylist.string_of_error e); raise (Xmlplaylist.Error e) let _ = Builtins_resolvers.add_playlist_parser ~format:"XML" "xml" tracks liquidsoap-2.3.2/src/core/plug.ml000066400000000000000000000000351477303350200167120ustar00rootroot00000000000000include Liquidsoap_lang.Plug liquidsoap-2.3.2/src/core/pos.ml000066400000000000000000000000341477303350200165430ustar00rootroot00000000000000include Liquidsoap_lang.Pos liquidsoap-2.3.2/src/core/protocols/000077500000000000000000000000001477303350200174375ustar00rootroot00000000000000liquidsoap-2.3.2/src/core/protocols/annotate.ml000066400000000000000000000051121477303350200216010ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (* The annotate protocol allows to set the initial metadata for a request: annotate:key1=val1,key2=val2,...:uri is resolved into uri, and adds the bindings to the request metadata. The values can be "strings", or directly integers, floats or identifiers. *) exception Error of string let log = Log.make ["annotate"] let parse = let processor = MenhirLib.Convert.Simplified.traditional2revised Liquidsoap_lang.Parser.annotate in fun s -> let lexbuf = Sedlexing.Utf8.from_string s in try let tokenizer = Liquidsoap_lang.Preprocessor.mk_tokenizer lexbuf in let metadata = processor tokenizer in let b = Buffer.create 10 in let rec f () = match Sedlexing.next lexbuf with | Some c -> Buffer.add_utf_8_uchar b c; f () | None -> Buffer.contents b in (metadata, f ()) with _ -> let startp, endp = Sedlexing.loc lexbuf in let err = Printf.sprintf "Char %d-%d: Syntax error" startp endp in log#info "Error while parsing annotate URI %s: %s" (Lang_string.quote_string s) err; raise (Error err) let annotate s ~log _ = try let metadata, uri = parse s in Some (Request.indicator ~metadata:(Frame.Metadata.from_list metadata) uri) with Error err -> log err; None let () = Lang.add_protocol ~doc:"Add metadata to a request" ~syntax:"annotate:key=\"val\",key2=\"val2\",...:uri" ~static:(fun uri -> try let _, uri = parse uri in Request.is_static uri with _ -> false) "annotate" annotate liquidsoap-2.3.2/src/core/protocols/mpd.ml000066400000000000000000000117201477303350200205520ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) module Conf = Dtools.Conf let conf = Conf.void ~p:(Configure.conf#plug "mpd") "Parameters for the mpd protocol." let conf_host = Conf.string ~p:(conf#plug "host") ~d:"127.0.0.1" "MPD host." let conf_port = Conf.int ~p:(conf#plug "port") ~d:6600 "MPD port." let conf_path_prefix = Conf.string ~p:(conf#plug "path") ~d:"/var/lib/mpd/music" "Directory where MPD's music is located." let conf_randomize = Conf.bool ~p:(conf#plug "randomize") ~d:true "Randomize order of MPD's results." let conf_debug = Conf.bool ~p:(conf#plug "debug") ~d:false "Debug communications with MPD server." exception Error of string let connect () = let host = try Unix.gethostbyname conf_host#get with Not_found -> raise (Error "Host not found") in let sockaddr = Unix.ADDR_INET (host.Unix.h_addr_list.(0), conf_port#get) in let socket = Unix.socket ~cloexec:true Unix.PF_INET Unix.SOCK_STREAM 0 in let read () = let buflen = Utils.pagesize in let buf = Bytes.create buflen in let ans = ref "" in let n = ref buflen in while !n = buflen do n := Unix.recv socket buf 0 buflen []; ans := !ans ^ Bytes.sub_string buf 0 !n done; if conf_debug#get then Printf.printf "R: %s%!" !ans; !ans in let write s = let len = String.length s in if conf_debug#get then Printf.printf "W: %s%!" s; let l = Unix.send socket (Bytes.of_string s) 0 len [] in assert (l = len) in Unix.connect socket sockaddr; (socket, read, write) let re_newline = Str.regexp "[\r\n]+" let cmd read write name args = let args = List.map (fun s -> "\"" ^ s ^ "\"") args in write (name ^ " " ^ String.concat " " args ^ "\n"); let ans = read () in let ans = Str.split re_newline ans in let ans = List.rev ans in (List.hd ans, List.rev (List.tl ans)) let re_file = Str.regexp "^file: \\(.*\\)$" let re_metadata = Str.regexp "^\\([^:]+\\): \\(.*\\)$" let valid_metadata = ["artist"; "title"; "album"; "genre"; "date"; "track"] let search read write field v = let l = let ret, l = cmd read write "search" [field; v] in assert (ret = "OK"); l in let ans = ref [] in let file = ref "" in let metadata = ref [] in let add () = if !file <> "" then ans := Request.indicator ~metadata:(Frame.Metadata.from_list !metadata) !file :: !ans in List.iter (fun s -> if Str.string_match re_file s 0 then ( let f = Str.matched_group 1 s in let prefix = conf_path_prefix#get in let f = prefix ^ "/" ^ f in if conf_debug#get then Printf.printf "Found: %s\n%!" f; add (); file := f) else if Str.string_match re_metadata s 0 then ( let field = Str.matched_group 1 s in let field = String.lowercase_ascii field in let value = Str.matched_group 2 s in if List.mem field valid_metadata then metadata := (field, value) :: !metadata)) l; add (); if conf_randomize#get then Extralib.List.shuffle !ans else List.rev !ans let re_request = Str.regexp "^\\([^=]+\\)=\\(.*\\)$" let re_version = Str.regexp "OK MPD \\([0-9\\.]+\\)" let mpd s ~log _ = if not (Str.string_match re_request s 0) then raise (Error "Invalid request"); let field = Str.matched_group 1 s in let value = Str.matched_group 2 s in let value = let len = String.length value in if len > 0 && value.[0] = '"' && value.[len - 1] = '"' then String.sub value 1 (len - 2) else value in let _, read, write = connect () in let search = search read write in let version = let v = read () in if Str.string_match re_version v 0 then Str.matched_group 1 v else raise (Error "Not an MPD server") in log (Printf.sprintf "Connected to MPD version %s\n" version); let files = search field value in write "close\n"; match files with f :: _ -> Some f | [] -> None let () = Lang.add_protocol ~doc:"Finds all files with a tag equal to a given value using mpd." ~syntax:"mpd:tag=value" ~static:(fun _ -> false) "mpd" mpd liquidsoap-2.3.2/src/core/request.ml000066400000000000000000000607341477303350200174470ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** Plug for resolving, that is obtaining a file from an URI. [src/protocols] plugins provide ways to resolve URIs: fetch, generate, ... *) exception No_indicator exception Request_resolved exception Duration of float module Queue = Queues.Queue let conf = Dtools.Conf.void ~p:(Configure.conf#plug "request") "requests configuration" let conf_add_on_air = Dtools.Conf.bool ~p:(conf#plug "deprecated_on_air_metadata") ~d:false "DEPRECATED: Add `on_air` and `on_air_timestamp` request metadata." ~comments: [ "`on_air` and `on_air_timestamp` are DEPRECATED! Requests can be used in"; "multiple sources and/or outputs. Its recommended to use output's"; "`on_track` method to track the latest metadata currently being played"; "by an output."; ] let conf_timeout = Dtools.Conf.float ~p:(conf#plug "timeout") ~d:29. "Default request resolution timeout." let log = Log.make ["request"] let pretty_date date = Printf.sprintf "%d/%02d/%02d %02d:%02d:%02d" (date.Unix.tm_year + 1900) (date.Unix.tm_mon + 1) date.Unix.tm_mday date.Unix.tm_hour date.Unix.tm_min date.Unix.tm_sec (** File utilities. *) let remove_file_proto s = (* First remove file:// 🤮 *) let s = Re.Pcre.substitute ~rex:(Re.Pcre.regexp "^file://") ~subst:(fun _ -> "") s in (* Then remove file: 😇 *) Re.Pcre.substitute ~rex:(Re.Pcre.regexp "^file:") ~subst:(fun _ -> "") s let home_unrelate s = Lang_string.home_unrelate (remove_file_proto s) let parse_uri uri = try let i = String.index uri ':' in Some (String.sub uri 0 i, String.sub uri (i + 1) (String.length uri - (i + 1))) with _ -> None type resolve_flag = [ `Resolved | `Failed | `Timeout ] type metadata_resolver = { priority : unit -> int; resolver : metadata:Frame.metadata -> extension:string option -> mime:string -> string -> (string * string) list; } type indicator = { uri : string; temporary : bool; metadata : Frame.metadata } type resolving = { since : float; pending : (Condition.t * Mutex.t) list } type status = [ `Idle | `Resolving of resolving | `Ready | `Destroyed | `Failed ] type decoder = string * (unit -> Decoder.file_decoder_ops) type on_air = { source : Source.source; timestamp : float } type t = { id : int; resolve_metadata : bool; excluded_metadata_resolvers : string list; cue_in_metadata : string option; cue_out_metadata : string option; persistent : bool; decoders : (Frame.content_type, decoder option) Hashtbl.t; status : status Atomic.t; logger : Log.t; log : (Unix.tm * string) Queue.t; initial_uri : string; indicators : indicator Queue.t; file_metadata : Frame.Metadata.t Atomic.t; on_air : on_air Queue.t; } let resolved t = match Atomic.get t.status with `Ready -> true | _ -> false let last_indicator r = match List.rev (Queue.elements r.indicators) with | el :: _ -> el | [] -> assert false let initial_uri { initial_uri } = initial_uri let status { status } = Atomic.get status let indicator ?(metadata = Frame.Metadata.empty) ?temporary s = { uri = home_unrelate s; temporary = temporary = Some true; metadata } type dresolver = { dpriority : unit -> int; file_extensions : unit -> string list; dresolver : metadata:Frame.metadata -> string -> float; } let dresolvers_doc = "Methods to extract duration from a file." let conf_dresolvers = Dtools.Conf.list ~p:(conf#plug "dresolvers") ~d:[] "Methods to extract file duration." let f c v = match c#get_d with | None -> c#set_d (Some [v]) | Some d -> c#set_d (Some (d @ [v])) let dresolvers = Plug.create ~doc:dresolvers_doc ~register_hook:(fun name _ -> f conf_dresolvers name) "audio file formats (duration)" let get_dresolvers ~file () = let extension = try Utils.get_ext file with _ -> "" in let f cur name = match Plug.get dresolvers name with | Some ({ file_extensions } as p) when List.mem extension (file_extensions ()) -> (name, p) :: cur | Some _ -> cur | None -> log#severe "Cannot find duration resolver %s" name; cur in let resolvers = List.fold_left f [] conf_dresolvers#get in List.sort (fun (_, a) (_, b) -> compare (b.dpriority ()) (a.dpriority ())) resolvers let compute_duration ?resolvers ~metadata file = try List.iter (fun (name, { dpriority; dresolver }) -> try log#info "Trying duration resolver %s (priority: %d) for file %s.." name (dpriority ()) (Lang_string.quote_string file); (match resolvers with | Some l when not (List.mem name l) -> raise Not_found | _ -> ()); let ans = dresolver ~metadata file in raise (Duration ans) with | Duration e -> raise (Duration e) | _ -> ()) (get_dresolvers ~file ()); raise Not_found with Duration d -> d let duration ?resolvers ~metadata file = try match ( Frame.Metadata.find_opt "duration" metadata, Frame.Metadata.find_opt "cue_in" metadata, Frame.Metadata.find_opt "cue_out" metadata ) with | _, Some cue_in, Some cue_out -> Some (float_of_string cue_out -. float_of_string cue_in) | _, None, Some cue_out -> Some (float_of_string cue_out) | Some v, _, _ -> Some (float_of_string v) | None, cue_in, None -> let duration = compute_duration ?resolvers ~metadata file in let duration = match cue_in with | Some cue_in -> duration -. float_of_string cue_in | None -> duration in Some duration with _ -> None (** [get_filename request] returns [Some f] if the request successfully lead to a local file [f], [None] otherwise. *) let get_filename t = if resolved t then Some (last_indicator t).uri else None (** Manage requests' metadata *) let add_root_metadata t m = let m = Frame.Metadata.add "rid" (string_of_int t.id) m in let m = Frame.Metadata.add "initial_uri" (initial_uri t) m in (* TOP INDICATOR *) let m = Frame.Metadata.add "temporary" (match last_indicator t with | h -> if h.temporary then "true" else "false" | exception _ -> "false") m in let m = match get_filename t with | Some f -> Frame.Metadata.add "filename" f m | None -> m in let timestamp = if conf_add_on_air#get then ( if 1 < Queue.length t.on_air then log#important "Request %d is used by multiple sources, `on_air` and \ `on_air_timestamp` are not accurate!" t.id; Queue.fold t.on_air (fun { timestamp } cur -> match cur with | Some cur -> Some (min cur timestamp) | None -> Some timestamp) None) else None in (* STATUS *) match (timestamp, Atomic.get t.status) with | Some d, _ -> let m = Frame.Metadata.add "on_air" (pretty_date (Unix.localtime d)) m in Frame.Metadata.add "on_air_timestamp" (Printf.sprintf "%.02f" d) m | _, `Idle -> Frame.Metadata.add "status" "idle" m | _, `Resolving { since } -> let m = Frame.Metadata.add "resolving" (pretty_date (Unix.localtime since)) m in Frame.Metadata.add "status" "resolving" m | _, `Ready -> Frame.Metadata.add "status" "ready" m | _, `Destroyed -> Frame.Metadata.add "status" "destroyed" m | _, `Failed -> Frame.Metadata.add "status" "failed" m let plain_metadata t = List.fold_left (fun m h -> Frame.Metadata.append h.metadata m) (Atomic.get t.file_metadata) (Queue.elements t.indicators) let metadata t = add_root_metadata t (plain_metadata t) (** Logging *) let add_log t i = t.logger#info "%s" i; Queue.push t.log (Unix.localtime (Unix.gettimeofday ()), i) (* Indicator tree management *) let () = Printexc.register_printer (function | No_indicator -> Some "All options exhausted while processing request" | _ -> None) let string_of_indicators t = let i = List.rev (Queue.elements t.indicators) in let string_of_list l = "[" ^ String.concat ", " l ^ "]" in let i = (List.map (fun i -> i.uri)) i in string_of_list i let conf_metadata_decoders = Dtools.Conf.list ~p:(conf#plug "metadata_decoders") ~d:[] "Decoders and order used to decode files' metadata." let conf_metadata_decoder_priorities = Dtools.Conf.void ~p:(conf_metadata_decoders#plug "priorities") "Priorities used for applying metadata decoders. Decoder with the highest \ priority take precedence." let conf_request_metadata_priority = Dtools.Conf.int ~d:5 ~p:(conf_metadata_decoder_priorities#plug "request_metadata") "Priority for the request metadata. This include metadata set via \ `annotate`." let f c v = match c#get_d with | None -> c#set_d (Some [v]) | Some d -> c#set_d (Some (d @ [v])) let get_decoders conf decoders = let f cur name = match Plug.get decoders name with | Some p -> (name, p) :: cur | None -> log#severe "Cannot find decoder %s" name; cur in List.sort (fun (_, d) (_, d') -> Stdlib.compare (d'.priority ()) (d.priority ())) (List.fold_left f [] (List.rev conf#get)) let mresolvers_doc = "Methods to extract metadata from a file." let mresolvers = Plug.create ~register_hook:(fun name _ -> f conf_metadata_decoders name) ~doc:mresolvers_doc "metadata formats" let conf_duration = Dtools.Conf.bool ~p:(conf_metadata_decoders#plug "duration") ~d:false "Compute duration in the \"duration\" metadata, if the metadata is not \ already present. This can take a long time and the use of this option is \ not recommended: the proper way is to have a script precompute the \ \"duration\" metadata." let conf_recode = Dtools.Conf.bool ~p:(conf_metadata_decoders#plug "recode") ~d:true "Re-encode metadata strings in UTF-8" let conf_recode_excluded = Dtools.Conf.list ~d:["pic"; "apic"; "metadata_block_picture"; "coverart"] ~p:(conf_recode#plug "exclude") "Exclude these metadata from automatic recording." let resolve_metadata ~initial_metadata ~excluded name = let decoders = get_decoders conf_metadata_decoders mresolvers in let decoders = List.filter (fun (name, _) -> not (List.mem name excluded)) decoders in let high_priority_decoders, low_priority_decoders = List.partition (fun (_, { priority }) -> conf_request_metadata_priority#get < priority ()) decoders in let convert = if conf_recode#get then ( let excluded = conf_recode_excluded#get in fun k v -> if not (List.mem (String.lowercase_ascii k) excluded) then Charset.convert v else v) else fun _ x -> x in let extension = try Some (Utils.get_ext name) with _ -> None in let mime = Magic_mime.lookup name in let get_metadata ~metadata decoders = List.fold_left (fun metadata (_, { resolver }) -> try let ans = resolver ~metadata:initial_metadata ~extension ~mime name in List.fold_left (fun metadata (k, v) -> let k = String.lowercase_ascii (convert k k) in let v = convert k v in if not (Frame.Metadata.mem k metadata) then Frame.Metadata.add k v metadata else metadata) metadata ans with _ -> metadata) metadata decoders in let metadata = get_metadata ~metadata:Frame.Metadata.empty high_priority_decoders in let metadata = get_metadata ~metadata:(Frame.Metadata.append initial_metadata metadata) low_priority_decoders in if conf_duration#get then ( match duration ~metadata name with | None -> metadata | Some d -> Frame.Metadata.add "duration" (string_of_float d) metadata) else metadata (** Sys.file_exists doesn't make a difference between existing files and files without enough permissions to list their attributes, for example when they are in a directory without x permission. The two following functions allow a more precise diagnostic. We do not use them everywhere in this file, but only when splitting existence and readability checks yields better logs. *) let file_exists name = try Unix.access name [Unix.F_OK]; true with Unix.Unix_error _ -> false let file_is_readable name = try Unix.access name [Unix.R_OK]; true with Unix.Unix_error _ -> false let read_metadata r = let i = last_indicator r in let metadata = resolve_metadata ~initial_metadata:(plain_metadata r) ~excluded:r.excluded_metadata_resolvers i.uri in Atomic.set r.file_metadata metadata let push_indicator t i = add_log t (Printf.sprintf "Pushed [%s;...]." (Lang_string.quote_string i.uri)); Queue.push t.indicators i (** Global management *) module Pool = Pool.Make (struct type req = t type t = req let id { id } = id let destroyed = { id = 0; cue_in_metadata = None; cue_out_metadata = None; resolve_metadata = false; excluded_metadata_resolvers = []; persistent = false; status = Atomic.make `Destroyed; logger = Log.make []; log = Queue.create (); decoders = Hashtbl.create 1; initial_uri = ""; indicators = Queue.create (); file_metadata = Atomic.make Frame.Metadata.empty; on_air = Queue.create (); } let destroyed id = { destroyed with id } let is_destroyed { status } = Atomic.get status = `Destroyed end) let id { id } = id let from_id id = Pool.find id let all () = Pool.fold (fun _ r l -> r :: l) [] (** Creation *) let leak_warning = Dtools.Conf.int ~p:(conf#plug "leak_warning") ~d:100 "Number of requests at which a leak warning should be issued." let destroy ?force t = if Atomic.get t.status <> `Destroyed then if force = Some true || not t.persistent then ( Queue.iter t.indicators (fun i -> if i.temporary && file_exists i.uri then ( try Unix.unlink i.uri with e -> log#severe "Unlink failed: %S" (Printexc.to_string e))); Queue.flush_iter t.indicators (fun _ -> ()); Hashtbl.reset t.decoders; Atomic.set t.status `Destroyed; add_log t "Request destroyed.") let finalise = destroy ~force:true let cleanup () = Pool.iter (fun _ r -> if Atomic.get r.status <> `Destroyed then destroy ~force:true r); Pool.clear () let create ?(resolve_metadata = true) ?(excluded_metadata_resolvers = []) ?metadata ?(persistent = false) ?temporary ~cue_in_metadata ~cue_out_metadata uri = (* Find instantaneous request loops *) let n = Pool.size () in if n > 0 && n mod leak_warning#get = 0 then log#severe "There are currently %d RIDs, possible request leak! Please check that \ you don't have a loop on empty/unavailable requests." n; let t = let req = { id = 0; cue_in_metadata; cue_out_metadata; resolve_metadata; excluded_metadata_resolvers; (* This is fixed when resolving the request. *) persistent; status = Atomic.make `Idle; logger = Log.make []; log = Queue.create (); decoders = Hashtbl.create 1; initial_uri = uri; indicators = Queue.create (); file_metadata = Atomic.make Frame.Metadata.empty; on_air = Queue.create (); } in Pool.add (fun id -> { req with id; logger = Log.make ["request"; string_of_int id] }) in push_indicator t (indicator ?metadata ?temporary uri); Gc.finalise finalise t; t let get_cue ~r = function | None -> None | Some m -> ( match Frame.Metadata.find_opt m (metadata r) with | None -> None | Some v -> ( match float_of_string_opt v with | None -> r.logger#important "Invalid cue metadata %s: %s" m v; None | Some v -> Some v)) exception Found_decoder of decoder option let get_base_decoder ~ctype r = if not (resolved r) then None else ( try Hashtbl.iter (fun c d -> if Frame.compatible c ctype then raise (Found_decoder d)) r.decoders; let filename = (last_indicator r).uri in let metadata = metadata r in let d = Decoder.get_file_decoder ~metadata ~ctype filename in Hashtbl.replace r.decoders ctype d; d with Found_decoder d -> d) let has_decoder ~ctype r = get_base_decoder ~ctype r <> None let get_decoder ~ctype r = match get_base_decoder ~ctype r with | None -> None | Some (_, d) -> ( let decoder = d () in let open Decoder in let initial_pos = match get_cue ~r r.cue_in_metadata with | Some cue_in -> r.logger#info "Cueing in to position: %.02f" cue_in; let cue_in = Frame.main_of_seconds cue_in in let seeked = decoder.fseek cue_in in if seeked <> cue_in then r.logger#important "Initial seek mismatch! Expected: %d, effective: %d" cue_in seeked; seeked | None -> 0 in match get_cue ~r r.cue_out_metadata with | None -> Some decoder | Some cue_out -> let cue_out = Frame.main_of_seconds cue_out in let pos = Atomic.make initial_pos in let fread len = if cue_out <= Atomic.get pos then decoder.fread 0 else ( let old_pos = Atomic.get pos in let len = min len (cue_out - old_pos) in let buf = decoder.fread len in let filled = Frame.position buf in let new_pos = old_pos + filled in Atomic.set pos new_pos; if cue_out <= new_pos then ( r.logger#info "Cueing out at position: %.02f" (Frame.seconds_of_main cue_out); Frame.slice buf (cue_out - old_pos)) else ( if Frame.is_partial buf then r.logger#important "End of track reached at %.02f before cue-out point at \ %.02f!" (Frame.seconds_of_main new_pos) (Frame.seconds_of_main cue_out); buf)) in let remaining () = match (decoder.remaining (), cue_out - Atomic.get pos) with | -1, r -> r | r, r' -> min r r' in Some { decoder with fread; remaining }) (** Plugins registration. *) type resolver = string -> log:(string -> unit) -> float -> indicator option type protocol = { resolve : resolver; static : string -> bool } let protocols_doc = "Methods to get a file. They are the first part of URIs: 'protocol:args'." let protocols = Plug.create ~doc:protocols_doc "protocols" let is_static s = if file_exists (home_unrelate s) then true else ( match parse_uri s with | Some (proto, uri) -> ( match Plug.get protocols proto with | Some handler -> handler.static uri | None -> false) | None -> false) (** Resolving engine. *) exception ExnTimeout let should_fail = Atomic.make false let () = Lifecycle.before_core_shutdown ~name:"Requests shutdown" (fun () -> Atomic.set should_fail true) let resolve_req t timeout = let timeout = Option.value ~default:conf_timeout#get timeout in log#debug "Resolving request %s." (string_of_indicators t); let since = Unix.gettimeofday () in Atomic.set t.status (`Resolving { since; pending = [] }); let maxtime = since +. timeout in let rec resolve i = if Atomic.get should_fail then raise No_indicator; let timeleft = maxtime -. Unix.gettimeofday () in if timeleft <= 0. then ( add_log t "Global timeout."; raise ExnTimeout); log#f 6 "Resolve step %s in %s." i.uri (string_of_indicators t); (* If the file is local, this loop should always terminate. *) if file_exists i.uri then ( if not (file_is_readable i.uri) then ( log#important "Read permission denied for %s!" (Lang_string.quote_string i.uri); add_log t "Read permission denied!"; raise No_indicator); if t.resolve_metadata then read_metadata t; raise Request_resolved); match parse_uri i.uri with | Some (proto, arg) -> ( match Plug.get protocols proto with | Some handler -> ( add_log t (Printf.sprintf "Resolving %s (timeout %.0fs)..." (Lang_string.quote_string i.uri) timeout); match handler.resolve ~log:(add_log t) arg maxtime with | None -> log#info "Failed to resolve %s! For more info, see server \ command `request.trace %d`." (Lang_string.quote_string i.uri) t.id; raise No_indicator | Some i -> push_indicator t i; resolve i) | None -> log#important "Unknown protocol %S in URI %s!" proto (Lang_string.quote_string i.uri); add_log t "Unknown protocol!"; raise No_indicator) | None -> let log_level = if i.uri = "" then 4 else 3 in log#f log_level "Nonexistent file or ill-formed URI %s!" (Lang_string.quote_string i.uri); add_log t "Nonexistent file or ill-formed URI!"; raise No_indicator in let result = try if Atomic.get should_fail then raise No_indicator; resolve (Queue.peek t.indicators) with | Request_resolved -> `Resolved | ExnTimeout -> `Timeout | No_indicator -> add_log t "Every possibility failed!"; `Failed in log#debug "Resolved to %s." (string_of_indicators t); let excess = Unix.gettimeofday () -. maxtime in if excess > 0. then log#severe "Time limit exceeded by %.2f secs (timeout: %.2f)!" excess timeout; let status = if result <> `Resolved then `Failed else `Ready in (match Atomic.exchange t.status status with | `Resolving { pending } -> List.iter (fun (c, m) -> Mutex_utils.mutexify m (fun () -> Condition.signal c) ()) pending | _ -> assert false); result let rec resolve ?timeout t = match Atomic.get t.status with | `Idle -> resolve_req t timeout | `Resolving ({ pending } as r) as status -> let m = Mutex.create () in let c = Condition.create () in Mutex_utils.mutexify m (fun () -> if Atomic.compare_and_set t.status status (`Resolving { r with pending = (c, m) :: pending }) then Condition.wait c m) (); resolve ?timeout t | `Ready -> `Resolved | `Destroyed | `Failed -> `Failed let log r = Queue.fold r.log (fun (date, msg) s -> s ^ if s = "" then Printf.sprintf "[%s] %s" (pretty_date date) msg else Printf.sprintf "\n[%s] %s" (pretty_date date) msg) "" let on_air ~source r = Queue.push r.on_air { source; timestamp = Unix.gettimeofday () } let done_playing ~source r = Queue.flush_iter r.on_air (fun v -> if v.source != source then Queue.push r.on_air v) module Value = Value.MkCustom (struct type content = t let name = "request" let to_json ~pos _ = Runtime_error.raise ~pos ~message:"Requests cannot be represented as json" "json" let to_string r = Printf.sprintf "" r.id let compare r r' = Stdlib.compare r.id r'.id end) liquidsoap-2.3.2/src/core/request.mli000066400000000000000000000163401477303350200176120ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (** A request is something from which we can produce a file. *) (** An indicator is a resource location (URI), when meaningful, it can be declared as temporary if liquidsoap should destroy it after usage (this means deleting a local file). *) type indicator (** Root configuration node. *) val conf : Dtools.Conf.ut (** Create an indicator. *) val indicator : ?metadata:Frame.metadata -> ?temporary:bool -> string -> indicator (** Return a prettified string. *) val pretty_date : Unix.tm -> string (** Type of requests, which are devices for obtaining a local file from an URI. *) type t (** Create a request. *) val create : ?resolve_metadata:bool -> ?excluded_metadata_resolvers:string list -> ?metadata:Frame.Metadata.t -> ?persistent:bool -> ?temporary:bool -> cue_in_metadata:string option -> cue_out_metadata:string option -> string -> t (** Return the request's initial uri. *) val initial_uri : t -> string (** Destroying of a requests causes its file to be deleted if it's a temporary one, for example a downloaded file. If the metadata ["persistent"] is set to ["true"], destroying doesn't happen, unless [force] is set too. Persistent sources are useful for static URIs (see below for the definition of staticity, and in [src/sources/one_file.ml] for an example of use). *) val destroy : ?force:bool -> t -> unit type resolving (** Status of a request. *) type status = [ `Idle | `Resolving of resolving | `Ready | `Destroyed | `Failed ] (** Current status of a request. *) val status : t -> status (** {1 General management} *) (** Called at exit, for cleaning temporary files and destroying all the requests, even persistent ones. *) val cleanup : unit -> unit (** Identifier of a request. *) val id : t -> int (** Get the list of all requests. *) val all : unit -> t list (** Retrieve a request from its id. *) val from_id : int -> t option (** {1 Resolving} Resolving consists in many steps. Every step consist in rewriting the first URI into other URIs. The process ends when the last URI is a local filename. For example, the initial URI can be a database query, which is then turned into a remote locations, which is then tentatively downloaded... At each step [protocol.resolve uri timeout] is called, and the function is expected to push the new URIs in the request. *) (** Something that resolves an URI. *) type resolver = string -> log:(string -> unit) -> float -> indicator option (** A protocol, which can resolve associated URIs. *) type protocol = { resolve : resolver; static : string -> bool } (** A static request [r] is such that every resolving leads to the same file. Sometimes, it allows removing useless destroy/create/resolve. *) val is_static : string -> bool (** Resolving can fail because an URI is invalid, or doesn't refer to a valid audio file, or simply because there was no enough time left. *) type resolve_flag = [ `Resolved | `Failed | `Timeout ] (** Metadata resolvers priorities. *) val conf_metadata_decoder_priorities : Dtools.Conf.ut (** Read the request's metadata. *) val read_metadata : t -> unit (** [resolve ?timeout request] tries to resolve the request within [timeout] seconds. Defaults to [settings.request.timeout] when [timeout] is not passed. *) val resolve : ?timeout:float -> t -> resolve_flag (** [resolved r] if there's an available local filename. It can be true even if the resolving hasn't been run, if the initial URI was already a local filename. *) val resolved : t -> bool (** Return a valid local filename if there is one, which means that the request is ready. *) val get_filename : t -> string option (** {1 Metadatas} *) (** Metadata are resolved from the first indicator to the last, the last one overriding the ones before. The only exception are root metadata, which are metadata internal to liquidsoap such as request id and etc. These cannot be overridden by resolvers. *) val metadata : t -> Frame.metadata (** {1 Logging} Every request has a separate log in which its history can be written. *) val log : t -> string (** {1 Media operations} These operations are only meaningful for media requests, and might raise exceptions otherwise. *) (** Duration resolvers. *) val conf_dresolvers : string list Dtools.Conf.t (** [duration ?resolvers ~metadata filename] computes the duration of audio data contained in [filename]. The computation may be expensive. Set [resolvers] to a list of specific decoders to use for getting duration. @raise Not_found if no duration computation method is found. *) val duration : ?resolvers:string list -> metadata:Frame.metadata -> string -> float option (** [true] is a decoder exists for the given content-type. *) val has_decoder : ctype:Frame.content_type -> t -> bool (** Return a decoder if the file has been resolved, guaranteed to have available data to deliver. *) val get_decoder : ctype:Frame.content_type -> t -> Decoder.file_decoder_ops option (** Mark the request as being on_air for the given source. *) val on_air : source:Source.source -> t -> unit (** Mark the request as being done playing by the given source. *) val done_playing : source:Source.source -> t -> unit (** {1 Plugs} *) type dresolver = { dpriority : unit -> int; file_extensions : unit -> string list; dresolver : metadata:Frame.metadata -> string -> float; } (** Functions for computing duration. *) val dresolvers : dresolver Plug.t (** Type for a metadata resolver. Resolvers are executed in priority order and the first returned metadata take precedence over any other one later returned. *) type metadata_resolver = { priority : unit -> int; resolver : metadata:Frame.metadata -> extension:string option -> mime:string -> string -> (string * string) list; } (** Functions for resolving metadata. Metadata filling isn't included in Decoder because we want it to occur immediately after request resolution. *) val mresolvers : metadata_resolver Plug.t (** Resolve metadata for a local file: *) val resolve_metadata : initial_metadata:Frame.metadata -> excluded:string list -> string -> Frame.metadata (** Functions for resolving URIs. *) val protocols : protocol Plug.t module Value : Value.Custom with type content := t liquidsoap-2.3.2/src/core/runtime_error.ml000066400000000000000000000000461477303350200206410ustar00rootroot00000000000000include Liquidsoap_lang.Runtime_error liquidsoap-2.3.2/src/core/shebang.ml000066400000000000000000000060641477303350200173620ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (* Equality of basenames, as a weak form of path equivalence. *) let ( === ) a b = Filename.basename a = Filename.basename b (** Re-parse the command line to handle #! calls. *) let argv = (* Full path to the executed binary. *) let binname = Sys.argv.(0) in (* Name of the script executed. *) match Sys.getenv_opt "_" with | Some scriptname -> if (* Normal invocation. *) binname === scriptname || (* Invocation from gdb/strace/valgrind... When liquidsoap is invoked through gdb, env[_] is "../gdb". For a real #! invocation, env[_] (the script name) should be found on the command-line, either at position 1 or 2. *) not ((Array.length Sys.argv > 1 && Sys.getenv "_" === Sys.argv.(1)) || (Array.length Sys.argv > 2 && Sys.getenv "_" === Sys.argv.(2))) then Sys.argv else ( (* Liquidsoap has been invoked using a #!. We have: Sys.argv = $0 "opt0 .. optN" script.liq argv3 .. argvN We build: argv = $0 opt0 .. optN script.liq -- argv3 .. argvN There may or may not be a list of options "opt0 .. optN" on the shebang line, in which case the second parameter will be the script name. Currently I don't implement a full parsing of opts (quotations and escapings are missing) but that should do for a long time. *) let opts, script, more = if Sys.argv.(1) === Sys.getenv "_" then ( [||], [| Sys.argv.(1) |], Array.sub Sys.argv 2 (Array.length Sys.argv - 2) ) else ( Array.of_list (Re.Pcre.split ~rex:(Re.Pcre.regexp "\\s+") Sys.argv.(1)), [| Sys.argv.(2) |], Array.sub Sys.argv 3 (Array.length Sys.argv - 3) ) in Array.concat [[| binname |]; opts; script; [| "--" |]; more]) | None -> (* In case ENV[_] is not defined, for compatibility. *) Sys.argv liquidsoap-2.3.2/src/core/source.ml000066400000000000000000000624041477303350200172530ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm exception Unavailable module Queue = Queues.Queue type streaming_state = [ `Pending | `Unavailable | `Ready of unit -> unit | `Done of Frame.t ] type active = < reset : unit ; output : unit > type source_type = [ `Passive | `Active of active | `Output of active ] type sync = [ `Auto | `CPU | `None ] module SourceSync = Clock.MkSyncSource (struct type t = < id : string > let to_string s = Printf.sprintf "source(id=%s)" s#id end) (** {1 Sources} *) (** Instrumentation. *) type metadata = (int * Frame.metadata) list type watcher = { wake_up : fallible:bool -> source_type:source_type -> id:string -> ctype:Frame.content_type -> clock_id:string -> unit; sleep : unit -> unit; generate_frame : start_time:float -> end_time:float -> length:int -> has_track_mark:bool -> metadata:metadata -> unit; before_streaming_cycle : unit -> unit; after_streaming_cycle : unit -> unit; } let source_log = Log.make ["source"] let finalise s = source_log#debug "Source %s is collected." s#id; try s#force_sleep with e -> let bt = Printexc.get_backtrace () in Utils.log_exception ~log:source_log ~bt (Printf.sprintf "Error when leaving output %s: %s!" s#id (Printexc.to_string e)) class virtual operator ?(stack = []) ?clock ~name sources = let frame_type = Type.var () in let clock = match clock with Some c -> c | None -> Clock.create ~stack () in object (self) (** Monitoring *) val mutable watchers = [] method add_watcher w = watchers <- w :: watchers method private iter_watchers fn = List.iter fn watchers method clock = clock initializer List.iter (fun s -> Clock.unify ~pos:self#pos self#clock s#clock) sources; Clock.attach self#clock (self :> Clock.source) val stack = Unifier.make stack method stack = Unifier.deref stack method set_stack p = Unifier.set stack p; Clock.set_stack clock p method stack_unifier = stack method pos = match Unifier.deref stack with [] -> None | p :: _ -> Some p (** Logging and identification *) val mutable log = source_log method private create_log = log <- Log.make [self#id] method log = log val mutable id = Lang_string.generate_id name method id = id method set_id ?(force = true) s = let s = Re.Pcre.substitute ~rex:(Re.Pcre.regexp "[ \t\n.]") ~subst:(fun _ -> "_") s in if force && s <> self#id then ( id <- Lang_string.generate_id s; (* Sometimes the ID is changed during initialization, in order to make it equal to the server name, which is only registered at initialization time in order to avoid bloating from unused sources. If the ID changes, and [log] has already been initialized, reset it. *) if log != source_log then self#create_log) val mutex = Mutex.create () method private mutexify : 'a 'b. ('a -> 'b) -> 'a -> 'b = Mutex_utils.mutexify mutex method virtual fallible : bool method source_type : source_type = `Passive val mutable registered_commands = Queue.create () method register_command ?usage ~descr name cmd = self#on_wake_up (fun () -> let ns = [self#id] in Server.add ~ns ?usage ~descr name cmd; Queue.push registered_commands (ns, name)) initializer self#on_sleep (fun () -> Queue.flush_iter registered_commands (fun (ns, name) -> Server.remove ~ns name)) method active = match self#source_type with | `Passive -> false | `Output _ | `Active _ -> true (** Children sources *) val mutable sources : operator list = sources method virtual self_sync : Clock.self_sync val mutable self_sync_source = None method private self_sync_source = match self_sync_source with | None -> let s = SourceSync.make (self :> < id : string >) in self_sync_source <- Some s; s | Some s -> s method source_sync self_sync = if self_sync then Some self#self_sync_source else None (* Type describing the contents of the frame: this should be a record whose fields (audio, video, etc.) indicate the kind of contents we have (e.g. {audio : pcm}). *) method frame_type = frame_type (* Content type should not be computed before the source has been asked to wake up. *) val mutable content_type_computation_allowed = false method content_type_computation_allowed = content_type_computation_allowed <- true val mutable ctype = None method has_content_type = ctype <> None (* Content type. *) method content_type = match ctype with | Some ctype -> ctype | None -> if not content_type_computation_allowed then failwith (Printf.sprintf "Early computation of source content-type detected for \ source %s!" self#id); self#log#debug "Assigning source content type for frame type: %s" (Type.to_string self#frame_type); let ct = Frame_type.content_type self#frame_type in self#log#debug "Content type: %s" (Frame.string_of_content_type ct); ctype <- Some ct; ct val mutable audio_channels = -1 method private audio_channels = match audio_channels with | -1 -> let c = match Frame.Fields.find_opt Frame.Fields.audio self#content_type with | Some c when Content.Audio.is_format c -> Content.Audio.channels_of_format c | Some c when Content_pcm_s16.is_format c -> Content_pcm_s16.channels_of_format c | Some c when Content_pcm_f32.is_format c -> Content_pcm_f32.channels_of_format c | _ -> raise Content.Invalid in audio_channels <- c; c | c -> c val mutable samplerate = -1. method private samplerate = match samplerate with | -1. -> let s = float_of_int (Lazy.force Frame.audio_rate) in samplerate <- s; s | s -> s val mutable video_dimensions = None method private video_dimensions = match video_dimensions with | None -> let dim = Content.Video.dimensions_of_format (Option.get (Frame.Fields.find_opt Frame.Fields.video self#content_type)) in video_dimensions <- Some dim; dim | Some dim -> dim val mutable on_wake_up = [] method on_wake_up fn = on_wake_up <- fn :: on_wake_up initializer self#on_wake_up (fun () -> List.iter (fun s -> s#wake_up) sources; self#iter_watchers (fun w -> w.wake_up ~fallible:self#fallible ~source_type:self#source_type ~id:self#id ~ctype:self#content_type ~clock_id:(Clock.id self#clock))) val is_up : [ `False | `True | `Error ] Atomic.t = Atomic.make `False method is_up = Atomic.get is_up = `True val streaming_state : streaming_state Atomic.t = Atomic.make `Pending method wake_up = if Atomic.compare_and_set is_up `False `True then ( try self#content_type_computation_allowed; if log == source_log then self#create_log; source_log#info "Source %s gets up with content type: %s and frame type: %s." self#id (Frame.string_of_content_type self#content_type) (Type.to_string self#frame_type); self#log#debug "Clock is %s." (Clock.id self#clock); self#log#important "Content type is %s." (Frame.string_of_content_type self#content_type); List.iter (fun fn -> fn ()) on_wake_up with exn -> Atomic.set is_up `Error; let bt = Printexc.get_raw_backtrace () in Utils.log_exception ~log ~bt:(Printexc.raw_backtrace_to_string bt) (Printf.sprintf "Error while starting source %s: %s!" self#id (Printexc.to_string exn)); Printexc.raise_with_backtrace exn bt) val mutable on_sleep = [] method on_sleep fn = on_sleep <- fn :: on_sleep method force_sleep = if Atomic.compare_and_set is_up `True `False then ( source_log#info "Source %s gets down." self#id; List.iter (fun fn -> fn ()) on_sleep) method sleep = match (Clock.started self#clock, Atomic.get streaming_state) with | true, (`Ready _ | `Unavailable) -> Clock.after_tick self#clock (fun () -> self#force_sleep) | _ -> self#force_sleep initializer Gc.finalise finalise self; self#on_sleep (fun () -> self#iter_watchers (fun w -> w.sleep ())) (** Streaming *) (* Number of maste ticks left in the current track: -1 means unknown, time unit is master tick. *) method virtual remaining : int val mutable elapsed = 0 method elapsed = elapsed method duration = let r = self#remaining in let e = self#elapsed in if r < 0 || e < 0 then -1 else e + r method virtual seek_source : source method seek n = let s = self#seek_source in if (s :> < seek : int -> int >) == (self :> < seek : int -> int >) then 0 else s#seek n method virtual private can_generate_frame : bool method virtual private generate_frame : Frame.t method is_ready = if self#is_up then ( self#before_streaming_cycle; match Atomic.get streaming_state with | `Ready _ | `Done _ -> true | _ -> false) else false val mutable _cache = None val mutable consumed = 0 val mutable on_before_streaming_cycle = [] method on_before_streaming_cycle fn = on_before_streaming_cycle <- fn :: on_before_streaming_cycle initializer self#on_before_streaming_cycle (fun () -> self#iter_watchers (fun w -> w.before_streaming_cycle ())) val mutable on_after_streaming_cycle = [] method on_after_streaming_cycle fn = on_after_streaming_cycle <- fn :: on_after_streaming_cycle initializer self#on_after_streaming_cycle (fun () -> self#iter_watchers (fun w -> w.after_streaming_cycle ())) (* This is the implementation of the main streaming logic. *) method private before_streaming_cycle = match Atomic.get streaming_state with | `Pending -> List.iter (fun fn -> fn ()) on_before_streaming_cycle; consumed <- 0; let cache = Option.value ~default:self#empty_frame _cache in let cache_pos = Frame.position cache in let size = Lazy.force Frame.size in let can_generate_frame = self#can_generate_frame in if cache_pos > 0 || can_generate_frame then Atomic.set streaming_state (`Ready (fun () -> let buf = if can_generate_frame && cache_pos < size then Frame.append cache self#instrumented_generate_frame else cache in let buf_pos = Frame.position buf in let buf = if size < buf_pos then ( _cache <- Some (Frame.after buf size); Frame.slice buf size) else ( _cache <- None; buf) in Atomic.set streaming_state (`Done buf))) else Atomic.set streaming_state `Unavailable; Clock.after_tick self#clock (fun () -> self#after_streaming_cycle) | _ -> () method private after_streaming_cycle = (match (Atomic.get streaming_state, consumed) with | `Done buf, n when n < Frame.position buf -> let cache = Option.value ~default:self#empty_frame _cache in _cache <- Some (Frame.append (Frame.after buf n) cache) | _ -> ()); List.iter (fun fn -> fn ()) on_after_streaming_cycle; Atomic.set streaming_state `Pending method peek_frame = match Atomic.get streaming_state with | `Pending | `Unavailable -> log#critical "source called while not ready!"; raise Unavailable | `Ready fn -> fn (); self#peek_frame | `Done data -> data method get_partial_frame cb = let data = cb self#peek_frame in consumed <- max consumed (Frame.position data); data method consumed n = consumed <- max consumed n method get_frame = self#get_partial_frame (fun f -> f) method get_mutable_content field = let content = Frame.get self#get_frame field in (* Optimization is disabled for now. *) Content.copy content method get_mutable_frame field = Frame.set self#get_frame field (self#get_mutable_content field) method set_frame_data : 'a. Frame.field -> (?offset:int -> ?length:int -> 'a -> Content.data) -> 'a -> Frame.t = fun field lift data -> Frame.set_data self#get_frame field lift data method private split_frame frame = match Frame.track_marks frame with | 0 :: _ -> (self#empty_frame, Some frame) | p :: _ -> (Frame.slice frame p, Some (Frame.after frame p)) | [] -> (frame, None) method frame_has_track_mark = Frame.has_track_marks self#get_frame method frame_track_mark = match Frame.track_marks self#get_frame with | pos :: _ -> Some pos | _ -> None method frame_metadata = Frame.get_all_metadata self#get_frame method frame_position = Frame.position self#get_frame method frame_audio_position = Frame.audio_of_main self#frame_position (* If possible, end the current track. Typically, that signal is just re-routed, or makes the next file to be played if there's anything like a file. *) method virtual abort_track : unit val mutable buffer = None method buffer = match buffer with | Some buffer -> buffer | None -> let buf = Generator.create ~log:self#log self#content_type in buffer <- Some buf; buf val mutable empty_frame = None method empty_frame = match empty_frame with | Some frame -> frame | None -> let f = Frame.create ~length:0 self#content_type in empty_frame <- Some f; f method end_of_track = Frame.add_track_mark self#empty_frame 0 val mutable last_metadata = None method last_metadata = last_metadata val mutable on_metadata : (Frame.metadata -> unit) List.t = [] method on_metadata fn = on_metadata <- fn :: on_metadata val mutable on_track_called = false initializer self#on_before_streaming_cycle (fun () -> on_track_called <- false) val mutable reset_last_metadata_on_track = Atomic.make true method reset_last_metadata_on_track = Atomic.get reset_last_metadata_on_track method set_reset_last_metadata_on_track = Atomic.set reset_last_metadata_on_track val mutable on_track : (Frame.metadata -> unit) List.t = [] method on_track fn = on_track <- fn :: on_track method private set_last_metadata buf = match List.rev (Frame.get_all_metadata buf) with | (_, m) :: _ -> last_metadata <- Some m | _ -> () method private execute_on_track buf = if not on_track_called then ( on_track_called <- true; if self#reset_last_metadata_on_track then last_metadata <- None; self#set_last_metadata buf; let m = Option.value ~default:Frame.Metadata.empty last_metadata in self#log#debug "calling on_track handlers.."; List.iter (fun fn -> fn m) on_track) val mutable last_images = Hashtbl.create 0 method last_image field = match Hashtbl.find_opt last_images field with | Some i -> i | None -> let width, height = self#video_dimensions in let i = Video.Canvas.Image.create width height in Hashtbl.replace last_images field i; i method private set_last_image ~field img = Hashtbl.replace last_images field img val mutable video_generators = Hashtbl.create 0 method private video_generator ~priv field = match Hashtbl.find_opt video_generators (priv, field) with | Some g -> g | None -> let params = Content.Video.get_params (Frame.Fields.find field self#content_type) in let g = Content.Video.make_generator params in Hashtbl.replace video_generators (priv, field) g; g method private internal_generate_video ?create ~priv ~field length = Content.Video.generate ?create (self#video_generator ~priv field) length method private generate_video = self#internal_generate_video ~priv:false method private nearest_image ~pos ~last_image buf = let nearest = List.fold_left (fun current (p, img) -> match current with | Some (p', _) when abs (p' - pos) < abs (p - pos) -> current | _ -> Some (p, img)) None buf.Content.Video.data in match nearest with Some (_, img) -> img | None -> last_image method private normalize_video ~field content = let buf = Content.Video.get_data content in let data = buf.Content.Video.data in let last_image = match List.rev data with | (_, img) :: _ -> self#set_last_image ~field img; img | [] -> self#last_image field in Content.Video.lift_data (self#internal_generate_video ~field ~priv:true ~create:(fun ~pos ~width:_ ~height:_ () -> self#nearest_image ~pos ~last_image buf) (Content.length content)) method private normalize_video_content = Frame.Fields.mapi (fun field content -> if Content.Video.is_data content && Frame.Fields.mem field self#content_type then self#normalize_video ~field content else content) method private instrumented_generate_frame = let start_time = Unix.gettimeofday () in let buf = self#normalize_video_content self#generate_frame in let end_time = Unix.gettimeofday () in let length = Frame.position buf in let track_marks = Frame.track_marks buf in let buf = match track_marks with | p :: _ :: _ -> self#log#important "Source created multiple tracks in a single frame! Sub-frame \ tracks are not supported and are merged into a single one.."; Frame.add_track_mark (Frame.drop_track_marks buf) p | _ -> buf in let has_track_mark = track_marks <> [] in if has_track_mark then elapsed <- 0 else elapsed <- elapsed + length; let metadata = Frame.get_all_metadata buf in let on_metadata = self#mutexify (fun () -> on_metadata) () in List.iter (fun (i, m) -> self#log#debug "generate_frame: got metadata at position %d: calling handlers..." i; List.iter (fun fn -> fn m) on_metadata) metadata; if has_track_mark then self#execute_on_track buf else self#set_last_metadata buf; self#iter_watchers (fun w -> w.generate_frame ~start_time ~end_time ~length ~has_track_mark ~metadata); buf end (** Entry-point sources, which need to actively perform some task. *) and virtual active_operator ?stack ?clock ~name sources = object (self) inherit operator ?stack ?clock ~name sources method! source_type : source_type = `Active (self :> active) (** Do whatever needed when the latency gets too big and is reset. *) method virtual reset : unit end (** Shortcuts for defining sources with no children *) and virtual source ?stack ?clock ~name () = object inherit operator ?stack ?clock ~name [] end class virtual active_source ?stack ?clock ~name () = object inherit active_operator ?stack ?clock ~name [] end (* Reselect type. This drives the choice of next source. In cases where the underlying source returns the same choice after calling [get_source], we need to refuse that source unless if can continue filling up the frame that is being worked on. This what [`After_position] captures. *) type reselect = [ `Ok | `Force | `After_position of int ] class virtual generate_from_multiple_sources ~merge ~track_sensitive () = object (self) method virtual get_source : reselect:reselect -> unit -> source option method virtual split_frame : Frame.t -> Frame.t * Frame.t option method virtual empty_frame : Frame.t method virtual private execute_on_track : Frame.t -> unit method virtual private set_last_metadata : Frame.t -> unit method virtual log : Log.t method virtual id : string val mutable ready_source = None method private can_generate_frame = match self#get_source ~reselect:(if track_sensitive () then `Ok else `Force) () with | Some s when s#is_ready -> ready_source <- Some s; true | _ -> ready_source <- None; false val mutable current_source = None method private begin_track buf = if merge () then Frame.drop_track_marks buf else ( self#execute_on_track buf; Frame.add_track_mark buf 0) method private can_reselect ~(reselect : reselect) (s : source) = s#is_ready && match reselect with | `Ok -> true | `Force -> false | `After_position p -> p < Frame.position s#get_frame method private continue_frame s = s#get_partial_frame (fun frame -> match self#split_frame frame with | buf, Some next_track when Frame.position buf = 0 -> ( match current_source with | Some s' when s == s' -> self#empty_frame | _ -> self#begin_track next_track) | buf, _ -> buf) method private generate_frame = let s = Option.get ready_source in assert s#is_ready; let buf = self#continue_frame s in let size = Lazy.force Frame.size in let rec f ~last_source ~last_chunk buf = let pos = Frame.position buf in let last_chunk_pos = Frame.position last_chunk in self#set_last_metadata last_chunk; if pos < size then ( let rem = size - pos in match self#get_source ~reselect:(`After_position last_chunk_pos) () with | Some s when last_source == s -> let remainder = s#get_partial_frame (fun frame -> if Frame.position frame <= last_chunk_pos then ( self#log#critical "Source %s was re-selected but did not produce \ enough data: %d if not s#is_ready then ( self#log#critical "Underlying source %s is not ready!" s#id; assert false); let new_track = s#get_partial_frame (fun frame -> match self#split_frame frame with | buf, _ when Frame.position buf = 0 -> Frame.slice frame rem | buf, _ -> Frame.slice buf rem) in f ~last_source:s ~last_chunk:new_track (Frame.append buf (self#begin_track new_track)) | _ -> (last_source, buf)) else (last_source, buf) in let last_source, buf = f ~last_source:s ~last_chunk:buf buf in current_source <- Some last_source; buf end liquidsoap-2.3.2/src/core/source.mli000066400000000000000000000344541477303350200174300ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (** In [`CPU] mode, synchronization is governed by the CPU clock. In [`None] mode, there is no synchronization control. Latency in is governed by the time it takes for the sources to produce and output data. In [`Auto] mode, synchronization is governed by the CPU unless at least one active source is declared [self_sync] in which case latency is delegated to this source. A typical example being a source linked to a sound card, in which case the source latency is governed by the sound card's clock. Another case is synchronous network protocol such as [input.srt]. *) type sync = [ `Auto | `CPU | `None ] (** A source can be: passive, active or output. Active sources and outputs are animated on each clock cycle. Output are kept around until they are manually shutdown. Active and passive sources can be garbage collected if they are not connected to any output. The [output] method is called on each clock cycle on active sources and outputs. The [reset] method is called when there is too much latency. *) type active = < reset : unit ; output : unit > type source_type = [ `Passive | `Active of active | `Output of active ] exception Unavailable type streaming_state = [ `Pending | `Unavailable | `Ready of unit -> unit | `Done of Frame.t ] (** Instrumentation. *) type metadata = (int * Frame.metadata) list type watcher = { wake_up : fallible:bool -> source_type:source_type -> id:string -> ctype:Frame.content_type -> clock_id:string -> unit; sleep : unit -> unit; generate_frame : start_time:float -> end_time:float -> length:int -> has_track_mark:bool -> metadata:metadata -> unit; before_streaming_cycle : unit -> unit; after_streaming_cycle : unit -> unit; } (** The [source] use is to send data frames through the [get] method. *) class virtual source : ?stack:Pos.t list -> ?clock:Clock.t -> name:string -> unit -> object method private mutexify : 'a 'b. ('a -> 'b) -> 'a -> 'b (** {1 Naming} *) (** Identifier of the source. *) method id : string method set_id : ?force:bool -> string -> unit (** Position in script *) method pos : Pos.Option.t method stack : Pos.t list method set_stack : Pos.t list -> unit method stack_unifier : Pos.t list Unifier.t (** {1 Source characteristics} *) (** If [false], [is_ready] should always return [true]. *) method virtual fallible : bool (** The source type. Defaults to [`Passive] *) method source_type : source_type (** [true] if the source needs to be animated on each clock tick. *) method active : bool (** {1 Init/shutdown} *) (** Register a callback, to be executed when source shuts down. *) method on_sleep : (unit -> unit) -> unit (** The clock under which the source will run. *) method clock : Clock.t (** Does the source provide its own synchronization? Examples: Alsa, AO, SRT I/O, etc.. This information is used at the clock level to decide whether or not we should synchronize with the CPU clock after producing a frame (for [`Auto] clocks). Please note that in the case of multiple sources filling the frame with different notion notion of synchronization, there is no consistent notion of time or synchronization. In this case (and with a [`Auto] clock), we simply decide based on whether there is one [self_sync] source or not. This logic should dictate how the method is implemented by the various operators. *) method virtual self_sync : Clock.self_sync method source_sync : bool -> Clock.sync_source option (** Register a callback when wake_up is called. *) method on_wake_up : (unit -> unit) -> unit (** Called when the source must be ready. Can be called multiple times *) method wake_up : unit (** Register a callback when sleep is called. *) method on_sleep : (unit -> unit) -> unit (** Called when the source can release all its resources. Can be called concurrently and multiple times. *) method sleep : unit (** Force the source to sleep. Should be called by the clocks only. *) method force_sleep : unit (** Check if a source is up or not. *) method is_up : bool (** {1 Streaming} *) method frame_type : Type.t (** This is called when content-type can be computed, i.e. either after frame type has been passed from the typing system during `check_eval` or at `wake_up` *) method content_type_computation_allowed : unit method has_content_type : bool (** What type of content does this source produce. *) method content_type : Frame.content_type (** This method fails when content is not PCM. *) method private audio_channels : int method private samplerate : float method private video_dimensions : int * int (** A buffer that can be used by the source. *) method buffer : Generator.t method private generate_video : ?create:(pos:int -> width:int -> height:int -> unit -> Video.Canvas.image) -> field:Frame.Fields.field -> int -> Content.Video.data method last_image : Frame.Fields.field -> Video.Canvas.image method private nearest_image : pos:int -> last_image:Video.Canvas.image -> Content.Video.data -> Video.Canvas.image (** An empty frame that can be used by the source. *) method empty_frame : Frame.t (** An empty frame with a track mark. *) method end_of_track : Frame.t (** Number of main ticks left in the current track. Defaults to -1=unknown. *) method virtual remaining : int (* Elapsed time since the last track mark. *) method elapsed : int (* Estimated total duration of the current track. -1=unknown. *) method duration : int (** Return the source effectively used to seek. Used by the muxer to determine if there is a unique seeking source. Should return [self] if there isn't such a unique source. *) method virtual seek_source : source (** [self#seek_ticks x] skips [x] main ticks. returns the number of ticks actually skipped. By default it always returns 0, refusing to seek at all. That method may be called from any thread, concurrently with [#get_frame], so they should not interfere. *) method seek : int -> int (** The source's last metadata. *) method last_metadata : Frame.metadata option method reset_last_metadata_on_track : bool method set_reset_last_metadata_on_track : bool -> unit (** Register a server command. The command is registered when the source wakes up under its own id as namespace and deregistered when it goes down. *) method register_command : ?usage:string -> descr:string -> string -> (string -> string) -> unit (** Register a callback to be called on new metadata *) method on_metadata : (Frame.metadata -> unit) -> unit (** Register a callback to be called on new track. Callback is called with the most recent metadata before a given track mark. *) method on_track : (Frame.metadata -> unit) -> unit (** These two are used by [generate_from_multiple_sources] and should not be used otherwise. *) method private execute_on_track : Frame.t -> unit method private set_last_metadata : Frame.t -> unit (** Sources must implement this method. It should return [true] when the source can produce data during the current streaming cycle. *) method virtual private can_generate_frame : bool method on_before_streaming_cycle : (unit -> unit) -> unit method on_after_streaming_cycle : (unit -> unit) -> unit (** Sources must implement this method. It should return the data produced during the current streaming cycle. Sources are responsible for producing as much data as possible, up-to the frame size setting. *) method virtual private generate_frame : Frame.t (** This method is based on [can_generate_frame] and has the same value through the whole streaming cycle. *) method is_ready : bool (** If the source is ready, this method computes the frame generated by the source during the current streaming cycle. Returned value is cached and should be the same throughout the whole streaming cycle. *) method get_frame : Frame.t (** This method passes the frame returned by [#get_frame] to the given callback. The callback should return the portion of the frame (of the form: [start, end)) that was effectively used. This method is used when a consumer of the source's data only uses an initial chunk of the frame. In this case, the remaining data is cached whenever possible and returned during the next streaming cycle. Final returned value is the same as the partial chunk returned from the callback for easy method call chaining. Calling this method is equivalent to doing: {[ let frame = Frame.slice source#peek_frame len in source#consumed (Frame.position frame); frame ]} *) method get_partial_frame : (Frame.t -> Frame.t) -> Frame.t (** Check a frame without consuming any of its data. *) method peek_frame : Frame.t (** Manually mark amount of consumed data from the source. *) method consumed : int -> unit (** This method requests a specific field of the frame that can be mutated. It is used by a consumer of the source that will modify the source's data (e.g. [amplify]). The source will do its best to minimize data copy according to the streaming context. Typically, if there is only one consumer of the source's data, it should be safe to pass its data without copying it. *) method get_mutable_content : Frame.field -> Content.data (** This method is the same as [#get_mutable_content] but returns a full frame with the requested mutable field included. *) method get_mutable_frame : Frame.field -> Frame.t (** By convention, frames produced during the streaming cycle can only have at most one track mark. In case of multiple track marks (which most likely indicate a programming problem), all subsequent track marks past the first one are dropped. This function returns a pair: [(initial_frame, new_track option)] of an initial frame and, if a track mark is present in the frame, the optional portion of the frame contained after this track mark. This method can be used to implement operations that should be aware of new tracks. *) method private split_frame : Frame.t -> Frame.t * Frame.t option (** This method is a convenience function to set some data. It returns the frame produced by the source during the current streaming cycle with the given field data replaced by the data passed to the function with length set as the frame's length. *) method set_frame_data : Frame.field -> (?offset:int -> ?length:int -> 'a -> Content.data) -> 'a -> Frame.t (** Various information related to the current frame. *) method frame_position : int method frame_audio_position : int method frame_has_track_mark : bool method frame_track_mark : int option method frame_metadata : (int * Frame.Metadata.t) list (** Tells the source to end its current track. *) method virtual abort_track : unit (** {1 Utilities} *) method log : Log.t method add_watcher : watcher -> unit end (* Entry-points sources, which need to actively perform some task. *) and virtual active_source : ?stack:Pos.t list -> ?clock:Clock.t -> name:string -> unit -> object inherit source (** Do whatever needed when the latency gets too big and is reset. *) method virtual reset : unit method virtual output : unit end (* This is for defining a source which has children *) class virtual operator : ?stack:Pos.t list -> ?clock:Clock.t -> name:string -> source list -> object inherit source end (* Most usual active source: the active_operator, pulling one source's data and outputting it. *) class virtual active_operator : ?stack:Pos.t list -> ?clock:Clock.t -> name:string -> source list -> object inherit active_source end (** Type governing whether or not the same source should be re-selected. - [`Force] means that the operator should at least try to see if a new source should be selected. - [`Ok] means that the operator should re-select the current source whenever possible. - [`After_position p] means that the operator should re-select the current source only if it can produce data past the given position The [generate_from_multiple_sources] implements a [can_reselect] that can be called with the currently selected source to validate this logic. *) type reselect = [ `Ok | `Force | `After_position of int ] (* Helper to generate data from a sequence of source. Data generation calls [get_source] on track marks. When frame is partial, a track mark is added unless [merge] is set to [true]. *) class virtual generate_from_multiple_sources : merge:(unit -> bool) -> track_sensitive:(unit -> bool) -> unit -> object method virtual get_source : reselect:reselect -> unit -> source option method virtual split_frame : Frame.t -> Frame.t * Frame.t option method virtual empty_frame : Frame.t method virtual private execute_on_track : Frame.t -> unit method virtual private set_last_metadata : Frame.t -> unit method virtual log : Log.t method virtual id : string method private can_reselect : reselect:reselect -> source -> bool method private can_generate_frame : bool method private generate_frame : Frame.t end liquidsoap-2.3.2/src/core/source_tracks.ml000066400000000000000000000044361477303350200206230ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) include Liquidsoap_lang.Lang_core.MkCustom (struct type content = Source.source let name = "source.tracks" let to_string s = Printf.sprintf "source.tracks(source=%s)" s#id let to_json ~pos _ = Runtime_error.raise ~pos ~message:"Source tracks cannot be represented as json" "json" let compare s1 s2 = Stdlib.compare s1#id s2#id end) let to_value ?pos s = match to_value ?pos s with | Liquidsoap_lang.Value.Custom p -> Liquidsoap_lang.Value.Custom { p with dynamic_methods = Some { hidden_methods = []; methods = (fun v -> Some (Track.to_value (Frame.Fields.register v, s))); }; } | _ -> assert false let source = of_value let fields = function | Liquidsoap_lang.Value.Custom { dynamic_methods = Some { hidden_methods } } as v when is_value v -> let source = of_value v in let fields = Frame.Fields.metadata :: Frame.Fields.track_marks :: List.map fst (Frame.Fields.bindings source#content_type) in List.filter (fun field -> not (List.mem (Frame.Fields.string_of_field field) hidden_methods)) fields | _ -> assert false liquidsoap-2.3.2/src/core/source_tracks.mli000066400000000000000000000022771477303350200207750ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) type content = Source.source val to_value : ?pos:Pos.t -> content -> Value.t val source : Value.t -> content val is_value : Value.t -> bool val fields : Value.t -> Frame.Fields.field list liquidsoap-2.3.2/src/core/sources/000077500000000000000000000000001477303350200170765ustar00rootroot00000000000000liquidsoap-2.3.2/src/core/sources/audio_gen.ml000066400000000000000000000072341477303350200213700ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm (** Generate a square *) open Source let get_params = function | format when Content.Audio.is_format format -> format | format when Content_pcm_s16.is_format format -> Content.Audio.lift_params (Content_pcm_s16.get_params format) | format when Content_pcm_f32.is_format format -> Content.Audio.lift_params (Content_pcm_f32.get_params format) | _ -> raise Content.Invalid let to_content ~format c = match format with | _ when Content.Audio.is_format format -> Content.Audio.lift_data c | _ when Content_pcm_s16.is_format format -> Content_pcm_s16.(lift_data (from_audio c)) | _ when Content_pcm_f32.is_format format -> Content_pcm_f32.(lift_data (from_audio c)) | _ -> raise Content.Invalid class gen ~seek name g freq duration ampl = let g = g (freq ()) in object (self) inherit Synthesized.source ~seek ~name duration method private synthesize length = let frame = Frame.create ~length Frame.Fields.empty in let format = Frame.Fields.find Frame.Fields.audio self#content_type in let content = Content.make ~length (get_params format) in let buf = Content.Audio.get_data content in g#set_frequency (freq ()); g#set_volume (ampl ()); g#fill buf 0 (Frame.audio_of_main length); Frame.Fields.add Frame.Fields.audio (to_content ~format buf) frame end let add name g = let return_t = Lang.frame_t Lang.unit_t (Frame.Fields.make ~audio:(Lang.pcm_audio_t ()) ()) in Lang.add_operator name ~category:`Input ~descr:("Generate a " ^ name ^ " wave.") ~return_t [ ( "duration", Lang.nullable_t Lang.float_t, Some Lang.null, Some "Duration in seconds (`null` means infinite)." ); ( "amplitude", Lang.getter_t Lang.float_t, Some (Lang.float 1.), Some "Maximal value of the waveform." ); ( "", Lang.getter_t Lang.float_t, Some (Lang.float 440.), Some ("Frequency of the " ^ name ^ ".") ); ] (fun p -> (new gen ~seek:true name g (Lang.to_float_getter (List.assoc "" p)) (Lang.to_valued_option Lang.to_float (List.assoc "duration" p)) (Lang.to_float_getter (List.assoc "amplitude" p)) :> source)) let sine f = new Audio.Generator.of_mono (new Audio.Mono.Generator.sine (Lazy.force Frame.audio_rate) f) let square f = new Audio.Generator.of_mono (new Audio.Mono.Generator.square (Lazy.force Frame.audio_rate) f) let saw f = new Audio.Generator.of_mono (new Audio.Mono.Generator.saw (Lazy.force Frame.audio_rate) f) let sine = add "sine" sine let () = ignore (add "square" square); ignore (add "saw" saw) liquidsoap-2.3.2/src/core/sources/bjack_in.ml000066400000000000000000000116451477303350200211770ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm let log = Log.make ["input"; "jack"] module SyncSource = Clock.MkSyncSource (struct type t = unit let to_string _ = "jack" end) let sync_source = SyncSource.make () class jack_in ~self_sync ~on_start ~on_stop ~fallible ~autostart ~server = let samples_per_frame = AFrame.size () in let samples_per_second = Lazy.force Frame.audio_rate in let bytes_per_sample = 2 in object (self) inherit Start_stop.active_source ~name:"input.jack" ~on_start ~on_stop ~fallible ~autostart () as active_source method seek_source = (self :> Source.source) method private can_generate_frame = active_source#started method abort_track = () method remaining = -1 val mutable sample_freq = samples_per_second val mutable device = None method self_sync = if self_sync then (`Dynamic, if device <> None then Some sync_source else None) else (`Static, None) method stop = match device with | Some d -> Bjack.close d; device <- None | None -> () initializer self#on_sleep (fun () -> self#stop) method start = ignore self#get_device method private get_device = match device with | None -> let server_name = match server with "" -> None | s -> Some s in let dev = try Bjack.open_t ~rate:samples_per_second ~bits_per_sample:(bytes_per_sample * 8) ~input_channels:self#audio_channels ~output_channels:0 ~flags:[] ?server_name ~ringbuffer_size:(samples_per_frame * bytes_per_sample) ~client_name:self#id () with Bjack.Open -> failwith "Could not open JACK device: is the server running?" in Bjack.set_all_volume dev 100; device <- Some dev; dev | Some d -> d val cache = Strings.Mutable.empty () method private read_data blen = let dev = self#get_device in while Strings.Mutable.length cache < blen do Strings.Mutable.add cache (Bjack.read dev blen) done method private generate_frame = let length = Lazy.force Frame.size in let alen = Frame.audio_of_main length in let blen = Audio.S16LE.size self#audio_channels alen in self#read_data blen; let pcm = Strings.Mutable.(to_string (sub cache 0 blen)) in Strings.Mutable.drop cache blen; let frame = Frame.create ~length self#content_type in let buf = Content.Audio.get_data (Frame.get frame Frame.Fields.audio) in Audio.S16LE.to_audio pcm 0 buf 0 alen; Frame.set_data frame Frame.Fields.audio Content.Audio.lift_data buf method! reset = () end let _ = let return_t = Lang.frame_t Lang.unit_t (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:Modules.input "jack" (Start_stop.active_source_proto ~fallible_opt:(`Yep false) @ [ ( "self_sync", Lang.bool_t, Some (Lang.bool true), Some "Mark the source as being synchronized by the jack server." ); ( "server", Lang.string_t, Some (Lang.string ""), Some "Jack server to connect to." ); ]) ~meth:(Start_stop.meth ()) ~return_t ~category:`Input ~descr:"Get stream from jack." (fun p -> let self_sync = Lang.to_bool (List.assoc "self_sync" p) in let fallible = Lang.to_bool (List.assoc "fallible" p) in let autostart = Lang.to_bool (List.assoc "start" p) in let on_start = let f = List.assoc "on_start" p in fun () -> ignore (Lang.apply f []) in let on_stop = let f = List.assoc "on_stop" p in fun () -> ignore (Lang.apply f []) in let server = Lang.to_string (List.assoc "server" p) in (new jack_in ~self_sync ~server ~fallible ~on_start ~on_stop ~autostart :> Start_stop.active_source)) liquidsoap-2.3.2/src/core/sources/blank.ml000066400000000000000000000114371477303350200205250ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Source class blank duration = object (self) inherit source ~name:"blank" () val position : [ `New_track | `Elapsed of int ] Atomic.t = Atomic.make `New_track (** Remaining time, -1 for infinity. *) method remaining = match (Atomic.get position, duration ()) with | `New_track, _ -> 0 | `Elapsed _, d when d < 0. -> -1 | `Elapsed e, d -> max 0 (Frame.main_of_seconds d - e) method fallible = false method private can_generate_frame = true method self_sync = (`Static, None) method! seek x = x method seek_source = (self :> Source.source) method abort_track = Atomic.set position `New_track val mutable frame = None method private make_frame = let length = Lazy.force Frame.size in let audio_len = Frame.audio_of_main length in Frame.Fields.fold (fun field format frame -> match format with | _ when Content.Audio.is_format format -> let data = Content.Audio.get_data (Content.make ~length format) in Audio.clear data 0 audio_len; Frame.set_data frame field Content.Audio.lift_data data | _ when Content_pcm_s16.is_format format -> let data = Content_pcm_s16.get_data (Content.make ~length format) in Content_pcm_s16.clear data 0 audio_len; Frame.set_data frame field Content_pcm_s16.lift_data data | _ when Content_pcm_f32.is_format format -> let data = Content_pcm_f32.get_data (Content.make ~length format) in Content_pcm_f32.clear data 0 audio_len; Frame.set_data frame field Content_pcm_f32.lift_data data | _ when Content.Video.is_format format -> let data = self#generate_video ~field ~create:(fun ~pos:_ ~width ~height () -> let img = Video.Canvas.Image.create width height in Video.Canvas.Image.iter Video.Image.blank img) length in Frame.set_data frame field Content.Video.lift_data data | _ when Content.Metadata.is_format format || Content.Track_marks.is_format format -> frame | _ -> failwith "Invalid content type!") self#content_type (Frame.create ~length Frame.Fields.empty) method private blank_frame = match frame with | Some f -> f | None -> let f = self#make_frame in frame <- Some f; f method generate_frame = let frame = self#blank_frame in let length = Lazy.force Frame.size in match (Atomic.get position, self#remaining) with | `New_track, _ -> Atomic.set position (`Elapsed length); Frame.add_track_mark frame 0 | `Elapsed d, -1 -> Atomic.set position (`Elapsed (d + length)); frame | `Elapsed d, r -> if r < length then ( Atomic.set position (`Elapsed (length - r)); Frame.add_track_mark frame r) else ( Atomic.set position (`Elapsed (d + length)); frame) end let blank = let return_t = Lang.internal_tracks_t () in Lang.add_operator "blank" ~category:`Input ~descr:"Produce silence and blank images." ~return_t [ ( "duration", Lang.getter_t Lang.float_t, Some (Lang.float (-1.)), Some "Duration of blank tracks in seconds, Negative value means forever." ); ] (fun p -> let d = Lang.to_float_getter (List.assoc "duration" p) in (new blank d :> source)) liquidsoap-2.3.2/src/core/sources/debug_sources.ml000066400000000000000000000060531477303350200222650ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) class fail name = object (self) inherit Source.source ~name () method seek_source = (self :> Source.source) method fallible = true method private can_generate_frame = false method self_sync = (`Static, None) method remaining = 0 method abort_track = () method generate_frame = assert false end let fail () = (new fail "fail" :> Source.source) let empty = fail let fail = let return_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator ~base:Muxer.source "fail" ~category:`Input ~descr: "A source that does not produce anything. No silence, no track at all." ~return_t [] (fun _ -> (new fail "source.fail" :> Source.source)) class fail_init = object (self) inherit fail "source.fail.init" initializer self#on_wake_up (fun () -> Lang.raise_error ~pos:[] ~message:"Source's initialization failed" "debug") end let _ = let return_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator ~base:fail "init" ~category:`Input ~descr: "A source that errors during its initialization phase, used for testing \ and debugging." ~flags:[`Experimental] ~return_t [] (fun _ -> new fail_init) class is_ready s = object (self) inherit Source.operator ~name:"is_ready" [s] method seek_source = (self :> Source.source) method fallible = true method private can_generate_frame = true method self_sync = (`Static, None) method remaining = 0 method abort_track = () method generate_frame = if s#is_ready then s#get_frame else self#empty_frame end let _ = let return_t = Lang.frame_t (Lang.univ_t ()) Frame.Fields.empty in Lang.add_operator ~base:Modules.debug "is_ready" ~category:`Input ~descr: "A source that always produces an empty frame when the underlying source \ is not ready, used for testing and debugging." ~flags:[`Experimental] ~return_t [("", Lang.source_t return_t, None, None)] (fun p -> let s = Lang.to_source (List.assoc "" p) in new is_ready s) liquidsoap-2.3.2/src/core/sources/external_input_audio.ml000066400000000000000000000132001477303350200236460ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Extralib open Mm (* {1 External Input handling} *) exception Finished of string * bool class external_input ~name ~restart ~bufferize ~restart_on_error ~max ~converter ?read_header command = let abg_max_len = Frame.audio_of_seconds max in let buflen = Utils.pagesize in let buf = Bytes.create buflen in let on_data ~buffer reader = let ret = reader buf 0 buflen in let data, offset, length = converter buf 0 ret in let buffered = Generator.length buffer in Generator.put buffer Frame.Fields.audio (Content.Audio.lift_data ~offset ~length data); if abg_max_len < buffered + length then `Delay (Frame.seconds_of_audio (buffered + length - (3 * abg_max_len / 4))) else `Continue in object inherit External_input.base ~name ?read_header ~restart ~restart_on_error ~on_data command inherit! Generated.source ~empty_on_abort:false ~bufferize () end let proto = [ ( "buffer", Lang.float_t, Some (Lang.float 2.), Some "Duration of the pre-buffered data." ); ( "max", Lang.float_t, Some (Lang.float 10.), Some "Maximum duration of the buffered data." ); ( "restart", Lang.bool_t, Some (Lang.bool true), Some "Restart process when exited." ); ( "restart_on_error", Lang.bool_t, Some (Lang.bool false), Some "Restart process when exited with error." ); ("", Lang.getter_t Lang.string_t, None, Some "Command to execute."); ] let _ = let return_t = Lang.frame_t Lang.unit_t (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:Modules.input_external "rawaudio" ~category:`Input ~descr: "Stream raw PCM data (interleaved signed 16 bits little endian integers) \ from an external application." (proto @ [ ("channels", Lang.int_t, Some (Lang.int 2), Some "Number of channels."); ("samplerate", Lang.int_t, Some (Lang.int 44100), Some "Samplerate."); ]) ~return_t (fun p -> let command = Lang.to_string_getter (List.assoc "" p) in let bufferize = Lang.to_float (List.assoc "buffer" p) in let channels_v = List.assoc "channels" p in let channels = Lang.to_int channels_v in let samplerate = Lang.to_int (List.assoc "samplerate" p) in let resampler = Decoder_utils.samplerate_converter () in let convert = Decoder_utils.from_iff ~format:`Wav ~channels ~samplesize:16 in let converter data offset length = let data = convert data offset length in resampler ~samplerate data 0 (Audio.length data) in let restart = Lang.to_bool (List.assoc "restart" p) in let restart_on_error = Lang.to_bool (List.assoc "restart_on_error" p) in let max = Lang.to_float (List.assoc "max" p) in let s = new external_input ~restart ~bufferize ~restart_on_error ~max ~name:"input.external.rawaudio" ~converter command in let frame_t = Lang.frame_t Lang.unit_t (Frame.Fields.make ~audio:(Format_type.audio_n channels) ()) in Typing.(s#frame_type <: frame_t); s) let _ = let return_t = Lang.frame_t Lang.unit_t (Frame.Fields.make ~audio:(Format_type.audio ()) ()) in Lang.add_operator ~base:Modules.input_external "wav" ~category:`Input ~descr:"Stream WAV data from an external application." proto ~return_t (fun p -> let command = Lang.to_string_getter (List.assoc "" p) in let bufferize = Lang.to_float (List.assoc "buffer" p) in let converter_ref = ref (fun _ _ _ -> assert false) in let converter data ofs len = !converter_ref data ofs len in let read_header read = let header = Wav_aiff.read_header Wav_aiff.callback_ops read in let channels = Wav_aiff.channels header in let samplerate = Wav_aiff.sample_rate header in let samplesize = Wav_aiff.sample_size header in Wav_aiff.close header; let resampler = Decoder_utils.samplerate_converter () in let convert = Decoder_utils.from_iff ~format:`Wav ~channels ~samplesize in (converter_ref := fun data ofs len -> let data = convert data ofs len in resampler ~samplerate data 0 (Audio.length data)); `Reschedule `Non_blocking in let restart = Lang.to_bool (List.assoc "restart" p) in let restart_on_error = Lang.to_bool (List.assoc "restart_on_error" p) in let max = Lang.to_float (List.assoc "max" p) in new external_input ~restart ~bufferize ~read_header ~restart_on_error ~max ~name:"input.external.wav" ~converter command) liquidsoap-2.3.2/src/core/sources/external_input_video.ml000066400000000000000000000230531477303350200236620ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) open Mm open Extralib (* Video-only input. Ideally this should be merged with previous one. *) exception Finished of string * bool class video ~name ~restart ~bufferize ~restart_on_error ~max ~on_data ?read_header command = let abg_max_len = Frame.main_of_seconds max in let on_data ~buffer reader = on_data ~buffer reader; let buffered = Generator.length buffer in if abg_max_len < buffered then `Delay (Frame.seconds_of_audio (buffered - (3 * abg_max_len / 4))) else `Continue in object inherit External_input.base ~name ?read_header ~restart ~restart_on_error ~on_data command inherit! Generated.source ~empty_on_abort:false ~bufferize () end (***** AVI *****) let log = Log.make ["input"; "external"; "video"] let _ = let return_t = Lang.frame_t Lang.unit_t (Frame.Fields.make ~audio:(Format_type.audio ()) ~video:(Format_type.video ()) ()) in Lang.add_operator ~base:Modules.input_external "avi" ~category:`Input ~flags:[`Experimental] ~descr:"Stream data from an external application." [ ( "buffer", Lang.float_t, Some (Lang.float 1.), Some "Duration of the pre-buffered data." ); ( "max", Lang.float_t, Some (Lang.float 10.), Some "Maximum duration of the buffered data." ); ( "restart", Lang.bool_t, Some (Lang.bool true), Some "Restart process when exited." ); ( "restart_on_error", Lang.bool_t, Some (Lang.bool false), Some "Restart process when exited with error." ); ("", Lang.getter_t Lang.string_t, None, Some "Command to execute."); ] ~return_t (fun p -> let command = Lang.to_string_getter (List.assoc "" p) in let video_format = ref None in let width = ref None in let height = ref None in let audio_converter = ref None in let video_converter = ref None in let video_scaler = Video_converter.scaler () in let read_header read = (* Reset the state. *) video_format := None; width := None; height := None; audio_converter := None; video_converter := None; let h, _ = Avi.Read.headers_simple read in let check = function | `Video (fmt, w, h, fps) -> (* if w <> width then failwith (Printf.sprintf "Wrong video width (%d instead of %d)." w width); *) (* if h <> height then failwith (Printf.sprintf "Wrong video height (%d instead of %d)." h height); *) log#info "Format: %s." (match fmt with `RGB24 -> "RGB24" | `I420 -> "I420"); video_format := Some fmt; width := Some w; height := Some h; if fps <> float (Lazy.force Frame.video_rate) then failwith (Printf.sprintf "Wrong video rate (%f instead of %d). Support for \ timestretching should be added some day in the future." fps (Lazy.force Frame.video_rate)); let converter data = let video_format = Option.get !video_format in let of_string s = match video_format with | `RGB24 -> Image.YUV420.of_RGB24_string s w | `I420 -> (* TODO: can there be stride in avi videos? *) let h = String.length s * 4 / 6 / w in Image.YUV420.of_YUV420_string s w h in let src = of_string data in let in_width = Video.Image.width src in let in_height = Video.Image.height src in let out_width = Lazy.force Frame.video_width in let out_height = Lazy.force Frame.video_height in if out_width = in_width && out_height = in_height && video_format = `I420 then Video.Canvas.Image.make src else ( let dst = Video.Image.create out_width out_height in video_scaler src dst; Video.Canvas.Image.make dst) in video_converter := Some converter | `Audio (channels, samplerate) -> if !audio_converter <> None then failwith "Only one audio track is supported for now."; let resampler = Decoder_utils.samplerate_converter () in let converter = Decoder_utils.from_iff ~format:`Wav ~channels ~samplesize:16 in audio_converter := Some (fun data ofs len -> let data = converter data ofs len in resampler ~samplerate data 0 (Audio.length data)) in List.iter check h; `Continue in let on_data ~buffer reader = match Avi.Read.chunk reader with | `Frame (_, _, data) when String.length data = 0 -> () | `Frame (`Video, _, data) -> let width = Option.get !width in let height = Option.get !height in let video_format = Option.get !video_format in if video_format = `RGB24 && String.length data <> width * height * 3 || video_format = `I420 && String.length data <> width * height * 6 / 4 then failwith (Printf.sprintf "Wrong video frame size (%d instead of %d)" (String.length data) (width * height * 3)); let data = (Option.get !video_converter) data in Generator.put buffer Frame.Fields.video (Content.Video.lift_image data) | `Frame (`Audio, _, data) -> let converter = Option.get !audio_converter in let data, ofs, len = converter (Bytes.unsafe_of_string data) 0 (String.length data) in let duration = Frame.main_of_audio len in let offset = Frame.main_of_audio ofs in Generator.put buffer Frame.Fields.audio (Content.Audio.lift_data ~offset ~length:duration data) | _ -> failwith "Invalid chunk." in let bufferize = Lang.to_float (List.assoc "buffer" p) in let restart = Lang.to_bool (List.assoc "restart" p) in let restart_on_error = Lang.to_bool (List.assoc "restart_on_error" p) in let max = Lang.to_float (List.assoc "max" p) in new video ~name:"input.external.avi" ~restart ~bufferize ~restart_on_error ~max ~read_header ~on_data command) (***** raw video *****) let _ = let return_t = Lang.frame_t Lang.unit_t (Frame.Fields.make ~video:(Format_type.video ()) ()) in Lang.add_operator ~base:Modules.input_external "rawvideo" ~category:`Input ~flags:[`Experimental] ~descr:"Stream data from an external application." [ ( "buffer", Lang.float_t, Some (Lang.float 1.), Some "Duration of the pre-buffered data." ); ( "max", Lang.float_t, Some (Lang.float 10.), Some "Maximum duration of the buffered data." ); ( "restart", Lang.bool_t, Some (Lang.bool true), Some "Restart process when exited." ); ( "restart_on_error", Lang.bool_t, Some (Lang.bool false), Some "Restart process when exited with error." ); ("", Lang.getter_t Lang.string_t, None, Some "Command to execute."); ] ~return_t (fun p -> let command = Lang.to_string_getter (List.assoc "" p) in let width = Lazy.force Frame.video_width in let height = Lazy.force Frame.video_height in let buflen = width * height * 3 in let buf = Bytes.create buflen in let on_data ~buffer reader = let ret = reader buf 0 buflen in let data = Image.YUV420.of_YUV420_string (Bytes.sub_string buf 0 ret) width height in (* Img.swap_rb data; *) (* Img.Effect.flip data; *) Generator.put buffer Frame.Fields.video (Content.Video.lift_image (Video.Canvas.Image.make data)) in let bufferize = Lang.to_float (List.assoc "buffer" p) in let restart = Lang.to_bool (List.assoc "restart" p) in let restart_on_error = Lang.to_bool (List.assoc "restart_on_error" p) in let max = Lang.to_float (List.assoc "max" p) in new video ~name:"input.external.rawvideo" ~restart ~bufferize ~restart_on_error ~max ~on_data command) liquidsoap-2.3.2/src/core/sources/generated.ml000066400000000000000000000105011477303350200213630ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) (* Reads data from an audio buffer generator. A thread safe generator should be used if it has to be fed concurrently. Store [bufferize] seconds before declaring itself as ready. *) class virtual source ?(seek = false) ?(replay_meta = false) ~bufferize ~empty_on_abort () = let bufferize = Frame.main_of_seconds bufferize in object (self : < Source.source ; .. > as 'a) val mutable buffering = true val mutable add_track_mark = false val mutable cur_meta : Frame.metadata option = None method virtual private log : Log.t method virtual private mutexify : 'a 'b. ('a -> 'b) -> 'a -> 'b method virtual buffer : Generator.t method self_sync : Clock.self_sync = (`Static, None) method seek len = if (not seek) || len <= 0 then 0 else self#mutexify (fun () -> let len = min len (Generator.remaining self#buffer) in Generator.truncate self#buffer len; len) () method seek_source = (self :> Source.source) method abort_track = add_track_mark <- true method private length = Generator.length self#buffer val mutable last_buffering_warning = -1 method private can_generate_frame = let r = self#length in if buffering then ( (* We have some data, but not enough for safely starting to play it. *) if bufferize > 0 && r <= bufferize && r <> last_buffering_warning then ( last_buffering_warning <- r; self#log#debug "Not ready: need more buffering (%i/%i)." r bufferize); r > bufferize) else r > 0 method remaining = if add_track_mark then 0 else if buffering && self#length <= bufferize then 0 else Generator.remaining self#buffer (* Returns true if metadata should be replayed. *) method private save_metadata frame = let new_meta = match List.fold_left (function | None -> fun (p, m) -> Some (p, m) | Some (curp, curm) -> fun (p, m) -> Some (if p >= curp then (p, m) else (curp, curm))) (match cur_meta with None -> None | Some m -> Some (-1, m)) (Frame.get_all_metadata frame) with | None -> None | Some (_, m) -> Some m in if cur_meta = new_meta then true else ( cur_meta <- new_meta; false) method private replay_metadata frame = match cur_meta with | None -> frame | Some m -> Frame.add_metadata frame 0 m method private generate_frame = self#mutexify (fun () -> let was_buffering = buffering in buffering <- false; if add_track_mark && empty_on_abort then Generator.clear self#buffer; let buf = Generator.slice self#buffer (Lazy.force Frame.size) in let buf = if was_buffering || add_track_mark then ( self#log#info "Adding track mark."; add_track_mark <- false; Frame.add_track_mark buf 0) else buf in if Generator.length self#buffer = 0 then ( self#log#info "Buffer emptied, buffering needed."; buffering <- true); if self#save_metadata buf && was_buffering && replay_meta then self#replay_metadata buf else buf) () end liquidsoap-2.3.2/src/core/sources/harbor_input.ml000066400000000000000000000434041477303350200221310ustar00rootroot00000000000000(***************************************************************************** Liquidsoap, a programmable stream generator. Copyright 2003-2024 Savonet team This program 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. This program 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, fully stated in the COPYING file at the root of the liquidsoap distribution. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *****************************************************************************) module Http = Liq_http let address_resolver s = let s = Harbor.file_descr_of_socket s in Utils.name_of_sockaddr ~rev_dns:Harbor_base.conf_revdns#get (Unix.getpeername s) let should_shutdown = Atomic.make false let () = Lifecycle.before_core_shutdown ~name:"input.harbor shutdown" (fun () -> Atomic.set should_shutdown true) class http_input_server ~pos ~transport ~dumpfile ~logfile ~bufferize ~max ~icy ~port ~meta_charset ~icy_charset ~replay_meta ~mountpoint ~on_connect ~on_disconnect ~login ~debug ~timeout () = let max_length = Some (Frame.main_of_seconds max) in object (self) inherit Source.active_source ~name:"input.harbor" () inherit! Generated.source ~empty_on_abort:false ~replay_meta ~bufferize () val mutable relay_socket = None (** Function to read on socket. *) val mutable relay_read = fun _ _ _ -> assert false (* Mutex used to protect socket's state (close) *) val relay_m = Mutex.create () val mutable create_decoder = fun _ -> assert false val mutable mime_type = None val mutable dump = None val mutable logf = None method connected_client = Mutex_utils.mutexify relay_m (fun () -> Option.map address_resolver relay_socket) () method status_cmd = match self#connected_client with | Some addr -> Printf.sprintf "source client connected from %s" addr | None -> "no source client connected" method private output = if self#is_ready then ignore self#get_frame method reset = self#disconnect ~lock:true method buffer_length_cmd = Frame.seconds_of_audio self#length method login : string * (socket:Harbor.socket -> string -> string -> bool) = login method fallible = true method icy_charset = icy_charset method meta_charset = meta_charset (* Insert metadata *) method insert_metadata m = (* Metadata may contain only the "song" value * or "artist" and "title". Here, we use "song" * as the "title" field if "title" is not provided. *) let m = if not (Frame.Metadata.mem "title" m) then ( try Frame.Metadata.add "title" (Frame.Metadata.find "song" m) m with _ -> m) else m in self#log#important "New metadata chunk %s -- %s." (try Frame.Metadata.find "artist" m with _ -> "?") (try Frame.Metadata.find "title" m with _ -> "?"); Generator.add_metadata self#buffer m method get_mime_type = mime_type method feed = self#log#important "Decoding..."; let t0 = Unix.gettimeofday () in let read buf ofs len = let input = (fun buf len -> let socket = Mutex_utils.mutexify relay_m (fun () -> relay_socket) () in match socket with | None -> 0 | Some socket -> ( try let rec f () = try let fd = Harbor.file_descr_of_socket socket in (* Wait for `Read event on socket. *) Tutils.wait_for ~log:(self#log#info "%s") (`Read fd) timeout; (* Now read. *) relay_read socket buf ofs len with Harbor.Retry -> f () in f () with e -> let bt = Printexc.get_backtrace () in Utils.log_exception ~log:self#log ~bt (Printf.sprintf "Error while reading from client: %s" (Printexc.to_string e)); (try self#disconnect ~lock:false with _ -> ()); 0)) buf len in begin match dump with | Some b -> output_string b (Bytes.sub_string buf 0 input); flush b | None -> () end; begin match logf with | Some b -> let time = (Unix.gettimeofday () -. t0) /. 60. in Printf.fprintf b "%f %d\n%!" time self#length | None -> () end; input in let input = { Decoder.read; tell = None; length = None; lseek = None } in try let decoder, buffer = create_decoder input in Fun.protect ~finally:decoder.Decoder.close (fun () -> while true do Mutex_utils.mutexify relay_m (fun () -> if relay_socket = None then failwith "relaying stopped") (); if Atomic.get should_shutdown then failwith "shutdown called"; decoder.Decoder.decode buffer done) with e -> (* Feeding has stopped: adding a break here. *) Generator.add_track_mark self#buffer; self#log#severe "Feeding stopped: %s." (Printexc.to_string e); self#disconnect ~lock:true; if debug then raise e val mutable is_registered = false initializer self#on_wake_up (fun () -> Generator.set_max_length self#buffer max_length; Harbor.add_source ~pos ~transport ~port ~mountpoint ~icy (self :> Harbor.source); is_registered <- true); self#on_sleep (fun () -> self#disconnect ~lock:true; if is_registered then Harbor.remove_source ~port ~mountpoint (); is_registered <- false) method register_decoder mime = let mime = try let sub = Re.Pcre.exec ~rex:(Re.Pcre.regexp "^([^;]+);.*$") mime in Re.Pcre.get_substring sub 1 with Not_found -> mime in match Decoder.get_stream_decoder ~ctype:self#content_type mime with | Some decoder -> let decoder args = let buffer = Decoder.mk_buffer ~ctype:self#content_type self#buffer in (decoder args, buffer) in create_decoder <- decoder; mime_type <- Some mime | None -> raise Harbor.Unknown_codec method relay stype (headers : (string * string) list) ?(read = Harbor.read) socket = Mutex_utils.mutexify relay_m (fun () -> if relay_socket <> None then raise Harbor.Mount_taken; self#register_decoder stype; relay_socket <- Some socket; relay_read <- read) (); on_connect headers; begin match dumpfile with | Some f -> ( try dump <- Some (open_out_bin (Lang_string.home_unrelate f)) with e -> self#log#severe "Could not open dump file: %s" (Printexc.to_string e)) | None -> () end; begin match logfile with | Some f -> ( try logf <- Some (open_out_bin (Lang_string.home_unrelate f)) with e -> self#log#severe "Could not open log file: %s" (Printexc.to_string e)) | None -> () end; ignore (Tutils.create (fun () -> self#feed) () "harbor source feeding") method private disconnect_no_lock = Option.iter (fun s -> try Harbor.close s with _ -> ()) relay_socket; relay_socket <- None method private disconnect_with_lock = Mutex_utils.mutexify relay_m (fun () -> self#disconnect_no_lock) () method private after_disconnect = begin match dump with | Some f -> close_out f; dump <- None | None -> () end; begin match logf with | Some f -> close_out f; logf <- None | None -> () end; on_disconnect () method disconnect ~lock : unit = if lock then self#disconnect_with_lock else self#disconnect_no_lock; self#after_disconnect end let _ = Lang.add_operator ~base:Modules.input "harbor" ~return_t:(Lang.univ_t ()) ~meth: [ ( "shutdown", ([], Lang.fun_t [] Lang.unit_t), "Shutdown the output or source.", fun s -> Lang.val_fun [] (fun _ -> Clock.detach s#clock (s :> Clock.source); s#sleep; Lang.unit) ); ( "stop", ([], Lang.fun_t [] Lang.unit_t), "Disconnect the client currently connected to the harbor. Does \ nothing if no client is connected.", fun s -> Lang.val_fun [] (fun _ -> s#disconnect ~lock:true; Lang.unit) ); ( "connected_client", ([], Lang.fun_t [] (Lang.nullable_t Lang.string_t)), "Returns the address of the client currently connected, if there is \ one.", fun s -> Lang.val_fun [] (fun _ -> match s#connected_client with | Some c -> Lang.string c | None -> Lang.null) ); ( "status", ([], Lang.fun_t [] Lang.string_t), "Current status of the input.", fun s -> Lang.val_fun [] (fun _ -> Lang.string s#status_cmd) ); ( "buffer_length", ([], Lang.fun_t [] Lang.float_t), "Length of the buffer (in seconds).", fun s -> Lang.val_fun [] (fun _ -> Lang.float s#buffer_length_cmd) ); ] ~category:`Input ~descr: "Create a source that receives a http/icecast stream and forwards it as \ a stream." [ ( "buffer", Lang.float_t, Some (Lang.float 12.), Some "Duration of the pre-buffered data (in seconds). Default value is \ set to make it possible to use `crossfade` transitions with \ `input.harbor`. You might be able to reduce it but, in this case, \ make sure to not use the operator with `crossfade` or make sure \ that it has enough buffered data for it." ); ( "max", Lang.float_t, Some (Lang.float 20.), Some "Maximum duration of the buffered data (in seconds)." ); ( "timeout", Lang.float_t, Some (Lang.float 30.), Some "Timeout for source connectionn." ); ( "on_connect", Lang.fun_t [(false, "", Lang.metadata_t)] Lang.unit_t, Some (Lang.val_cst_fun [("", None)] Lang.unit), Some "Function to execute when a source is connected. Its receives the \ list of headers, of the form: (