diff --git a/modules/core/mixer.py b/modules/core/mixer.py
new file mode 100644
index 0000000000000000000000000000000000000000..5db6e28c8ba47a9ade79a24cb75ff69efe9ada6c
--- /dev/null
+++ b/modules/core/mixer.py
@@ -0,0 +1,356 @@
+
+#
+# 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 time
+
+from enum                       import Enum
+from modules.base.exceptions    import LQConnectionError
+from modules.base.utils         import SimpleUtil as SU
+
+
+
+class MixerType(Enum):
+    """
+    Types of mixers mapped to the Liquidsoap mixer ids.
+    """
+    MAIN = "mixer"
+    FALLBACK = "mixer_fallback"
+
+
+
+class Mixer():
+    """
+    A virtual mixer.
+    """
+    config = None
+    logger = None
+    connector = None
+    mixer_id = None
+    channels = None
+    fade_in_active = None
+    fade_out_active = None
+
+
+    def __init__(self, config, mixer_id, connector):
+        """
+        Constructor
+
+        Args:
+            config (AuraConfig):    The configuration
+        """
+        self.config = config
+        self.logger = logging.getLogger("AuraEngine")
+        self.mixer_id = mixer_id
+        self.fade_in_active = None
+        self.fade_out_active = None
+        self.connector = connector
+        self.mixer_initialize()
+
+
+    #
+    #   Mixer
+    #
+
+
+    def mixer_initialize(self):
+        """
+            - Pull all faders down to volume 0.
+            - Initialize default channels per type
+        """
+        self.connector.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.connector.disable_transaction()
+
+
+    def mixer_status(self):
+        """
+        Returns the state of all mixer channels
+        """
+        cnt = 0
+        inputstate = {}
+
+        self.connector.enable_transaction()
+        inputs = self.mixer_channels()
+
+        for channel in inputs:
+            inputstate[channel] = self.channel_status(cnt)
+            cnt = cnt + 1
+
+        self.connector.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.connector.send_lqc_command(self.mixer_id.value, "mixer_inputs")
+        return self.channels
+
+
+    def mixer_channels_selected(self):
+        """
+        Retrieves all selected channels of the mixer.
+        """
+        cnt = 0
+        activeinputs = []
+
+        self.connector.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.connector.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()
+
+
+    def channel_status(self, channel_number):
+        """
+        Retrieves the status of a channel identified by the channel number.
+        """
+        return self.connector.send_lqc_command(self.mixer_id.value, "mixer_status", channel_number)
+
+
+    #
+    #   Channel
+    #
+
+
+    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.connector.send_lqc_command(self.mixer_id.value, "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.connector.send_lqc_command(self.mixer_id.value, "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 = f"Cannot set volume of channel '{channel}' to {str(volume)}. Reason: {str(e)}"
+            self.logger.error(SU.red(msg))
+            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.connector.send_lqc_command(self.mixer_id.value, "mixer_volume", str(index), str(int(volume)))
+
+                if not self.connector.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 + " has gone wrong! Liquidsoap message: " + message)
+                        self.logger.warning(msg)
+
+                return message
+        except AttributeError as e: #(LQConnectionError, AttributeError):
+            self.connector.disable_transaction(force=True)
+            msg = SU.red("Ran into exception when setting volume of channel " + channel + ". Reason: " + str(e))
+            self.logger.error(msg)
+
+
+
+    #
+    #   Fading
+    #
+
+
+    def fade_in(self, channel, volume):
+        """
+        Performs a fade-in for the given channel.
+
+        Args:
+            channel (Channel):  The channel to fade
+            volume (Integer):   The target volume
+        
+        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 = volume
+
+                step = fade_in_time / target_volume
+
+                msg = "Starting to fading-in '%s'. Step is %ss and target volume is %s." % \
+                    (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.connector.disable_logging = True
+                self.connector.client.disable_logging = True
+
+                for i in range(target_volume):
+                    self.channel_volume(channel.value, i + 1)
+                    time.sleep(step)
+
+                msg = "Finished with fading-in '%s'." % channel
+                self.logger.info(SU.pink(msg))
+
+                self.fade_in_active = False
+                if not self.fade_out_active:
+                    self.connector.disable_logging = False
+                    self.connector.client.disable_logging = False
+
+        except LQConnectionError as e:
+            self.logger.critical(str(e))
+            return False
+        return True
+
+
+
+    def fade_out(self, channel, volume):
+        """
+        Performs a fade-out for the given channel.
+        
+        Args:
+            channel (Channel):  The channel to fade
+            volume (Integer):   The start volume
+        
+        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) / volume
+
+                msg = "Starting to fading-out '%s'. Step is %ss." % (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.connector.disable_logging = True
+                self.connector.client.disable_logging = True
+
+                for i in range(volume):
+                    self.channel_volume(channel.value, volume-i-1)
+                    time.sleep(step)
+
+                msg = "Finished with fading-out '%s'" % channel
+                self.logger.info(SU.pink(msg))
+
+                # Enable logging again
+                self.fade_out_active = False
+                if not self.fade_in_active:
+                    self.connector.disable_logging = False
+                    self.connector.client.disable_logging = False
+
+        except LQConnectionError as e:
+            self.logger.critical(str(e))
+            return False
+        return True
\ No newline at end of file