From 21115c53f1cdc179f5fe55a38724abba208cda7d Mon Sep 17 00:00:00 2001
From: Loxbie <ole@freirad.at>
Date: Tue, 2 Apr 2024 15:47:06 +0200
Subject: [PATCH] Refactor: Replace hardcoded I/O with unit-based approach

This replaces the code for most of the in and output definitions. The are now read through a yaml config file where they are defined as a list of devices/endpoints. #71, #60, #41, #36
---
 src/engine.liq          |   3 +-
 src/library.liq         |  49 +++---
 src/out_soundcard.liq   |  39 ++---
 src/out_stream.liq      | 321 +++++++++++++---------------------------
 src/serverfunctions.liq |   5 +-
 5 files changed, 140 insertions(+), 277 deletions(-)

diff --git a/src/engine.liq b/src/engine.liq
index 6205b98..4ac9053 100644
--- a/src/engine.liq
+++ b/src/engine.liq
@@ -83,8 +83,7 @@ output_source = attach_fallback_source(stripped_stream)
 %include "out_soundcard.liq"
 
 # stream output
-# FIXME: this needs to be refactored
-# %include "out_stream.liq"
+%include "out_stream.liq"
 
 # enable socket functions
 %include "serverfunctions.liq"
diff --git a/src/library.liq b/src/library.liq
index a9ec263..65a4cbe 100644
--- a/src/library.liq
+++ b/src/library.liq
@@ -38,6 +38,7 @@ end
 # stream to icecast #
 #####################
 
