diff mbox series

[BlueZ,3/5] test: Add a script to test ASHA

Message ID 20240508154604.276763-4-arun@asymptotic.io (mailing list archive)
State Superseded
Headers show
Series ASHA plugin | expand

Checks

Context Check Description
tedd_an/pre-ci_am success Success
tedd_an/CheckPatch fail ERROR:EXECUTE_PERMISSIONS: do not set execute permissions for source files #104: FILE: test/simple-asha /github/workspace/src/src/13658880.patch total: 1 errors, 0 warnings, 158 lines checked NOTE: For some of the reported defects, checkpatch may be able to mechanically convert to the typical style using --fix or --fix-inplace. /github/workspace/src/src/13658880.patch has style problems, please review. NOTE: Ignored message types: COMMIT_MESSAGE COMPLEX_MACRO CONST_STRUCT FILE_PATH_CHANGES MISSING_SIGN_OFF PREFER_PACKED SPDX_LICENSE_TAG SPLIT_STRING SSCANF_TO_KSTRTO NOTE: If any of the errors are false positives, please report them to the maintainer, see CHECKPATCH in MAINTAINERS.
tedd_an/GitLint success Gitlint PASS

Commit Message

Arun Raghavan May 8, 2024, 3:46 p.m. UTC
Plays out an audio file to the device. Depends on GStreamer for media
file reading and decoding (specifically, gstreamer core,
gst-plugins-base, gst-ffmpeg, and gst-python, or equivalent packages).
---
 test/simple-asha | 158 +++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 158 insertions(+)
 create mode 100755 test/simple-asha

Comments

Luiz Augusto von Dentz May 8, 2024, 4:34 p.m. UTC | #1
Hi Arun,

