~martijnbraam/pyatem

bf860dd902533dee06969ac80ed57ee83a2ee1dc — Martijn Braam a month ago 72b2769
Add audio levels for atem mixer
5 files changed, 142 insertions(+), 2 deletions(-)

M gtk_switcher/atemwindow.py
M gtk_switcher/audio.py
M pyatem/command.py
M pyatem/field.py
M pyatem/protocol.py
M gtk_switcher/atemwindow.py => gtk_switcher/atemwindow.py +6 -0
@@ 345,6 345,8 @@ class AtemWindow(SwitcherPage, MediaPage, AudioPage, CameraPage):
            self.on_dkey_properties_base_change(data)
        elif field == 'macro-properties':
            self.on_macro_properties_change(data)
        elif field == 'audio-meter-levels':
            self.on_audio_meter_levels_change(data)
        else:
            if field == 'time':
                return


@@ 375,6 377,10 @@ class AtemWindow(SwitcherPage, MediaPage, AudioPage, CameraPage):
        page = widget.get_visible_child_name()
        if page == 'media':
            self.on_page_media_open()
        if page == 'audio':
            self.enable_levels()
        else:
            self.disable_levels()

#    @field('input-properties')
#    def on_input_properties_changed(self, data):

M gtk_switcher/audio.py => gtk_switcher/audio.py +48 -2
@@ 4,7 4,7 @@ from gtk_switcher.adjustmententry import AdjustmentEntry
from gtk_switcher.dial import Dial
from gtk_switcher.gtklogadjustment import LogAdjustment
from pyatem.command import AudioInputCommand, FairlightStripPropertiesCommand, FairlightMasterPropertiesCommand, \
    AudioMasterPropertiesCommand, AudioMonitorPropertiesCommand
    AudioMasterPropertiesCommand, AudioMonitorPropertiesCommand, SendAudioLevelsCommand

gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib, GObject, Gio, Gdk


@@ 28,6 28,7 @@ class AudioPage:
        self.audio_on = {}
        self.audio_afv = {}
        self.audio_monitor = {}
        self.vu = {}

        self.master_level = None
        self.master_afv = None


@@ 189,7 190,10 @@ class AudioPage:
                vu_left = Gtk.ProgressBar()
                vu_right = Gtk.ProgressBar()
                vu_left.set_orientation(Gtk.Orientation.VERTICAL)
                vu_left.set_inverted(True)
                vu_right.set_orientation(Gtk.Orientation.VERTICAL)
                vu_right.set_inverted(True)
                self.vu[strip_id] = (vu_left, vu_right)
                volume_box.pack_start(vu_left, False, True, 0)
                volume_box.pack_start(vu_right, False, True, 0)
                self.audio_channels.attach(volume_frame, strip_index + c, 5, 1, 1)


@@ 293,7 297,10 @@ class AudioPage:
            vu_left = Gtk.ProgressBar()
            vu_right = Gtk.ProgressBar()
            vu_left.set_orientation(Gtk.Orientation.VERTICAL)
            vu_left.set_inverted(True)
            vu_right.set_orientation(Gtk.Orientation.VERTICAL)
            vu_right.set_inverted(True)
            self.vu['master'] = (vu_left, vu_right)
            volume_box.pack_start(vu_left, False, True, 0)
            volume_box.pack_start(vu_right, False, True, 0)
            self.audio_channels.attach(volume_frame, strip_index + c, 5, 1, 1)


@@ 312,10 319,16 @@ class AudioPage:
            monitoring_wrap.add(monitoring_frame)
            self.audio_channels.attach(monitoring_wrap, strip_index + c, 6, 1, 2)
            monitor_grid = Gtk.Grid()
            monitor_grid.set_column_spacing(4)
            monitor_grid.set_row_spacing(4)
            monitor_grid.set_margin_top(2)
            monitor_grid.set_margin_bottom(4)
            monitor_grid.set_margin_start(6)
            monitor_grid.set_margin_end(6)
            monitoring_frame.add(monitor_grid)
            monitor_dial = Dial()
            monitor_dial.set_adjustment(self.monitor_level)
            monitor_grid.attach(label, 0, 0, 2, 1)
            monitor_grid.attach(label, 0, 0, 3, 1)
            monitor_grid.attach(monitor_dial, 0, 1, 2, 1)

            self.monitor_on = Gtk.Button(label="ON")


@@ 326,6 339,17 @@ class AudioPage:
            self.monitor_dim.connect('clicked', self.do_monitor_dim)
            monitor_grid.attach(self.monitor_on, 0, 2, 1, 1)
            monitor_grid.attach(self.monitor_dim, 1, 2, 1, 1)
            vu_left = Gtk.ProgressBar()
            vu_right = Gtk.ProgressBar()
            vu_left.set_orientation(Gtk.Orientation.VERTICAL)
            vu_left.set_inverted(True)
            vu_right.set_orientation(Gtk.Orientation.VERTICAL)
            vu_right.set_inverted(True)
            vu_box = Gtk.Box()
            vu_box.pack_start(vu_left, False, False, 0)
            vu_box.pack_start(vu_right, False, False, 0)
            self.vu['monitor'] = (vu_left, vu_right)
            monitor_grid.attach(vu_box, 2, 1, 1, 2)

        self.apply_css(self.audio_channels, self.provider)
        self.audio_channels.show_all()


