Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • aura/engine
  • hermannschwaerzler/engine
  • sumpfralle/aura-engine
3 results
Select Git revision
Show changes
Showing
with 3474 additions and 0 deletions
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2017-2020 - The Aura Engine Team.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import time
import logging
from contextlib import suppress
from threading import Thread
import meta
from modules.base.utils import TerminalColors, SimpleUtil as SU, EngineUtil
from modules.base.exceptions import LQConnectionError, InvalidChannelException, LQStreamException, LoadSourceException
from modules.core.channels import ChannelType, Channel, TransitionType, LiquidsoapResponse, EntryPlayState
from modules.core.startup import StartupThread
from modules.core.events import EngineEventDispatcher
from modules.core.liquidsoap.playerclient import LiquidSoapPlayerClient
# from modules.core.liquidsoap.recorderclient import LiquidSoapRecorderClient
class SoundSystem():
"""
The Soundsystem Mixer Control.
This class represents a virtual mixer as an abstraction layer to the actual audio hardware.
It uses LiquidSoapClient, but introduces more complex commands, transactions and error handling.
From one layer above it is used by `AuraScheduler` as if a virtual DJ is remote controling the mixer.
"""
client = None
logger = None
transaction = 0
channels = None
scheduler = None
event_dispatcher = None
is_liquidsoap_running = False
connection_attempts = 0
disable_logging = False
fade_in_active = False
fade_out_active = False
active_channel = None
plugins=None
def __init__(self, config):
"""
Initializes the sound-system by establishing a Socket connection
to Liquidsoap.
Args:
config (AuraConfig): The configuration
"""
self.config = config
self.logger = logging.getLogger("AuraEngine")
self.client = LiquidSoapPlayerClient(config, "engine.sock")
# self.lqcr = LiquidSoapRecorderClient(config, "record.sock")
self.is_active() # TODO Check if it makes sense to move it to the boot-phase
self.plugins = dict()
def start(self):
"""
Starts the engine. Called when the connection to the sound-system implementation
has been established.
"""
self.event_dispatcher = EngineEventDispatcher(self, self.scheduler)
# Sleep needed, because the socket is created too slowly by Liquidsoap
time.sleep(1)
self.mixer_initialize()
self.is_liquidsoap_running = True
self.event_dispatcher.on_initialized()
self.logger.info(SU.green("Engine Core ------[ connected ]-------- Liquidsoap"))
self.event_dispatcher.on_boot()
self.logger.info(EngineUtil.engine_info("Engine Core", meta.__version__))
self.event_dispatcher.on_ready()
#
# MIXER : GENERAL
#
def mixer_initialize(self):
"""
- Pull all faders down to volume 0.
- Initialize default channels per type
"""
self.enable_transaction()
time.sleep(1) # TODO Check is this is still required
channels = self.mixer_channels_reload()
for channel in channels:
self.channel_volume(channel, "0")
self.disable_transaction()
self.active_channel = {
ChannelType.FILESYSTEM: Channel.FILESYSTEM_A,
ChannelType.HTTP: Channel.HTTP_A,
ChannelType.HTTPS: Channel.HTTPS_A,
ChannelType.LIVE: Channel.LIVE_0
}
def mixer_status(self):
"""
Returns the state of all mixer channels
"""
cnt = 0
inputstate = {}
self.enable_transaction()
inputs = self.mixer_channels()
for channel in inputs:
inputstate[channel] = self.channel_status(cnt)
cnt = cnt + 1
self.disable_transaction()
return inputstate
def mixer_channels(self):
"""
Retrieves all mixer channels
"""
if self.channels is None or len(self.channels) == 0:
self.channels = self.__send_lqc_command__(self.client, "mixer", "inputs")
return self.channels
def mixer_channels_selected(self):
"""
Retrieves all selected channels of the mixer.
"""
cnt = 0
activeinputs = []
self.enable_transaction()
inputs = self.mixer_channels()
for channel in inputs:
status = self.channel_status(cnt)
if "selected=true" in status:
activeinputs.append(channel)
cnt = cnt + 1
self.disable_transaction()
return activeinputs
def mixer_channels_except(self, input_type):
"""
Retrieves all mixer channels except the ones of the given type.
"""
try:
activemixer_copy = self.mixer_channels().copy()
activemixer_copy.remove(input_type)
except ValueError as e:
self.logger.error("Requested channel (%s) not in channel-list. Reason: %s" % (input_type, str(e)))
except AttributeError:
self.logger.critical("Empty channel list")
return activemixer_copy
def mixer_channels_reload(self):
"""
Reloads all mixer channels.
"""
self.channels = None
return self.mixer_channels()
#
# MIXER : CONTROL SECTION
#
def preroll(self, entry):
"""
Pre-Rolls/Pre-Loads the entry. This is required before the actual `play(..)` can happen.
Be aware when using this method to queue a very short entry (shorter than ``) this may
result in sitations with incorrect timing. In this case bundle multiple short entries as
one queue using `preroll_playlist(self, entries)`.
It's important to note, that his method is blocking until loading has finished. If this
method is called asynchronously, the progress on the preloading state can be looked up in
`entry.state`.
Args:
entries ([Entry]): An array holding filesystem entries
"""
entry.status = EntryPlayState.LOADING
self.logger.info("Loading entry '%s'" % entry)
is_ready = False
# LIVE
if entry.get_type() == ChannelType.LIVE:
entry.channel = "linein_" + entry.source.split("line://")[1]
is_ready = True
else:
# Choose and save the input channel
entry.previous_channel, entry.channel = self.channel_swap(entry.get_type())
# PLAYLIST
if entry.get_type() == ChannelType.FILESYSTEM:
is_ready = self.playlist_push(entry.channel, entry.source)
# STREAM
elif entry.get_type() == ChannelType.HTTP or entry.get_type() == ChannelType.HTTPS:
is_ready = self.stream_load_entry(entry)
if is_ready:
entry.status = EntryPlayState.READY
self.event_dispatcher.on_queue([entry])
def preroll_group(self, entries):
"""
Pre-Rolls/Pre-Loads multiple filesystem entries at once. This call is required before the
actual `play(..)` can happen. Due to their nature, non-filesystem entries cannot be queued
using this method. In this case use `preroll(self, entry)` instead. This method also allows
queuing of very short files, such as jingles.
It's important to note, that his method is blocking until loading has finished. If this
method is called asynchronously, the progress on the preloading state can be looked up in
`entry.state`.
Args:
entries ([Entry]): An array holding filesystem entries
"""
channel = None
# Validate entry type
for entry in entries:
if entry.get_type() != ChannelType.FILESYSTEM:
raise InvalidChannelException
# Determine channel
channel = self.channel_swap(entries[0].get_type())
# Queue entries
for entry in entries:
entry.status = EntryPlayState.LOADING
self.logger.info("Loading entry '%s'" % entry)
# Choose and save the input channel
entry.previous_channel, entry.channel = channel
if self.playlist_push(entry.channel, entry.source) == True:
entry.status = EntryPlayState.READY
self.event_dispatcher.on_queue(entries)
def play(self, entry, transition):
"""
Plays a new `Entry`. In case of a new schedule (or some intented, immediate transition),
a clean channel is selected and transitions between old and new channel is performed.
This method expects that the entry is pre-loaded using `preroll(..)` or `preroll_group(self, entries)`
before being played. In case the pre-roll has happened for a group of entries, only the
first entry of the group needs to be passed.
Args:
entry (PlaylistEntry): The audio source to be played
transition (TransitionType): The type of transition to use e.g. fade-in or instant volume level.
queue (Boolean): If `True` the entry is queued if the `ChannelType` does allow so;
otherwise a new channel of the same type is activated
"""
with suppress(LQConnectionError):
# Instant activation or fade-in
self.enable_transaction()
if transition == TransitionType.FADE:
self.channel_select(entry.channel.value, True)
self.fade_in(entry)
else:
self.channel_activate(entry.channel.value, True)
self.disable_transaction()
# Update active channel and type
self.active_channel[entry.get_type()] = entry.channel
# Dear filesystem channels, please leave the room as you would like to find it!
if entry.previous_channel and entry.previous_channel in ChannelType.FILESYSTEM.channels:
def clean_up():
# Wait a little, if there is some long fade-out. Note, this also means,
# this channel should not be used for at least some seconds (including clearing time).
time.sleep(2)
self.enable_transaction()
self.channel_activate(entry.previous_channel.value, False)
res = self.playlist_clear(entry.previous_channel)
self.logger.info("Clear Queue Response: " + res)
self.disable_transaction()
Thread(target=clean_up).start()
# Filesystem meta-changes trigger the event via Liquidsoap
if not entry.channel in ChannelType.FILESYSTEM.channels:
self.on_play(entry)
def on_play(self, source):
"""
Event Handler which is called by the soundsystem implementation (i.e. Liquidsoap)
when some entry is actually playing.
Args:
source (String): The `Entry` or URI or of the media source currently being played
"""
self.event_dispatcher.on_play(source)
def stop(self, entry, transition):
"""
Stops the currently playing entry.
Args:
entry (Entry): The entry to stop playing
transition (TransitionType): The type of transition to use e.g. fade-out.
"""
with suppress(LQConnectionError):
self.enable_transaction()
if not entry.channel:
self.logger.warn(SU.red("Trying to stop entry %s, but it has no channel assigned" % entry))
return
if transition == TransitionType.FADE:
self.fade_out(entry)
else:
self.channel_volume(entry.channel, 0)
self.logger.info(SU.pink("Stopped channel '%s' for entry %s" % (entry.channel, entry)))
self.disable_transaction()
self.event_dispatcher.on_stop(entry)
#
# MIXER : CHANNEL
#
def channel_swap(self, channel_type):
"""
Returns the currently in-active channel for a given type. For example if the currently some
file on channel FILESYSTEM_A is playing, the channel FILESYSTEM B is returned for being used
to queue new entries.
Args:
channel_type (ChannelType): The channel type such es filesystem, stream or live channel
"""
previous_channel = self.active_channel[channel_type]
new_channel = None
msg = None
if channel_type == ChannelType.FILESYSTEM:
if previous_channel == Channel.FILESYSTEM_A:
new_channel = Channel.FILESYSTEM_B
msg = "Swapped filesystem channel from A > B"
else:
new_channel = Channel.FILESYSTEM_A
msg = "Swapped filesystem channel from B > A"
elif channel_type == ChannelType.HTTP:
if previous_channel == Channel.HTTP_A:
new_channel = Channel.HTTP_B
msg = "Swapped HTTP Stream channel from A > B"
else:
new_channel = Channel.HTTP_A
msg = "Swapped HTTP Stream channel from B > A"
elif channel_type == ChannelType.HTTPS:
if previous_channel == Channel.HTTPS_A:
new_channel = Channel.HTTPS_B
msg = "Swapped HTTPS Stream channel from A > B"
else:
new_channel = Channel.HTTPS_A
msg = "Swapped HTTPS Stream channel from B > A"
if msg: self.logger.info(SU.pink(msg))
return (previous_channel, new_channel)
def channel_status(self, channel_number):
"""
Retrieves the status of a channel identified by the channel number.
"""
return self.__send_lqc_command__(self.client, "mixer", "status", channel_number)
def channel_select(self, channel, select):
"""
Selects/deselects some mixer channel
Args:
pos (Integer): The channel number
select (Boolean): Select or deselect
Returns:
(String): Liquidsoap server response
"""
channels = self.mixer_channels()
try:
index = channels.index(channel)
if len(channel) < 1:
self.logger.critical("Cannot select channel. There are no channels!")
else:
message = self.__send_lqc_command__(self.client, "mixer", "select", index, select)
return message
except Exception as e:
self.logger.critical("Ran into exception when selecting channel. Reason: " + str(e))
def channel_activate(self, channel, activate):
"""
Combined call of following to save execution time:
- Select some mixer channel
- Increase the volume to 100,
Args:
pos (Integer): The channel number
activate (Boolean): Activate or deactivate
Returns:
(String): Liquidsoap server response
"""
channels = self.mixer_channels()
try:
index = channels.index(channel)
if len(channel) < 1:
self.logger.critical("Cannot activate channel. There are no channels!")
else:
message = self.__send_lqc_command__(self.client, "mixer", "activate", index, activate)
return message
except Exception as e:
self.logger.critical("Ran into exception when activating channel. Reason: " + str(e))
def channel_volume(self, channel, volume):
"""
Set volume of a channel
Args:
channel (Channel): The channel
volume (Integer) Volume between 0 and 100
"""
channel = str(channel)
try:
if str(volume) == "100":
channels = self.mixer_channels()
index = channels.index(channel)
else:
channels = self.mixer_channels()
index = channels.index(channel)
except ValueError as e:
msg = SU.red("Cannot set volume of channel " + channel + " to " + str(volume) + "!. Reason: " + str(e))
self.logger.error(msg)
self.logger.info("Available channels: %s" % str(channels))
return
try:
if len(channel) < 1:
msg = SU.red("Cannot set volume of channel " + channel + " to " + str(volume) + "! There are no channels.")
self.logger.warning(msg)
else:
message = self.__send_lqc_command__(self.client, "mixer", "volume", str(index), str(int(volume)))
if not self.disable_logging:
if message.find('volume=' + str(volume) + '%'):
self.logger.info(SU.pink("Set volume of channel '%s' to %s" % (channel, str(volume))))
else:
msg = SU.red("Setting volume of channel " + channel + " gone wrong! Liquidsoap message: " + message)
self.logger.warning(msg)
return message
except AttributeError as e: #(LQConnectionError, AttributeError):
self.disable_transaction(force=True)
msg = SU.red("Ran into exception when setting volume of channel " + channel + ". Reason: " + str(e))
self.logger.error(msg)
#
# Channel Type - Stream
#
def stream_load_entry(self, entry):
"""
Loads the given stream entry and updates the entries's status codes.
Args:
entry (Entry): The entry to be pre-loaded
Returns:
(Boolean): `True` if successfull
"""
self.stream_load(entry.channel, entry.source)
time.sleep(1)
retry_delay = self.config.get("input_stream_retry_delay")
max_retries = self.config.get("input_stream_max_retries")
retries = 0
while not self.stream_is_ready(entry.channel, entry.source):
self.logger.info("Loading Stream ...")
if retries >= max_retries:
raise LoadSourceException("Could not connect to stream while waiting for %s seconds!" % retries*retry_delay)
time.sleep(retry_delay)
retries += 1
return True
def stream_load(self, channel, url):
"""
Preloads the stream URL on the given channel. Note this method is blocking
some serious amount of time; hence it's worth being called asynchroneously.
Args:
channel (Channel): The stream channel
uri (String): The stream URL
Returns:
(Boolean): `True` if successful
"""
result = None
self.enable_transaction()
result = self.__send_lqc_command__(self.client, channel, "stream_stop")
if result != LiquidsoapResponse.SUCCESS.value:
self.logger.error("%s.stop result: %s" % (channel, result))
raise LQStreamException("Error while stopping stream!")
result = self.__send_lqc_command__(self.client, channel, "stream_set_url", url)
if result != LiquidsoapResponse.SUCCESS.value:
self.logger.error("%s.set_url result: %s" % (channel, result))
raise LQStreamException("Error while setting stream URL!")
# Liquidsoap ignores commands sent without a certain timeout
time.sleep(2)
result = self.__send_lqc_command__(self.client, channel, "stream_start")
self.logger.info("%s.start result: %s" % (channel, result))
self.disable_transaction()
return result
def stream_is_ready(self, channel, url):
"""
Checks if the stream on the given channel is ready to play. Note this method is blocking
some serious amount of time even when successfull; hence it's worth being called asynchroneously.
Args:
channel (Channel): The stream channel
uri (String): The stream URL
Returns:
(Boolean): `True` if successful
"""
result = None
self.enable_transaction()
result = self.__send_lqc_command__(self.client, channel, "stream_status")
self.logger.info("%s.status result: %s" % (channel, result))
if not result.startswith(LiquidsoapResponse.STREAM_STATUS_CONNECTED.value):
return False
lqs_url = result.split(" ")[1]
if not url == lqs_url:
self.logger.error("Wrong URL '%s' set for channel '%s', expected: '%s'." % (lqs_url, channel, url))
return False
self.disable_transaction()
stream_buffer = self.config.get("input_stream_buffer")
self.logger.info("Ready to play stream, but wait %s seconds until the buffer is filled..." % str(stream_buffer))
time.sleep(round(float(stream_buffer)))
return True
#
# Channel Type - Filesystem
#
def playlist_push(self, channel, uri):
"""
Adds an filesystem URI to the given `ChannelType.FILESYSTEM` channel.
Args:
channel (Channel): The channel to push the file to
uri (String): The URI of the file
Returns:
(Boolean): `True` if successful
"""
if channel not in ChannelType.FILESYSTEM.channels:
raise InvalidChannelException
self.logger.info(SU.pink("playlist.push('%s', '%s'" % (channel, uri)))
self.enable_transaction()
audio_store = self.config.get("audiofolder")
filepath = EngineUtil.uri_to_filepath(audio_store, uri)
result = self.__send_lqc_command__(self.client, channel, "playlist_push", filepath)
self.logger.info("%s.playlist_push result: %s" % (channel, result))
self.disable_transaction()
# If successful, Liquidsoap returns a resource ID of the queued track
return int(result) >= 0
def playlist_seek(self, channel, seconds_to_seek):
"""
Forwards the player of the given `ChannelType.FILESYSTEM` channel by (n) seconds.
Args:
channel (Channel): The channel to push the file to
seconds_to_seeks (Float): The seconds to skip
Returns:
(String): Liquidsoap response
"""
if channel not in ChannelType.FILESYSTEM.channels:
raise InvalidChannelException
self.enable_transaction()
result = self.__send_lqc_command__(self.client, channel, "playlist_seek", str(seconds_to_seek))
self.logger.info("%s.playlist_seek result: %s" % (channel, result))
self.disable_transaction()
return result
def playlist_clear(self, channel):
"""
Removes all tracks currently queued in the given `ChannelType.FILESYSTEM` channel.
Args:
channel (Channel): The channel to push the file to
Returns:
(String): Liquidsoap response
"""
if channel not in ChannelType.FILESYSTEM.channels:
raise InvalidChannelException
self.logger.info(SU.pink("Clearing filesystem queue '%s'!" % channel))
self.enable_transaction()
result = self.__send_lqc_command__(self.client, channel, "playlist_clear")
self.logger.info("%s.playlist_clear result: %s" % (channel, result))
self.disable_transaction()
return result
#
# Fading
#
def fade_in(self, entry):
"""
Performs a fade-in for the given `entry` to the `entry.volume` loudness
at channel `entry.channel`.
Args:
entry (Entry): The entry to fade
Returns:
(Boolean): `True` if successful
"""
try:
fade_in_time = float(self.config.get("fade_in_time"))
if fade_in_time > 0:
self.fade_in_active = True
target_volume = entry.volume
step = fade_in_time / target_volume
msg = "Starting to fading-in '%s'. Step is %ss and target volume is %s." % \
(entry.channel, str(step), str(target_volume))
self.logger.info(SU.pink(msg))
# Enable logging, which might have been disabled in a previous fade-out
self.disable_logging = True
self.client.disable_logging = True
for i in range(target_volume):
self.channel_volume(entry.channel.value, i + 1)
time.sleep(step)
msg = "Finished with fading-in '%s'." % entry.channel
self.logger.info(SU.pink(msg))
self.fade_in_active = False
if not self.fade_out_active:
self.disable_logging = False
self.client.disable_logging = False
except LQConnectionError as e:
self.logger.critical(str(e))
return True
def fade_out(self, entry):
"""
Performs a fade-out for the given `entry` at channel `entry.channel`.
Args:
entry (Entry): The entry to fade
Returns:
(Boolean): `True` if successful
"""
try:
fade_out_time = float(self.config.get("fade_out_time"))
if fade_out_time > 0:
step = abs(fade_out_time) / entry.volume
msg = "Starting to fading-out '%s'. Step is %ss." % (entry.channel, str(step))
self.logger.info(SU.pink(msg))
# Disable logging... it is going to be enabled again after fadein and -out is finished
self.disable_logging = True
self.client.disable_logging = True
for i in range(entry.volume):
self.channel_volume(entry.channel.value, entry.volume-i-1)
time.sleep(step)
msg = "Finished with fading-out '%s'" % entry.channel
self.logger.info(SU.pink(msg))
# Enable logging again
self.fade_out_active = False
if not self.fade_in_active:
self.disable_logging = False
self.client.disable_logging = False
except LQConnectionError as e:
self.logger.critical(str(e))
return True
#
# Recording
#
# # ------------------------------------------------------------------------------------------ #
# def recorder_stop(self):
# self.enable_transaction()
# for i in range(5):
# if self.config.get("rec_" + str(i)) == "y":
# self.__send_lqc_command__(self.client, "recorder_" + str(i), "stop")
# self.disable_transaction()
# # ------------------------------------------------------------------------------------------ #
# def recorder_start(self, num=-1):
# if not self.is_liquidsoap_running:
# if num==-1:
# msg = "Want to start recorder, but LiquidSoap is not running"
# else:
# msg = "Want to start recorder " + str(num) + ", but LiquidSoap is not running"
# self.logger.warning(msg)
# return False
# self.enable_transaction()
# if num == -1:
# self.recorder_start_all()
# else:
# self.recorder_start_one(num)
# self.disable_transaction()
# # ------------------------------------------------------------------------------------------ #
# def recorder_start_all(self):
# if not self.is_liquidsoap_running:
# self.logger.warning("Want to start all recorder, but LiquidSoap is not running")
# return False
# self.enable_transaction()
# for i in range(5):
# self.recorder_start_one(i)
# self.disable_transaction()
# # ------------------------------------------------------------------------------------------ #
# def recorder_start_one(self, num):
# if not self.is_liquidsoap_running:
# return False
# if self.config.get("rec_" + str(num)) == "y":
# returnvalue = self.__send_lqc_command__(self.client, "recorder", str(num), "status")
# if returnvalue == "off":
# self.__send_lqc_command__(self.client, "recorder", str(num), "start")
# # ------------------------------------------------------------------------------------------ #
# def get_recorder_status(self):
# self.enable_transaction(self.client)
# recorder_state = self.__send_lqc_command__(self.client, "record", "status")
# self.disable_transaction(self.client)
# return recorder_state
#
# Basic Methods
#
def init_player(self):
"""
Initializes the LiquidSoap Player after startup of the engine.
Returns:
(String): Message that the player is started.
"""
t = StartupThread(self)
t.start()
return "Engine Core startup done!"
# ------------------------------------------------------------------------------------------ #
def __send_lqc_command__(self, lqs_instance, namespace, command, *args):
"""
Ein Kommando an Liquidsoap senden
@type lqs_instance: object
@param lqs_instance: Instance of LiquidSoap Client
@type namespace: string
@param namespace: Namespace of function
@type command: string
@param command: Function name
@type args: list
@param args: List of parameters
@rtype: string
@return: Response from LiquidSoap
"""
try:
if not self.disable_logging:
if namespace == "recorder":
self.logger.debug("LiquidSoapCommunicator is calling " + str(namespace) + "_" + str(command) + "." + str(args))
else:
if command == "":
self.logger.debug("LiquidSoapCommunicator is calling " + str(namespace) + str(args))
else:
self.logger.debug("LiquidSoapCommunicator is calling " + str(namespace) + "." + str(command) + str(args))
# call wanted function ...
# FIXME REFACTOR all calls in a common way
if command in ["playlist_push", "playlist_seek", "playlist_clear", "stream_set_url", "stream_start", "stream_stop", "stream_status"]:
func = getattr(lqs_instance, command)
result = func(str(namespace), *args)
else:
func = getattr(lqs_instance, namespace)
result = func(command, *args)
if not self.disable_logging:
self.logger.debug("LiquidSoapCommunicator got response " + str(result))
self.connection_attempts = 0
return result
except LQConnectionError as e:
self.logger.error("Connection Error when sending " + str(namespace) + "." + str(command) + str(args))
if self.try_to_reconnect():
time.sleep(0.2)
self.connection_attempts += 1
if self.connection_attempts < 5:
# reconnect
self.__open_conn(self.client)
self.logger.info("Trying to resend " + str(namespace) + "." + str(command) + str(args))
# grab return value
retval = self.__send_lqc_command__(lqs_instance, namespace, command, *args)
# disconnect
self.__close_conn(self.client)
# return the val
return retval
else:
if command == "":
msg = "Rethrowing Exception while trying to send " + str(namespace) + str(args)
else:
msg = "Rethrowing Exception while trying to send " + str(namespace) + "." + str(command) + str(args)
self.logger.info(msg)
self.disable_transaction(socket=self.client, force=True)
raise e
else:
self.event_dispatcher.on_critical("Criticial Liquidsoap connection issue", \
"Could not connect to Liquidsoap (Multiple attempts)", e)
raise e
def is_active(self):
"""
Checks if Liquidsoap is running
"""
try:
self.uptime()
self.is_liquidsoap_running = True
except LQConnectionError as e:
self.logger.info("Liquidsoap is not running so far")
self.is_liquidsoap_running = False
except Exception as e:
self.logger.error("Cannot check if Liquidsoap is running. Reason: " + str(e))
self.is_liquidsoap_running = False
return self.is_liquidsoap_running
def engine_state(self):
"""
Retrieves the state of all inputs and outputs.
"""
state = self.__send_lqc_command__(self.client, "engine", "state")
return state
def liquidsoap_help(self):
"""
Retrieves the Liquidsoap help.
"""
data = self.__send_lqc_command__(self.client, "help", "")
if not data:
self.logger.warning("Could not get Liquidsoap's help")
else:
self.logger.debug("Got Liquidsoap's help")
return data
def version(self):
"""
Get the version of Liquidsoap.
"""
data = self.__send_lqc_command__(self.client, "version", "")
self.logger.debug("Got Liquidsoap's version")
return data
def uptime(self):
"""
Retrieves the uptime of Liquidsoap.
"""
data = self.__send_lqc_command__(self.client, "uptime", "")
self.logger.debug("Got Liquidsoap's uptime")
return data
#
# Connection and Transaction Handling
#
# ------------------------------------------------------------------------------------------ #
def try_to_reconnect(self):
self.enable_transaction()
return self.transaction > 0
# ------------------------------------------------------------------------------------------ #
def enable_transaction(self, socket=None):
# set socket to playout if nothing else is given
if socket is None:
socket = self.client
self.transaction = self.transaction + 1
self.logger.debug(TerminalColors.WARNING.value + "Enabling transaction! cnt: " + str(self.transaction) + TerminalColors.ENDC.value)
if self.transaction > 1:
return
try:
self.__open_conn(socket)
except FileNotFoundError:
self.disable_transaction(socket=socket, force=True)
subject = "CRITICAL Exception when connecting to Liquidsoap"
msg = "socket file " + socket.socket_path + " not found. Is liquidsoap running?"
self.logger.critical(SU.red(msg))
self.event_dispatcher.on_critical(subject, msg, None)
# ------------------------------------------------------------------------------------------ #
def disable_transaction(self, socket=None, force=False):
if not force:
# nothing to disable
if self.transaction == 0:
return
# decrease transaction counter
self.transaction = self.transaction - 1
# debug msg
self.logger.debug(TerminalColors.WARNING.value + "DISabling transaction! cnt: " + str(self.transaction) + TerminalColors.ENDC.value)
# return if connection is still needed
if self.transaction > 0:
return
else:
self.logger.debug(TerminalColors.WARNING.value + "Forcefully DISabling transaction! " + TerminalColors.ENDC.value)
# close conn and set transactioncounter to 0
self.__close_conn(socket)
self.transaction = 0
# ------------------------------------------------------------------------------------------ #
def __open_conn(self, socket):
# already connected
if self.transaction > 1:
return
self.logger.debug(TerminalColors.GREEN.value + "LiquidSoapCommunicator opening conn" + TerminalColors.ENDC.value)
# try to connect
socket.connect()
# ------------------------------------------------------------------------------------------ #
def __close_conn(self, socket):
# set socket to playout
if socket is None:
socket = self.client
# do not disconnect if a transaction is going on
if self.transaction > 0:
return
# say bye
socket.byebye()
# debug msg
self.logger.debug(TerminalColors.BLUE.value + "LiquidSoapCommunicator closed conn" + TerminalColors.ENDC.value)
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2020 - The Aura Engine Team.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import datetime
from modules.base.utils import SimpleUtil as SU
from modules.base.exceptions import NoActiveEntryException
from modules.base.mail import AuraMailer
from modules.plugins.monitor import AuraMonitor
from modules.core.state import PlayerStateService
from modules.plugins.trackservice import TrackserviceHandler
class EventBinding():
"""
A binding between the event dispatcher and some event handler.
It allows you to subscribe to events in a chained way:
```
binding = dispatcher.attach(AuraMonitor)
binding.subscribe("on_boot").subscribe("on_play")
```
"""
dispatcher = None
instance = None
def __init__(self, dispatcher, instance):
self.dispatcher = dispatcher
self.instance = instance
def subscribe(self, event_type):
"""
Subscribes the instance to some event identified by the `event_type` string.
"""
self.dispatcher.subscribe(self.instance, event_type)
return self
def get_instances(self):
"""
Returns the object within that binding.
"""
return self.instance
class EngineEventDispatcher():
"""
Executes handlers for engine events.
"""
logger = None
config = None
subscriber_registry = None
mailer = None
soundsystem = None
player_state = None
scheduler = None
api_handler = None
def __init__(self, soundsystem, scheduler):
"""
Initialize EventDispatcher
"""
self.subscriber_registry = dict()
self.logger = logging.getLogger("AuraEngine")
self.config = soundsystem.config
self.mailer = AuraMailer(self.config)
self.soundsystem = soundsystem
self.scheduler = scheduler
self.player_state = PlayerStateService(self.config)
binding = self.attach(AuraMonitor)
binding.subscribe("on_boot")
binding = self.attach(TrackserviceHandler)
binding.subscribe("on_play")
def attach(self, clazz):
"""
Creates an intance of the given Class.
"""
instance = clazz(self.config, self.soundsystem)
return EventBinding(self, instance)
def subscribe(self, instance, event_type):
"""
Subscribes to some event type. Preferably use it via `EventBinding.subscribe(..)`.
"""
if not event_type in self.subscriber_registry:
self.subscriber_registry[event_type] = []
self.subscriber_registry[event_type].append(instance)
def call_event(self, event_type, args):
"""
Calls all subscribers for the given event type.
"""
if not event_type in self.subscriber_registry:
return
listeners = self.subscriber_registry[event_type]
if not listeners:
return
for listener in listeners:
method = getattr(listener, event_type)
if method:
if args:
method(args)
else:
method()
#
# Events
#
def on_initialized(self):
"""
Called when the engine is initialized e.g. connected to Liquidsoap.
"""
self.logger.debug("on_initialized(..)")
self.scheduler.on_initialized()
self.call_event("on_initialized", None)
def on_boot(self):
"""
Called when the engine is starting up. This happens after the initialization step.
"""
self.logger.debug("on_boot(..)")
self.call_event("on_boot", None)
def on_ready(self):
"""
Called when the engine is booted and ready to play.
"""
self.logger.debug("on_ready(..)")
self.scheduler.on_ready()
def on_play(self, source):
"""
Event Handler which is called by the soundsystem implementation (i.e. Liquidsoap)
when some entry is actually playing. Note that this event resolves the source URI
and passes an `PlaylistEntry` to event handlers.
Args:
source (String): The `Entry` object *or* the URI of the media source currently playing
"""
self.logger.debug("on_play(..)")
entry = None
if isinstance(source, str):
try:
self.logger.info(SU.pink("Source '%s' started playing. Resolving ..." % source))
entry = self.player_state.resolve_entry(source)
except NoActiveEntryException:
self.logger.error("Cannot resolve '%s'" % source)
else:
entry = source
# Assign timestamp for play time
entry.entry_start_actual = datetime.datetime.now()
self.call_event("on_play", entry)
def on_stop(self, entry):
"""
The entry on the assigned channel has been stopped playing.
"""
self.logger.debug("on_stop(..)")
self.call_event("on_stop", entry)
def on_idle(self):
"""
Callend when no entry is playing
"""
self.logger.debug("on_idle(..)")
self.logger.error(SU.red("Currently there's nothing playing!"))
self.call_event("on_idle", None)
def on_schedule_change(self, schedule):
"""
Called when the playlist or entries of the current schedule have changed.
"""
self.logger.debug("on_schedule_change(..)")
self.call_event("on_schedule_change", schedule)
def on_queue(self, entries):
"""
One or more entries have been queued and are currently pre-loaded.
"""
self.logger.debug("on_queue(..)")
self.player_state.add_to_history(entries)
self.call_event("on_queue", entries)
def on_sick(self):
"""
Called when the engine is in some unhealthy state.
"""
self.logger.debug("on_sick(..)")
self.call_event("on_sick", None)
def on_resurrect(self):
"""
Called when the engine turned healthy again after being sick.
"""
self.logger.debug("on_resurrect(..)")
self.call_event("on_resurrect", None)
def on_critical(self, subject, message, data=None):
"""
Callend when some critical event occurs
"""
self.logger.debug("on_critical(..)")
if not data: data = ""
self.mailer.send_admin_mail(subject, message + "\n\n" + str(data))
self.call_event("on_critical", (subject, message, data))
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2017-2020 - The Aura Engine Team.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import socket
import urllib.parse
import configparser
import logging
from multiprocessing import Lock
from modules.base.exceptions import LQConnectionError
from modules.base.utils import TerminalColors
"""
LiquidSoapClient Class
Connects to a LiquidSoap instance over a socket and sends commands to it
"""
class LiquidSoapClient:
mutex = None
logger = None
debug = False
socket_path = ""
disable_logging = True
def __init__(self, config, socket_filename):
"""
Constructor
@type socket_path: string
@param socket_path: Der Pfad zum Socket des Liquidsoap-Scripts
"""
self.logger = logging.getLogger("AuraEngine")
self.socket_path = config.get('socketdir') + '/' + socket_filename
self.logger.debug("LiquidSoapClient using socketpath: " + self.socket_path)
# init
self.mutex = Lock()
self.connected = False
self.can_connect = True
self.message = ''
self.socket = None
self.metareader = configparser.ConfigParser()
# ------------------------------------------------------------------------------------------ #
def connect(self):
"""
Verbindung herstellen
"""
try:
self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.connect(self.socket_path)
except socket.error as e:
msg = "Cannot connect to socketpath " + self.socket_path + ". Reason: "+str(e)
self.logger.critical(TerminalColors.RED.value+msg+TerminalColors.ENDC.value)
self.can_connect = False
self.connected = False
# raise e
else:
self.can_connect = True
self.connected = True
return True
# AttributeError('characters_written')
# ------------------------------------------------------------------------------------------ #
def is_connected(self):
return self.connected
# ------------------------------------------------------------------------------------------ #
def write(self, data):
"""
Auf den Socket schreiben
@type data: string
@param data: Der String der gesendet wird
"""
if self.connected:
self.socket.sendall(data.decode("UTF-8"))
# ------------------------------------------------------------------------------------------ #
def read_all(self, timeout=2):
"""
Vom Socket lesen, bis dieser "END" sendet
@type timeout: int
@param timeout: Ein optionales Timeout
@rtype: string
@return: Die Antwort des Liquidsoap-Servers
"""
# make socket non blocking
# self.client.setblocking(0)
data = ''
try:
# set timeout
self.socket.settimeout(timeout)
# acquire the lock
self.mutex.acquire()
while True:
data += self.socket.recv(1).decode("utf-8")
# receive as long as we are not at the END or recv a Bye! from liquidsoap
if data.find("END\r\n") != -1 or data.find("Bye!\r\n") != -1:
data.replace("END\r\n", "")
break
# release the lock
self.mutex.release()
except Exception as e:
self.logger.error(TerminalColors.RED.value+str(e)+TerminalColors.ENDC.value)
self.mutex.release()
return data
# ------------------------------------------------------------------------------------------ #
def read(self):
"""
read from socket and store return value in self.message
@rtype: string
@return: The answer of liquidsoap server
"""
if self.connected:
ret = self.read_all().splitlines()
try:
last = ret.pop() # pop out end
if len(ret) > 1:
self.message = str.join(" - ", ret)
elif len(ret) == 1:
self.message = ret[0]
if last == "Bye!":
self.message = last
except Exception as e:
self.logger.error(str(e))
return self.message
# ------------------------------------------------------------------------------------------ #
def close(self):
"""
Quit senden und Verbindung schließen
"""
if self.connected:
message = "quit\r"
self.socket.sendall(message.decode("UTF-8"))
self.socket.close()
self.connected = False
# ------------------------------------------------------------------------------------------ #
def command(self, namespace, command, param=""):
"""
Kommando an Liquidosap senden
@type command: string
@param command: Kommando
@type namespace: string
@param namespace: Namespace/Kanal der angesprochen wird
@type param: mixed
@param param: ein optionaler Parameter
@rtype: string
@return: Die Antwort des Liquidsoap-Servers
"""
param = (param.strip() if param.strip() == "" else " " + urllib.parse.unquote(param.strip()))
if self.connected:
# print namespace + '.' + command + param + "\n"
if namespace is "":
message = str(command) + str(param) + str("\n")
else:
message = str(namespace) + str(".") + str(command) + str(param) + str("\n")
try:
if not self.disable_logging:
self.logger.debug("LiquidSoapClient sending to LiquidSoap Server: " + message[0:len(message)-1])
# send all the stuff over the socket to liquidsoap server
self.socket.sendall(message.encode())
if not self.disable_logging:
self.logger.debug("LiquidSoapClient waiting for reply from LiquidSoap Server")
# wait for reply
self.read()
if not self.disable_logging:
self.logger.debug("LiquidSoapClient got reply: " + self.message)
except BrokenPipeError as e:
self.logger.error(TerminalColors.RED.value+"Detected a problem with liquidsoap connection while sending: " + message + ". Reason: " + str(e) + "! Trying to reconnect."+TerminalColors.RED.value)
self.connect()
raise
except Exception as e:
self.logger.error("Unexpected error: " + str(e))
raise
return self.message
else:
msg = "LiquidsoapClient not connected to LiquidSoap Server"
self.logger.error(msg)
raise LQConnectionError(msg)
# ------------------------------------------------------------------------------------------ #
def help(self):
"""
get liquidsoap server help
@rtype: string
@return: the response of the liquidsoap server
"""
if self.connected:
self.command('help', '')
return self.message
# ------------------------------------------------------------------------------------------ #
def version(self):
"""
Liquidsoap get version
@rtype: string
@return: the response of the liquidsoap server
"""
if self.connected:
message = 'version'
self.command(message, '')
return self.message
# ------------------------------------------------------------------------------------------ #
def uptime(self):
"""
Liquidsoap get uptime
@rtype: string
@return: Die Antwort des Liquidsoap-Servers
"""
if self.connected:
self.command('uptime', '')
return self.message
# ------------------------------------------------------------------------------------------ #
def byebye(self):
"""
Liquidsoap say byebye
@rtype: string
@return: Die Antwort des Liquidsoap-Servers
"""
if self.connected:
self.command("", "quit")
return self.message
\ No newline at end of file
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2017-2020 - The Aura Engine Team.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# FIXME Refactor to avoid use of Channel Class here
from modules.core.channels import Channel
from modules.core.liquidsoap.client import LiquidSoapClient
class LiquidSoapPlayerClient(LiquidSoapClient):
#
# Mixer
#
def mixer(self, command, *args):
if command == "status":
return self.mixerstatus(*args)
if command == "inputs":
return self.mixerinputs()
if command == "volume":
return self.mixer_volume(*args)
if command == "select":
if len(args) == 2:
return self.mixer_select(args[0], args[1])
if command == "activate":
if len(args) == 2:
return self.mixer_activate(args[0], args[1])
return "LiquidSoapPlayerClient does not understand mixer."+command+str(args)
# ------------------------------------------------------------------------------------------ #
def mixerinputs(self):
# send command
self.command("mixer", "inputs")
# convert to list and return it
return self.message.strip().split(' ')
# ------------------------------------------------------------------------------------------ #
def mixerstatus(self, pos=""):
"""
Get state of a source in the mixer
@type pos: string
@param pos: Mixerposition
@rtype: string
@return: Response from LiquidSoap
"""
self.command("mixer", "status", str(pos))
return self.message
def mixer_volume(self, pos, volume):
"""
Sets some mixer channel to the given volume
Args:
pos (Integer): The channel number
volume (Integer): The volume
Returns:
(String): Liquidsoap server response
"""
self.command("mixer", "volume", str(pos) + " " + str(volume))
return self.message
def mixer_select(self, pos, select):
"""
Selects some mixer channel or vice versa.
Args:
pos (Integer): The channel number
select (Boolean): Select or deselect
Returns:
(String): Liquidsoap server response
"""
self.command("mixer", "select", str(pos) + " " + str(select).lower())
return self.message
def mixer_activate(self, pos, activate):
"""
Selects some mixer channel and increases the volume to 100 or vice versa.
Args:
pos (Integer): The channel number
activate (Boolean): Activate or deactivate
Returns:
(String): Liquidsoap server response
"""
self.command("mixer", "activate", str(pos) + " " + str(activate).lower())
return self.message
#
# Playlist
#
def playlist_push(self, channel, uri):
"""
Pushes the passed file URI to the playlist channel.
Args:
channel (String): Liquidsoap Channel ID
uri (String): Path to the file
"""
self.command(channel, 'push', uri)
return self.message
def playlist_seek(self, channel, duration):
"""
Forward the playing track/playlist of the given channel.
Args:
channel (String): Liquidsoap Channel ID
duration (Integer): Seek duration ins seconds
Returns:
Liquidsoap server response
"""
self.command(channel, 'seek', str(duration))
return self.message
def playlist_clear(self, channel):
"""
Clears all playlist entries of the given channel.
Args:
channel (String): Liquidsoap Channel ID
duration (Integer): Seek duration ins seconds
Returns:
Liquidsoap server response
"""
if channel == Channel.FILESYSTEM_A.value:
self.command(channel, 'clear_filesystem_0')
elif channel == Channel.FILESYSTEM_B.value:
self.command(channel, 'clear_filesystem_1')
else:
return "Invalid filesystem channel '%s'" % channel
return self.message
#
# Stream
#
def stream_set_url(self, channel, url):
"""
Sets the URL on the given HTTP channel.
"""
self.command(channel, 'url', url)
return self.message
def stream_start(self, channel):
"""
Starts the HTTP stream set with `stream_set_url` on the given channel.
"""
self.command(channel, 'start')
return self.message
def stream_stop(self, channel):
"""
Stops the HTTP stream on the given channel.
"""
self.command(channel, 'stop')
return self.message
def stream_status(self, channel):
"""
Returns the status of the HTTP stream on the given channel.
"""
self.command(channel, 'status')
return self.message
#
# General Entries
#
def entry_status(self, rid):
"""
Retrieves the status of a given entry.
Args:
rid (String): Resource ID (RID)
Returns:
Liquidsoap server response
"""
self.command("request", "status", str(rid))
return self.message
#
# Other
#
def uptime(self, command=""): # no command will come
"""
Retrieves how long the engine is running already.
"""
return self.command("", "uptime")
def version(self, command=""): # no command will come
"""
Retrieves the Liquidsoap version.
"""
return self.command("", "version")
def engine(self, command, *args):
"""
Retrieves the state of all input and outputs.
"""
if command == "state":
return self.engine_state()
return "LiquidSoapPlayerClient does not understand engine." + command + str(args)
def engine_state(self):
"""
Retrieves the state of all input and outputs.
"""
self.command('auraengine', 'state')
return self.message
# ------------------------------------------------------------------------------------------ #
# def recorder(self, num, command, *args):
# if command == "status":
# return self.recorderstatus(num)
# if command == "start":
# return self.recorderstart(num)
# if command == "stop":
# return self.recorderstop(num)
# return "LiquidSoapPlayerClient does not understand mixer." + command + str(args)
# ------------------------------------------------------------------------------------------ #
# def recorderstatus(self, num):
# """
# get status of a recorder
# :return:
# """
# self.command("recorder_" + str(num), "status")
# return self.message
# # ------------------------------------------------------------------------------------------ #
# def recorderstart(self, num):
# """
# get status of a recorder
# :return:
# """
# self.command("recorder_" + str(num), "start")
# return self.message
# # ------------------------------------------------------------------------------------------ #
# def recorderstop(self, num):
# """
# get status of a recorder
# :return:
# """
# self.command("recorder_" + str(num), "stop")
# return self.message
# ------------------------------------------------------------------------------------------ #
# def skip(self, namespace="playlist", pos=""):
# """
# Source skippen
# @type namespace: string
# @param namespace: Namespace der Source
# @type pos: string
# @param pos: Die Position - optional - Position des Channels vom Mixer benötigt
# @rtype: string
# @return: Die Antwort des Liquidsoap-Servers
# """
# self.command('skip', namespace, pos)
# return self.message
# # ------------------------------------------------------------------------------------------ #
# def remove(self, pos, namespace="playlist"):
# """
# Track aus der secondary_queue oder der Playlist entfernen
# @type pos: string
# @param pos: Die Position
# @type namespace: string
# @param namespace: Namespace der Source
# @rtype: string
# @return: Die Antwort des Liquidsoap-Servers
# """
# self.command('remove', namespace, str(pos))
# return self.message
# # ------------------------------------------------------------------------------------------ #
# def insert(self, uri, pos='0', namespace="playlist"):
# """
# Track einfügen
# @type uri: string
# @param uri: Uri einer Audiodatei
# @type pos: string
# @param pos: Die Position
# @type namespace: string
# @param namespace: Namespace der Source
# @rtype: string
# @return: Die Antwort des Liquidsoap-Servers
# """
# self.command('insert', namespace, str(pos) + ' ' + uri)
# return self.message
# # ------------------------------------------------------------------------------------------ #
# def move(self, fromPos, toPos, namespace="playlist"):
# """
# Track von Position fromPos nach Position toPos verschieben
# @type fromPos: string/int
# @param fromPos: Position des zu verschiebenden Tracks
# @type toPos: string
# @param toPos: Die Position zu der verschoben werden soll
# @type namespace: string
# @param namespace: Namespace der Source
# @rtype: string
# @return: Die Antwort des Liquidsoap-Servers
# """
# self.command('move', namespace, str(fromPos) + ' ' + str(toPos))
# return self.message
# ------------------------------------------------------------------------------------------ #
# def play(self, namespace="playlist"):
# """
# Source abspielen - funktioniert nur bei Playlist
# @type namespace: string
# @param namespace: Namespace der Source
# @rtype: string
# @return: Die Antwort des Liquidsoap-Servers
# """
# self.command('play', namespace)
# return self.message
# # ------------------------------------------------------------------------------------------ #
# def pause(self, namespace="playlist"):
# """
# Source pausieren/stoppen - funktioniert nur bei Playlist
# @type namespace: string
# @param namespace: Namespace der Source
# @rtype: string
# @return: Die Antwort des Liquidsoap-Servers
# """
# self.command('pause', namespace)
# return self.message
# # ------------------------------------------------------------------------------------------ #
# def flush(self, namespace="playlist"):
# """
# Playlist leeren
# @type namespace: string
# @param namespace: Namespace der Source
# @rtype: string
# @return: Die Antwort des Liquidsoap-Servers
# """
# self.command('flush', namespace)
# return self.message
# # ------------------------------------------------------------------------------------------ #
# def playlistData(self):
# """
# Metadaten der Playlist ausgeben
# @rtype: string
# @return: Ein Json-String
# """
# self.command('data', 'playlist')
# return self.message
# # ------------------------------------------------------------------------------------------ #
# def get_queue(self, namespace="ch1", queue='queue'):
# """
# Queue eines Kanals ausgeben
# @type namespace: string
# @param namespace: Namespace der Source
# @type queue: string
# @param queue: Name des queues (queue, primary_queue, secondary_queue)
# @rtype: string
# @return: Die Antwort des Liquidsoap-Servers
# """
# self.command(queue, namespace)
# return self.message
# ------------------------------------------------------------------------------------------ #
# def loadPlaylist(self, uri, params="", namespace="playlist"):
# """
# Playlist laden
# @type uri: string
# @param uri: Uri einer Playlist im XSPF-Format
# @type params: string
# @param params: obsolete
# @type namespace: string
# @param namespace: Namespace der Source - hier nur playlist
# @rtype: string
# @return: Die Antwort des Liquidsoap-Servers
# """
# self.command('load', namespace, uri + params)
# return self.message
# ------------------------------------------------------------------------------------------ #
# def currentTrack(self, namespace="request"):
# """
# Das oder die ID(s) der gerade abgespielten requests erhalten
# @type namespace: string
# @param namespace: Namespace der Source
# @rtype: string
# @return: Die Antwort des Liquidsoap-Servers (als String)
# """
# self.command('on_air', namespace)
# return self.message
# ------------------------------------------------------------------------------------------ #
def volume(self, pos, volume, namespace="mixer"):
"""
Lautstärke eines Kanals setzen
@type pos: int/string
@param pos: Die Position/ Nummer des Kanals (playlist=0)
@type volume: int/string
@param volume: Zahl von 1 -100
@type namespace: string
@param namespace: Namespace der Source (immer mixer)
@rtype: string
@return: Die Antwort des Liquidsoap-Servers
"""
self.command('volume', namespace, str(pos) + ' ' + str(volume))
return self.message
# ------------------------------------------------------------------------------------------ #
# def playlist_remaining(self):
# """
# Wie lange läuft der aktuelle Track der Playlist noch
# @rtype: string
# @return: Die Antwort des Liquidsoap-Servers
# """
# self.command('remaining', 'playlist')
# return self.message
# ------------------------------------------------------------------------------------------ #
# def list_channels(self):
# """
# Channels auflisten (Simple JSON)
# """
# # Liquidsoap Kommando
# channels = self.sendLqcCommand(self.lqc, 'mixer', 'inputs')
# if not isinstance(channels, list):
# self.error('02')
# elif len(channels) < 1:
# self.warning('01')
# else:
# self.success('00', channels)
# self.notifyClient()
\ No newline at end of file
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2017-2020 - The Aura Engine Team.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import threading
from modules.base.exceptions import NoActiveScheduleException
from modules.base.utils import SimpleUtil as SU
class StartupThread(threading.Thread):
"""
StartupThread class.
Boots the engine and starts playing the current schedule.
"""
logger = None
active_entry = None
engine = None
def __init__(self, engine):
"""
Initialize the thread.
"""
threading.Thread.__init__(self)
self.logger = logging.getLogger("AuraEngine")
self.engine = engine
def run(self):
"""
Boots the soundsystem.
"""
try:
self.engine.start()
except NoActiveScheduleException as e:
self.logger.info("Nothing scheduled at startup time. Please check if there are follow-up schedules.")
except Exception as e:
self.logger.error(SU.red("Error while initializing the soundsystem: " + str(e)), e)
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2020 - The Aura Engine Team.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
from collections import deque
from modules.base.exceptions import NoActiveEntryException
from modules.base.utils import SimpleUtil as SU, EngineUtil
from modules.core.channels import ChannelType
class PlayerStateService:
"""
PlayerStateService keeps a short history of currently playing entries. It stores the recent
active entries to a local cache `entry_history` being able to manage concurrently playing entries.
It also is in charge of storing relevant meta information of the currently playing entry to
the TrackService table.
"""
config = None
logger = None
entry_history = None
def __init__(self, config):
"""
Constructor
Args:
config (AuraConfig): Holds the engine configuration
"""
self.config = config
self.logger = logging.getLogger("AuraEngine")
self.entry_history = deque([None, None, None])
#
# PUBLIC METHODS
#
def add_to_history(self, entries):
"""
Saves the currently pre-rolled [`Entry`] to the local cache.
"""
self.entry_history.pop()
self.entry_history.appendleft(entries)
def get_recent_entries(self):
"""
Retrieves the currently playing [`Entry`] from the local cache.
"""
return self.entry_history[0]
def resolve_entry(self, source):
"""
Retrieves the `PlaylistEntry` matching the provied source URI.
Args:
source (String): The URI of the source playing
Raises:
(NoActiveEntryException)
"""
result = None
entries = self.get_recent_entries()
if not entries:
raise NoActiveEntryException
for entry in entries:
entry_source = entry.source
if entry.channel in ChannelType.FILESYSTEM.channels:
base_dir = self.config.get("audiofolder")
entry_source = EngineUtil.uri_to_filepath(base_dir, entry.source)
if entry_source == source:
self.logger.info("Resolved '%s' entry '%s' for URI '%s'" % (entry.get_type(), entry, source))
result = entry
break
if not result:
msg = "Found no entry in the recent history which matches the given source '%s'" % (source)
self.logger.critical(SU.red(msg))
return result
def print_entry_history(self):
"""
Prints all recents entries of the history.
"""
msg = "Active entry history:\n"
for entries in self.entry_history:
msg += "["
for e in entries:
msg += "\n" + str(e)
msg += "]"
self.logger.info(msg)
#!/usr/bin/liquidsoap
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2017-2020 - The Aura Engine Team.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
icecast_vorbis_metadata = false
inputs = ref []
# load settings from ini file
%include "settings.liq"
# include some functions
%include "library.liq"
# include fallback functions
%include "fallback.liq"
#################
# create inputs #
#################
# enable play from filesystem
%include "in_filesystem.liq"
# enable stream overtakes
%include "in_stream.liq"
# enabled line in from soundcard
%include "in_soundcard.liq"
# fill the mixer
mixer = mix(id="mixer", list.append([input_filesystem_0, input_filesystem_1, input_http_0, input_http_1, input_https_0, input_https_1], !inputs))
# output source with fallbacks
stripped_stream = strip_blank(id='strip_blank', track_sensitive=false, max_blank=fallback_max_blank, min_noise=fallback_min_noise, threshold=fallback_threshold, mixer)
# enable fallback
# output_source = fallback(id="fallback", track_sensitive=false, [stripped_stream, timeslot_fallback, show_fallback, mksafe(station_fallback)])
#output_source = fallback(id="fallback", track_sensitive=false, [stripped_stream, timeslot_fallback, show_fallback, mksafe(station_fallback)])
output_source = mksafe(stripped_stream)
ignore(timeslot_fallback)
ignore(show_fallback)
ignore(station_fallback)
##################
# create outputs #
##################
# create soundcard output
%include "out_soundcard.liq"
# recording output
%include "out_filesystem.liq"
# stream output
%include "out_stream.liq"
# enable socket functions
%include "serverfunctions.liq"
########################
# start initialization #
########################
system('#{list.assoc(default="", "install_dir", ini)}/guru.py --init-player --quiet')
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2017-2020 - The Aura Engine Team.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Crossfade between tracks,
# taking the respective volume levels
# into account in the choice of the
# transition.
# @category Source / Track Processing
# @param ~start_next Crossing duration, if any.
# @param ~fade_in Fade-in duration, if any.
# @param ~fade_out Fade-out duration, if any.
# @param ~width Width of the volume analysis window.
# @param ~conservative Always prepare for
# a premature end-of-track.
# @param s The input source.
def crossfade (~start_next=5.,~fade_in=3.,
~fade_out=3., ~width=2.,
~conservative=false,s)
high = -20.
medium = -32.
margin = 4.
fade.out = fade.out(type="sin",duration=fade_out)
fade.in = fade.in(type="sin",duration=fade_in)
add = fun (a,b) -> add(normalize=false,[b,a])
log = log(label="crossfade")
def transition(a,b,ma,mb,sa,sb)
list.iter(fun(x)->
log(level=4,"Before: #{x}"),ma)
list.iter(fun(x)->
log(level=4,"After : #{x}"),mb)
if
# If A and B and not too loud and close,
# fully cross-fade them.
a <= medium and
b <= medium and
abs(a - b) <= margin
then
log("Transition: crossed, fade-in, fade-out.")
add(fade.out(sa),fade.in(sb))
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 >= a + margin and a >= medium and b <= high
then
log("Transition: crossed, fade-out.")
add(fade.out(sa),sb)
elsif
# Do not fade if it's already very low.
b >= a + margin and a <= medium and b <= high
then
log("Transition: crossed, no fade-out.")
add(sa,sb)
elsif
# Opposite as the previous one.
a >= b + margin and b >= medium and a <= high
then
log("Transition: crossed, fade-in.")
add(sa,fade.in(sb))
# 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: just sequencing.")
sequence([sa, sb])
end
end
cross(width=width, duration=start_next,
conservative=conservative,
transition,s)
end
# Custom crossfade to deal with jingles.
# def smarter_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.,~conservative=false,s)
# fade.out = fade.out(type="sin",duration=fade_out)
# fade.in = fade.in(type="sin",duration=fade_in)
# add = fun (a,b) -> add(normalize=false,[b, a])
# log = log(label="smarter_crossfade")
# def transition(a,b,ma,mb,sa,sb)
# list.iter(fun(x)-> log(level=4,"Before: #{x}"),ma)
# list.iter(fun(x)-> log(level=4,"After : #{x}"),mb)
# if ma["type"] == "jingles" or mb["type"] == "jingles" then
# log("Old or new file is a jingle: sequenced transition.")
# sequence([sa, sb])
# elsif
# # If A and B are not too loud and close, fully cross-fade them.
# a <= medium and b <= medium and abs(a - b) <= 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(sa),fade.in(sb))
# 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 >= a + margin and a >= medium and b <= 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(sa),sb)
# elsif
# # Opposite as the previous one.
# a >= b + margin and b >= medium and a <= 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(sa,fade.in(sb))
# elsif
# # Do not fade if it's already very low.
# b >= a + margin and a <= medium and b <= 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(sa,sb)
# # 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(sa, sb)
# end
# end
# #smart_cross(width=width, duration=start_next, conservative=conservative, transition, s)
# smart_crossfade(duration=start_next, fade_in=fade_in, fade_out=fade_out, width=width, conservative=conservative, transition, s)
# end
# create a pool
def fallback_create(~skip=true, name, requestor)
log("Creating channel #{name}")
# Create the request.dynamic source
# Set conservative to true to queue several songs in advance
#source = request.dynamic(conservative=true, length=50., id="pool_"^name, requestor, timeout=60.)
source = request.dynamic(length=50., id="pool_"^name, requestor, timeout=60.)
# Apply normalization using replaygain information
source = amplify(1., override="replay_gain", source)
# Skip blank when asked to
source =
if skip then
skip_blank(max_blank=fallback_max_blank, min_noise=fallback_min_noise, threshold=fallback_threshold, source)
else
source
end
# Tell the system when a new track is played
def do_meta(meta) =
filename = meta["filename"]
# artist = meta["artist"]
# title = meta["title"]
system('#{list.assoc(default="", "install_dir", ini)}/guru.py --on_play "#{filename}"')
end
source = on_metadata(do_meta, source)
log("channel created")
# Finally apply a smart crossfading
#smarter_crossfade(source)
crossfade(source)
end
def create_dynamic_playlist(next)
request.create(list.hd(default="", next))
end
def create_playlist() =
log("requesting next song for PLAYLIST")
result = get_process_lines('#{list.assoc(default="", "install_dir", ini)}/guru.py --get-next-file-for "playlist" --quiet')
create_dynamic_playlist(result)
end
def create_station_fallback() =
result = get_process_lines('#{list.assoc(default="", "install_dir", ini)}/guru.py --get-next-file-for station --quiet')
log("next song for STATION fallback is: #{result}")
create_dynamic_playlist(result)
end
def create_show_fallback() =
result = get_process_lines('#{list.assoc(default="", "install_dir", ini)}/guru.py --get-next-file-for show --quiet')
log("next song for SHOW fallback is: #{result}")
create_dynamic_playlist(result)
end
def create_timeslot_fallback() =
result = get_process_lines('#{list.assoc(default="", "install_dir", ini)}/guru.py --get-next-file-for timeslot --quiet')
log("next song for TIMESLOT fallback is: #{result}")
create_dynamic_playlist(result)
end
# create fallbacks
timeslot_fallback = fallback_create(skip=true, "timeslot_fallback", create_timeslot_fallback)
station_fallback = fallback_create(skip=true, "station_fallback", create_station_fallback)
show_fallback = fallback_create(skip=true, "show_fallback", create_show_fallback)
\ No newline at end of file
import os
import sys
#!/bin/bash
pack_int(){ printf "%08X\n" $1 | sed 's/\([0-9A-F]\{2\}\)\([0-9A-F]\{2\}\)\([0-9A-F]\{2\}\)\([0-9A-F]\{2\}\)/\\\\\\x\4\\\\\\x\3\\\\\\x\2\\\\\\x\1/I' | xargs printf; }
pack_short(){ printf "%04X\n" $1 | sed 's/\([0-9A-F]\{2\}\)\([0-9A-F]\{2\}\)/\\\\\\x\2\\\\\\x\1/I' | xargs printf; }
duration=1800
if [[ $# -eq 1 ]]; then
duration=$1
fi
channels=2
bps=16
sample=44100
Subchunk1Size=18
Subchunk2Size=$(echo "$duration*$sample*$channels*$bps/8" | bc)
ChunkSize=$((20 + $Subchunk1Size + $Subchunk2Size))
echo -n RIFF
pack_int $ChunkSize
echo -n "WAVEfmt "
pack_int $Subchunk1Size
pack_short 1
pack_short $channels
pack_int $sample
pack_int $((bps/8 * channels * sample))
pack_short $((bps/8 * channels))
pack_short $bps
pack_short 0
echo -n data
pack_int $Subchunk2Size
dd if=/dev/zero bs=1 count=$Subchunk2Size 2>/dev/null
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2017-2020 - The Aura Engine Team.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
input_filesystem_0 = request.equeue(id="in_filesystem_0")
input_filesystem_1 = request.equeue(id="in_filesystem_1")
# input_filesystem_2 = request.equeue(id="in_filesystem_2")
# input_filesystem_3 = request.equeue(id="in_filesystem_3")
# input_filesystem_4 = request.equeue(id="in_filesystem_4")
#input_fs = cue_cut(mksafe(request.equeue(id="fs")))
# Call engine core handler to "on_play" (e.g. to update track service)
def do_meta_filesystem(meta) =
filename = meta["filename"]
# artist = meta["artist"]
# title = meta["title"]
# print('CALLING GURU #{list.assoc(default="", "install_dir", ini)}/guru.py')
system('#{list.assoc(default="", "install_dir", ini)}/guru.py --on_play "#{filename}" &')
end
input_filesystem_0 = on_metadata(id="in_filesystem_0", do_meta_filesystem, input_filesystem_0)
input_filesystem_1 = on_metadata(id="in_filesystem_1", do_meta_filesystem, input_filesystem_1)
# def clear_queue(s) =
# ret = server.execute("fs.queue")
# #ret = request.equeue(id="fs")
# ret = list.hd(default="",ret)
# ret = string.split(separator=" ",ret)
# #print("input FS.list: #{ret}")
# list.iter(fun(x) -> begin
# print("IGNORE: #{x}")
# ignore(server.execute("fs.ignore #{x}"))
# end, ret)
# res = source.skip(s)
# #(0.5)
# print("SKIP RES: #{res}")
# res = source.skip(s)
# print("SKIP RES: #{res}")
# end
# server.register(namespace="fs",
# description="Flush queue and stop request source.",
# usage="stop",
# "stop",
# fun (s) -> begin clear_queue(input_fs) "Done." end)
def clear_items(ns) =
ret = server.execute("#{source.id(ns)}.primary_queue")
ret = list.hd(default="", ret)
if ret == "" then
log("Queue cleared.")
(-1.)
else
log("There are still items in the queue, trying skip ...")
source.skip(ns)
(0.1)
end
end
def clear_queue(ns) =
add_timeout(fast=false, 0.5, {clear_items(ns)})
end
# Clear Queue 0
server.register(namespace=source.id(input_filesystem_0),
description="Clear all items of the filesystem Queue A.",
usage="clear",
"clear_filesystem_0",
fun (s) -> begin clear_queue(input_filesystem_0) "Done." end)
# Clear Queue 1
server.register(namespace=source.id(input_filesystem_1),
description="Clear all items of the filesystem Queue B.",
usage="clear",
"clear_filesystem_1",
fun (s) -> begin clear_queue(input_filesystem_1) "Done." end)
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2017-2020 - The Aura Engine Team.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
if a0_in != "" then
# we can ignore the result, since it is stored in the list 'inputs'
set_input(a0_in, "linein_0")
end
if a1_in != "" then
ignore(set_input(a1_in, "linein_1"))
end
if a2_in != "" then
ignore(set_input(a2_in, "linein_2"))
end
if a3_in != "" then
ignore(set_input(a3_in, "linein_3"))
end
if a4_in != "" then
ignore(set_input(a4_in, "linein_4"))
# input_4 = ref output.dummy(blank())
# set_input(input_4, a4_in, "linein_4")
# inputs := list.append([!input_4], !inputs)
end
\ No newline at end of file
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2017-2020 - The Aura Engine Team.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Pre-population of stream input sources, as Liquidsoap needs it to initialize this input.
# This is overwritten as soon as some other Stream is scheduled.
# http_starturl = "http://stream01.kapper.net:8001/live.mp3"
# http_starturl = "http://stream.fro.at/fro-128.ogg"
http_starturl = "http://trance.out.airtime.pro:8000/trance_a"
# http_starturl = "http://chill.out.airtime.pro:8000/chill_a"
# http_starturl = "http://212.89.182.114:8008/frf"
https_starturl = "https://securestream.o94.at/live.mp3"
# https_starturl = "https://live.helsinki.at:8088/live160.ogg"
# https_starturl = "https://stream.fro.at/fro-128.ogg"
input_http_0 = input.http(id="in_http_0", buffer=input_stream_buffer, max=60.0, timeout=60.0, autostart=false, http_starturl)
input_http_1 = input.http(id="in_http_1", buffer=input_stream_buffer, max=60.0, timeout=60.0, autostart=false, http_starturl)
input_https_0 = input.https(id="in_https_0", buffer=input_stream_buffer, max=60.0, timeout=60.0, autostart=false, https_starturl)
input_https_1 = input.https(id="in_https_1", buffer=input_stream_buffer, max=60.0, timeout=60.0, autostart=false, https_starturl)
# Route input stream to an dummy output to avoid buffer-overrun messages
# output.dummy(id="SPAM_HTTP_OUTPUT_0", fallible=true, input_http_0)
# output.dummy(id="SPAM_HTTP_OUTPUT_1", fallible=true, input_http_1)
# output.dummy(id="SPAM_HTTPS_OUTPUT_0", fallible=true, input_https_0)
# output.dummy(id="SPAM_HTTPS_OUTPUT_1", fallible=true, input_https_1)
# output.dummy(blank())
\ No newline at end of file
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2017-2020 - The Aura Engine Team.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#####################
# stream to icecast #
#####################
def stream_to_icecast(id, encoding, bitrate, host, port, pass, mount_point, url, description, genre, user, stream, streamnumber, connected, name, channels) =
source = ref stream
def on_error(msg)
connected := "false"
log(msg)
5.
end
def on_connect()
connected := "true"
log("Successfully connected to stream_#{streamnumber}")
end
stereo = (int_of_string(channels) >= 2)
user_ref = ref user
if user == "" then
user_ref := "source"
end
# Liquidsoap cannot handle one output definition for mono and stereo
output_icecast_mono = output.icecast(id = id, host = host, port = port, password = pass, mount = mount_point, fallible = true, url = url, description = description, name = name, genre = genre, user = !user_ref, on_error = on_error, on_connect = on_connect, icy_metadata = "true")
output_icecast_stereo = output.icecast(id = id, host = host, port = port, password = pass, mount = mount_point, fallible = true, url = url, description = description, name = name, genre = genre, user = !user_ref, on_error = on_error, on_connect = on_connect, icy_metadata = "true")
# %ifencoder %aac
# if encoding == "aac" then
# log("ENABLING AAC to ICECAST")
# %include "outgoing_streams/aac.liq"
# end
# %endif
#
# %ifencoder %flac
# if encoding == "flac" then
# log("ENABLING FLAC to ICECAST")
# %include "outgoing_streams/flac.liq"
# end
# %endif
if encoding == "mp3" then
log("ENABLING Mp3 to ICECAST")
%include "outgoing_streams/mp3.liq"
end
if encoding == "ogg" then
log("ENABLING OGG to ICECAST")
%include "outgoing_streams/ogg.liq"
end
# %ifencoder %opus
# if encoding == "opus" then
# log("ENABLING OPUS to ICECAST")
# %include "outgoing_streams/opus.liq"
# end
# %endif
end
###########
# line in #
###########
def set_input(device, name) =
if use_alsa == true then
alsa_in = input.alsa(id=name, device=a0_in, clock_safe=false, bufferize = false)
inputs := list.append([alsa_in], !inputs)
elsif use_jack == true then
jack_in = input.jack(id=name, clock_safe=false)
inputs := list.append([jack_in], !inputs)
else
pulse_in = input.pulseaudio(id=name, client="AuraEngine Line IN")
inputs := list.append([pulse_in], !inputs)
end
end
############
# line out #
############
def get_output(source, device, name) =
if device != "" then
if use_alsa == true then
log("--- Set ALSA Output ---")
if device == "default" then
output.alsa(id="lineout", bufferize = false, source)
else
output.alsa(id=name, device=device, bufferize = false, source)
end
elsif use_jack == true then
log("--- Set JACK AUDIO Output ---")
output.jack(id=name, source)
else
log("--- Set PULSE AUDIO Output ---")
output.pulseaudio(id=name, client="AuraEngine Line OUT", source)
end
else
log("OUTPUT DUMMY")
output.dummy(id=name^"_DUMMY", blank())
end
end
########################
# record to filesystem #
########################
# shows current file and how many bytes were written so far
def currecording(recfile)
if recfile != "" then
bytes_written = list.hd(default="", get_process_lines("echo $(($(stat -c%s "^recfile^")))"))
"#{recfile}, #{bytes_written}B"
else
""
end
end
def start_recorder(folder, duration, encoding, bitrate, channels, filenamepattern, is_recording, stream, recorder_number) =
source = ref stream
stereo = (int_of_string(channels) >= 2)
# define on_start, on_close (good case) and on_stop (error case)
recfile = ref ''
def on_start()
is_recording := true
recfile := list.hd(default="", get_process_lines("date +#{filenamepattern}"))
end
def on_close(filename)
is_recording := false
recfile := list.hd(default="", get_process_lines("date +#{filenamepattern}"))
end
def on_stop()
is_recording := false
end
# register server function
server.register(namespace="recorder_"^recorder_number, description="Show current file.", usage="curfile", "curfile", fun (s) -> currecording(!recfile) )
# dumbass liquidsoap cannot handle one output definition for mono and stereo
output_filesystem_mono = output.file(id="recorder_"^recorder_number, perm = 0o664, on_start=on_start, on_close=on_close, on_stop=on_stop, reopen_when={ int_of_float(gettimeofday()/60.) mod duration == 0 })
output_filesystem_stereo = output.file(id="recorder_"^recorder_number, perm = 0o664, on_start=on_start, on_close=on_close, on_stop=on_stop, reopen_when={ int_of_float(gettimeofday()/60.) mod duration == 0 })
# %ifencoder %aac
# if encoding == "aac" then
# log("ENABLING aac recorder to filesystem")
# %include "outgoing_recordings/aac.liq"
# end
# %endif
# %ifencoder %flac
if encoding == "flac" then
log("ENABLING flac recorder to filesystem")
%include "outgoing_recordings/flac.liq"
end
# %endif
# %ifencoder %mp3
if encoding == "mp3" then
log("ENABLING mp3 recorder to filesystem")
%include "outgoing_recordings/mp3.liq"
end
# %endif
# %ifencoder %vorbis
if encoding == "ogg" then
log("ENABLING ogg recorder to filesystem")
%include "outgoing_recordings/ogg.liq"
end
# %endif
# %ifencoder %opus
if encoding == "opus" then
log("ENABLING opus recorder to filesystem")
%include "outgoing_recordings/opus.liq"
end
# %endif
# %ifencoder %wav
if encoding == "wav" then
log("ENABLING wav recorder to filesystem")
%include "outgoing_recordings/wav.liq"
end
# %endif
end
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2017-2020 - The Aura Engine Team.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
r0_enable = list.assoc(default="", "rec_0", ini) == "y"
r1_enable = list.assoc(default="", "rec_1", ini) == "y"
r2_enable = list.assoc(default="", "rec_2", ini) == "y"
r3_enable = list.assoc(default="", "rec_3", ini) == "y"
r4_enable = list.assoc(default="", "rec_4", ini) == "y"
r0_folder = list.assoc(default="", "rec_0_folder", ini)
r0_duration = int_of_string(list.assoc(default="", "rec_0_duration", ini))
r0_encoding = list.assoc(default="", "rec_0_encoding", ini)
r0_bitrate = int_of_string(list.assoc(default="", "rec_0_bitrate", ini))
r0_channels = list.assoc(default="", "rec_0_channels", ini)
r0_filenamepattern = r0_folder^"/%Y-%m-%d/%Y-%m-%d-%H-%M."^r0_encoding
r1_folder = list.assoc(default="", "rec_1_folder", ini)
r1_duration = int_of_string(list.assoc(default="", "rec_1_duration", ini))
r1_encoding = list.assoc(default="", "rec_1_encoding", ini)
r1_bitrate = int_of_string(list.assoc(default="", "rec_1_bitrate", ini))
r1_channels = list.assoc(default="", "rec_1_channels", ini)
r1_filenamepattern = r1_folder^"/%Y-%m-%d/%Y-%m-%d-%H-%M."^r1_encoding
r2_folder = list.assoc(default="", "rec_2_folder", ini)
r2_duration = int_of_string(list.assoc(default="", "rec_2_duration", ini))
r2_encoding = list.assoc(default="", "rec_2_encoding", ini)
r2_bitrate = int_of_string(list.assoc(default="", "rec_2_bitrate", ini))
r2_channels = list.assoc(default="", "rec_2_channels", ini)
r2_filenamepattern = r2_folder^"/%Y-%m-%d/%Y-%m-%d-%H-%M."^r2_encoding
r3_folder = list.assoc(default="", "rec_3_folder", ini)
r3_duration = int_of_string(list.assoc(default="", "rec_3_duration", ini))
r3_encoding = list.assoc(default="", "rec_3_encoding", ini)
r3_bitrate = int_of_string(list.assoc(default="", "rec_3_bitrate", ini))
r3_channels = list.assoc(default="", "rec_3_channels", ini)
r3_filenamepattern = r3_folder^"/%Y-%m-%d/%Y-%m-%d-%H-%M."^r3_encoding
r4_folder = list.assoc(default="", "rec_4_folder", ini)
r4_duration = int_of_string(list.assoc(default="", "rec_4_duration", ini))
r4_encoding = list.assoc(default="", "rec_4_encoding", ini)
r4_bitrate = int_of_string(list.assoc(default="", "rec_4_bitrate", ini))
r4_channels = list.assoc(default="", "rec_4_channels", ini)
r4_filenamepattern = r4_folder^"/%Y-%m-%d/%Y-%m-%d-%H-%M."^r4_encoding
r0_is_recording = ref false
r1_is_recording = ref false
r2_is_recording = ref false
r3_is_recording = ref false
r4_is_recording = ref false
if r0_enable == true then
# enable recording status for that recorder
server.register(namespace="out_filesystem_0", "recording", fun (s) -> begin if !r0_is_recording == false then "false" else "true" end end)
# start the recorder
start_recorder(r0_folder, r0_duration, r0_encoding, r0_bitrate, r0_channels, r0_filenamepattern, r0_is_recording, output_source, "0")
end
if r1_enable == true then
server.register(namespace="out_filesystem_1", "recording", fun (s) -> begin if !r1_is_recording == false then "false" else "true" end end)
start_recorder(r1_folder, r1_duration, r1_encoding, r1_bitrate, r1_channels, r1_filenamepattern, r1_is_recording, output_source, "1")
end
if r2_enable == true then
server.register(namespace="out_filesystem_2", "recording", fun (s) -> begin if !r2_is_recording == false then "false" else "true" end end)
start_recorder(r2_folder, r2_duration, r2_encoding, r2_bitrate, r2_channels, r2_filenamepattern, r2_is_recording, output_source, "2")
end
if r3_enable == true then
server.register(namespace="out_filesystem_3", "recording", fun (s) -> begin if !r3_is_recording == false then "false" else "true" end end)
start_recorder(r3_folder, r3_duration, r3_encoding, r3_bitrate, r3_channels, r3_filenamepattern, r3_is_recording, output_source, "3")
end
if r4_enable == true then
server.register(namespace="out_filesystem_4", "recording", fun (s) -> begin if !r4_is_recording == false then "false" else "true" end end)
start_recorder(r4_folder, r4_duration, r4_encoding, r4_bitrate, r4_channels, r4_filenamepattern, r4_is_recording, output_source, "4")
end
\ No newline at end of file
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2017-2020 - The Aura Engine Team.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
if a0_out != "" then
ignore(get_output(output_source, a0_out, "lineout_0"))
end
if a1_out != "" then
ignore(get_output(output_source, a1_out, "lineout_1"))
end
if a2_out != "" then
ignore(get_output(output_source, a2_out, "lineout_2"))
end
if a3_out != "" then
ignore(get_output(output_source, a3_out, "lineout_3"))
end
if a4_out != "" then
ignore(get_output(output_source, a4_out, "lineout_4"))
#output_4 = ref output.dummy(blank())
#get_output(output_4, output_source, a4_out, "lineout_4")
#output_4 := get_output(output_source, a4_out, "lineout_4")
#get_output(output_source, a4_out, "aura_lineout_4")
end
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2017-2020 - The Aura Engine Team.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Output streaming settings
# What a mess...
s0_enable = list.assoc(default="", "stream_0", ini) == "y"
s1_enable = list.assoc(default="", "stream_1", ini) == "y"
s2_enable = list.assoc(default="", "stream_2", ini) == "y"
s3_enable = list.assoc(default="", "stream_3", ini) == "y"
s4_enable = list.assoc(default="", "stream_4", ini) == "y"
s0_encoding = list.assoc(default="", "stream_0_encoding", ini)
s0_bitrate = int_of_string(list.assoc(default="", "stream_0_bitrate", ini))
s0_host = list.assoc(default="", "stream_0_host", ini)
s0_port = int_of_string(list.assoc(default="", "stream_0_port", ini))
s0_user = list.assoc(default="", "stream_0_user", ini)
s0_pass = list.assoc(default="", "stream_0_password", ini)
s0_mount = list.assoc(default="", "stream_0_mountpoint", ini)
s0_url = list.assoc(default="", "stream_0_displayurl", ini)
s0_desc = list.assoc(default="", "stream_0_description", ini)
s0_genre = list.assoc(default="", "stream_0_genre", ini)
s0_name = list.assoc(default="", "stream_0_name", ini)
s0_channels = list.assoc(default="", "stream_0_channels", ini)
s1_encoding = list.assoc(default="", "stream_1_encoding", ini)
s1_bitrate = int_of_string(list.assoc(default="", "stream_1_bitrate", ini))
s1_host = list.assoc(default="", "stream_1_host", ini)
s1_port = int_of_string(list.assoc(default="", "stream_1_port", ini))
s1_user = list.assoc(default="", "stream_1_user", ini)
s1_pass = list.assoc(default="", "stream_1_password", ini)
s1_mount = list.assoc(default="", "stream_1_mountpoint", ini)
s1_url = list.assoc(default="", "stream_1_displayurl", ini)
s1_desc = list.assoc(default="", "stream_1_description", ini)
s1_genre = list.assoc(default="", "stream_1_genre", ini)
s1_name = list.assoc(default="", "stream_1_name", ini)
s1_channels = list.assoc(default="", "stream_1_channels", ini)
s2_encoding = list.assoc(default="", "stream_2_encoding", ini)
s2_bitrate = int_of_string(list.assoc(default="", "stream_2_bitrate", ini))
s2_host = list.assoc(default="", "stream_2_host", ini)
s2_port = int_of_string(list.assoc(default="", "stream_2_port", ini))
s2_user = list.assoc(default="", "stream_2_user", ini)
s2_pass = list.assoc(default="", "stream_2_password", ini)
s2_mount = list.assoc(default="", "stream_2_mountpoint", ini)
s2_url = list.assoc(default="", "stream_2_displayurl", ini)
s2_desc = list.assoc(default="", "stream_2_description", ini)
s2_genre = list.assoc(default="", "stream_2_genre", ini)
s2_name = list.assoc(default="", "stream_2_name", ini)
s2_channels = list.assoc(default="", "stream_2_channels", ini)
s3_encoding = list.assoc(default="", "stream_3_encoding", ini)
s3_bitrate = int_of_string(list.assoc(default="", "stream_3_bitrate", ini))
s3_host = list.assoc(default="", "stream_3_host", ini)
s3_port = int_of_string(list.assoc(default="", "stream_3_port", ini))
s3_user = list.assoc(default="", "stream_3_user", ini)
s3_pass = list.assoc(default="", "stream_3_password", ini)
s3_mount = list.assoc(default="", "stream_3_mountpoint", ini)
s3_url = list.assoc(default="", "stream_3_displayurl", ini)
s3_desc = list.assoc(default="", "stream_3_description", ini)
s3_genre = list.assoc(default="", "stream_3_genre", ini)
s3_name = list.assoc(default="", "stream_3_name", ini)
s3_channels = list.assoc(default="", "stream_3_channels", ini)
s4_encoding = list.assoc(default="", "stream_4_encoding", ini)
s4_bitrate = int_of_string(list.assoc(default="", "stream_4_bitrate", ini))
s4_host = list.assoc(default="", "stream_4_host", ini)
s4_port = int_of_string(list.assoc(default="", "stream_4_port", ini))
s4_user = list.assoc(default="", "stream_4_user", ini)
s4_pass = list.assoc(default="", "stream_4_password", ini)
s4_mount = list.assoc(default="", "stream_4_mountpoint", ini)
s4_url = list.assoc(default="", "stream_4_displayurl", ini)
s4_desc = list.assoc(default="", "stream_4_description", ini)
s4_genre = list.assoc(default="", "stream_4_genre", ini)
s4_name = list.assoc(default="", "stream_4_name", ini)
s4_channels = list.assoc(default="", "stream_4_channels", ini)
s0_connected = ref ''
s1_connected = ref ''
s2_connected = ref ''
s3_connected = ref ''
s4_connected = ref ''
if s0_enable == true then
# enable connection status for that stream
server.register(namespace="out_http_0", "connected", fun (s) -> begin !s0_connected end)
# aaand stream
stream_to_icecast("out_http_0", s0_encoding, s0_bitrate, s0_host, s0_port, s0_pass, s0_mount, s0_url, s0_desc, s0_genre, s0_user, output_source, "0", s0_connected, s0_name, s0_channels)
end
if s1_enable == true then
server.register(namespace="out_http_1", "connected", fun (s) -> begin !s1_connected end)
stream_to_icecast("out_http_1", s1_encoding, s1_bitrate, s1_host, s1_port, s1_pass, s1_mount, s1_url, s1_desc, s1_genre, s1_user, output_source, "1", s1_connected, s1_name, s1_channels)
end
if s2_enable == true then
server.register(namespace="out_http_2", "connected", fun (s) -> begin !s2_connected end)
stream_to_icecast("out_http_2", s2_encoding, s2_bitrate, s2_host, s2_port, s2_pass, s2_mount, s2_url, s2_desc, s2_genre, s2_user, output_source, "2", s2_connected, s2_name, s2_channels)
end
if s3_enable == true then
server.register(namespace="out_http_3", "connected", fun (s) -> begin !s3_connected end)
stream_to_icecast("out_http_3", s3_encoding, s3_bitrate, s3_host, s3_port, s3_pass, s3_mount, s3_url, s3_desc, s3_genre, s3_user, output_source, "3", s3_connected, s3_name, s3_channels)
end
if s4_enable == true then
server.register(namespace="out_http_4", "connected", fun (s) -> begin !s4_connected end)
stream_to_icecast("out_http_4", s4_encoding, s4_bitrate, s4_host, s4_port, s4_pass, s4_mount, s4_url, s4_desc, s4_genre, s4_user, output_source, "4", s4_connected, s4_name, s4_channels)
end
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2017-2020 - The Aura Engine Team.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
if bitrate == 24 then
# if stereo then
ignore(output_filesystem_stereo(%fdkaac(bitrate = 24, channels = 2), filenamepattern, !source))
# else
# ignore(output_filesystem_mono(%fdkaac(bitrate = 24, channels = 1), filenamepattern, mean(!source)))
# end
elsif bitrate == 32 then
# if stereo then
ignore(output_filesystem_stereo(%fdkaac(bitrate = 32, channels = 2), filenamepattern, !source))
# else
# ignore(output_filesystem_mono(%fdkaac(bitrate = 32, channels = 1), filenamepattern, mean(!source)))
# end
elsif bitrate == 48 then
# if stereo then
ignore(output_filesystem_stereo(%fdkaac(bitrate = 48, channels = 2), filenamepattern, !source))
# else
# ignore(output_filesystem_mono(%fdkaac(bitrate = 48, channels = 1), filenamepattern, mean(!source)))
# end
elsif bitrate == 64 then
# if stereo then
ignore(output_filesystem_stereo(%fdkaac(bitrate = 64, channels = 2), filenamepattern, !source))
# else
# ignore(output_filesystem_mono(%fdkaac(bitrate = 64, channels = 1), filenamepattern, mean(!source)))
# end
elsif bitrate == 96 then
# if stereo then
ignore(output_filesystem_stereo(%fdkaac(bitrate = 96, channels = 2), filenamepattern, !source))
# else
# ignore(output_filesystem_mono(%fdkaac(bitrate = 96, channels = 1), filenamepattern, mean(!source)))
# end
elsif bitrate == 128 then
# if stereo then
ignore(output_filesystem_stereo(%fdkaac(bitrate = 128, channels = 2), filenamepattern, !source))
# else
# ignore(output_filesystem_mono(%fdkaac(bitrate = 128, channels = 1), filenamepattern, mean(!source)))
# end
elsif bitrate == 160 then
# if stereo then
ignore(output_filesystem_stereo(%fdkaac(bitrate = 160, channels = 2), filenamepattern, !source))
# else
# ignore(output_filesystem_mono(%fdkaac(bitrate = 160, channels = 1), filenamepattern, mean(!source)))
# end
elsif bitrate == 192 then
# if stereo then
ignore(output_filesystem_stereo(%fdkaac(bitrate = 192, channels = 2), filenamepattern, !source))
# else
# ignore(output_filesystem_mono(%fdkaac(bitrate = 192, channels = 1), filenamepattern, mean(!source)))
# end
elsif bitrate == 224 then
# if stereo then
ignore(output_filesystem_stereo(%fdkaac(bitrate = 224, channels = 2), filenamepattern, !source))
# else
# ignore(output_filesystem_mono(%fdkaac(bitrate = 224, channels = 1), filenamepattern, mean(!source)))
# end
elsif bitrate == 256 then
# if stereo then
ignore(output_filesystem_stereo(%fdkaac(bitrate = 256, channels = 2), filenamepattern, !source))
# else
# ignore(output_filesystem_mono(%fdkaac(bitrate = 256, channels = 1), filenamepattern, mean(!source)))
# end
elsif bitrate == 320 then
# if stereo then
ignore(output_filesystem_stereo(%fdkaac(bitrate = 320, channels = 2), filenamepattern, !source))
# else
# ignore(output_filesystem_mono(%fdkaac(bitrate = 320, channels = 1), filenamepattern, mean(!source)))
# end
end
#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2017-2020 - The Aura Engine Team.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
if bitrate == 24 or bitrate == 32 or bitrate == 48 then
if stereo then
ignore(output_filesystem_stereo(%flac(samplerate=44100, channels = 2, compression = 7, bits_per_sample=8), filenamepattern, !source))
else
ignore(output_filesystem_mono(%flac(samplerate=44100, channels = 1, compression = 7, bits_per_sample=8), filenamepattern, mean(!source)))
end
elsif bitrate == 64 then
if stereo then
ignore(output_filesystem_stereo(%flac(samplerate=44100, channels = 2, compression = 7, bits_per_sample=8), filenamepattern, !source))
else
ignore(output_filesystem_mono(%flac(samplerate=44100, channels = 1, compression = 7, bits_per_sample=8), filenamepattern, mean(!source)))
end
elsif bitrate == 96 then
if stereo then
ignore(output_filesystem_stereo(%flac(samplerate=44100, channels = 2, compression = 7, bits_per_sample=8), filenamepattern, !source))
else
ignore(output_filesystem_mono(%flac(samplerate=44100, channels = 1, compression = 7, bits_per_sample=8), filenamepattern, mean(!source)))
end
elsif bitrate == 128 then
if stereo then
ignore(output_filesystem_stereo(%flac(samplerate=44100, channels = 2, compression = 6, bits_per_sample=16), filenamepattern, !source))
else
ignore(output_filesystem_mono(%flac(samplerate=44100, channels = 1, compression = 6, bits_per_sample=16), filenamepattern, mean(!source)))
end
elsif bitrate == 160 then
if stereo then
ignore(output_filesystem_stereo(%flac(samplerate=44100, channels = 2, compression = 5, bits_per_sample=16), filenamepattern, !source))
else
ignore(output_filesystem_mono(%flac(samplerate=44100, channels = 1, compression = 5, bits_per_sample=16), filenamepattern, mean(!source)))
end
elsif bitrate == 192 then
if stereo then
ignore(output_filesystem_stereo(%flac(samplerate=44100, channels = 2, compression = 4, bits_per_sample=16), filenamepattern, !source))
else
ignore(output_filesystem_mono(%flac(samplerate=44100, channels = 1, compression = 4, bits_per_sample=16), filenamepattern, mean(!source)))
end
elsif bitrate == 224 then
if stereo then
ignore(output_filesystem_stereo(%flac(samplerate=44100, channels = 2, compression = 3, bits_per_sample=32), filenamepattern, !source))
else
ignore(output_filesystem_mono(%flac(samplerate=44100, channels = 1, compression = 3, bits_per_sample=32), filenamepattern, mean(!source)))
end
elsif bitrate == 256 then
if stereo then
ignore(output_filesystem_stereo(%flac(samplerate=44100, channels = 2, compression = 2, bits_per_sample=32), filenamepattern, !source))
else
ignore(output_filesystem_mono(%flac(samplerate=44100, channels = 1, compression = 2, bits_per_sample=32), filenamepattern, mean(!source)))
end
elsif bitrate == 320 then
if stereo then
ignore(output_filesystem_stereo(%flac(samplerate=44100, channels = 2, compression = 1, bits_per_sample=32), filenamepattern, !source))
else
ignore(output_filesystem_mono(%flac(samplerate=44100, channels = 1, compression = 1, bits_per_sample=32), filenamepattern, mean(!source)))
end
end