On Wed, May 8, 2024 at 11:48 AM Arun Raghavan <arun@asymptotic.io> wrote:
>
> Plays out an audio file to the device. Depends on GStreamer for media
> file reading and decoding (specifically, gstreamer core,
> gst-plugins-base, gst-ffmpeg, and gst-python, or equivalent packages).
> ---
>  test/simple-asha | 158 +++++++++++++++++++++++++++++++++++++++++++++++
>  1 file changed, 158 insertions(+)
>  create mode 100755 test/simple-asha
>
> diff --git a/test/simple-asha b/test/simple-asha
> new file mode 100755
> index 000000000..feff9d29c
> --- /dev/null
> +++ b/test/simple-asha
> @@ -0,0 +1,158 @@
> +#!/usr/bin/env python3
> +# SPDX-License-Identifier: LGPL-2.1-or-later
> +
> +import os
> +import signal
> +import sys
> +
> +import dbus
> +import dbus.service
> +import dbus.mainloop.glib
> +
> +import gi
> +
> +gi.require_version("Gst", "1.0")
> +gi.require_version("GLib", "2.0")
> +from gi.repository import GLib, Gst
> +
> +import bluezutils
> +
> +mainloop = None
> +pipeline = None
> +seqnum: int = 0
> +
> +
> +def signal_handler(_sig, _frame):
> +    print("Got interrupt")
> +    mainloop.quit()
> +
> +
> +signal.signal(signal.SIGINT, signal_handler)
> +
> +
> +def usage():
> +    print(f"Usage: simple-asha <remote addr> <audio file name> (optional volume 0-127)")
> +
> +
> +def start_playback(fd: int):
> +    global mainloop, pipeline
> +
> +    outdata = bytearray(161)
> +
> +    Gst.init(None)
> +
> +    pipeline = Gst.parse_launch(
> +        f"""
> +          filesrc location="{sys.argv[2]}" ! decodebin !
> +          audioconvert ! audioresample !
> +          audiobuffersplit output-buffer-duration="20/1000" ! avenc_g722 !
> +          appsink name=sink emit-signals=true
> +    """
> +    )
> +
> +    def on_new_sample(sink):
> +        global seqnum
> +
> +        sample = sink.emit("pull-sample")
> +        buf = sample.get_buffer()
> +
> +        with buf.map(Gst.MapFlags.READ) as info:
> +            pos = 0
> +
> +            if info.size != 160:
> +                print("Unexpected buffer size: ", info.size)
> +
> +            outdata[pos] = seqnum % 256
> +            pos += 1
> +
> +            for byte in info.data:
> +                outdata[pos] = byte
> +                pos += 1
> +
> +            try:
> +                n = os.write(fd, outdata)
> +                if n != 161:
> +                    print("Wrote less than expected: ", n)
> +            except:
> +                return Gst.FlowReturn.ERROR
> +
> +        seqnum += 1
> +
> +        return Gst.FlowReturn.OK
> +
> +    sink = pipeline.get_by_name("sink")
> +    sink.connect("new-sample", on_new_sample)
> +
> +    pipeline.set_state(Gst.State.PLAYING)
> +
> +    def bus_message(_bus, message, _data) -> bool:
> +        typ = message.type
> +
> +        if typ == Gst.MessageType.EOS:
> +            print("End of stream")
> +            mainloop.quit()
> +        elif typ == Gst.MessageType.ERROR:
> +            err, debug = message.parse_error()
> +            print(f"Pipeline error: {err} ({debug})")
> +            mainloop.quit()
> +
> +    bus = pipeline.get_bus()
> +    bus.add_watch(GLib.PRIORITY_DEFAULT, bus_message, None)
> +
> +
> +if __name__ == "__main__":
> +    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
> +
> +    mainloop = GLib.MainLoop()
> +    bus = dbus.SystemBus()
> +
> +    if (len(sys.argv) == 3) or (len(sys.argv) == 4):
> +        device = bluezutils.find_device(sys.argv[1])
> +        if device is None:
> +            print("Could not find device: ", sys.argv[1])
> +            exit(255)
> +    else:
> +        usage()
> +        sys.exit(255)
> +
> +    asha = bus.get_object("org.bluez", device.object_path + "/asha")
> +    media = dbus.Interface(
> +        bus.get_object("org.bluez", device.object_path + "/asha"),
> +        "org.bluez.MediaEndpoint1",
> +    )
> +
> +    props = asha.GetAll(
> +        "org.bluez.MediaEndpoint1",
> +        dbus_interface="org.freedesktop.DBus.Properties",
> +    )
> +    path = props["Transport"]
> +
> +    print("Trying to acquire", path)
> +
> +    transport = dbus.Interface(
> +        bus.get_object("org.bluez", path),
> +        "org.bluez.MediaTransport1",
> +    )
> +
> +    # Keep default volume at 25%
> +    volume = 32
> +    if len(sys.argv) == 4:
> +        volume = int(sys.argv[3])
> +        if volume < 0 or volume > 127:
> +            print("Volume must be between 0 (mute) and 127 (max)")
> +
> +    transport.Set(
> +        "org.bluez.MediaTransport1",
> +        "Volume",
> +        dbus.UInt16(volume, variant_level=1),
> +        dbus_interface="org.freedesktop.DBus.Properties",
> +    )
> +
> +    (fd, imtu, omtu) = transport.Acquire()
> +
> +    start_playback(fd.take())
> +
> +    mainloop.run()
> +
> +    pipeline.set_state(Gst.State.NULL)
> +    transport.Release()
> --
> 2.45.0

While I don't mind having a python example I think we are much better
of adding such support in bluetoothctl, most should already work with
transport submenu but perhaps we want to add support for gstreamer
pipeline instead of just a file which would be useful for creating
A2DP sbc and BAP lc3 streams.

>
Arun Raghavan May 9, 2024, 2:20 a.m. UTC | #2
On Wed, 8 May 2024 at 12:34, Luiz Augusto von Dentz
<luiz.dentz@gmail.com> wrote:
[...]
> While I don't mind having a python example I think we are much better
> of adding such support in bluetoothctl, most should already work with
> transport submenu but perhaps we want to add support for gstreamer
> pipeline instead of just a file which would be useful for creating
> A2DP sbc and BAP lc3 streams.

Makes sense, I can take a look at this, but if possible I'd like to
punt this till I get the end-to-end flow working with the PipeWire
BlueZ modules.

Cheers,
Arun
diff mbox series

Patch