+# FIXME: this function is deprecated and could be removed?
 def stream_to_icecast(
   id,
   encoding,
@@ -57,7 +58,6 @@ def stream_to_icecast(
   channels
 ) =
   source = ref(stream)
-
   def on_error(msg) =
     connected := "false"
     log(msg)
@@ -73,40 +73,25 @@ def stream_to_icecast(
   user_ref = ref(user)
   if user == "" then user_ref := "source" end
 
-  # TODO Refactor all outgoing stream formats this way
-  let stereo = (int_of_string(channels) >= 2)
-  let format = %vorbis(stereo = true)
-  # let format = %mp3(bitrate = 128, stereo = true)
-  # FIXME: the format is never overwritten, it is alwas mp3
+  snd_icy_metadata = ref(false)
+
+  enc = ref(%vorbis(stereo = true))
 
-  if encoding == "mp3" then%include "outgoing_streams/mp3.liq" end
-  if encoding == "ogg" then%include "outgoing_streams/ogg.liq" end
+  # TODO: move this into out_stream.liq?
+  # if encoding == "ogg" then%include "outgoing_streams/ogg.liq" end
+  # https://github.com/savonet/liquidsoap/pull/1858
+  if
+    encoding == "mp3"
+  then
+    enc := %mp3(stereo = true)
+    snd_icy_metadata := true
+  end
+  # if encoding == "ogg" then%include "outgoing_streams/ogg.liq" end
 
   log(
-    "Icecast output format: #{encoding} #{bitrate} - #{format}"
+    "Icecast output format: #{encoding} #{bitrate} - #{enc()}"
   )
 
-  # Liquidsoap cannot handle one output definition for mono and stereo
-  # FIXME should be working since Liquidsoap 2
-  # 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,
-  #     send_icy_metadata=true,
-  #     format,
-  #     source()
-  #   )
   output_icecast_stereo =
     output.icecast(
       id=id,
@@ -122,8 +107,8 @@ def stream_to_icecast(
       user=user_ref(),
       on_error=on_error,
       on_connect=on_connect,
-      send_icy_metadata=true,
-      format,
+      send_icy_metadata=snd_icy_metadata(),
+      enc(),
       source()
     )
 
diff --git a/src/out_soundcard.liq b/src/out_soundcard.liq
index b210a27..3a19013 100644
--- a/src/out_soundcard.liq
+++ b/src/out_soundcard.liq
@@ -16,27 +16,18 @@
 # 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
\ No newline at end of file
+playout_count = ref(0)
+lo_list = ref([])
+
+# create a playout source
+# Note: this could be improved by providing more information about
+# the device itself via the config file
+def create_playout(output_device) =
+  out =
+    get_output(output_source, output_device.name, "lineout_#{playout_count()}")
+  lo_list := [...lo_list(), "linout_#{playout_count()}"]
+  playout_count := playout_count() + 1
+end
+
+# iterate over every output unit
+list.iter(create_playout, config.audio.devices.output)
diff --git a/src/out_stream.liq b/src/out_stream.liq
index b8cc253..6cc45d9 100644
--- a/src/out_stream.liq
+++ b/src/out_stream.liq
@@ -18,96 +18,65 @@
 
 # Output streaming settings
 # What a mess...
-s0_encoding =
-  get_setting("ogg", "stream_0_encoding", "AURA_ENGINE_STREAM_OUTPUT_ENCODING")
-s0_bitrate =
-  int_of_string(
-    get_setting("192", "stream_0_bitrate", "AURA_ENGINE_STREAM_OUTPUT_BITRATE")
-  )
-s0_host = get_setting("", "stream_0_host", "AURA_ENGINE_STREAM_OUTPUT_HOST")
-s0_port =
-  int_of_string(
-    get_setting("0", "stream_0_port", "AURA_ENGINE_STREAM_OUTPUT_PORT")
-  )
-s0_user = get_setting("", "stream_0_user", "AURA_ENGINE_STREAM_OUTPUT_USER")
-s0_pass =
-  get_setting("", "stream_0_password", "AURA_ENGINE_STREAM_OUTPUT_PASSWORD")
-s0_mount =
-  get_setting("", "stream_0_mountpoint", "AURA_ENGINE_STREAM_OUTPUT_MOUNTPOINT")
-s0_url = get_setting("", "stream_0_url", "AURA_ENGINE_STREAM_OUTPUT_URL")
-s0_desc =
-  get_setting(
-    "", "stream_0_description", "AURA_ENGINE_STREAM_OUTPUT_DESCRIPTION"
-  )
-s0_genre = get_setting("", "stream_0_genre", "AURA_ENGINE_STREAM_OUTPUT_GENRE")
-s0_name = get_setting("", "stream_0_name", "AURA_ENGINE_STREAM_OUTPUT_NAME")
-s0_channels =
-  get_setting("", "stream_0_channels", "AURA_ENGINE_STREAM_OUTPUT_CHANNELS")
-
-# 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_url", 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_url", 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_url", 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)
+# s0_encoding =
+#   get_setting("ogg", "stream_0_encoding", "AURA_ENGINE_STREAM_OUTPUT_ENCODING")
+# s0_bitrate =
+#   int_of_string(
+#     get_setting("192", "stream_0_bitrate", "AURA_ENGINE_STREAM_OUTPUT_BITRATE")
+#   )
+# s0_host = get_setting("", "stream_0_host", "AURA_ENGINE_STREAM_OUTPUT_HOST")
+# s0_port =
+#   int_of_string(
+#     get_setting("0", "stream_0_port", "AURA_ENGINE_STREAM_OUTPUT_PORT")
+#   )
+# s0_user = get_setting("", "stream_0_user", "AURA_ENGINE_STREAM_OUTPUT_USER")
+# s0_pass =
+#   get_setting("", "stream_0_password", "AURA_ENGINE_STREAM_OUTPUT_PASSWORD")
+# s0_mount =
+#   get_setting("", "stream_0_mountpoint", "AURA_ENGINE_STREAM_OUTPUT_MOUNTPOINT")
+# s0_url = get_setting("", "stream_0_url", "AURA_ENGINE_STREAM_OUTPUT_URL")
+# s0_desc =
+#   get_setting(
+#     "", "stream_0_description", "AURA_ENGINE_STREAM_OUTPUT_DESCRIPTION"
+#   )
+# s0_genre = get_setting("", "stream_0_genre", "AURA_ENGINE_STREAM_OUTPUT_GENRE")
+# s0_name = get_setting("", "stream_0_name", "AURA_ENGINE_STREAM_OUTPUT_NAME")
+# s0_channels =
+#   get_setting("", "stream_0_channels", "AURA_ENGINE_STREAM_OUTPUT_CHANNELS")
 
-# 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_url", 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)
+# FIXME: this should move into the list of streams
+# What is this used for anyway?
+s0_connected = ref("")
 
-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
+# number of streams
+stream_count = ref(0)
+
+# list of streams
+stream_list = ref([])
+
+def create_stream(stream) =
+  let url = string(stream.url)
+  enc = ref(%vorbis(stereo = true))
+
+  # set icy_metadata accoring to the encoder (enc) to send
+  # metadata via icecast
+  snd_icy_metadata = ref(false)
+  if
+    stream.encoding == "mp3"
+  then
+    enc := %mp3(stereo = true)
+    snd_icy_metadata := true
+  end
+
+  # register a server function for every stream
+  # TODO: what is this used for?
   server.register(
-    namespace="out_http_0",
+    namespace="out_http_#{stream_count()}",
     "connected",
     fun (s) ->
       begin
@@ -116,152 +85,74 @@ then
       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
+  # create a list of streams to keep track of the created streams
+  # if
+  #   list.assoc.mem(url, stream_list())
+  # then
+  #   "Stream for url #{url} already exists!"
+  # else
+  out_stream =
+    output.icecast(
+      id="out_http_#{stream_count()}",
+      host=stream.host,
+      port=int_of_float(stream.port),
+      password=stream.password,
+      mount=stream.mountpoint,
+      fallible=true,
+      url=stream.url,
+      description=stream.description,
+      name=stream.name,
+      genre=stream.genre,
+      user=stream.user,
+      send_icy_metadata=snd_icy_metadata(),
+      enc(),
+      output_source
+    )
+  stream_count := stream_count() + 1
+
+  # append the new stream to the list of streams, the key is the url
+  # if we could alter the unit (stream) with all its values we could
+  # save all of this in the stream unit itself
+  stream_list := [...stream_list(), (url, out_stream.shutdown)]
+
+  print(
+    "Stream: #{url}"
   )
 end
 
-# if
-#   s1_enable == true
-# then
+# def create_stream(stream) =
 #   server.register(
-#     namespace="out_http_1",
+#     namespace="out_http_#{stream_count()}",
 #     "connected",
 #     fun (s) ->
 #       begin
 #         ignore(s)
-#         s1_connected()
+#         s0_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,
+#     "out_http_#{stream_count()}",
+#     stream.encoding,
+#     int_of_string(stream.bitrate),
+#     stream.host,
+#     int_of_float(stream.port),
+#     stream.password,
+#     stream.mount,
+#     stream.url,
+#     stream.desc,
+#     stream.genre,
+#     stream.user,
 #     output_source,
-#     "1",
-#     s1_connected,
-#     s1_name,
-#     s1_channels
+#     "#{stream_count()}",
+#     s0_connected,
+#     stream.name,
+#     stream.channels
 #   )
-# end
-
-# if
-#   s2_enable == true
-# then
-#   server.register(
-#     namespace="out_http_2",
-#     "connected",
-#     fun (s) ->
-#       begin
-#         ignore(s)
-#         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
-#         ignore(s)
-#         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
+#   stream_count := stream_count() + 1
+#   print(
+#     "Registered stream #{stream.url}"
 #   )
 # end
 
-
-# if
-#   s4_enable == true
-# then
-#   server.register(
-#     namespace="out_http_4",
-#     "connected",
-#     fun (s) ->
-#       begin
-#         ignore(s)
-#         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
\ No newline at end of file
+# itterate over all stream units (read from the yaml config)
+list.iter(create_stream, config.stream)
diff --git a/src/serverfunctions.liq b/src/serverfunctions.liq
index d235f89..24b269f 100644
--- a/src/serverfunctions.liq
+++ b/src/serverfunctions.liq
@@ -134,15 +134,12 @@ server.register(
       #   s4_enable
       #   ? list.add(("out_line_4", ("connected", "#{s4_connected()}")), so) : so
 
-      lo = []
-      lo = a0_out != '' ? list.add("out_line_0", lo) : lo
-
       # lo = a1_out != '' ? list.add("out_line_1", lo) : lo
       # lo = a2_out != '' ? list.add("out_line_2", lo) : lo
       # lo = a3_out != '' ? list.add("out_line_3", lo) : lo
       # lo = a4_out != '' ? list.add("out_line_4", lo) : lo
       json_data = json()
-      json_data.add("line", lo)
+      json_data.add("line", lo_list())
       json_data.add("stream", so)
       json.stringify(json_data)
     end
-- 
GitLab