diff options
Diffstat (limited to 'plugins/alsa/alsa-api-streams.c')
-rw-r--r-- | plugins/alsa/alsa-api-streams.c | 340 |
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", ¶msJ); + 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(¶msJ, "{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 |