diff mbox series

[1/2] support/testing: test_audio_codec_base.py: new helper class

Message ID 20240615091612.9579-1-ju.o@free.fr
State New
Headers show
Series [1/2] support/testing: test_audio_codec_base.py: new helper class | expand

Commit Message

Julien Olivain June 15, 2024, 9:16 a.m. UTC
This is a helper class providing a template for testing audio codec
programs such as lame mp3 encoder, flac tools, ogg vorbis-tools, ...

Signed-off-by: Julien Olivain <ju.o@free.fr>
---
 DEVELOPERS                                    |  1 +
 .../tests/package/test_audio_codec_base.py    | 86 +++++++++++++++++++
 2 files changed, 87 insertions(+)
 create mode 100644 support/testing/tests/package/test_audio_codec_base.py

Comments

Yann E. MORIN June 15, 2024, 6:09 p.m. UTC | #1
Julien, All,

On 2024-06-15 11:16 +0200, Julien Olivain spake thusly:
> This is a helper class providing a template for testing audio codec
> programs such as lame mp3 encoder, flac tools, ogg vorbis-tools, ...
> 
> Signed-off-by: Julien Olivain <ju.o@free.fr>
> ---
[--SNIP--]
> diff --git a/support/testing/tests/package/test_audio_codec_base.py b/support/testing/tests/package/test_audio_codec_base.py
> new file mode 100644
> index 00000000000..59c7efcad3c
> --- /dev/null
> +++ b/support/testing/tests/package/test_audio_codec_base.py
> @@ -0,0 +1,86 @@
> +import math
> +import os
> +
> +import infra.basetest
> +
> +
> +class TestAudioCodecBase(infra.basetest.BRTest):
> +    """Common class to test an audio codec package.
> +
> +    This base class builds a Buildroot system image containing the
> +    package enabled in its config, start the emulator, login to it. It
> +    prepares an input test WAV file containing a tone. This WAV file
> +    is encoded into a new output file in the native encoder format. It
> +    is then decoded to a new output WAV file. This final output WAV
> +    file is checked to contain the expected tone generated in the
> +    initial input WAV file. There is no specific check about file
> +    size, nor audio quality. The acceptance criteria is the
> +    recognition of the tone. Note: the tone generation is made with
> +    the Sox package, and the tone recognition is made with the Aubio
> +    package.
> +
> +    Each test case that inherits from this class must have:
> +    __test__ = True  - to let nose2 know that it is a test case

If the class name does not stat with "Test", then nose2 will not
consider it a test case, it would not be necxessary to set __test__ =
False here, and rely on inheriters to set it.

I see that there are many such cases already in the tree, but I don;t
think we should continue in that direction.

Maybe just name this base calss as such:

    class BaseAudioCodec(infra.basetest.BRTest):

But see below: I think we should consider it a test by itself: it
validates that the sox-aubio combo does work as expected.

[--SNIP--]
> +    def encode_test(self, input_filename):
> +        msg = "method must be implemented in derived class"
> +        raise NotImplementedError(msg)

I am not python expert, but I think that would be better handled by an
"ABC", and Abstract Base Class (https://docs.python.org/3/library/abc.html),
which I think would look like:

    import abc
    class BaseAudioCodec(abc.ABC):
        @abc.abstractmethod
        def encode_test(self, input_filename):
            ...

Note that the ellipsis is really a real ellipsis, not a placeholder (see
https://docs.python.org/3/reference/datamodel.html#ellipsis).

But as I suggested above, we should just make this class a proper test,
and have empty encode() and decode() functions.

It would validate that the tone generated by sox can be decoded by
aubio.

Regards,
Yann E. MORIN.
diff mbox series

Patch

diff --git a/DEVELOPERS b/DEVELOPERS
index 52c9b84a9d4..44ff8c01e03 100644
--- a/DEVELOPERS
+++ b/DEVELOPERS
@@ -1796,6 +1796,7 @@  F:	support/testing/tests/package/test_acl.py
 F:	support/testing/tests/package/test_acpica.py
 F:	support/testing/tests/package/test_acpica/
 F:	support/testing/tests/package/test_apache.py
+F:	support/testing/tests/package/test_audio_codec_base.py
 F:	support/testing/tests/package/test_bc.py
 F:	support/testing/tests/package/test_bitcoin.py
 F:	support/testing/tests/package/test_brotli.py
diff --git a/support/testing/tests/package/test_audio_codec_base.py b/support/testing/tests/package/test_audio_codec_base.py
new file mode 100644
index 00000000000..59c7efcad3c
--- /dev/null
+++ b/support/testing/tests/package/test_audio_codec_base.py
@@ -0,0 +1,86 @@ 
+import math
+import os
+
+import infra.basetest
+
+
+class TestAudioCodecBase(infra.basetest.BRTest):
+    """Common class to test an audio codec package.
+
+    This base class builds a Buildroot system image containing the
+    package enabled in its config, start the emulator, login to it. It
+    prepares an input test WAV file containing a tone. This WAV file
+    is encoded into a new output file in the native encoder format. It
+    is then decoded to a new output WAV file. This final output WAV
+    file is checked to contain the expected tone generated in the
+    initial input WAV file. There is no specific check about file
+    size, nor audio quality. The acceptance criteria is the
+    recognition of the tone. Note: the tone generation is made with
+    the Sox package, and the tone recognition is made with the Aubio
+    package.
+
+    Each test case that inherits from this class must have:
+    __test__ = True  - to let nose2 know that it is a test case
+    config           - defconfig fragment with the packages to run the test
+    encode_test()    - the function that runs the encoder and produces an
+                       encoded file.
+    decode_test()    - the function that runs the decoder and produces a
+                       decode file.
+    """
+    __test__ = False
+    config = infra.basetest.BASIC_TOOLCHAIN_CONFIG + \
+        """
+        BR2_PACKAGE_AUBIO=y
+        BR2_PACKAGE_SOX=y
+        BR2_TARGET_ROOTFS_CPIO=y
+        # BR2_TARGET_ROOTFS_TAR is not set
+        """
+    input_file = "reference.wav"
+    decoded_file = "decoded.wav"
+    tone_freq = 440  # General Midi note A3
+
+    def login(self):
+        cpio_file = os.path.join(self.builddir, "images", "rootfs.cpio")
+        self.emulator.boot(arch="armv5",
+                           kernel="builtin",
+                           options=["-initrd", cpio_file])
+        self.emulator.login()
+
+    def test_run(self):
+        self.login()
+        self.prepare_data()
+        self.encode_test(self.input_file)
+        self.decode_test(self.decoded_file)
+        self.check_test()
+
+    def prepare_data(self):
+        # Generate a sinusoidal tone.
+        cmd = "sox -V -r 48000 -n -b 16 -c 1 "
+        cmd += self.input_file
+        cmd += f" synth 3 sin {self.tone_freq} vol -10dB"
+        self.assertRunOk(cmd)
+
+    def encode_test(self, input_filename):
+        msg = "method must be implemented in derived class"
+        raise NotImplementedError(msg)
+
+    def decode_test(self, output_filename):
+        msg = "method must be implemented in derived class"
+        raise NotImplementedError(msg)
+
+    def note_from_freq(self, freq):
+        """Return a note number from the input frequency in Hertz."""
+        return round((12 * math.log(freq / 440) / math.log(2)) + 69)
+
+    def check_test(self):
+        expected_note = self.note_from_freq(self.tone_freq)
+        out, ret = self.emulator.run(f"aubionotes {self.decoded_file}", timeout=20)
+        self.assertEqual(ret, 0)
+        note_found = False
+        for line in out:
+            values = line.split()
+            if len(values) == 3:
+                note = round(float(values[0]))
+                if note == expected_note:
+                    note_found = True
+        self.assertTrue(note_found, "The expected note was not found")