111 lines
4.1 KiB
Go
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
|
|
}
|
|
}
|
|
}
|