1# Copyright 2014 Patrick Dawson <pat@dw.is>
2#
3# This software is provided 'as-is', without any express or implied
4# warranty.  In no event will the authors be held liable for any damages
5# arising from the use of this software.
6#
7# Permission is granted to anyone to use this software for any purpose,
8# including commercial applications, and to alter it and redistribute it
9# freely, subject to the following restrictions:
10#
11# 1. The origin of this software must not be misrepresented; you must not
12#    claim that you wrote the original software. If you use this software
13#    in a product, an acknowledgment in the product documentation would be
14#    appreciated but is not required.
15# 2. Altered source versions must be plainly marked as such, and must not be
16#    misrepresented as being the original software.
17# 3. This notice may not be removed or altered from any source distribution.
18
19from sdl2 cimport *
20from sdl2_mixer cimport *
21from pygame_sdl2.rwobject cimport to_rwops
22from libc.string cimport memset
23
24import sys
25import threading
26from pygame_sdl2.error import error
27
28import pygame_sdl2.mixer_music as music
29import pygame_sdl2
30
31cdef object preinit_args = None
32cdef object output_spec = None
33
34cdef dict channel_events = {}
35cdef dict channel_queued = {}
36cdef dict current_sounds = {}
37
38# The lock protects channel_queued and current_sounds.
39_lock = threading.Lock()
40
41def _play_current(int channel):
42    """
43    Caled by channel_callback to play the next sound. This has to be called
44    from a different thread, as the channel callback isn't allowed to call
45    MIX functions.
46    """
47
48    cdef Sound next_sound
49
50    with _lock:
51        next_sound = channel_queued[channel]
52        current_sounds[channel] = next_sound
53        channel_queued[channel] = None
54
55    if next_sound:
56        with nogil:
57            Mix_PlayChannelTimed(channel, next_sound.chunk, 0, -1)
58
59
60cdef void channel_callback(int channel) with gil:
61
62    cdef int etype = 0
63    cdef SDL_Event e
64
65    etype = channel_events.get(channel, 0)
66    if etype != 0:
67        memset(&e, 0, sizeof(SDL_Event))
68        e.type = etype
69        SDL_PushEvent(&e)
70
71    with _lock:
72        next_sound = channel_queued.get(channel)
73        if next_sound:
74            threading.Thread(target=_play_current, args=(channel,)).start()
75
76# A list of errors that occured during mixer initialization.
77errors = [ ]
78
79@pygame_sdl2.register_init
80def init(frequency=22050, size=MIX_DEFAULT_FORMAT, channels=2, buffer=4096):
81    if get_init() is not None:
82        return
83
84    for flag in (MIX_INIT_FLAC, MIX_INIT_MP3, MIX_INIT_OGG):
85
86        if Mix_Init(flag) != flag:
87            errors.append("{}\n".format(SDL_GetError()))
88
89    if preinit_args:
90        frequency, size, channels, buffer = preinit_args
91
92    if Mix_OpenAudio(frequency, size, channels, buffer) != 0:
93        raise error()
94
95    global output_spec
96    output_spec = get_init()
97
98    Mix_ChannelFinished(channel_callback)
99
100def pre_init(frequency=22050, size=MIX_DEFAULT_FORMAT, channels=2, buffersize=4096):
101    global preinit_args
102    preinit_args = (frequency, size, channels, buffersize)
103
104@pygame_sdl2.register_quit
105def quit(): # @ReservedAssignment
106    Mix_CloseAudio()
107    Mix_Quit()
108
109def get_init():
110    cdef int frequency
111    cdef Uint16 format
112    cdef int channels
113
114    if Mix_QuerySpec(&frequency, &format, &channels) == 0:
115        return None
116    else:
117        return frequency, format, channels
118
119def stop():
120    with nogil:
121        Mix_HaltChannel(-1)
122
123def pause():
124    with nogil:
125        Mix_Pause(-1)
126
127def unpause():
128    with nogil:
129        Mix_Resume(-1)
130
131def fadeout(time):
132    cdef int ms = time
133    with nogil:
134        Mix_FadeOutChannel(-1, ms)
135
136def set_num_channels(count):
137    Mix_AllocateChannels(count)
138
139def get_num_channels():
140    return Mix_AllocateChannels(-1)
141
142def set_reserved(count):
143    Mix_ReserveChannels(count)
144
145def find_channel(force=False):
146    cdef int chan
147    chan = Mix_GroupAvailable(-1)
148    if chan == -1:
149        if not force:
150            return None
151        chan = Mix_GroupOldest(-1)
152        if chan == -1:
153            raise error()
154    return Channel(chan)
155
156def get_busy():
157    return Mix_GroupNewer(-1) != -1
158
159
160cdef class Sound:
161    cdef Mix_Chunk *chunk
162
163    def __cinit__(self):
164        self.chunk = NULL
165
166    def __dealloc__(self):
167        if self.chunk:
168            Mix_FreeChunk(self.chunk)
169
170    def __init__(self, fi):
171        self.chunk = Mix_LoadWAV_RW(to_rwops(fi), 1)
172        if self.chunk == NULL:
173            raise error()
174
175    def play(self, loops=0, maxtime=-1, fade_ms=0):
176        cdef int cid
177        cdef int _loops = loops
178        cdef int _maxtime = maxtime
179        cdef int _fade_ms = fade_ms
180
181        with nogil:
182            if _fade_ms != 0:
183                cid = Mix_FadeInChannelTimed(-1, self.chunk, _loops, _fade_ms, _maxtime)
184            else:
185                cid = Mix_PlayChannelTimed(-1, self.chunk, _loops, _maxtime)
186
187        if cid == -1:
188            raise error()
189        return Channel(cid)
190
191    def stop(self):
192        cdef int i = 0
193        while i < Mix_AllocateChannels(-1):
194            if Mix_GetChunk(i) == self.chunk:
195                with nogil:
196                    Mix_HaltChannel(i)
197            i += 1
198
199    def pause(self):
200        cdef int i = 0
201        while i < Mix_AllocateChannels(-1):
202            if Mix_GetChunk(i) == self.chunk:
203                with nogil:
204                    Mix_Pause(i)
205            i += 1
206
207    def unpause(self):
208        cdef int i = 0
209        while i < Mix_AllocateChannels(-1):
210            if Mix_GetChunk(i) == self.chunk:
211                with nogil:
212                    Mix_Resume(i)
213            i += 1
214
215    def fadeout(self, time):
216        cdef int i = 0
217        cdef int ms = time
218        while i < Mix_AllocateChannels(-1):
219            if Mix_GetChunk(i) == self.chunk:
220                with nogil:
221                    Mix_FadeOutChannel(i, ms)
222            i += 1
223
224    def set_volume(self, value):
225        Mix_VolumeChunk(self.chunk, MIX_MAX_VOLUME * value)
226
227    def get_volume(self):
228        return Mix_VolumeChunk(self.chunk, -1)
229
230    def get_num_channels(self):
231        cdef int i = 0
232        cdef int n = 0
233        while i < Mix_AllocateChannels(-1):
234            if Mix_GetChunk(i) == self.chunk:
235                n += 1
236            i += 1
237        return n
238
239    def get_length(self):
240        # TODO: Adjust for actual format, rather than assuming 16-bit.
241        return <double>self.chunk.alen / output_spec[0] / 2 / output_spec[2]
242
243    def get_raw(self):
244        # return self.chunk.abuf
245        raise error("Not implemented.")
246
247
248cdef class Channel(object):
249    cdef int cid
250
251    def __init__(self, cid):
252        self.cid = cid
253
254    def play(self, Sound sound not None, loops=0, maxtime=-1, fade_ms=0):
255        cdef int _loops = loops
256        cdef int _maxtime = maxtime
257        cdef int _fade_ms = fade_ms
258
259        with nogil:
260            if _fade_ms != 0:
261                cid = Mix_FadeInChannelTimed(self.cid, sound.chunk, _loops, _fade_ms, _maxtime)
262            else:
263                cid = Mix_PlayChannelTimed(self.cid, sound.chunk, _loops, _maxtime)
264
265        if cid == -1:
266            raise error()
267
268        with _lock:
269            current_sounds[self.cid] = sound
270
271    def stop(self):
272        with nogil:
273            Mix_HaltChannel(self.cid)
274
275    def pause(self):
276        with nogil:
277            Mix_Pause(self.cid)
278
279    def unpause(self):
280        with nogil:
281            Mix_Resume(self.cid)
282
283    def fadeout(self, time):
284        cdef int ms = time
285        with nogil:
286            Mix_FadeOutChannel(self.cid, ms)
287
288    def set_volume(self, volume):
289        Mix_Volume(self.cid, int(MIX_MAX_VOLUME * volume))
290
291    def get_volume(self):
292        cdef int vol = Mix_Volume(self.cid, -1)
293        return vol / <double>MIX_MAX_VOLUME
294
295    def get_busy(self):
296        return Mix_Playing(self.cid) != 0
297
298    def get_sound(self):
299        with _lock:
300            return current_sounds.get(self.cid)
301
302    def queue(self, Sound sound):
303        if self.get_busy():
304            with _lock:
305                channel_queued[self.cid] = sound
306        else:
307            self.play(sound)
308
309    def get_queue(self):
310        with _lock:
311            return channel_queued.get(self.cid)
312
313    def set_endevent(self, type=None):
314        channel_events[self.cid] = type or 0
315
316    def get_endevent(self):
317        return channel_events.get(self.cid, 0)
318