aboutsummaryrefslogtreecommitdiffstats
path: root/plugins/alsa/alsa-api-streams.c
diff options
context:
space:
mode:
authorFulup Ar Foll <fulup@iot.bzh>2018-05-17 21:43:24 +0200
committerFulup Ar Foll <fulup@iot.bzh>2018-05-17 21:43:24 +0200
commit253df14bd84f535a54f4d12c95399899e1343c20 (patch)
tree16093cc86d2874afb3311124613e69e23e74acbc /plugins/alsa/alsa-api-streams.c
parent29f5fc4e093b8793eaeeef056d0b998182269718 (diff)
Initial version with dynamic APIs
Diffstat (limited to 'plugins/alsa/alsa-api-streams.c')
-rw-r--r--plugins/alsa/alsa-api-streams.c340
1 files changed, 340 insertions, 0 deletions
diff --git a/plugins/alsa/alsa-api-streams.c b/plugins/alsa/alsa-api-streams.c
new file mode 100644
index 0000000..42d8ade
--- /dev/null
+++ b/plugins/alsa/alsa-api-streams.c
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2018 "IoT.bzh"
+ * Author Fulup Ar Foll <fulup@iot.bzh>
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+#define _GNU_SOURCE // needed for vasprintf
+
+#include "alsa-softmixer.h"
+#include <string.h>
+
+// Set stream volume control in %
+#define VOL_CONTROL_MAX 100
+#define VOL_CONTROL_MIN 0
+#define VOL_CONTROL_STEP 1
+
+// Fulup need to be cleanup with new controller version
+extern Lua2cWrapperT Lua2cWrap;
+
+typedef struct {
+ const char* verb;
+ AlsaSndStreamT *streams;
+ SoftMixerHandleT *mixer;
+} apiHandleT;
+
+static void StreamApiVerbCB(AFB_ReqT request) {
+ int close=0, error=0, quiet=0;
+ long volume=-1, mute=-1;
+ json_object *responseJ, *argsJ= afb_request_json(request);
+ apiHandleT *handle = (apiHandleT*) afb_request_get_vcbdata(request);
+
+ CtlSourceT *source = alloca(sizeof (CtlSourceT));
+ source->uid = handle->verb;
+ source->api = request->dynapi;
+ source->request = NULL;
+ source->context = NULL;
+
+ error = wrap_json_unpack(argsJ, "{s?b s?b,s?b, s?i !}"
+ , "quiet", &quiet
+ , "close", &close
+ , "mute", &mute
+ , "volume", &volume
+ );
+ if (error) {
+ AFB_ReqFailF(request, "StreamApiVerbCB", "Missing 'close|mute|volume|quiet' args=%s", json_object_get_string(argsJ));
+ goto OnErrorExit;
+ }
+
+ snd_ctl_t *ctlDev= AlsaCtlOpenCtl (source, handle->mixer->loop->cardid);
+ if (!ctlDev) {
+ AFB_ReqFailF(request, "StreamApiVerbCB", "Fail to open sndcard=%s", handle->mixer->loop->cardid);
+ goto OnErrorExit;
+ }
+
+ if (close) {
+ AFB_ReqFailF(request, "StreamApiVerbCB", "(Fulup) Close action still to be done mixer=%s", json_object_get_string(argsJ));
+ goto OnErrorExit;
+ }
+
+ if (volume != -1) {
+ error= AlsaCtlNumidSetLong (source, ctlDev, handle->streams->volume, volume);
+ if (error) {
+ AFB_ReqFailF(request, "StreamApiVerbCB", "Fail to set stream volume numid=%d value=%ld", handle->streams->volume, volume);
+ goto OnErrorExit;
+ }
+ }
+
+ if (mute != -1) {
+ error= AlsaCtlNumidSetLong (source, ctlDev, handle->streams->volume, !mute);
+ if (error) {
+ AFB_ReqFailF(request, "StreamApiVerbCB", "Fail to set stream volume numid=%d value=%d", handle->streams->volume, !mute);
+ goto OnErrorExit;
+ }
+ }
+
+ // if not in quiet mode return effective selected control values
+ if (quiet) responseJ=NULL;
+ else {
+ error+= AlsaCtlNumidGetLong (source, ctlDev, handle->streams->volume, &volume);
+ error+= AlsaCtlNumidGetLong (source, ctlDev, handle->streams->mute, &mute);
+ if (error) {
+ AFB_ReqFailF(request, "StreamApiVerbCB", "Fail to get stream numids volume=%ld mute=%ld",volume, mute);
+ goto OnErrorExit;
+ }
+ wrap_json_pack (&responseJ,"{volume:si, mute:sb}", volume, mute);
+ }
+
+ AFB_ReqSucess(request, responseJ, handle->verb);
+ return;
+
+OnErrorExit:
+ return;
+
+}
+
+STATIC int ProcessOneStream(CtlSourceT *source, json_object *streamJ, AlsaSndStreamT *stream) {
+ int error;
+ json_object *paramsJ = NULL;
+
+ // Make sure default runs
+ stream->volume = ALSA_DEFAULT_PCM_VOLUME;
+ stream->mute = 0;
+ stream->info = NULL;
+
+ error = wrap_json_unpack(streamJ, "{ss,s?s,ss,s?i,s?b,s?o !}"
+ , "uid", &stream->uid
+ , "info", &stream->info
+ , "zone", &stream->zone
+ , "volume", &stream->volume
+ , "mute", stream->mute
+ , "params", &paramsJ);
+ if (error) {
+ AFB_ApiNotice(source->api, "ProcessOneStream missing 'uid|[info]|zone|[volume]|[mute]|[params]' stream=%s", json_object_get_string(streamJ));
+ goto OnErrorExit;
+ }
+
+ if (paramsJ) error = ProcessSndParams(source, stream->uid, paramsJ, &stream->params);
+ if (error) {
+ AFB_ApiError(source->api, "ProcessOneSndCard: sndcard=%s invalid params=%s", stream->uid, json_object_get_string(paramsJ));
+ goto OnErrorExit;
+ } else {
+ stream->params.rate = ALSA_DEFAULT_PCM_RATE;
+ stream->params.rate = ALSA_DEFAULT_PCM_RATE;
+ stream->params.access = SND_PCM_ACCESS_RW_INTERLEAVED;
+ stream->params.format = SND_PCM_FORMAT_S16_LE;
+ stream->params.channels = 2;
+ stream->params.sampleSize = 0;
+ }
+
+ // make sure remain valid even when json object is removed
+ stream->uid = strdup(stream->uid);
+ stream->zone = strdup(stream->zone);
+
+ return 0;
+
+OnErrorExit:
+ return -1;
+}
+
+PUBLIC int SndStreams(CtlSourceT *source, json_object *argsJ, json_object **responseJ) {
+ SoftMixerHandleT *mixerHandle = (SoftMixerHandleT*) source->context;
+ AlsaSndStreamT *sndStream;
+ int error;
+ long value;
+ size_t count;
+
+ assert(mixerHandle);
+
+ // assert static/global softmixer handle get requited info
+ AlsaSndLoopT *ctlLoop = mixerHandle->loop;
+ if (!ctlLoop) {
+ AFB_ApiError(source->api, "SndStreams: mixer=%s No Loop found [should register snd_loop first]", mixerHandle->uid);
+ goto OnErrorExit;
+ }
+
+ switch (json_object_get_type(argsJ)) {
+ case json_type_object:
+ count = 1;
+ sndStream = calloc(count + 1, sizeof (AlsaSndStreamT));
+ error = ProcessOneStream(source, argsJ, &sndStream[0]);
+ if (error) {
+ AFB_ApiError(source->api, "SndStreams: mixer=%s invalid stream= %s", mixerHandle->uid, json_object_get_string(argsJ));
+ goto OnErrorExit;
+ }
+ break;
+
+ case json_type_array:
+ count = json_object_array_length(argsJ);
+ sndStream = calloc(count + 1, sizeof (AlsaSndStreamT));
+ for (int idx = 0; idx < count; idx++) {
+ json_object *sndStreamJ = json_object_array_get_idx(argsJ, idx);
+ error = ProcessOneStream(source, sndStreamJ, &sndStream[idx]);
+ if (error) {
+ AFB_ApiError(source->api, "sndstreams: mixer=%s invalid stream= %s", mixerHandle->uid, json_object_get_string(sndStreamJ));
+ goto OnErrorExit;
+ }
+ }
+ break;
+ default:
+ AFB_ApiError(source->api, "SndStreams: mixer=%s invalid argsJ= %s", mixerHandle->uid, json_object_get_string(argsJ));
+ goto OnErrorExit;
+ }
+
+
+ // return stream data to application as a json array
+ *responseJ = json_object_new_array();
+
+ for (int idx = 0; sndStream[idx].uid != NULL; idx++) {
+ json_object *streamJ, *paramsJ;
+
+ // Search for a free loop capture device
+ AFB_ApiNotice(source->api, "SndStreams: mixer=%s stream=%s Start", mixerHandle->uid, (char*) sndStream[idx].uid);
+ ctlLoop->scount--;
+ if (ctlLoop->scount < 0) {
+ AFB_ApiError(source->api, "SndStreams: mixer=%s stream=%s no more subdev avaliable in loopback=%s", mixerHandle->uid, sndStream[idx].uid, ctlLoop->uid);
+ goto OnErrorExit;
+ }
+
+ // Retrieve subdev loop device and open corresponding pcm
+ AlsaPcmInfoT *playbackDev = &ctlLoop->subdevs[ctlLoop->scount];
+
+ // capture use the same card/subdev as playback with a different device
+ playbackDev->device = ctlLoop->capture;
+ AlsaPcmInfoT *captureDev = AlsaByPathOpenPcm(source, playbackDev, SND_PCM_STREAM_CAPTURE);
+ if (!captureDev) goto OnErrorExit;
+
+ // configure with default loopback subdev params
+ error = AlsaPcmConf(source, captureDev, &playbackDev->params);
+ if (error) goto OnErrorExit;
+
+ // Register capture PCM for active/pause event
+ if (captureDev->numid) {
+ error = AlsaCtlRegister(source, captureDev, captureDev->numid);
+ if (error) goto OnErrorExit;
+ }
+
+ // Try to create/setup volume control.
+ snd_ctl_t* ctlDev = AlsaCrlFromPcm(source, captureDev->handle);
+ if (!ctlDev) {
+ AFB_ApiError(source->api, "SndStreams: mixer=%s [pcm=%s] fail attache sndcard", mixerHandle->uid, captureDev->cardid);
+ goto OnErrorExit;
+ }
+
+ // create mute control and register it as pause/resume ctl)
+ char runName[ALSA_CARDID_MAX_LEN];
+ snprintf(runName, sizeof (runName), "run-%s", sndStream[idx].uid);
+
+ // create a single boolean value control for pause/resume
+ int runNumid = AlsaCtlCreateControl(source, ctlDev, playbackDev, runName, 1, 0, 1, 1, !sndStream[idx].mute);
+ if (runNumid <= 0) goto OnErrorExit;
+
+ // register mute/unmute as a pause/resume control
+ error = AlsaCtlRegister(source, captureDev, runNumid);
+ if (error) goto OnErrorExit;
+
+ // create stream and delay pcm openning until vol control is created
+ char volName[ALSA_CARDID_MAX_LEN];
+ snprintf(volName, sizeof (volName), "vol-%s", sndStream[idx].uid);
+ AlsaPcmInfoT *streamPcm = AlsaCreateSoftvol(source, &sndStream[idx], captureDev, volName, VOL_CONTROL_MAX, 0);
+ if (!streamPcm) {
+ AFB_ApiError(source->api, "SndStreams: mixer=%s%s(pcm) fail to create stream", mixerHandle->uid, sndStream[idx].uid);
+ goto OnErrorExit;
+ }
+
+ // create volume control before softvol pcm is opened
+ int volNumid = AlsaCtlCreateControl(source, ctlDev, playbackDev, volName, streamPcm->params.channels, VOL_CONTROL_MIN, VOL_CONTROL_MAX, VOL_CONTROL_STEP, sndStream[idx].volume);
+ if (volNumid <= 0) goto OnErrorExit;
+
+ // **** Fulup (would need some help to get automatic rate converter to work).
+ // // add a rate converter plugin to match stream params config
+ // char rateName[ALSA_CARDID_MAX_LEN];
+ // snprintf(rateName, sizeof (rateName), "rate-%s", sndStream[idx].uid);
+ // AlsaPcmInfoT *ratePcm= AlsaCreateRate(source, rateName, streamPcm, 1);
+ // if (!ratePcm) {
+ // AFB_ApiError(source->api, "SndStreams: mixer=%s%s(pcm) fail to create rate converter", sndStream[idx].uid);
+ // goto OnErrorExit;
+ // }
+
+ // everything is not ready to open capture pcm
+ error = snd_pcm_open(&streamPcm->handle, sndStream[idx].uid, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
+ if (error) {
+ AFB_ApiError(source->api, "SndStreams: mixer=%s%s(pcm) fail to open capture", mixerHandle->uid, sndStream[idx].uid);
+ goto OnErrorExit;
+ }
+
+ // capture stream inherit channel from targeted zone
+ captureDev->ccount = streamPcm->ccount;
+ streamPcm->params.channels = streamPcm->ccount;
+
+ // start stream pcm copy (at this both capture & sink pcm should be open)
+ error = AlsaPcmCopy(source, captureDev, streamPcm, &streamPcm->params);
+ if (error) goto OnErrorExit;
+
+ // retrieve active/pause control and set PCM status accordingly
+ error = AlsaCtlNumidGetLong(source, ctlDev, captureDev->numid, &value);
+ if (error) goto OnErrorExit;
+
+ // toggle pause/resume (should be done after pcm_start)
+ if ((error = snd_pcm_pause(captureDev->handle, !value)) < 0) {
+ AFB_ApiError(source->api, "SndStreams: mixer=%s [capture=%s] fail to pause error=%s", mixerHandle->uid, captureDev->cardid, snd_strerror(error));
+ goto OnErrorExit;
+ }
+
+ // prepare response for application
+ playbackDev->device = ctlLoop->playback;
+ error = AlsaByPathDevid(source, playbackDev);
+
+ error += wrap_json_pack(&paramsJ, "{si si si si}", "rate", streamPcm->params.rate, "channels", streamPcm->params.channels, "format", streamPcm->params.format, "access", streamPcm->params.access);
+ error += wrap_json_pack(&streamJ, "{ss ss si si so}", "uid", streamPcm->uid, "alsa", playbackDev->cardid, "volid", volNumid, "runid", runNumid, "params", paramsJ);
+ error += json_object_array_add(*responseJ, streamJ);
+ if (error) {
+ AFB_ApiError(source->api, "SndStreams: mixer=%s stream=%s fail to prepare response", mixerHandle->uid, captureDev->cardid);
+ goto OnErrorExit;
+ }
+
+ // create a dedicated verb for this stream compose of mixeruid/streamuid
+ apiHandleT *apiHandle = calloc(1, sizeof (apiHandleT));
+ char apiVerb[128];
+ error = snprintf(apiVerb, sizeof (apiVerb), "%s/%s", mixerHandle->uid, sndStream[idx].uid);
+ if (error == sizeof (apiVerb)) {
+ AFB_ApiError(source->api, "SndStreams mixer=%s fail to register Stream API too long %s/%s", mixerHandle->uid, mixerHandle->uid, sndStream[idx].uid);
+ return -1;
+ }
+
+ apiHandle->mixer = mixerHandle;
+ apiHandle->streams = &sndStream[idx];
+ apiHandle->verb = strdup(apiVerb);
+ error = afb_dynapi_add_verb(source->api, apiHandle->verb, sndStream[idx].info, StreamApiVerbCB, mixerHandle, NULL, 0);
+ if (error) {
+ AFB_ApiError(source->api, "SndStreams mixer=%s fail to register API verb=%s", mixerHandle->uid, apiHandle->verb);
+ return -1;
+ }
+
+ // free tempry resource
+ snd_ctl_close(ctlDev);
+
+ // Debug Alsa Config
+ //AlsaDumpElemConfig (source, "\n\nAlsa_Config\n------------\n", "pcm");
+ //AlsaDumpPcmInfo(source, "\n\nPcm_config\n-----------\n", streamPcm->handle);
+
+ AFB_ApiNotice(source->api, "SndStreams: mixer=%s stream=%s OK reponse=%s\n", mixerHandle->uid, streamPcm->uid, json_object_get_string(streamJ));
+ }
+
+ return 0;
+
+OnErrorExit:
+ return -1;
+} \ No newline at end of file