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 | 166 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100755 test/simple-asha diff --git a/test/simple-asha b/test/simple-asha new file mode 100755 index 000000000..c90f8a12b --- /dev/null +++ b/test/simple-asha @@ -0,0 +1,166 @@ +#!/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, omtu: int): + global mainloop, pipeline + pktsize = 161 + + if omtu < pktsize: + print("Weird mtu", omtu) + + outdata = bytearray(pktsize) + + 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 != pktsize - 1: + 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 != pktsize: + 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) + + 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() + + return True + + bus = pipeline.get_bus() + bus.add_watch(GLib.PRIORITY_DEFAULT, bus_message, None) + + pipeline.set_state(Gst.State.PLAYING) + + +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_object_path = device.object_path + "/asha" + + print("Looking up ASHA object", asha_object_path) + asha = bus.get_object("org.bluez", asha_object_path) + + print("Looking up endpoint properties for", asha.object_path) + 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)") + + print("Setting initial volume to", volume) + transport.Set( + "org.bluez.MediaTransport1", + "Volume", + dbus.UInt16(volume, variant_level=1), + dbus_interface="org.freedesktop.DBus.Properties", + ) + + print("Acquiring transport") + (fd, imtu, omtu) = transport.Acquire() + + print("Starting playback, hit Ctrl-C to stop") + start_playback(fd.take(), omtu) + + mainloop.run() + + pipeline.set_state(Gst.State.NULL) + transport.Release() -- 2.45.1