Files

111 lines
4.1 KiB
Go

package sound
/*
#cgo pkg-config: gstreamer-1.0
#include <gst/gst.h>
#include <stdlib.h>
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
}
}
}