package sound /* #cgo pkg-config: gstreamer-1.0 #include #include static GstElement *start_sound(const void *data, size_t length, double volume, double pan, const char *sink, GError **error) { if (!gst_init_check(NULL, NULL, error)) return NULL; gchar *description = g_strdup_printf("appsrc name=source ! wavparse ! audioconvert ! audioresample ! audiopanorama name=panorama ! volume name=volume ! %s", sink); GError *parse_error = NULL; GstElement *pipeline = gst_parse_launch(description, &parse_error); g_free(description); if (parse_error != NULL) { if (pipeline) gst_object_unref(pipeline); g_error_free(parse_error); // audiopanorama is an optional GStreamer plugin. Preserve playback // when it is unavailable; the typed cue remains safely centered. description = g_strdup_printf("appsrc name=source ! wavparse ! audioconvert ! audioresample ! volume name=volume ! %s", sink); pipeline = gst_parse_launch(description, error); g_free(description); if (*error != NULL) { if (pipeline) gst_object_unref(pipeline); return NULL; } } GstElement *source = gst_bin_get_by_name(GST_BIN(pipeline), "source"); GstElement *gain = gst_bin_get_by_name(GST_BIN(pipeline), "volume"); GstElement *panorama = gst_bin_get_by_name(GST_BIN(pipeline), "panorama"); g_object_set(gain, "volume", volume, NULL); if (panorama != NULL) g_object_set(panorama, "panorama", pan, NULL); gst_object_unref(gain); if (panorama != NULL) gst_object_unref(panorama); GstBuffer *buffer = gst_buffer_new_allocate(NULL, length, NULL); gst_buffer_fill(buffer, 0, data, length); GstFlowReturn flow; g_signal_emit_by_name(source, "push-buffer", buffer, &flow); gst_buffer_unref(buffer); g_signal_emit_by_name(source, "end-of-stream", &flow); gst_object_unref(source); if (gst_element_set_state(pipeline, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) { g_set_error_literal(error, g_quark_from_static_string("cthulhu-sound"), 1, "cannot start audio pipeline"); gst_element_set_state(pipeline, GST_STATE_NULL); gst_object_unref(pipeline); return NULL; } return pipeline; } static int poll_sound(GstElement *pipeline, GError **error) { GstBus *bus = gst_element_get_bus(pipeline); GstMessage *message = gst_bus_timed_pop_filtered(bus, 20 * GST_MSECOND, GST_MESSAGE_ERROR | GST_MESSAGE_EOS); gst_object_unref(bus); if (message == NULL) return 0; int result = 1; if (GST_MESSAGE_TYPE(message) == GST_MESSAGE_ERROR) { gst_message_parse_error(message, error, NULL); result = -1; } gst_message_unref(message); return result; } static void stop_sound(GstElement *pipeline) { gst_element_set_state(pipeline, GST_STATE_NULL); gst_object_unref(pipeline); } */ import "C" import ( "context" "errors" "fmt" "unsafe" ) type Player interface { Play(context.Context, []byte, float64, float64) error } type NativePlayer struct{ Sink string } func (player NativePlayer) Play(ctx context.Context, data []byte, volume, pan float64) error { if err := ctx.Err(); err != nil { return err } if len(data) == 0 { return errors.New("empty sound") } sink := map[string]string{"auto": "autoaudiosink", "pipewire": "pipewiresink", "pulse": "pulsesink", "alsa": "alsasink", "test": "fakesink"}[player.Sink] if sink == "" { return fmt.Errorf("unknown sound sink %q", player.Sink) } nativeSink := C.CString(sink) defer C.free(unsafe.Pointer(nativeSink)) var nativeErr *C.GError pan = normalizedPan(pan) pipeline := C.start_sound(unsafe.Pointer(&data[0]), C.size_t(len(data)), C.double(volume), C.double(pan), nativeSink, &nativeErr) if nativeErr != nil { defer C.g_error_free(nativeErr) return errors.New(C.GoString(nativeErr.message)) } if pipeline == nil { return errors.New("cannot create sound pipeline") } defer C.stop_sound(pipeline) for { if err := ctx.Err(); err != nil { return err } result := C.poll_sound(pipeline, &nativeErr) if nativeErr != nil { defer C.g_error_free(nativeErr) return errors.New(C.GoString(nativeErr.message)) } if result > 0 { return nil } } }