1from test import support
2from test.support import import_helper, warnings_helper
3import warnings
4support.requires('audio')
5
6from test.support import findfile
7
8with warnings.catch_warnings():
9    warnings.simplefilter("ignore", DeprecationWarning)
10    ossaudiodev = import_helper.import_module('ossaudiodev')
11audioop = warnings_helper.import_deprecated('audioop')
12sunau = warnings_helper.import_deprecated('sunau')
13
14import errno
15import sys
16import time
17import unittest
18
19# Arggh, AFMT_S16_NE not defined on all platforms -- seems to be a
20# fairly recent addition to OSS.
21try:
22    from ossaudiodev import AFMT_S16_NE
23except ImportError:
24    if sys.byteorder == "little":
25        AFMT_S16_NE = ossaudiodev.AFMT_S16_LE
26    else:
27        AFMT_S16_NE = ossaudiodev.AFMT_S16_BE
28
29
30def read_sound_file(path):
31    with open(path, 'rb') as fp:
32        au = sunau.open(fp)
33        rate = au.getframerate()
34        nchannels = au.getnchannels()
35        encoding = au._encoding
36        fp.seek(0)
37        data = fp.read()
38
39    if encoding != sunau.AUDIO_FILE_ENCODING_MULAW_8:
40        raise RuntimeError("Expect .au file with 8-bit mu-law samples")
41
42    # Convert the data to 16-bit signed.
43    data = audioop.ulaw2lin(data, 2)
44    return (data, rate, 16, nchannels)
45
46class OSSAudioDevTests(unittest.TestCase):
47
48    def play_sound_file(self, data, rate, ssize, nchannels):
49        try:
50            dsp = ossaudiodev.open('w')
51        except OSError as msg:
52            if msg.args[0] in (errno.EACCES, errno.ENOENT,
53                               errno.ENODEV, errno.EBUSY):
54                raise unittest.SkipTest(msg)
55            raise
56
57        # at least check that these methods can be invoked
58        dsp.bufsize()
59        dsp.obufcount()
60        dsp.obuffree()
61        dsp.getptr()
62        dsp.fileno()
63
64        # Make sure the read-only attributes work.
65        self.assertFalse(dsp.closed)
66        self.assertEqual(dsp.name, "/dev/dsp")
67        self.assertEqual(dsp.mode, "w", "bad dsp.mode: %r" % dsp.mode)
68
69        # And make sure they're really read-only.
70        for attr in ('closed', 'name', 'mode'):
71            try:
72                setattr(dsp, attr, 42)
73            except (TypeError, AttributeError):
74                pass
75            else:
76                self.fail("dsp.%s not read-only" % attr)
77
78        # Compute expected running time of sound sample (in seconds).
79        expected_time = float(len(data)) / (ssize/8) / nchannels / rate
80
81        # set parameters based on .au file headers
82        dsp.setparameters(AFMT_S16_NE, nchannels, rate)
83        self.assertTrue(abs(expected_time - 3.51) < 1e-2, expected_time)
84        t1 = time.monotonic()
85        dsp.write(data)
86        dsp.close()
87        t2 = time.monotonic()
88        elapsed_time = t2 - t1
89
90        percent_diff = (abs(elapsed_time - expected_time) / expected_time) * 100
91        self.assertTrue(percent_diff <= 10.0,
92                        "elapsed time (%s) > 10%% off of expected time (%s)" %
93                        (elapsed_time, expected_time))
94
95    def set_parameters(self, dsp):
96        # Two configurations for testing:
97        #   config1 (8-bit, mono, 8 kHz) should work on even the most
98        #      ancient and crufty sound card, but maybe not on special-
99        #      purpose high-end hardware
100        #   config2 (16-bit, stereo, 44.1kHz) should work on all but the
101        #      most ancient and crufty hardware
102        config1 = (ossaudiodev.AFMT_U8, 1, 8000)
103        config2 = (AFMT_S16_NE, 2, 44100)
104
105        for config in [config1, config2]:
106            (fmt, channels, rate) = config
107            if (dsp.setfmt(fmt) == fmt and
108                dsp.channels(channels) == channels and
109                dsp.speed(rate) == rate):
110                break
111        else:
112            raise RuntimeError("unable to set audio sampling parameters: "
113                               "you must have really weird audio hardware")
114
115        # setparameters() should be able to set this configuration in
116        # either strict or non-strict mode.
117        result = dsp.setparameters(fmt, channels, rate, False)
118        self.assertEqual(result, (fmt, channels, rate),
119                         "setparameters%r: returned %r" % (config, result))
120
121        result = dsp.setparameters(fmt, channels, rate, True)
122        self.assertEqual(result, (fmt, channels, rate),
123                         "setparameters%r: returned %r" % (config, result))
124
125    def set_bad_parameters(self, dsp):
126        # Now try some configurations that are presumably bogus: eg. 300
127        # channels currently exceeds even Hollywood's ambitions, and
128        # negative sampling rate is utter nonsense.  setparameters() should
129        # accept these in non-strict mode, returning something other than
130        # was requested, but should barf in strict mode.
131        fmt = AFMT_S16_NE
132        rate = 44100
133        channels = 2
134        for config in [(fmt, 300, rate),       # ridiculous nchannels
135                       (fmt, -5, rate),        # impossible nchannels
136                       (fmt, channels, -50),   # impossible rate
137                      ]:
138            (fmt, channels, rate) = config
139            result = dsp.setparameters(fmt, channels, rate, False)
140            self.assertNotEqual(result, config,
141                             "unexpectedly got requested configuration")
142
143            try:
144                result = dsp.setparameters(fmt, channels, rate, True)
145            except ossaudiodev.OSSAudioError as err:
146                pass
147            else:
148                self.fail("expected OSSAudioError")
149
150    def test_playback(self):
151        sound_info = read_sound_file(findfile('audiotest.au'))
152        self.play_sound_file(*sound_info)
153
154    def test_set_parameters(self):
155        dsp = ossaudiodev.open("w")
156        try:
157            self.set_parameters(dsp)
158
159            # Disabled because it fails under Linux 2.6 with ALSA's OSS
160            # emulation layer.
161            #self.set_bad_parameters(dsp)
162        finally:
163            dsp.close()
164            self.assertTrue(dsp.closed)
165
166    def test_mixer_methods(self):
167        # Issue #8139: ossaudiodev didn't initialize its types properly,
168        # therefore some methods were unavailable.
169        with ossaudiodev.openmixer() as mixer:
170            self.assertGreaterEqual(mixer.fileno(), 0)
171
172    def test_with(self):
173        with ossaudiodev.open('w') as dsp:
174            pass
175        self.assertTrue(dsp.closed)
176
177    def test_on_closed(self):
178        dsp = ossaudiodev.open('w')
179        dsp.close()
180        self.assertRaises(ValueError, dsp.fileno)
181        self.assertRaises(ValueError, dsp.read, 1)
182        self.assertRaises(ValueError, dsp.write, b'x')
183        self.assertRaises(ValueError, dsp.writeall, b'x')
184        self.assertRaises(ValueError, dsp.bufsize)
185        self.assertRaises(ValueError, dsp.obufcount)
186        self.assertRaises(ValueError, dsp.obufcount)
187        self.assertRaises(ValueError, dsp.obuffree)
188        self.assertRaises(ValueError, dsp.getptr)
189
190        mixer = ossaudiodev.openmixer()
191        mixer.close()
192        self.assertRaises(ValueError, mixer.fileno)
193
194def setUpModule():
195    try:
196        dsp = ossaudiodev.open('w')
197    except (ossaudiodev.error, OSError) as msg:
198        if msg.args[0] in (errno.EACCES, errno.ENOENT,
199                           errno.ENODEV, errno.EBUSY):
200            raise unittest.SkipTest(msg)
201        raise
202    dsp.close()
203
204if __name__ == "__main__":
205    unittest.main()
206