DOMOSYS.ORG

TuxKatana

2025-08-29

#Code #Hack #GTK #Python #Dev #Guitare

TuxKatana

Introduction

Je possède un ampli guitare Boss Katana MK-II.

Les boutons d’interface permettent des réglages de base.

Mais en fait, il y a à l’intérieur tout un tas de programmes non référencés !

Pour les exploiter, il y a plusieurs applications :

Bilan : évidemment, pour faire tourner l’application officielle, sous Linux, on peut le faire avec wine, mais c’est quelque-chose que je ne fais plus : me battre avec des systèmes que j’aborre : j’ai abdiqué.

J’utilisais alors Librarian sur une tablette Android.

Mais le sujet est quelque peu le même : une tablette Android, on ne maîtrise rien du tout. Même si j’avoue que cette aplication est plutôt bien faite.

Quant à FxFloorBoard, d’abord son aspect m’est assez peu ragoutant, mais surtout, elle fait chauffer mon CPU pour des raisons que j’ignore.

Alors j’ai décidé de développer ma propre application en #Python.

La méthode

J’ai tout d’abord fait fonctionner FxFloorBoard sur mon PC, connecté en USB au Boss Katana.

Ensuite, j’ai utilisé WireShark pour littéralement sniffer les communications entre les deux.

Snif

J’ai ensuite fait du reverse-engineering sur les trames enregistrées, en faisant plusieurs scripts me permettant de relever les variations et corrélations, jusqu’à pouvoir en déduire le fonctionnement du protocole.

SysEx

Développement

Ceci fait, je n’ai plus eu qu’à créer des classes d’objet (notamment MIDI -128bits- en messages SysEx spécifiques)

class MIDI_Bytes
import logging
from lib.log_setup import LOGGER_NAME
log = logging.getLogger(LOGGER_NAME)