@@ 531,3 555,25 @@ class AudioPage:
        self.monitor_on.set_sensitive(data.enabled)
        self.monitor_dim.set_sensitive(data.enabled)
        self.model_changing = False

    def enable_levels(self):
        """ The audio page was opened, request data for the levels """
        if self.mixer == 'atem':
            cmd = SendAudioLevelsCommand(True)
            self.connection.mixer.send_commands([cmd])

    def disable_levels(self):
        """ The audio page was closed, stop getting the realtime levels """
        if self.mixer == 'atem':
            cmd = SendAudioLevelsCommand(False)
            self.connection.mixer.send_commands([cmd])

    def on_audio_meter_levels_change(self, data):
        self.vu['master'][0].set_fraction((data.master[0] + 60) / 60)
        self.vu['master'][1].set_fraction((data.master[1] + 60) / 60)
        self.vu['monitor'][0].set_fraction((data.monitor[0] + 60) / 60)
        self.vu['monitor'][1].set_fraction((data.monitor[1] + 60) / 60)
        for strip in data.input:
            strip_id = f'{strip}.0'
            self.vu[strip_id][0].set_fraction((data.input[strip][0] + 60) / 60)
            self.vu[strip_id][1].set_fraction((data.input[strip][1] + 60) / 60)

M pyatem/command.py => pyatem/command.py +25 -0
@@ 1962,3 1962,28 @@ class TransferAckCommand(Command):
    def get_command(self):
        data = struct.pack('>HH', self.transfer, self.slot)
        return self._make_command('FTUA', data)


class SendAudioLevelsCommand(Command):
    """
    Implementation of the `SALN` command. This is an acknowledgement for FTDa packets.

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      1    bool   Enable sending levels
    1      3    ?      unknown
    ====== ==== ====== ===========

    """

    def __init__(self, enable):
        """
        :param transfer: Unique transfer number
        :param slot: Slot index
        """
        self.enable = enable

    def get_command(self):
        data = struct.pack('>? 3x', self.enable)
        return self._make_command('SALN', data)

M pyatem/field.py => pyatem/field.py +62 -0
@@ 1,5 1,6 @@
import colorsys
import struct
import math

from hexdump import hexdump



@@ 2280,3 2281,64 @@ class MacroPropertiesField(FieldBase):
    def __repr__(self):
        return '<macro-properties: index={} used={} name={}>'.format(self.index, self.is_used,
                                                                     self.name)


class AudioMeterLevelsField(FieldBase):
    """
    Data from the `AMLv`. This contains the realtime audio levels for the audio mixer

    ====== ==== ====== ===========
    Offset Size Type   Description
    ====== ==== ====== ===========
    0      2    u16    Macro slot index
    ====== ==== ====== ===========

    After parsing:

    :ivar index: Macro slot index
    :ivar is_used: Slot contains data
    :ivar is_invalid: Slot contains invalid data
    :ivar name: Name of the macro
    :ivar description: Description of the macro
    """

    CODE = "AMLv"

    def __init__(self, raw):
        self.raw = raw
        field = struct.unpack_from('>H2x 4I 4I', raw, 0)
        self.count = field[0]
        self.master = (
                self._level(field[1]),
                self._level(field[2]),
                self._level(field[3]),
                self._level(field[4])
            )
        self.monitor = (
                self._level(field[5]),
                self._level(field[6]),
                self._level(field[7]),
                self._level(field[8])
            )
        self.input = {}
        offset = struct.calcsize('>H2x 4I 4I')
        sources = struct.unpack_from('>{}H'.format(self.count), raw, offset)
        offset = int(math.ceil((offset + (2 * self.count)) / 4.0) * 4)
        field = struct.unpack_from('>{}I'.format(self.count * 4), raw, offset)
        for i in range(0, self.count * 4, 4):
            level = (
                self._level(field[i]),
                self._level(field[i + 1]),
                self._level(field[i + 2]),
                self._level(field[i + 3])
            )
            self.input[sources[i // 4]] = level

    def _level(self, value):
        if value == 0:
            return -60
        val = math.log10(value / (128 * 65536)) * 20
        return val

    def __repr__(self):
        return '<audio-meter-levels count={}>'.format(self.count)

M pyatem/protocol.py => pyatem/protocol.py +1 -0
@@ 153,6 153,7 @@ class AtemProtocol:
            'LKST': 'lock-state',
            'FTDE': 'file-transfer-error',
            'FTDC': 'file-transfer-data-complete',
            'AMLv': 'audio-meter-levels'
        }

        fieldname_to_unique = {