summaryrefslogtreecommitdiff
path: root/blc2/functions/audio.py
blob: 0d59856e32a8494e5f39b82d1953fce6301ee8dc (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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
"""Audio function module.

Contains the definition of the Audio, the audio primitive. 
"""

import xml.etree.ElementTree as et

from .function import Function

from ..constants import AUDIO, BXW, INTERNAL
from ..exceptions import LoadError

class Audio(Function):
    """Class representing an audio cue. 

    This is the primitive for audio, and all sound cues must be based in some manner off of 
    Audio. This function merely plays a single audio file once, starting at t=0. 

    The duration of the audio is automatically determined and is zero if the file does not 
    exist or is unsupported by ffmpeg. 
    """
    type = AUDIO
    fade_out_mode = INTERNAL

    def __init__(self, w, id_ = None, name = None, fade_in = 0, fade_out = 0,
                 filename: str = None):
        super().__init__(w=w, id_=id_, name=name)

        self._filename = filename
        self._audio_id = self.id

        if fade_in < 0 or fade_out < 0:
            raise ValueError("Fades must be nonnegative")
        self._fade_out = fade_out
        self._fade_in = fade_in

        if filename is not None:
            self._actual_duration = self.w.get_audio_length(filename)
            self._duration = max(0, self._actual_duration - self._fade_out)
            self._audio_scope = frozenset(((filename,),))
        else:
            self._duration = 0
            self._actual_duration = 0
            self._audio_scope = frozenset()

    @property
    def fade_in(self):
        return self._fade_in

    @fade_in.setter
    def fade_in(self, v):
        if v < 0:
            raise ValueError("Fades must be nonnegative")

        if v != self._fade_in:
            self._fade_in = v
            self.w.function_changed(self)

    @property
    def fade_out(self):
        return self._fade_out

    @fade_out.setter
    def fade_out(self, v):
        if v < 0:
            raise ValueError("Fades must be nonnegative")

        if v != self._fade_out:
            self._fade_out = v
            self.w.function_changed(self)

    @property
    def scope(self):
        return ()

    @property 
    def audio_scope(self):
        return self._audio_scope.union()

    @property
    def actual_duration(self):
        return self._actual_duration

    @property
    def duration(self):
        return self._duration

    def _set_duration(self, value):
        """Set the duration. 

        This is called by Workspace when the duration for the file has been rechecked; do 
        not call this elsewhere, duration is intended to be immutable from outside the 
        library.
        """
        if value != self._duration:
            self._duration = max(value-self._fade_out, 0)
            self._actual_duration = value
            self.w.function_changed(self)

    def get_data(self):
        return None

    def copy_data(self, data):
        return None

    def render(self, t, data = None):
        return ((), 
                ((self._audio_id, self._filename, 0, self.fade_in, max(0, self.duration-self.fade_out), self.fade_out),),
                None)

    @property
    def filename(self):
        """Return the current audio filename."""
        return self._filename

    @filename.setter 
    def filename(self, value: str):
        """Set the current audio filename, updating the duration."""
        if value is not None:
            self.duration = self.w.get_audio_length(value)
            self.audio_scope = frozenset(((value,),))
        else:
            self.duration = 0
            self.audio_scope = frozenset()

        self._filename = value
        self._audio_id = hash((self._audio_id, self._filename))
        self.w.function_changed(self)

    def serialize(self) -> et.Element:
        e = et.Element(BXW+"function")
        e.set("type", self.type)
        e.set("id", str(self.id))
        e.set("name", self.name)
        e.set("fade-in", str(self.fade_in))
        e.set("fade-out", str(self.fade_out))
        if self.filename is not None:
            filename = et.SubElement(e, BXW+"filename")
            filename.text = self.filename

        return e

    @classmethod
    def deserialize(cls, w, e):
        if e.tag != BXW+"function":
            raise LoadError("Invalid function tag")
        elif e.get("type") != AUDIO:
            raise LoadError("Load delegated to wrong class (this is a bug)")
        
        id_ = cls.int_or_none(e.get("id"))
        if id_ is None:
            raise LoadError("Function tag has invalid/missing ID")

        name = e.get("name")

        fade_in = e.get("fade-in")
        try:
            fade_in = int(fade_in) if fade_in else 0
        except ValueError:
            raise LoadError("Invalid fade in")

        fade_out = e.get("fade-out")
        try:
            fade_out = int(fade_out) if fade_out else 0
        except ValueError:
            raise LoadError("Invalid fade out")

        if len(e) > 1:
            raise LoadError("Audio tag can have at most one filename")
        elif len(e) == 1:
            filename, = e
            filename = filename.text
        else:
            filename = None

        return cls(w=w, id_=id_, name=name, filename=filename, fade_in=fade_in,
                   fade_out=fade_out)