class MIDIBytes:
    def __init__(self, mb=None, length: int = None):
        if mb is None or mb == '':
            self.bytes = []
            return
        if isinstance(mb, int):
            if length is None:
                length = max(1, (mb.bit_length() + 6) // 7)
            mb = self.int_to_hexstring(mb, length)
        if isinstance(mb, list):
            mb = " ".join(f"{c:02X}" for c in mb).strip()
        parts = mb.strip().split()
        if not parts:
            self.bytes = []
            return
        try:
            vals = [int(p, 16) for p in parts]
        except ValueError:
            raise ValueError(f"Invalid Hex byte: {parts}")
        for v in vals:
            if v < 0 or v > 0x7F:
                raise ValueError(f"Invalid MIDI byte: 0 < {v} < 0x7F")
        if length and len(vals) < length:
            diff = length - len(vals)
            vals = [0]*diff + vals
            self.length = length
        self.bytes = vals

    def __str__(self):
        return " ".join(f"{b:02X}" for b in self.bytes)

    def __repr__(self):
        return f"{self.__class__.__name__}('{self.__str__()}')"

    def __eq__(self, other):
        if not isinstance(other, (Address,MIDIBytes,)):
            return NotImplemented
        return self.bytes == other.bytes

    def __lt__(self, other):
        if not isinstance(other, MIDIBytes):
            return NotImplemented
        return self.to_int() < other.to_int()

    def __le__(self, other):
        if not isinstance(other, MIDIBytes):
            return NotImplemented
        return self.to_int() <= other.to_int()

    def __gt__(self, other):
        if not isinstance(other, MIDIBytes):
            return NotImplemented
        return self.to_int() > other.to_int()

    def __ge__(self, other):
        if not isinstance(other, MIDIBytes):
            return NotImplemented
        return self.to_int() >= other.to_int()

    def __add__(self, other):
        if not isinstance(other, (int, MIDIBytes,)):
            return NotImplemented
        else:
            if isinstance(other, int):
                value = self.to_int() + other
                return MIDIBytes.from_int(value, len(self.bytes))
            else:
                return MIDIBytes(self.bytes + other.bytes)

    def __getitem__(self, i):
        return self.bytes[i]

    def __setitem__(self, i, value):
        if isinstance(i, slice):
            if isinstance(value, MIDIBytes):
                self.bytes[i] = value.bytes
            elif isinstance(value, list):
                self.bytes[i] = value
            else:
                raise TypeError("slice assignment requires list or MIDIBytes")
        else:
            if isinstance(value, int):
                if not (0 <= value <= 0x7F):
                    raise ValueError("MIDI byte must be in 0..127")
                self.bytes[i] = value
            else:
                raise TypeError("single item must be int")

    def __len__(self):
        return len(self.bytes)

    def __iter__(self):
        return iter(self.bytes)

    @property
    def int(self)  int:
        return self.to_int()

    @property
    def bool(self)  bool:
        if len(self.bytes) > 1:
            raise ValueError(f"Cannot convert multi-byte {self} to bool")
        if len(self.bytes) == 0:
            log.warning(f"midi_bytes.py:108- len(self.bytes) == 0")
            return False
        return self.bytes[0] != 0

    @property
    def str(self)  str:
        return str(self)

    def to_gtype(self, val_type: str):
        # log.debug(f"{val_type=}")
        if val_type == 'gboolean':
            return self.bool
        elif val_type == 'gint':
            return self.int
        elif val_type == 'gchararray':
            return self.str
        elif val_type == 'gdouble':
            return float(self.int)
        else:
            log.warning(f"midi_bytes.py:127-Not recognized GType: {val_type}")
            return self

    def to_int(self)  int:
        value = 0
        n = len(self.bytes)
        for i, b in enumerate(self.bytes):
            shift = 7 * (n - 1 - i)
            value |= (b & 0x7F) << shift
        return value

    def to_chars(self)  str:
        chars = ''.join(chr(b) for b in self.bytes)
        return chars

    @staticmethod
    def int_to_hexstring(value: int, length: int)  str:
        if not isinstance(value, int):
            raise TypeError("value must be int")
        if value < 0:
            raise ValueError("value must be >= 0")
        maxval = 1 << (7 * length)
        if value >= maxval:
            raise OverflowError(f"value {value} not containing in {length} 7bits bytes")

        parts = []
        for i in range(length):
            shift = 7 * (length - 1 - i)
            parts.append((value >> shift) & 0x7F)
        return " ".join(f"{b:02X}" for b in parts)

    @classmethod
    def from_int(cls, value: int, length: int = None):
        if length is None:
            length = max(1, (value.bit_length() + 6) // 7)
        return cls(cls.int_to_hexstring(value, length))

class Address(MIDIBytes):
    def __init__(self, addr=None):
        if isinstance(addr, Address):
            log.debug(f"Double declaration of {addr}")
            return
        super().__init__(addr, length=4)

    def __add__(self, other):
        if not isinstance(other, (int, MIDIBytes,)):
            return NotImplemented
        if isinstance(other, int):
            value = self.to_int() + other
            return Address.from_int(value, len(self.bytes))
        if isinstance(other, MIDIBytes):
            return MIDIBytes(self.bytes + other.bytes)

    def __hash__(self):
        return self.int

    def __sub__(self, other):
        if isinstance(other, Address):
            val_s = self.to_int()
            val_o = other.to_int()
            return abs(val_s - val_o)
        elif isinstance(other, int):
            value = self.to_int() - other
            return Address.from_int(value, len(self.bytes))
        return NotImplemented

le plus pénible en fait, c’est surtout l’organisation de l’interface, et le passage de messages entre classes, pour la prise en compte de tous les changements en messages Glib.

Work Bench

Disclaimer

Je n’ai pas totalement terminé : les enregistrement de presets ne sont pas fonctionnels, mais je m’en sers régulièrement en direct, pour me faire des réglages sur le vif, et ça fonctionne plutôt bien !

le petit plus miraculeux, étant le Tuner intégré !

Publication

Screenshot

le code est ici : https://framagit.org/s4mdf0o1/TuxKatana

et la doc générée par IA, ici : https://deepwiki.com/s4mdf0o1/TuxKatana

Il y a bien une vidéo de démo, mais de très très mauvaise qualité. Je voulais juste tenter, c’est pas du tout un truc qui m’amuse, donc… Peut-être que je retenterai un jour…

Postface

le code du Tuner, qui peut être utilisé à part, est ici : Guit_tunix

(j’en avais fait une version dans le terminal)