summaryrefslogtreecommitdiff
path: root/tools/audio/extraction/envelope.py
blob: 0dab8c8d993c15ca61ee301b29b1335966dead0c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# SPDX-FileCopyrightText: © 2024 ZeldaRET
# SPDX-License-Identifier: CC0-1.0
#
#   Implements envelopes and envelope point structures
#

import collections

from .util import XMLWriter

class EnvDelay(int):
    def __str__(self):
        return {
             0 : "ADSR_DISABLE",
            -1 : "ADSR_HANG",
            -2 : "ADSR_GOTO",
            -3 : "ADSR_RESTART",
        }.get(self, super().__str__())

class Envelope:
    """
    Array of envelope points
    """

    class EnvelopePoint:
        """
        typedef struct {
            /* 0x0 */ s16 delay;
            /* 0x2 */ s16 arg;
        } EnvelopePoint; // size = 0x4
        """

        def __init__(self, delay, arg):
            self.delay = EnvDelay(delay)
            self.arg = arg

        def __repr__(self):
            return str(self)

        def __str__(self):
            return f"{{ {self.delay}, {self.arg} }}"

        def is_disable(self):
            return self.delay == 0 and self.arg == 0

        def is_hang(self):
            return self.delay == -1 and self.arg == 0

        def to_xml(self, xml : XMLWriter):
            if self.delay == 0: # Disable
                assert self.arg == 0
                xml.write_element("Disable")
            elif self.delay == -1: # Hang
                assert self.arg == 0
                xml.write_element("Hang")
            elif self.delay == -2: # Goto
                xml.write_element("Goto",
                    { "Arg" : self.arg }
                )
            elif self.delay == -3: # Restart
                assert self.arg == 0
                xml.write_element("Restart")
            else:
                assert self.delay >= 0
                xml.write_element("Point",
                    {
                        "Delay" : self.delay,
                        "Arg"   : self.arg,
                    }
                )

    def __init__(self, points, is_zero=False):
        self.name = None # Assigned when bank is finalized

        self.is_zero = is_zero
        self.release_rates = []
        self._release_rate = None # cached

        assert len(points) != 0
        assert type(points[0]) == Envelope.EnvelopePoint
        self.points = points

        if not self.is_zero:
            while self.points[-1].is_disable():
                self.points.pop()

            assert self.points[-1].is_hang()

    def __str__(self):
        out = "{\n"
        out += "    " + ", ".join([str(point) for point in self.points]) + "\n"
        out += "}\n"
        return out

    def release_rate(self):
        if self._release_rate is not None:
            return self._release_rate

        rates = collections.Counter(self.release_rates).most_common()
        assert len(rates) in [0, 1], rates # TODO handle ties?

        self._release_rate = 0 if len(rates) == 0 else rates[0][0]
        return self._release_rate

    def to_xml(self, xml : XMLWriter, name : str):
        if self.is_zero:
            return xml.write_element("Envelope")

        xml.write_start_tag("Envelope",
            {
                "Name" : name,
                "Release" : self.release_rate(),
            }
        )

        for point in self.points[:-1]: # exclude final hang command, will be added by the soundfont compiler on build
            point.to_xml(xml)

        xml.write_end_tag()