diff --git a/test/simple-asha b/test/simple-asha
new file mode 100755
index 000000000..feff9d29c
--- /dev/null
+++ b/test/simple-asha
@@ -0,0 +1,158 @@ 
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+import os
+import signal
+import sys
+
+import dbus
+import dbus.service
+import dbus.mainloop.glib
+
+import gi
+
+gi.require_version("Gst", "1.0")
+gi.require_version("GLib", "2.0")
+from gi.repository import GLib, Gst
+
+import bluezutils
+
+mainloop = None
+pipeline = None
+seqnum: int = 0
+
+
+def signal_handler(_sig, _frame):
+    print("Got interrupt")
+    mainloop.quit()
+
+
+signal.signal(signal.SIGINT, signal_handler)
+
+
+def usage():
+    print(f"Usage: simple-asha <remote addr> <audio file name> (optional volume 0-127)")
+
+
+def start_playback(fd: int):
+    global mainloop, pipeline
+
+    outdata = bytearray(161)
+
+    Gst.init(None)
+
+    pipeline = Gst.parse_launch(
+        f"""
+          filesrc location="{sys.argv[2]}" ! decodebin !
+          audioconvert ! audioresample !
+          audiobuffersplit output-buffer-duration="20/1000" ! avenc_g722 !
+          appsink name=sink emit-signals=true
+    """
+    )
+
+    def on_new_sample(sink):
+        global seqnum
+
+        sample = sink.emit("pull-sample")
+        buf = sample.get_buffer()
+
+        with buf.map(Gst.MapFlags.READ) as info:
+            pos = 0
+
+            if info.size != 160:
+                print("Unexpected buffer size: ", info.size)
+
+            outdata[pos] = seqnum % 256
+            pos += 1
+
+            for byte in info.data:
+                outdata[pos] = byte
+                pos += 1
+
+            try:
+                n = os.write(fd, outdata)
+                if n != 161:
+                    print("Wrote less than expected: ", n)
+            except:
+                return Gst.FlowReturn.ERROR
+
+        seqnum += 1
+
+        return Gst.FlowReturn.OK
+
+    sink = pipeline.get_by_name("sink")
+    sink.connect("new-sample", on_new_sample)
+
+    pipeline.set_state(Gst.State.PLAYING)
+
+    def bus_message(_bus, message, _data) -> bool:
+        typ = message.type
+
+        if typ == Gst.MessageType.EOS:
+            print("End of stream")
+            mainloop.quit()
+        elif typ == Gst.MessageType.ERROR:
+            err, debug = message.parse_error()
+            print(f"Pipeline error: {err} ({debug})")
+            mainloop.quit()
+
+    bus = pipeline.get_bus()
+    bus.add_watch(GLib.PRIORITY_DEFAULT, bus_message, None)
+
+
+if __name__ == "__main__":
+    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
+
+    mainloop = GLib.MainLoop()
+    bus = dbus.SystemBus()
+
+    if (len(sys.argv) == 3) or (len(sys.argv) == 4):
+        device = bluezutils.find_device(sys.argv[1])
+        if device is None:
+            print("Could not find device: ", sys.argv[1])
+            exit(255)
+    else:
+        usage()
+        sys.exit(255)
+
+    asha = bus.get_object("org.bluez", device.object_path + "/asha")
+    media = dbus.Interface(
+        bus.get_object("org.bluez", device.object_path + "/asha"),
+        "org.bluez.MediaEndpoint1",
+    )
+
+    props = asha.GetAll(
+        "org.bluez.MediaEndpoint1",
+        dbus_interface="org.freedesktop.DBus.Properties",
+    )
+    path = props["Transport"]
+
+    print("Trying to acquire", path)
+
+    transport = dbus.Interface(
+        bus.get_object("org.bluez", path),
+        "org.bluez.MediaTransport1",
+    )
+
+    # Keep default volume at 25%
+    volume = 32
+    if len(sys.argv) == 4:
+        volume = int(sys.argv[3])
+        if volume < 0 or volume > 127:
+            print("Volume must be between 0 (mute) and 127 (max)")
+
+    transport.Set(
+        "org.bluez.MediaTransport1",
+        "Volume",
+        dbus.UInt16(volume, variant_level=1),
+        dbus_interface="org.freedesktop.DBus.Properties",
+    )
+
+    (fd, imtu, omtu) = transport.Acquire()
+
+    start_playback(fd.take())
+
+    mainloop.run()
+
+    pipeline.set_state(Gst.State.NULL)
+    transport.Release()