From 6f13e28ba698a2b0145acbb926b79cd569a31f44 Mon Sep 17 00:00:00 2001 From: Fulup Ar Foll Date: Sun, 13 May 2018 22:07:22 +0200 Subject: First testable version. Mixing with volume and mute per audio role works. --- README.md | 359 +++---------------------- conf.d/cmake/config.cmake | 2 +- conf.d/project/lua.d/softmixer-01.lua | 217 --------------- conf.d/project/lua.d/softmixer-02.lua | 168 ++++++++++++ conf.d/project/samples/softmixer-multi-01.lua | 223 +++++++++++++++ conf.d/project/sounds/trio-divi-alkazabach.mp3 | Bin 0 -> 7254516 bytes plugins/alsa/alsa-api-sndcards.c | 118 +++++--- plugins/alsa/alsa-api-sndloops.c | 80 ++++-- plugins/alsa/alsa-api-sndstreams.c | 166 ++++++++---- plugins/alsa/alsa-api-sndzones.c | 3 +- plugins/alsa/alsa-core-ctl.c | 336 +++++++++++++++++------ plugins/alsa/alsa-core-pcm.c | 29 +- plugins/alsa/alsa-plug-dmix.c | 4 +- plugins/alsa/alsa-plug-multi.c | 4 +- plugins/alsa/alsa-plug-rate.c | 81 ++++++ plugins/alsa/alsa-plug-route.c | 4 +- plugins/alsa/alsa-plug-stream.c | 19 +- plugins/alsa/alsa-softmixer.c | 82 ------ plugins/alsa/alsa-softmixer.h | 27 +- plugins/alsa/alsa-utils-bypath.c | 1 - 20 files changed, 1067 insertions(+), 856 deletions(-) delete mode 100644 conf.d/project/lua.d/softmixer-01.lua create mode 100644 conf.d/project/lua.d/softmixer-02.lua create mode 100644 conf.d/project/samples/softmixer-multi-01.lua create mode 100644 conf.d/project/sounds/trio-divi-alkazabach.mp3 create mode 100644 plugins/alsa/alsa-plug-rate.c diff --git a/README.md b/README.md index 6239afd..2fbb6b7 100644 --- a/README.md +++ b/README.md @@ -6,344 +6,61 @@ Softmixer controller for 4A (AGL Advance Audio Architecture). * Author: Fulup Ar Foll fulup@iot.bzh * Date : April-2018 -## Functionalities: - - Create an application dedicate controller from a JSON config file - - Each controls (eg: navigation, multimedia, ...) is a suite of actions. When all actions succeed control is granted, if one fail control acces is denied. - - Actions can either be: - + Invocation to an other binding API, either internal or external (eg: a policy service, Alsa UCM, ...) - + C routines from a user provider plugin (eg: policy routine, proprietary code, ...) - + Lua script function. Lua provides access to every AGL appfw functionalities and can be extended from C user provided plugins. - -## Installation - - Controler is a native part of AGL Advance Audio Framework but may be used independently with any other service or application binder. - - Dependencies: the only dependencies are audio-common for JSON-WRAP and Filescan-utils capabilities. - - Controler relies on Lua-5.3, when not needed Lua might be removed at compilation time. - -## Monitoring - - Default test HTML page expect monitoring HTML page to be accessible from /monitoring for this to work you should - * place monitoring HTML pages in a well known location eg: $HOME/opt/monitoring - * start your binder with the alias option e.g. afb-daemon --port=1234 --alias=/monitoring:/home/fulup/opt/afb-monitoring --ldpaths=. --workdir=. --roothttp=../htdocs - -## Config - -Configuration is loaded dynamically during startup time. The controller scans CONTROL_CONFIG_PATH for a file corresponding to pattern -"onload-bindername-xxxxx.json". When controller runs within AAAA binder it searches for "onload-audio-xxxx.json". First file found in the path the loaded -any other files corresponding to the same pather are ignored and only generate a warning. - -Each bloc in the configuration file are defined with - * label: must be provided is used either for debugging or as input for the action (eg: signal name, control name, ...) - * info: optional used for documentation purpose only - -Note by default controller config search path is defined at compilation time, but path might be overloaded with CONTROL_CONFIG_PATH -environment variable. Setenv 'CONTROL_ONLOAD_PROFILE'=xxxx to overload 'onload-default-profile' initialisation sequence. - -### Config is organised in 4 sections: - - * metadata - * onload defines the set of action to be executed at startup time - * control defines the set of controls with corresponding actions - * event define the set of actions to be executed when receiving a given signal - -### Metadata - -As today matadata is only used for documentation purpose. - * label + version mandatory - * info optional - -### OnLoad section - -Defines startup time configuration. Onload may provide multiple initialisation profiles, each with a different label. - * label is mandatory. Label is used to select onload profile at initialisation through DispatchOneOnLoad("onload-label") API; - * info is optional - * plugin provides optional unique plugin name. Plugin should follow "onload-bindername-xxxxx.ctlso" patern - and are search into CONTROL_PLUGIN_PATH. When defined controller will execute user provided function context=CTLP_ONLOAD(label,version,info). - The context returned by this routine is provided back to any C routines call later by the controller. Note that Lua2C function - are prefix in Lua script with plugin label (eg: MyPlug: in following config sample) - * lua2c list of Lua commands shipped with provided plugin. - * require list of binding that should be initialised before the controller starts. Note that some listed requirer binding might be absent, - nevertheless any present binding from this list will be started before controller binding, missing ones generate a warning. - * action the list of action to execute during loadtime. Any failure in action will prevent controller binding from starting. - -### Control section - -Defines a list of controls that are accessible through (api="control", verb="request", control="control-label"). - - * label mandatory - * info optional - * permissions Cynara needed privileges to request this control (same as AppFw-V2) - * action the list of actions - -### Event section - -Defines a list of actions to be executed on event reception. Even can do anything a controller can (change state, -send back signal, ...) eg: if a controller subscribes to vehicule speed, then speed-event may ajust master-volume to speed. - - * label mandatory - * info optional - * action the list of actions - -### Actions Categories - -Controler support tree categories of actions. Each action return a status status where 0=success and 1=failure. - * AppFw API, Provides a generic model to request other bindings. Requested binding can be local (eg: ALSA/UCM) or - external (eg: vehicle signalling). - * api provides requested binding API name - * verb provides verb to requested binding - * args optionally provides a jsonc object for targeted binding API. Note that 'args' are statically defined - in JSON configuration file. Controler client may also provided its own arguments from the query list. Targeted - binding receives both arguments defined in the config file and the argument provided by controller client. - * C-API, when defined in the onload section, the plugin may provided C native API with CTLP-CAPI(apiname, label, args, query, context). - Plugin may also create Lua command with CTLP-Lua2C(LuaFuncName, label, args, query, context). Where args+query are JSONC object - and context the value return from CTLP_ONLOAD function. Any missing value is set to NULL. - * Lua-API, when compiled with Lua option, the controller support action defined directly in Lua script. During "onload" phase the - controller search in CONTROL_Lua_PATH file with pattern "onload-bindername-xxxx.lua". Any file corresponding to this pattern - is automatically loaded. Any function defined in those Lua script can be called through a controller action. Lua functions receive - three parameters (label, args, query). - -Note: Lua added functions systematically prefix. AGL standard AppFw functions are prefixed with AGL: (eg: AGL:notice(), AGL_success(), ...). -User Lua functions added though the plugin and CTLP_Lua2C are prefix with plugin label (eg: MyPlug:HelloWorld1). - -### Avaliable Application Framework Commands - -Each Lua AppFw commands should be prefixed by AFB: - - * AFB:notice ("format", arg1,... argn) LUA table are print directly as json string with '%s'. - AFB:error, AFB:warning, AFB:info, AFB:debug work on the same model. Printed message are limited to 512 characters. - - * AFB:service ('API', 'VERB', {query}, "Lua_Callback_Name", {context}) asynchronous call to an other binding. When empty query/context should be set to '{}' - and not to 'nil'. When 'nil' Lua does not send 'NULL' value but remove arguments to calling stack. WARNING:"Callback" - is the name of the callback as a string and not a pointer to the callback. (If someone as a solution to fix this, please - let me known). Callback is call as LUA "function Alsa_Get_Hal_CB (error, result, context)" where: - * error is a Boolean - * result is the full answer from AppFw (do not forget to extract response) - * context is a copy of the Lua table pas as argument (warning it's a copy not a pointer to original table) - - * error,result=AFB:servsync('API', 'VERB', {query}) Save as previous but for synchronous call. Note that Lua accept multiple - return. AFB:servsync return both the error message and the response as a Lua table. Like for AFB:service user should not - forget to extract response from result. - - * AFB:success(request, response) request is the opaque handle pass when Lua is called from (api="control", verb="docall"). - Response is a Lua table that will be return to client. - - * AFB:fail(request, response) same as for success. Note that LUA generate automatically the error code from Lua function name. - The response is tranformed to a json string before being return to client. - - * EventHandle=AFB:evtmake("MyEventName") Create an event and return the handle as an opaque handle. Note that due to a limitation - of json_object this opaque handle cannot be passed as argument in a callback context. - - * AFB:subscribe(request, MyEventHandle) Subscribe a given client to previously created event. - - * AFB:evtpush (MyEventHandle, MyEventData) Push an event to every subscribed client. MyEventData is a Lua table that will be - send as a json object to corresponding clients. +## Compile +``` + mkdir build + cd build + cmake .. + make +``` - * timerHandle=AFB:timerset (MyTimer, "Timer_Test_CB", context) Initialise a timer from MyTimer Lua table. This table should contend 3 elements: - MyTimer={[l"abel"]="MyTimerName", ["delay"]=timeoutInMs, ["count"]=nBOfCycles}. Note that is count==0 then timer is cycle - infinitively. Context is a standard Lua table. This function return an opaque handle to be use to further control the timer. +## Install Alsa Loopback - * AFB:timerclear(timerHandle) Kill an existing timer. Return an error when timer does not exit. +``` + sudo modprobe alsa-aloop +``` - * MyTimer=AFB:timerget(timerHandle) Return Label, Delay and Count of an active timer. Return an error when timerHandle does not - point on an active timer. +## Assert LUA config file match your config -Note: Except for function call during binding initialisation period. Lua call are protected and should return clean message - even when improperly used. If you find bug please report. +``` + vim $PROJECT_ROOT/conf.d/project/lua.d/softmixer-01.lua -### Adding Lua command from User Plugin + # make sure both your loopback and targeted sound card path are valid +``` -User Plugin is optional and may provide either native C-action accessible directly from controller actions as defined in -JSON config file, or alternatively may provide at set of Lua commands usable inside any script (onload, control,event). A simple -plugin that provide both natice C API and Lua commands is provided as example (see ctl-plugin-sample.c). Technically a -plugin is a simple sharelibrary and any code fitting in sharelib might be used as a plugin. Developer should nevertheless -not forget that except when no-concurrency flag was at binding construction time, any binding should to be thread safe. -A plugin must be declare with CTLP_REGISTER("MyCtlSamplePlugin"). This entry point defines a special structure that is check -at plugin load time by the controller. Then you have an optional init routine declare with CTLP_ONLOAD(label, version, info). -This init routine receives controller onload profile as selected by DispatchOnLoad("profile"). The init routine may create -a plugin context that is later one presented to every plugin API this for both LUA and native C ones. Then each: +## Run from shell - * C API declare with CTLP_CAPI (MyCFunction, label, argsJ, queryJ, context) {your code}. Where: - * MyFunction is your function - * Label is a string containing the name of your function - * ArgsJ a json_object containing the argument attach the this control in JSON config file. - * context your C context as return from CTLP_ONLOAD +``` + afb-daemon --name 4a-softmixer-afbd --port=1234 --workdir=/home/fulup/Workspace/Audio-4a/4a-softmixer/build \ + --binding=package/lib/softmixer-binding.so --roothttp=package/htdocs --token= --tracereq=common --verbose + + # lua test script should return a response looking like + response= { + [1] = { ["uid"] = navigation,["runid"] = 101,["alsa"] = hw:5,0,6,["volid"] = 103,} + , ["params"] = { ["channels"] = 2,["format"] = 2,["rate"] = 48000,["access"] = 3,} + ,[2] = { .... + } + + # runid: pause/resume alsa control you may change it with 'amixer -D hw:Loopback cset numid=101 on|off + # volid: volume alsa control you may change it from 'alsamixer -Dhw:Loppback' or with 'amixer -D hw:Loopback cset numid=103 NN (o-100%) +``` - * Lua API declarewith TLP_LUA2C (MyLuaCFunction, label, argsJ, context) {your code}. Where - * MyLuaCFunction is both the name of your C function and Lua command - * Label your function name as a string - * Args the arguments passed this time from Lua script and not from Json config file. - * Query is not provided as LuaC function are called from a script and not directly from controller action list. -Warning: Lua samples use with controller enforce strict mode. As a result every variables should be declare either as -local or as global. Unfortunately "luac" is not smart enough to handle strict mode at build time and errors only appear -at run time. Because of this strict mode every global variables (which include functions) should be prefix by '_'. -Note that LUA require an initialisation value for every variables and declaring something like "local myvar" wont -allocate "myvar" -### Debugging Facilities -Controler Lua script are check for syntax from CMAKE template with Luac. When needed to go further an developer API allow to -execute directly Lua command within controller context from Rest/Ws (api=control, verb=lua_doscript). DoScript API takes two -other optional arguments func=xxxx where xxxx is the function to execute within Lua script and args a JSON object to provide -input parameter. When funcname is not given by default the controller try to execute middle filename doscript-xxxx-????.lua. +Retrieve audio-stream alsa endpoint from response to 'L2C:snd_streams' command. Depending on your config 'hw:XXX' will change. +Alsa snd-aloop impose '0' as playback device. Soft mixer will start from last subdevice and allocates one subdev for each audio-stream. -When executed from controller Lua script may use any AppFw Apis as well as any L2C user defined commands in plugin. -### Running as Standalone Controller +## Play some music -Controller is a standard binding and can then be started independently of AAAA. When started with from build repository with +Current version does not handle audio rate conversion, using gstreamer or equivalent to match with audio hardware params is mandatory. ``` -afb-daemon --port=1234 --workdir=. --roothttp=../htdocs --tracereq=common --token= --verbose --binding=./Controller-afb/afb-control-afb.so -``` - -Afb-Daemon only load controller bindings without search for the other binding. In this case the name of the process is not change -to afb-audio and controller binding will search for a configuration file name 'onload-daemon-xxx.json'. This model can be used -to implement for testing purpose or simply to act as the glue in between a UI and other binder/services. - -## Config Sample + gst123 --audio-output alsa=hw:XXX,0,7 $PROJECT_ROOT/conf.d/project/sounds/trio-divi-alkazabach.wav -Here after a simple configuration sample. - -``` -{ - "$schema": "ToBeDone", - "metadata": { - "label": "sample-audio-control", - "info": "Provide Default Audio Policy for Multimedia, Navigation and Emergency", - "version": "1.0" - }, - "onload": [{ - "label": "onload-default", - "info": "onload initialisation config", - "plugin": { - "label" : "MyPlug", - "sharelib": "ctl-audio-plugin-sample.ctlso", - "lua2c": ["Lua2cHelloWorld1", "Lua2cHelloWorld2"] - }, - "require": ["intel-hda", "jabra-usb", "scarlett-usb"], - "actions": [ - { - "label": "onload-sample-cb", - "info": "Call control sharelib install entrypoint", - "callback": "SamplePolicyInit", - "args": { - "arg1": "first_arg", - "nextarg": "second arg value" - } - }, { - "label": "onload-sample-api", - "info": "Assert AlsaCore Presence", - "api": "alsacore", - "verb": "ping", - "args": "test" - }, { - "label": "onload-hal-lua", - "info": "Load avaliable HALs", - "lua": "Audio_Init_Hal" - } - ] - }], - "controls": - [ - { - "label": "multimedia", - "permissions": "urn:AGL:permission:audio:public:mutimedia", - "actions": { - "label": "multimedia-control-lua", - "info": "Call Lua Script function Test_Lua_Engin", - "lua": "Audio_Set_Multimedia" - } - }, { - "label": "navigation", - "permissions": "urn:AGL:permission:audio:public:navigation", - "actions": { - "label": "navigation-control-lua", - "info": "Call Lua Script to set Navigation", - "lua": "Audio_Set_Navigation" - } - }, { - "label": "emergency", - "permissions": "urn:AGL:permission:audio:public:emergency", - "actions": { - "label": "emergency-control-ucm", - "lua": "Audio_Set_Emergency" - } - }, { - "label": "multi-step-sample", - "info" : "all actions must succeed for control to be accepted", - "actions": [{ - "label": "multimedia-control-cb", - "info": "Call Sharelib Sample Callback", - "callback": "sampleControlNavigation", - "args": { - "arg1": "snoopy", - "arg2": "toto" - } - }, { - "label": "navigation-control-ucm", - "api": "alsacore", - "verb": "ping", - "args": { - "test": "navigation" - } - }, { - "label": "navigation-control-lua", - "info": "Call Lua Script to set Navigation", - "lua": "Audio_Set_Navigation" - }] - } - ], - "events": - [ - { - "label": "Vehicle-Speed", - "info": "Action when Vehicule speed change", - "actions": [ - { - "label": "speed-action-1", - "callback": "Blink-when-over-130", - "args": { - "speed": 130 - "blink-speed": 1000 - } - }, { - "label": "Adjust-Volume", - "lua": "Adjust_Volume_To_Speed", - } - ] - }, - { - "label": "Reverse-Engage", - "info": "When Reverse Gear is Engage", - "actions": [ - { - "label": "Display-Rear-Camera", - "callback": "Display-Rear-Camera", - }, { - "label": "Prevent-Phone-Call", - "api" : "phone", - "verb" : "status", - "args": { - "call-accepted": false - } - } - ] - }, - { - "label": "Neutral-Engage", - "info": "When Reverse Neutral is Engage", - "actions": [ - { - "label": "Authorize-Video", - "api" : "video", - "verb" : "status", - "args": { - "tv-accepted": true - } - } - ] - } - ] -} + gst123 --audio-output alsa=hw:XXX,0,??? other sound file + speaker-test -D hw:XXX:0:??? -twav -c!! 'cc' is the number of channel and depends on the audio stream zone target. ``` diff --git a/conf.d/cmake/config.cmake b/conf.d/cmake/config.cmake index b807a5e..1b5092c 100644 --- a/conf.d/cmake/config.cmake +++ b/conf.d/cmake/config.cmake @@ -198,7 +198,7 @@ set(AFB_REMPORT "1234" CACHE PATH "Default binder listening port") # Print a helper message when every thing is finished # ---------------------------------------------------- -set(CLOSING_MESSAGE "Typical binding launch: afb-daemon --name ${PROJECT_NAME}-afbd --port=${AFB_REMPORT} --workdir=${CMAKE_BINARY_DIR} --binding=package/lib/softmixer-binding.so --ldpath=${CMAKE_INSTALL_PREFIX}/4a-alsa-core/lib --roothttp=package/htdocs --token=\"${AFB_TOKEN}\" --tracereq=common --verbose") +set(CLOSING_MESSAGE "Typical binding launch: afb-daemon --name ${PROJECT_NAME}-afbd --port=${AFB_REMPORT} --workdir=${CMAKE_BINARY_DIR} --binding=package/lib/softmixer-binding.so --roothttp=package/htdocs --token=\"${AFB_TOKEN}\" --tracereq=common --verbose") set(PACKAGE_MESSAGE "Install widget file using in the target : afm-util install ${PROJECT_NAME}.wgt") # Optional schema validator about now only XML, LUA and JSON diff --git a/conf.d/project/lua.d/softmixer-01.lua b/conf.d/project/lua.d/softmixer-01.lua deleted file mode 100644 index ed60192..0000000 --- a/conf.d/project/lua.d/softmixer-01.lua +++ /dev/null @@ -1,217 +0,0 @@ ---[[ - Copyright (C) 2016 "IoT.bzh" - Author Fulup Ar Foll - - 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. - - - NOTE: strict mode: every global variables should be prefixed by '_' ---]] - --- Static variables should be prefixed with _ -_EventHandle={} - --- Call when AlsaCore return HAL active list -function _AlsaPingCB_ (source, result, context) - - AFB:notice (source, "--InLua-- PingCB: result='%s'", Dump_Table(result)) - -end - - --- Display receive arguments and echo them to caller -function _mixer_config_ (source, args) - do - local error - local response - - -- ============================= Sound Cards =================== - - local snd_params = { - ["rate"] = 48000, - ["channel"]= 2, - } - - local sndcard_0 = { - ["uid"]= "YAMAHA-APU70", - ["devpath"]= "/dev/snd/by-id/usb-YAMAHA_Corporation_YAMAHA_AP-U70_USB_Audio_00-00", - ["params"] = params, - ["sink"] = { - [0]= {["uid"]= "front-right", ["port"]= 0}, - [1]= {["uid"]= "front-left", ["port"]= 1}, - } - } - - local sndcard_1 = { - ["uid"]= "Jabra-Solemate", - ["devpath"]= "/dev/snd/by-id/usb-0b0e_Jabra_SOLEMATE_v1.34.0-00", - ["params"] = params, - ["sink"] = { - [0]= {["uid"]= "front-right", ["port"]= 0}, - [1]= {["uid"]= "front-left", ["port"]= 1}, - } - } - - local sndcard_2 = { - ["uid"]= "Jabra-410", - ["params"] = params, - ["devpath"]= "/dev/snd/by-id/usb-0b0e_Jabra_SPEAK_410_USB_745C4B15BD11x010900-00", - ["sink"] = { - [0]= {["uid"]= "back-right", ["port"]= 0}, - [1]= {["uid"]= "back-left", ["port"]= 1}, - } - } - - -- group sound card as one multi channels card - local sndcards= { - sndcard_0, - sndcard_2, - } - - error,response= L2C:snd_cards (source, sndcards) - if (error ~= 0) then - AFB:error (source, "--InLua-- L2C:snd_cards fail to attach sndcards=%s", Dump_Table(sndcards)) - goto OnErrorExit - else - AFB:notice (source, "--InLua-- L2C:snd_cards done response=%s\n", Dump_Table(response)) - end - - -- ============================= Zones =================== - - local zone_front= { - ["uid"] = "front-seats", - ["type"] = "playback", - ["mapping"] = { - {["target"]="front-right",["channel"]=0}, - {["target"]="front-left" ,["channel"]=1}, - } - } - - local zone_back= { - ["uid"] = "back-seats", - ["type"] = "playback", - ["mapping"] = { - {["target"]="back-right",["channel"]=0}, - {["target"]="back-left" ,["channel"]=1}, - } - } - - local zone_all= { - ["uid"] = "all-seats", - ["type"] = "playback", - ["mapping"] = { - {["target"]="front-right",["channel"]=0}, - {["target"]="front-left" ,["channel"]=1}, - {["target"]="back-right" ,["channel"]=0}, - {["target"]="back-left" ,["channel"]=1}, - } - } - - local multi_zones = { - zone_all, - zone_front, - zone_back, - } - - error,response= L2C:snd_zones (source, multi_zones) - if (error ~= 0) then - AFB:error (source, "--InLua-- L2C:snd_zones fail to attach sndcards=%s", Dump_Table(multi_zones)) - goto OnErrorExit - else - AFB:notice (source, "--InLua-- L2C:snd_zones done response=%s\n", Dump_Table(response)) - end - - -- ======================= Loop PCM =========================== - - local snd_aloop = { - ["uid"] = "Alsa-Loop", - ["devpath"] = "/dev/snd/by-path/platform-snd_aloop.0", - ["devices"] = {["playback"]=0,["capture"]=1}, - ["subdevs"] = { - {["subdev"]= 0, ["numid"]= 51}, - {["subdev"]= 1, ["numid"]= 57}, - {["subdev"]= 2, ["numid"]= 63}, - {["subdev"]= 3, ["numid"]= 69}, - {["subdev"]= 4, ["numid"]= 75}, - {["subdev"]= 5, ["numid"]= 81}, - {["subdev"]= 6, ["numid"]= 87}, - {["subdev"]= 7, ["numid"]= 93}, - } - } - - error,response= L2C:snd_loops (source, snd_aloop) - if (error ~= 0) then - AFB:error (source, "--InLua-- L2C:snd_loops fail to attach sndcards=%s", Dump_Table(aloop)) - goto OnErrorExit - else - AFB:notice (source, "--InLua-- L2C:snd_loops done response=%s\n", Dump_Table(response)) - end - - -- =================== Audio Stream ============================ - - local stream_music= { - ["uid"] = "multimedia", - ["zone"] = "all-seats", - ["volume"]= 70, - ["rate"] = 48000, - ["mute"] = false; - } - - local stream_navigation= { - ["uid"] = "navigation", - ["zone"] = "front-seats", - ["volume"]= 80, - ["mute"] = false; - } - - local stream_children= { - ["uid"] = "children", - ["zone"] = "back-seats", - ["volume"]= 50, - ["mute"] = false; - } - - local snd_streams = { - stream_music, - stream_navigation, - stream_children, - } - - error,response= L2C:snd_streams (source, snd_streams) - if (error ~= 0) then - AFB:error (source, "--InLua-- L2C:snd_streams fail to attach sndcards=%s", Dump_Table(aloop)) - goto OnErrorExit - else - AFB:notice (source, "--InLua-- L2C:streams_loops done response=%s\n", Dump_Table(response)) - end - - - -- ================== Happy End ============================= - AFB:notice (source, "--InLua-- _mixer_config_ done") - return 0 end - - -- ================= Unhappy End ============================ - ::OnErrorExit:: - AFB:error (source, "--InLua-- snd_attach fail") - return 1 -- unhappy end -- -end - --- Display receive arguments and echo them to caller -function _init_softmixer_ (source, args) - - -- create event to push change audio roles to potential listeners - _EventHandle=AFB:evtmake(source, "control") - - _mixer_config_ (source, args) - -end diff --git a/conf.d/project/lua.d/softmixer-02.lua b/conf.d/project/lua.d/softmixer-02.lua new file mode 100644 index 0000000..5ea52de --- /dev/null +++ b/conf.d/project/lua.d/softmixer-02.lua @@ -0,0 +1,168 @@ +--[[ + Copyright (C) 2016 "IoT.bzh" + Author Fulup Ar Foll + + 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. + + + NOTE: strict mode: every global variables should be prefixed by '_' +--]] + +-- Static variables should be prefixed with _ +_EventHandle={} + +-- Call when AlsaCore return HAL active list +function _AlsaPingCB_ (source, result, context) + + AFB:notice (source, "--InLua-- PingCB: result='%s'", Dump_Table(result)) + +end + + +-- Display receive arguments and echo them to caller +function _mixer_config_ (source, args) + do + local error + local response + + -- ==================== Default rate =========================== + + local audio_defaults = { + ["rate"] = 48000, + } + + -- ======================= Loop PCM =========================== + + local snd_aloop = { + ["uid"] = "Alsa-Loop", + ["devpath"] = "/dev/snd/by-path/platform-snd_aloop.0", + ["devices"] = {["playback"]=0,["capture"]=1}, + ["params"] = audio_defaults, + ["subdevs"] = { + {["subdev"]= 0, ["numid"]= 51}, + {["subdev"]= 1, ["numid"]= 57}, + {["subdev"]= 2, ["numid"]= 63}, + {["subdev"]= 3, ["numid"]= 69}, + {["subdev"]= 4, ["numid"]= 75}, + {["subdev"]= 5, ["numid"]= 81}, + {["subdev"]= 6, ["numid"]= 87}, + {["subdev"]= 7, ["numid"]= 93}, + } + } + + error,response= L2C:snd_loops (source, snd_aloop) + if (error ~= 0) then + AFB:error (source, "--InLua-- L2C:snd_loops fail to attach sndcards=%s", Dump_Table(aloop)) + goto OnErrorExit + else + AFB:notice (source, "--InLua-- L2C:snd_loops done response=%s\n", Dump_Table(response)) + end + + + -- ============================= Sound Cards =================== + + local sndcard_0 = { + ["uid"]= "YAMAHA-APU70", + ["devpath"]= "/dev/snd/by-id/usb-YAMAHA_Corporation_YAMAHA_AP-U70_USB_Audio_00-00", + ["params"] = snd_params, + ["sink"] = { + [0]= {["uid"]= "front-right", ["port"]= 0}, + [1]= {["uid"]= "front-left", ["port"]= 1}, + } + } + + -- group sound card as one multi channels card + local sndcards= { + sndcard_0, + } + + error,response= L2C:snd_cards (source, sndcards) + if (error ~= 0) then + AFB:error (source, "--InLua-- L2C:snd_cards fail to attach sndcards=%s", Dump_Table(sndcards)) + goto OnErrorExit + else + AFB:notice (source, "--InLua-- L2C:snd_cards done response=%s\n", Dump_Table(response)) + end + + -- ============================= Zones =================== + + local zone_front= { + ["uid"] = "front-seats", + ["type"] = "playback", + ["mapping"] = { + {["target"]="front-right",["channel"]=0}, + {["target"]="front-left" ,["channel"]=1}, + } + } + + local multi_zones = { + zone_front, + } + + error,response= L2C:snd_zones (source, multi_zones) + if (error ~= 0) then + AFB:error (source, "--InLua-- L2C:snd_zones fail to attach sndcards=%s", Dump_Table(multi_zones)) + goto OnErrorExit + else + AFB:notice (source, "--InLua-- L2C:snd_zones done response=%s\n", Dump_Table(response)) + end + + -- =================== Audio Stream ============================ + + local stream_music= { + ["uid"] = "multimedia", + ["zone"] = "front-seats", + ["volume"]= 70, + ["mute"] = false, + } + + local stream_navigation= { + ["uid"] = "navigation", + ["zone"] = "front-seats", + ["volume"]= 80, + ["mute"] = false, + } + + local snd_streams = { + stream_music, + stream_navigation, + } + + error,response= L2C:snd_streams (source, snd_streams) + if (error ~= 0) then + AFB:error (source, "--InLua-- L2C:snd_streams fail to attach sndcards=%s", Dump_Table(aloop)) + goto OnErrorExit + else + AFB:notice (source, "--InLua-- L2C:streams_loops done response=%s\n", Dump_Table(response)) + end + + + -- ================== Happy End ============================= + AFB:notice (source, "--InLua-- _mixer_config_ done") + return 0 end + + -- ================= Unhappy End ============================ + ::OnErrorExit:: + AFB:error (source, "--InLua-- snd_attach fail") + return 1 -- unhappy end -- +end + +-- Display receive arguments and echo them to caller +function _init_softmixer_ (source, args) + + -- create event to push change audio roles to potential listeners + _EventHandle=AFB:evtmake(source, "control") + + _mixer_config_ (source, args) + +end diff --git a/conf.d/project/samples/softmixer-multi-01.lua b/conf.d/project/samples/softmixer-multi-01.lua new file mode 100644 index 0000000..1b8ba02 --- /dev/null +++ b/conf.d/project/samples/softmixer-multi-01.lua @@ -0,0 +1,223 @@ +--[[ + Copyright (C) 2016 "IoT.bzh" + Author Fulup Ar Foll + + 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. + + + NOTE: strict mode: every global variables should be prefixed by '_' +--]] + +-- Static variables should be prefixed with _ +_EventHandle={} + +-- Call when AlsaCore return HAL active list +function _AlsaPingCB_ (source, result, context) + + AFB:notice (source, "--InLua-- PingCB: result='%s'", Dump_Table(result)) + +end + + +-- Display receive arguments and echo them to caller +function _mixer_config_ (source, args) + do + local error + local response + + + -- ======================= Loop PCM =========================== + + local snd_aloop = { + ["uid"] = "Alsa-Loop", + ["devpath"] = "/dev/snd/by-path/platform-snd_aloop.0", + ["devices"] = {["playback"]=0,["capture"]=1}, + ["subdevs"] = { + {["subdev"]= 0, ["numid"]= 51}, + {["subdev"]= 1, ["numid"]= 57}, + {["subdev"]= 2, ["numid"]= 63}, + {["subdev"]= 3, ["numid"]= 69}, + {["subdev"]= 4, ["numid"]= 75}, + {["subdev"]= 5, ["numid"]= 81}, + {["subdev"]= 6, ["numid"]= 87}, + {["subdev"]= 7, ["numid"]= 93}, + } + } + + error,response= L2C:snd_loops (source, snd_aloop) + if (error ~= 0) then + AFB:error (source, "--InLua-- L2C:snd_loops fail to attach sndcards=%s", Dump_Table(aloop)) + goto OnErrorExit + else + AFB:notice (source, "--InLua-- L2C:snd_loops done response=%s\n", Dump_Table(response)) + end + + + -- ============================= Sound Cards =================== + + local snd_params = { + ["rate"] = 48000, + ["channels"]= 2, + } + + local sndcard_0 = { + ["uid"]= "YAMAHA-APU70", + ["devpath"]= "/dev/snd/by-id/usb-YAMAHA_Corporation_YAMAHA_AP-U70_USB_Audio_00-00", + ["params"] = snd_params, + ["sink"] = { + [0]= {["uid"]= "front-right", ["port"]= 0}, + [1]= {["uid"]= "front-left", ["port"]= 1}, + } + } + + local sndcard_1 = { + ["uid"]= "Jabra-Solemate", + ["devpath"]= "/dev/snd/by-id/usb-0b0e_Jabra_SOLEMATE_v1.34.0-00", + ["params"] = snd_params, + ["sink"] = { + [0]= {["uid"]= "front-right", ["port"]= 0}, + [1]= {["uid"]= "front-left", ["port"]= 1}, + } + } + + local sndcard_2 = { + ["uid"]= "Jabra-410", + ["params"] = snd_params, + ["devpath"]= "/dev/snd/by-id/usb-0b0e_Jabra_SPEAK_410_USB_745C4B15BD11x010900-00", + ["sink"] = { + [0]= {["uid"]= "back-right", ["port"]= 0}, + [1]= {["uid"]= "back-left", ["port"]= 1}, + } + } + + -- group sound card as one multi channels card + local sndcards= { + sndcard_0, + sndcard_2, + } + + error,response= L2C:snd_cards (source, sndcards) + if (error ~= 0) then + AFB:error (source, "--InLua-- L2C:snd_cards fail to attach sndcards=%s", Dump_Table(sndcards)) + goto OnErrorExit + else + AFB:notice (source, "--InLua-- L2C:snd_cards done response=%s\n", Dump_Table(response)) + end + + -- ============================= Zones =================== + + local zone_front= { + ["uid"] = "front-seats", + ["type"] = "playback", + ["mapping"] = { + {["target"]="front-right",["channel"]=0}, + {["target"]="front-left" ,["channel"]=1}, + } + } + + local zone_back= { + ["uid"] = "back-seats", + ["type"] = "playback", + ["mapping"] = { + {["target"]="back-right",["channel"]=0}, + {["target"]="back-left" ,["channel"]=1}, + } + } + + local zone_all= { + ["uid"] = "all-seats", + ["type"] = "playback", + ["mapping"] = { + {["target"]="front-right",["channel"]=0}, + {["target"]="front-left" ,["channel"]=1}, + {["target"]="back-right" ,["channel"]=0}, + {["target"]="back-left" ,["channel"]=1}, + } + } + + local multi_zones = { + zone_all, + zone_front, + zone_back, + } + + error,response= L2C:snd_zones (source, multi_zones) + if (error ~= 0) then + AFB:error (source, "--InLua-- L2C:snd_zones fail to attach sndcards=%s", Dump_Table(multi_zones)) + goto OnErrorExit + else + AFB:notice (source, "--InLua-- L2C:snd_zones done response=%s\n", Dump_Table(response)) + end + + -- =================== Audio Stream ============================ + + local stream_music= { + ["uid"] = "multimedia", + ["zone"] = "all-seats", + ["volume"]= 70, + ["params"] = { + ["rate"] = 44100, + ["rate"] = 48000, + ["format"]= "S16_LE", + }, + ["mute"] = false, + } + + local stream_navigation= { + ["uid"] = "navigation", + ["zone"] = "front-seats", + ["volume"]= 80, + ["mute"] = false, + } + + local stream_children= { + ["uid"] = "children", + ["zone"] = "back-seats", + ["volume"]= 50, + ["mute"] = false, + } + + local snd_streams = { + stream_music, + stream_navigation, + stream_children, + } + + error,response= L2C:snd_streams (source, snd_streams) + if (error ~= 0) then + AFB:error (source, "--InLua-- L2C:snd_streams fail to attach sndcards=%s", Dump_Table(aloop)) + goto OnErrorExit + else + AFB:notice (source, "--InLua-- L2C:streams_loops done response=%s\n", Dump_Table(response)) + end + + + -- ================== Happy End ============================= + AFB:notice (source, "--InLua-- _mixer_config_ done") + return 0 end + + -- ================= Unhappy End ============================ + ::OnErrorExit:: + AFB:error (source, "--InLua-- snd_attach fail") + return 1 -- unhappy end -- +end + +-- Display receive arguments and echo them to caller +function _init_softmixer_ (source, args) + + -- create event to push change audio roles to potential listeners + _EventHandle=AFB:evtmake(source, "control") + + _mixer_config_ (source, args) + +end diff --git a/conf.d/project/sounds/trio-divi-alkazabach.mp3 b/conf.d/project/sounds/trio-divi-alkazabach.mp3 new file mode 100644 index 0000000..69ec868 Binary files /dev/null and b/conf.d/project/sounds/trio-divi-alkazabach.mp3 differ diff --git a/plugins/alsa/alsa-api-sndcards.c b/plugins/alsa/alsa-api-sndcards.c index e98a294..daa996a 100644 --- a/plugins/alsa/alsa-api-sndcards.c +++ b/plugins/alsa/alsa-api-sndcards.c @@ -25,77 +25,121 @@ extern Lua2cWrapperT Lua2cWrap; STATIC int ProcessOneChannel(CtlSourceT *source, const char* uid, json_object *channelJ, AlsaPcmChannels *channel) { const char*channelUid; - + int error = wrap_json_unpack(channelJ, "{ss,si !}", "uid", &channelUid, "port", &channel->port); if (error) goto OnErrorExit; - channel->uid=strdup(channelUid); + channel->uid = strdup(channelUid); return 0; OnErrorExit: - AFB_ApiError(source->api, "ProcessOneChannel: sndcard=%s channel: missing (uid||port) json=%s", uid, json_object_get_string(channelJ)); + AFB_ApiError(source->api, "ProcessOneChannel: sndcard=%s channel: missing (uid||port) json=%s", uid, json_object_get_string(channelJ)); return -1; } -STATIC int ProcessSndParams(CtlSourceT *source, const char* uid, json_object *paramsJ, AlsaPcmHwInfoT *params) { - - int error = wrap_json_unpack(paramsJ, "{s?i,s?i !}", "rate", ¶ms->rate, "channels", ¶ms->channels); +PUBLIC int ProcessSndParams(CtlSourceT *source, const char* uid, json_object *paramsJ, AlsaPcmHwInfoT *params) { + const char *format=NULL, *access=NULL; + + // some default values + params->rate= ALSA_DEFAULT_PCM_RATE; + params->channels= 2; + params->sampleSize=0; + + int error = wrap_json_unpack(paramsJ, "{s?i,s?i, s?s, s?s !}", "rate",¶ms->rate,"channels", ¶ms->channels, "format",&format, "access",&access); if (error) goto OnErrorExit; + + if (!format) params->format = SND_PCM_FORMAT_S16_LE; + else if (!strcasecmp(format, "S16_LE")) params->format = SND_PCM_FORMAT_S16_LE; + else if (!strcasecmp(format, "S16_BE")) params->format = SND_PCM_FORMAT_S16_BE; + else if (!strcasecmp(format, "U16_LE")) params->format = SND_PCM_FORMAT_U16_LE; + else if (!strcasecmp(format, "U16_BE")) params->format = SND_PCM_FORMAT_U16_BE; + else if (!strcasecmp(format, "S32_LE")) params->format = SND_PCM_FORMAT_S32_LE; + else if (!strcasecmp(format, "S32_BE")) params->format = SND_PCM_FORMAT_S32_BE; + else if (!strcasecmp(format, "U32_LE")) params->format = SND_PCM_FORMAT_U32_LE; + else if (!strcasecmp(format, "U32_BE")) params->format = SND_PCM_FORMAT_U32_BE; + else if (!strcasecmp(format, "S24_LE")) params->format = SND_PCM_FORMAT_S24_LE; + else if (!strcasecmp(format, "S24_BE")) params->format = SND_PCM_FORMAT_S24_BE; + else if (!strcasecmp(format, "U24_LE")) params->format = SND_PCM_FORMAT_U24_LE; + else if (!strcasecmp(format, "U24_BE")) params->format = SND_PCM_FORMAT_U24_BE; + else if (!strcasecmp(format, "S8")) params->format = SND_PCM_FORMAT_S8; + else if (!strcasecmp(format, "U8")) params->format = SND_PCM_FORMAT_U8; + else if (!strcasecmp(format, "FLOAT_LE")) params->format = SND_PCM_FORMAT_FLOAT_LE; + else if (!strcasecmp(format, "FLOAT_BE")) params->format = SND_PCM_FORMAT_FLOAT_LE; + else { + AFB_ApiNotice(source->api, "ProcessSndParams:%s(params) unsupported format 'S16_LE|S32_L|...' format=%s", uid, format); + goto OnErrorExit; + } + + if (!access) params->access = SND_PCM_ACCESS_RW_INTERLEAVED; + else if (!strcasecmp(access, "MMAP_INTERLEAVED")) params->access = SND_PCM_ACCESS_MMAP_INTERLEAVED; + else if (!strcasecmp(access, "MMAP_NONINTERLEAVED")) params->access = SND_PCM_ACCESS_MMAP_NONINTERLEAVED; + else if (!strcasecmp(access, "MMAP_COMPLEX")) params->access = SND_PCM_ACCESS_MMAP_COMPLEX; + else if (!strcasecmp(access, "RW_INTERLEAVED")) params->access = SND_PCM_ACCESS_RW_INTERLEAVED; + else if (!strcasecmp(access, "RW_NONINTERLEAVED")) params->access = SND_PCM_ACCESS_RW_NONINTERLEAVED; + + else { + AFB_ApiNotice(source->api, "ProcessSndParams:%s(params) unsupported access 'RW_INTERLEAVED|MMAP_INTERLEAVED|MMAP_COMPLEX' access=%s",uid, access); + goto OnErrorExit; + } return 0; OnErrorExit: - AFB_ApiError(source->api, "ProcessSndParams: sndcard=%s params: missing (rate|channel) params=%s", uid, json_object_get_string(paramsJ)); return -1; } STATIC int ProcessOneSndCard(CtlSourceT *source, json_object *sndcardJ, AlsaPcmInfoT *snd) { - json_object *sinkJ, *paramsJ=NULL; + json_object *sinkJ=NULL, *paramsJ = NULL; int error; - error = wrap_json_unpack(sndcardJ, "{ss,s?s,s?s,s?i,s?i,s?i,so,s?o !}", "uid",&snd->uid, "devpath",&snd->devpath, "cardid",&snd->cardid - , "cardidx",&snd->cardidx, "device",&snd->device, "subdev",&snd->subdev, "sink",&sinkJ, "params",¶msJ); + error = wrap_json_unpack(sndcardJ, "{ss,s?s,s?s,s?i,s?i,s?i,so,s?o !}", "uid", &snd->uid, "devpath", &snd->devpath, "cardid", &snd->cardid + , "cardidx", &snd->cardidx, "device", &snd->device, "subdev", &snd->subdev, "sink", &sinkJ, "params", ¶msJ); if (error || !snd->uid || !sinkJ || (!snd->devpath && !snd->cardid && snd->cardidx)) { AFB_ApiNotice(source->api, "ProcessOneSndCard missing 'uid|path|cardid|cardidx|channels|device|subdev|numid|params' devin=%s", json_object_get_string(sndcardJ)); goto OnErrorExit; } - - if (paramsJ) error= ProcessSndParams(source, snd->uid, paramsJ, &snd->params); - if (error) { - AFB_ApiError(source->api, "ProcessOneSndCard: sndcard=%s invalid params=%s", snd->uid, json_object_get_string(paramsJ)); - goto OnErrorExit; + + if (paramsJ) { + error = ProcessSndParams(source, snd->uid, paramsJ, &snd->params); + if (error) { + AFB_ApiError(source->api, "ProcessOneSndCard: sndcard=%s invalid params=%s", snd->uid, json_object_get_string(paramsJ)); + goto OnErrorExit; + } + } else { + snd->params.rate= ALSA_DEFAULT_PCM_RATE; + snd->params.access= SND_PCM_ACCESS_RW_INTERLEAVED; + snd->params.format=SND_PCM_FORMAT_S16_LE; + snd->params.channels=2; } - - + // check snd card is accessible error = AlsaByPathDevid(source, snd); if (error) { AFB_ApiError(source->api, "ProcessOneSndCard: sndcard=%s not found config=%s", snd->uid, json_object_get_string(sndcardJ)); goto OnErrorExit; } - + // protect each sndcard with a dmix plugin to enable audio-stream mixing char dmixUid[100]; - snprintf(dmixUid, sizeof(dmixUid),"Dmix-%s", snd->uid); - AlsaPcmInfoT *dmixPcm= AlsaCreateDmix(source, dmixUid, snd); + snprintf(dmixUid, sizeof (dmixUid), "Dmix-%s", snd->uid); + AlsaPcmInfoT *dmixPcm = AlsaCreateDmix(source, dmixUid, snd, 0); if (!dmixPcm) { AFB_ApiError(source->api, "ProcessOneSndCard: sndcard=%s fail to attach dmix plugin", snd->uid); - goto OnErrorExit; + goto OnErrorExit; } else { - snd_pcm_close(dmixPcm->handle); - snd->cardid=dmixPcm->cardid; + snd->cardid = dmixPcm->cardid; } switch (json_object_get_type(sinkJ)) { case json_type_object: - snd->ccount=1; - snd->channels = calloc(snd->ccount+1, sizeof (AlsaPcmChannels)); + snd->ccount = 1; + snd->channels = calloc(snd->ccount + 1, sizeof (AlsaPcmChannels)); error = ProcessOneChannel(source, snd->uid, sndcardJ, &snd->channels[0]); if (error) goto OnErrorExit; break; case json_type_array: - snd->ccount = (int)json_object_array_length(sinkJ); - snd->channels = calloc(snd->ccount+1, sizeof (AlsaPcmChannels)); + snd->ccount = (int) json_object_array_length(sinkJ); + snd->channels = calloc(snd->ccount + 1, sizeof (AlsaPcmChannels)); for (int idx = 0; idx < snd->ccount; idx++) { json_object *channelJ = json_object_array_get_idx(sinkJ, idx); error = ProcessOneChannel(source, snd->uid, channelJ, &snd->channels[idx]); @@ -115,13 +159,13 @@ OnErrorExit: CTLP_LUA2C(snd_cards, source, argsJ, responseJ) { AlsaPcmInfoT *sndcards; - + int error; size_t count; switch (json_object_get_type(argsJ)) { case json_type_object: - count= 1; + count = 1; sndcards = calloc(count + 1, sizeof (AlsaPcmInfoT)); error = ProcessOneSndCard(source, argsJ, &sndcards[0]); if (error) goto OnErrorExit; @@ -141,23 +185,21 @@ CTLP_LUA2C(snd_cards, source, argsJ, responseJ) { } // register Sound card and multi when needed - Softmixer->sndcardCtl= sndcards; - + Softmixer->sndcardCtl = sndcards; + if (count == 1) { - + // only one sound card we multi would be useless Softmixer->multiPcm = &sndcards[0]; - + } else { AlsaPcmInfoT *pcmMulti; - + // instantiate an alsa multi plugin - pcmMulti = AlsaCreateMulti(source, "PcmMulti"); + pcmMulti = AlsaCreateMulti(source, "PcmMulti", 0); if (!pcmMulti) goto OnErrorExit; - // Close Multi and save into globak handle for further use - snd_pcm_close(pcmMulti->handle); - Softmixer->multiPcm= pcmMulti; + Softmixer->multiPcm = pcmMulti; } return 0; diff --git a/plugins/alsa/alsa-api-sndloops.c b/plugins/alsa/alsa-api-sndloops.c index 5599ae0..be12c11 100644 --- a/plugins/alsa/alsa-api-sndloops.c +++ b/plugins/alsa/alsa-api-sndloops.c @@ -24,23 +24,34 @@ // Fulup need to be cleanup with new controller version extern Lua2cWrapperT Lua2cWrap; -STATIC int ProcessOneSubdev(CtlSourceT *source, AlsaSndLoopT *loop, json_object *subdevJ, AlsaPcmInfoT *subdev) { - - int error = wrap_json_unpack(subdevJ, "{si,si !}", "subdev", &subdev->subdev, "numid", &subdev->numid); +STATIC int ProcessOneSubdev(CtlSourceT *source, AlsaSndLoopT *loop, json_object *subdevJ, AlsaPcmHwInfoT *loopDefParams,AlsaPcmInfoT *subdev) { + json_object *paramsJ = NULL; + + int error = wrap_json_unpack(subdevJ, "{si,si,s?o !}", "subdev", &subdev->subdev, "numid", &subdev->numid, "params", ¶msJ); if (error) { AFB_ApiError(source->api, "ProcessOneSubdev: loop=%s missing (uid|subdev|numid) json=%s", loop->uid, json_object_get_string(subdevJ)); goto OnErrorExit; } + if (paramsJ) { + error = ProcessSndParams(source, loop->uid, paramsJ, loopDefParams); + if (error) { + AFB_ApiError(source->api, "ProcessOneLoop: sndcard=%s invalid params=%s", loop->uid, json_object_get_string(paramsJ)); + goto OnErrorExit; + } + } else { + // use global loop params definition as default + memcpy (&subdev->params, loopDefParams, sizeof(AlsaPcmHwInfoT)); + } // create a fake uid and complete subdev info from loop handle char subuid[30]; - snprintf(subuid,sizeof(subuid),"loop:/%i/%i", subdev->subdev,subdev->numid); + snprintf(subuid, sizeof (subuid), "loop:/%i/%i", subdev->subdev, subdev->numid); subdev->uid = strdup(subuid); - subdev->device = loop->capture; // Fulup: with alsaloop softmixer only use capture device (playback is used by applications) + subdev->device = loop->capture; // Fulup: with alsaloop softmixer only use capture device (playback is used by applications) subdev->devpath = loop->devpath; - subdev->cardid = NULL; // force AlsaByPathDevId to rebuild a new one for each subdev + subdev->cardid = NULL; // force AlsaByPathDevId to rebuild a new one for each subdev subdev->cardidx = loop->cardidx; - + // check if card exist error = AlsaByPathDevid(source, subdev); if (error) { @@ -55,27 +66,43 @@ OnErrorExit: } STATIC int ProcessOneLoop(CtlSourceT *source, json_object *loopJ, AlsaSndLoopT *loop) { - json_object *subdevsJ=NULL, *devicesJ=NULL; + json_object *subdevsJ = NULL, *devicesJ = NULL, *paramsJ = NULL; int error; - error = wrap_json_unpack(loopJ, "{ss,s?s,s?s,s?i,s?o,so}", "uid",&loop->uid, "devpath",&loop->devpath, "cardid",&loop->cardid - , "cardidx",&loop->cardidx, "devices",&devicesJ, "subdevs",&subdevsJ); + error = wrap_json_unpack(loopJ, "{ss,s?s,s?s,s?i,s?o,so,s?o !}", "uid", &loop->uid, "devpath", &loop->devpath, "cardid", &loop->cardid + , "cardidx", &loop->cardidx, "devices", &devicesJ, "subdevs", &subdevsJ, "params", ¶msJ); if (error || !loop->uid || !subdevsJ || (!loop->devpath && !loop->cardid && loop->cardidx)) { AFB_ApiNotice(source->api, "ProcessOneLoop missing 'uid|devpath|cardid|cardidx|devices|subdevs' loop=%s", json_object_get_string(loopJ)); goto OnErrorExit; } - + + AlsaPcmHwInfoT *loopDefParams =alloca(sizeof(AlsaPcmHwInfoT)); + if (paramsJ) { + error = ProcessSndParams(source, loop->uid, paramsJ, loopDefParams); + if (error) { + AFB_ApiError(source->api, "ProcessOneLoop: sndcard=%s invalid params=%s", loop->uid, json_object_get_string(paramsJ)); + goto OnErrorExit; + } + } else { + loopDefParams->rate = ALSA_DEFAULT_PCM_RATE; + loopDefParams->rate= ALSA_DEFAULT_PCM_RATE; + loopDefParams->access=SND_PCM_ACCESS_RW_INTERLEAVED; + loopDefParams->format=SND_PCM_FORMAT_S16_LE; + loopDefParams->channels=2; + loopDefParams->sampleSize=0; + } + // make sure useful information will not be removed - loop->uid=strdup(loop->uid); - if (loop->cardid) loop->cardid=strdup(loop->cardid); - if (loop->devpath) loop->cardid=strdup(loop->devpath); - + loop->uid = strdup(loop->uid); + if (loop->cardid) loop->cardid = strdup(loop->cardid); + if (loop->devpath) loop->cardid = strdup(loop->devpath); + // Default devices is payback=0 capture=1 if (!devicesJ) { - loop->playback=0; - loop->capture=1; + loop->playback = 0; + loop->capture = 1; } else { - error = wrap_json_unpack(devicesJ, "{si,si}", "capture",&loop->capture, "playback", &loop->playback); + error = wrap_json_unpack(devicesJ, "{si,si !}", "capture", &loop->capture, "playback", &loop->playback); if (error) { AFB_ApiNotice(source->api, "ProcessOneLoop=%s missing 'capture|playback' devices=%s", loop->uid, json_object_get_string(devicesJ)); goto OnErrorExit; @@ -85,24 +112,24 @@ STATIC int ProcessOneLoop(CtlSourceT *source, json_object *loopJ, AlsaSndLoopT * switch (json_object_get_type(subdevsJ)) { case json_type_object: loop->scount = 1; - loop->subdevs = calloc(loop->scount+1, sizeof (AlsaPcmInfoT)); - error = ProcessOneSubdev(source, loop, subdevsJ, &loop->subdevs[0]); + loop->subdevs = calloc(loop->scount + 1, sizeof (AlsaPcmInfoT)); + error = ProcessOneSubdev(source, loop, subdevsJ, loopDefParams, &loop->subdevs[0]); if (error) goto OnErrorExit; break; case json_type_array: - loop->scount = (int)json_object_array_length(subdevsJ); - loop->subdevs = calloc(loop->scount+1, sizeof (AlsaPcmInfoT)); + loop->scount = (int) json_object_array_length(subdevsJ); + loop->subdevs = calloc(loop->scount + 1, sizeof (AlsaPcmInfoT)); for (int idx = 0; idx < loop->scount; idx++) { json_object *subdevJ = json_object_array_get_idx(subdevsJ, idx); - error = ProcessOneSubdev(source, loop, subdevJ, &loop->subdevs[idx]); + error = ProcessOneSubdev(source, loop, subdevJ, loopDefParams, &loop->subdevs[idx]); if (error) goto OnErrorExit; } break; default: - AFB_ApiError(source->api, "L2C:ProcessOneLoop=%s invalid subdevs= %s", loop->uid, json_object_get_string(subdevsJ)); + AFB_ApiError(source->api, "L2C:ProcessOneLoop=%s invalid subdevs= %s", loop->uid, json_object_get_string(subdevsJ)); goto OnErrorExit; } - + return 0; OnErrorExit: @@ -111,7 +138,7 @@ OnErrorExit: CTLP_LUA2C(snd_loops, source, argsJ, responseJ) { int error; - AlsaSndLoopT *sndLoop = calloc (1, sizeof(AlsaSndLoopT)); + AlsaSndLoopT *sndLoop = calloc(1, sizeof (AlsaSndLoopT)); if (json_object_get_type(argsJ) != json_type_object) { AFB_ApiError(source->api, "L2C:sndloops: invalid object type= %s", json_object_get_string(argsJ)); @@ -127,7 +154,6 @@ CTLP_LUA2C(snd_loops, source, argsJ, responseJ) { // register routed into global softmixer handle Softmixer->loopCtl = sndLoop; - return 0; OnErrorExit: diff --git a/plugins/alsa/alsa-api-sndstreams.c b/plugins/alsa/alsa-api-sndstreams.c index 1ee0029..d02d47e 100644 --- a/plugins/alsa/alsa-api-sndstreams.c +++ b/plugins/alsa/alsa-api-sndstreams.c @@ -26,39 +26,32 @@ extern Lua2cWrapperT Lua2cWrap; STATIC int ProcessOneStream(CtlSourceT *source, json_object *streamJ, AlsaSndStreamT *stream) { int error; - const char*format=NULL; + json_object *paramsJ = NULL; - error = wrap_json_unpack(streamJ, "{ss,ss,s?i,s?b,s?i,s?i,s?i !}", "uid", &stream->uid, "zone", &stream->zone, "volume", &stream->volume, "mute", stream->mute - , "rate", &stream->params.rate, "format", &format, "access", &stream->params.access); + // Make sure default runs + stream->volume = ALSA_DEFAULT_PCM_VOLUME; + stream->mute = 0; + + error = wrap_json_unpack(streamJ, "{ss,ss,s?i,s?b,s?o !}", "uid", &stream->uid, "zone", &stream->zone, "volume", &stream->volume + , "mute", stream->mute, "params", ¶msJ); + if (error) { + AFB_ApiNotice(source->api, "ProcessOneStream missing 'uid|zone|volume|rate|mute|params' stream=%s", json_object_get_string(streamJ)); + goto OnErrorExit; + } + + if (paramsJ) error = ProcessSndParams(source, stream->uid, paramsJ, &stream->params); if (error) { - AFB_ApiNotice(source->api, "ProcessOneStream missing 'uid|zone|volume|rate|mute' stream=%s", json_object_get_string(streamJ)); + 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; } - if (!format) stream->params.format=SND_PCM_FORMAT_UNKNOWN; - else if (!strcasecmp (format, "S16_LE")) stream->params.format=SND_PCM_FORMAT_S16_LE; - else if (!strcasecmp (format, "S16_BE")) stream->params.format=SND_PCM_FORMAT_S16_BE; - else if (!strcasecmp (format, "U16_LE")) stream->params.format=SND_PCM_FORMAT_U16_LE; - else if (!strcasecmp (format, "U16_BE")) stream->params.format=SND_PCM_FORMAT_U16_BE; - else if (!strcasecmp (format, "S32_LE")) stream->params.format=SND_PCM_FORMAT_S32_LE; - else if (!strcasecmp (format, "S32_BE")) stream->params.format=SND_PCM_FORMAT_S32_BE; - else if (!strcasecmp (format, "U32_LE")) stream->params.format=SND_PCM_FORMAT_U32_LE; - else if (!strcasecmp (format, "U32_BE")) stream->params.format=SND_PCM_FORMAT_U32_BE; - else if (!strcasecmp (format, "S24_LE")) stream->params.format=SND_PCM_FORMAT_S24_LE; - else if (!strcasecmp (format, "S24_BE")) stream->params.format=SND_PCM_FORMAT_S24_BE; - else if (!strcasecmp (format, "U24_LE")) stream->params.format=SND_PCM_FORMAT_U24_LE; - else if (!strcasecmp (format, "U24_BE")) stream->params.format=SND_PCM_FORMAT_U24_BE; - else if (!strcasecmp (format, "S8")) stream->params.format=SND_PCM_FORMAT_S8; - else if (!strcasecmp (format, "U8")) stream->params.format=SND_PCM_FORMAT_U8; - else if (!strcasecmp (format, "FLOAT_LE")) stream->params.format=SND_PCM_FORMAT_FLOAT_LE; - else if (!strcasecmp (format, "FLOAT_BE")) stream->params.format=SND_PCM_FORMAT_FLOAT_LE; - else { - AFB_ApiNotice(source->api, "ProcessOneStream unsupported format 'uid|zone|volume|rate|mute' stream=%s", json_object_get_string(streamJ)); - goto OnErrorExit; - } - - if(!stream->params.rate) stream->params.rate=ALSA_DEFAULT_PCM_RATE; - // make sure remain valid even when json object is removed stream->uid = strdup(stream->uid); stream->zone = strdup(stream->zone); @@ -72,6 +65,7 @@ OnErrorExit: CTLP_LUA2C(snd_streams, source, argsJ, responseJ) { AlsaSndStreamT *sndStream; int error; + long value; size_t count; // assert static/global softmixer handle get requited info @@ -111,13 +105,13 @@ CTLP_LUA2C(snd_streams, source, argsJ, responseJ) { // return stream data to application as a json array - *responseJ = json_object_new_array(); - + *responseJ = json_object_new_array(); + for (int idx = 0; sndStream[idx].uid != NULL; idx++) { - json_object *streamJ; - + json_object *streamJ, *paramsJ; + // Search for a free loop capture device - AFB_ApiNotice(source->api, "L2C:sndstreams stream=%s Start", (char*)sndStream[idx].uid); + AFB_ApiNotice(source->api, "L2C:sndstreams stream=%s Start", (char*) sndStream[idx].uid); ctlLoop->scount--; if (ctlLoop->scount < 0) { AFB_ApiError(source->api, "L2C:sndstreams no more subdev avaliable in loopback=%s", ctlLoop->uid); @@ -126,43 +120,107 @@ CTLP_LUA2C(snd_streams, source, argsJ, responseJ) { // 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); + playbackDev->device = ctlLoop->capture; + AlsaPcmInfoT *captureDev = AlsaByPathOpenPcm(source, playbackDev, SND_PCM_STREAM_CAPTURE); if (!captureDev) goto OnErrorExit; - - AlsaPcmInfoT *streamPcm = AlsaCreateStream(source, &sndStream[idx], captureDev); + + // 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, "AlsaCtlRegister [pcm=%s] fail attache sndcard", 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 = AlsaCreateStream(source, &sndStream[idx], captureDev, volName, 0); if (!streamPcm) { - AFB_ApiError(source->api, "L2C:sndstreams fail to create stream=%s", (char*) sndStream[idx].uid); + AFB_ApiError(source->api, "L2C:sndstreams:%s(pcm) fail to create stream", sndStream[idx].uid); + goto OnErrorExit; + } + + // create volume control before softvol pcm is opened + int volNumid = AlsaCtlCreateControl(source, ctlDev, playbackDev, volName, streamPcm->params.channels, 0, 100, 1, 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, "L2C:sndstreams:%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, "L2C:sndstreams:%s(pcm) fail to open capture", sndStream[idx].uid); goto OnErrorExit; } - + // capture stream inherit channel from targeted zone captureDev->ccount = streamPcm->ccount; - sndStream[idx].params.channels=streamPcm->ccount; - - // start stream pcm copy - error = AlsaPcmCopy(source, captureDev, streamPcm, &sndStream[idx].params); + 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; - // Registration to event should be done after pcm_start - if (captureDev->numid) { - error = AlsaCtlRegister(source, captureDev, captureDev->numid); - 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, "L2C:sndstreams [capture=%s] fail to pause error=%s", captureDev->cardid, snd_strerror(error)); + goto OnErrorExit; } - + // prepare response for application - playbackDev->device= ctlLoop->playback; + playbackDev->device = ctlLoop->playback; error = AlsaByPathDevid(source, playbackDev); - wrap_json_pack(&streamJ, "{ss ss si}", "uid", sndStream[idx].uid, "alsa", playbackDev->cardid, "numid", captureDev->numid); - json_object_array_add(*responseJ,streamJ); - + + 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, "L2C:sndstreams:%s(stream) fail to prepare response", captureDev->cardid); + goto OnErrorExit; + } + + 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, "L2C:sndstreams stream=%s OK\n", (char*) sndStream[idx].uid); + AFB_ApiNotice(source->api, "L2C:sndstreams:%s(stream) OK reponse=%s\n", streamPcm->uid, json_object_get_string(streamJ)); } return 0; diff --git a/plugins/alsa/alsa-api-sndzones.c b/plugins/alsa/alsa-api-sndzones.c index 11d77c2..7ca61ad 100644 --- a/plugins/alsa/alsa-api-sndzones.c +++ b/plugins/alsa/alsa-api-sndzones.c @@ -128,12 +128,11 @@ CTLP_LUA2C(snd_zones, source, argsJ, responseJ) { // instantiate one route PCM per zone with multi plugin as slave for (int idx = 0; sndZone[idx].uid != NULL; idx++) { - Softmixer->zonePcms[idx] = AlsaCreateRoute(source, &sndZone[idx]); + Softmixer->zonePcms[idx] = AlsaCreateRoute(source, &sndZone[idx], 0); if (!Softmixer->zonePcms[idx]) { AFB_ApiNotice(source->api, "L2C:sndzones fail to create route zone=%s", sndZone[idx].uid); goto OnErrorExit; } - snd_pcm_close(Softmixer->zonePcms[idx]->handle); } // do not need this handle anymore diff --git a/plugins/alsa/alsa-core-ctl.c b/plugins/alsa/alsa-core-ctl.c index 38ce8cc..79bf0c8 100644 --- a/plugins/alsa/alsa-core-ctl.c +++ b/plugins/alsa/alsa-core-ctl.c @@ -46,11 +46,13 @@ typedef struct { typedef struct { SubStreamT stream[MAX_AUDIO_STREAMS + 1]; int count; + snd_ctl_t *ctlDev; } AudioStreamHandleT; static AudioStreamHandleT AudioStreamHandle; -STATIC snd_ctl_elem_id_t *AlsaCtlGetElemId(CtlSourceT *source, snd_ctl_t* ctlDev, int numid) { + +PUBLIC snd_ctl_elem_id_t *AlsaCtlGetNumidElemId(CtlSourceT *source, snd_ctl_t* ctlDev, int numid) { char string[32]; int error; int index; @@ -60,18 +62,18 @@ STATIC snd_ctl_elem_id_t *AlsaCtlGetElemId(CtlSourceT *source, snd_ctl_t* ctlDev snd_ctl_elem_list_alloca(&ctlList); if ((error = snd_ctl_elem_list(ctlDev, ctlList)) < 0) { - AFB_ApiError(source->api, "AlsaCtlElemIdGetInt [%s] fail retrieve controls", ALSA_CTL_UID(ctlDev, string)); + AFB_ApiError(source->api, "AlsaCtlGetNumidElemId [%s] fail retrieve controls", ALSA_CTL_UID(ctlDev, string)); goto OnErrorExit; } if ((error = snd_ctl_elem_list_alloc_space(ctlList, snd_ctl_elem_list_get_count(ctlList))) < 0) { - AFB_ApiError(source->api, "AlsaCtlElemIdGetInt [%s] fail retrieve count", ALSA_CTL_UID(ctlDev, string)); + AFB_ApiError(source->api, "AlsaCtlGetNumidElemId [%s] fail retrieve count", ALSA_CTL_UID(ctlDev, string)); goto OnErrorExit; } // Fulup: do not understand why snd_ctl_elem_list should be call twice to get a valid ctlCount if ((error = snd_ctl_elem_list(ctlDev, ctlList)) < 0) { - AFB_ApiError(source->api, "AlsaCtlElemIdGetInt [%s] fail retrieve controls", ALSA_CTL_UID(ctlDev, string)); + AFB_ApiError(source->api, "AlsaCtlGetNumidElemId [%s] fail retrieve controls", ALSA_CTL_UID(ctlDev, string)); goto OnErrorExit; } @@ -87,7 +89,7 @@ STATIC snd_ctl_elem_id_t *AlsaCtlGetElemId(CtlSourceT *source, snd_ctl_t* ctlDev } if (index == ctlCount) { - AFB_ApiError(source->api, "AlsaCtlRegister [%s] fail get numid=%i count", ALSA_CTL_UID(ctlDev, string), numid); + AFB_ApiError(source->api, "AlsaCtlGetNumidElemId [%s] fail get numid=%i count", ALSA_CTL_UID(ctlDev, string), numid); goto OnErrorExit; } @@ -100,6 +102,55 @@ OnErrorExit: return NULL; } +PUBLIC snd_ctl_elem_id_t *AlsaCtlGetNameElemId(CtlSourceT *source, snd_ctl_t* ctlDev, const char *ctlName) { + char string[32]; + int error; + int index; + snd_ctl_elem_list_t *ctlList = NULL; + snd_ctl_elem_id_t *elemId; + + snd_ctl_elem_list_alloca(&ctlList); + + if ((error = snd_ctl_elem_list(ctlDev, ctlList)) < 0) { + AFB_ApiError(source->api, "AlsaCtlGetNameElemId [%s] fail retrieve controls", ALSA_CTL_UID(ctlDev, string)); + goto OnErrorExit; + } + + if ((error = snd_ctl_elem_list_alloc_space(ctlList, snd_ctl_elem_list_get_count(ctlList))) < 0) { + AFB_ApiError(source->api, "AlsaCtlGetNameElemId [%s] fail retrieve count", ALSA_CTL_UID(ctlDev, string)); + goto OnErrorExit; + } + + // Fulup: do not understand why snd_ctl_elem_list should be call twice to get a valid ctlCount + if ((error = snd_ctl_elem_list(ctlDev, ctlList)) < 0) { + AFB_ApiError(source->api, "AlsaCtlGetNameElemId [%s] fail retrieve controls", ALSA_CTL_UID(ctlDev, string)); + goto OnErrorExit; + } + + // loop on control to find the right one + int ctlCount = snd_ctl_elem_list_get_used(ctlList); + for (index = 0; index < ctlCount; index++) { + + if (!strcasecmp(ctlName, snd_ctl_elem_list_get_name(ctlList, index))) { + snd_ctl_elem_id_malloc(&elemId); + snd_ctl_elem_list_get_id(ctlList, index, elemId); + break; + } + } + + if (index == ctlCount) { + AFB_ApiError(source->api, "AlsaCtlGetNameElemId [%s] fail get ctl name=%s", ALSA_CTL_UID(ctlDev, string), ctlName); + goto OnErrorExit; + } + + // clear ctl list and return elemid + snd_ctl_elem_list_clear(ctlList); + return elemId; + +OnErrorExit: + if (ctlList) snd_ctl_elem_list_clear(ctlList); + return NULL; +} PUBLIC snd_ctl_t *AlsaCtlOpenCtl(CtlSourceT *source, const char *cardid) { int error; @@ -135,7 +186,50 @@ OnErrorExit: return -1; } -STATIC int CtlElemIdGetInt(AFB_ApiT api, snd_ctl_t *ctlDev, snd_ctl_elem_id_t *elemId, long *value) { +STATIC void CtlElemIdDisplay(AFB_ApiT api, snd_ctl_elem_info_t *elemInfo, snd_ctl_elem_value_t *elemData) { + + int numid = snd_ctl_elem_info_get_numid(elemInfo); + int count = snd_ctl_elem_info_get_count(elemInfo); + const char* name = snd_ctl_elem_info_get_name(elemInfo); + snd_ctl_elem_type_t elemType = snd_ctl_elem_info_get_type(elemInfo); + + + if (!elemData) { + AFB_ApiWarning(api, "CtlElemIdDisplay: numid=%d name=%s value=unreadable", numid, name); + } else + for (int idx = 0; idx < count; idx++) { + long valueL; + + switch (elemType) { + case SND_CTL_ELEM_TYPE_BOOLEAN: + valueL = snd_ctl_elem_value_get_boolean(elemData, idx); + AFB_ApiWarning(api, "CtlElemIdDisplay: numid=%d name=%s value=%ld", numid, name, valueL); + break; + case SND_CTL_ELEM_TYPE_INTEGER: + valueL = snd_ctl_elem_value_get_integer(elemData, idx); + AFB_ApiWarning(api, "CtlElemIdDisplay: numid=%d name=%s value=%ld", numid, name, valueL); + break; + case SND_CTL_ELEM_TYPE_INTEGER64: + valueL = snd_ctl_elem_value_get_integer64(elemData, idx); + AFB_ApiWarning(api, "CtlElemIdDisplay: numid=%d name=%s value=%ld", numid, name, valueL); + break; + case SND_CTL_ELEM_TYPE_ENUMERATED: + valueL = snd_ctl_elem_value_get_enumerated(elemData, idx); + AFB_ApiWarning(api, "CtlElemIdDisplay: numid=%d name=%s value=%ld", numid, name, valueL); + break; + case SND_CTL_ELEM_TYPE_BYTES: + valueL = snd_ctl_elem_value_get_byte(elemData, idx); + AFB_ApiWarning(api, "CtlElemIdDisplay: numid=%d name=%s value=%ld", numid, name, valueL); + break; + case SND_CTL_ELEM_TYPE_IEC958: + default: + AFB_ApiWarning(api, "CtlElemIdDisplay: numid=%d name=%s Unsupported type=%d", numid, name, elemType); + break; + } + } +} + +PUBLIC int CtlElemIdGetLong(AFB_ApiT api, snd_ctl_t *ctlDev, snd_ctl_elem_id_t *elemId, long *value) { int error; snd_ctl_elem_value_t *elemData; snd_ctl_elem_info_t *elemInfo; @@ -146,58 +240,68 @@ STATIC int CtlElemIdGetInt(AFB_ApiT api, snd_ctl_t *ctlDev, snd_ctl_elem_id_t *e if (!snd_ctl_elem_info_is_readable(elemInfo)) goto OnErrorExit; // as we have static rate/channel we should have only one boolean as value - snd_ctl_elem_type_t elemType = snd_ctl_elem_info_get_type(elemInfo); - int count = snd_ctl_elem_info_get_count(elemInfo); - if (count != 1) goto OnErrorExit; snd_ctl_elem_value_alloca(&elemData); snd_ctl_elem_value_set_id(elemData, elemId); error = snd_ctl_elem_read(ctlDev, elemData); - if (error) goto OnSuccessExit; + if (error) { + elemData = NULL; + goto OnErrorExit; + } + + // warning multi channel are always view as grouped + //int count = snd_ctl_elem_info_get_count(elemInfo); + //if (count != 1) goto OnErrorExit; // value=1 when active and 0 when not active - *value = snd_ctl_elem_value_get_integer(elemData, 0); + *value = (int) snd_ctl_elem_value_get_integer(elemData, 0); -OnSuccessExit: return 0; OnErrorExit: + CtlElemIdDisplay(api, elemInfo, elemData); + return -1; +} - AFB_ApiWarning(api, "CtlSubscribeEventCB: ignored unsupported event Numid=%i", snd_ctl_elem_info_get_numid(elemInfo)); - for (int idx = 0; idx < count; idx++) { - long valueL; - - switch (elemType) { - case SND_CTL_ELEM_TYPE_BOOLEAN: - valueL = snd_ctl_elem_value_get_boolean(elemData, idx); - AFB_ApiNotice(api, "CtlElemIdGetBool: value=%ld", valueL); - break; - case SND_CTL_ELEM_TYPE_INTEGER: - valueL = snd_ctl_elem_value_get_integer(elemData, idx); - AFB_ApiNotice(api, "CtlElemIdGetInt: value=%ld", valueL); - break; - case SND_CTL_ELEM_TYPE_INTEGER64: - valueL = snd_ctl_elem_value_get_integer64(elemData, idx); - AFB_ApiNotice(api, "CtlElemIdGetInt64: value=%ld", valueL); - break; - case SND_CTL_ELEM_TYPE_ENUMERATED: - valueL = snd_ctl_elem_value_get_enumerated(elemData, idx); - AFB_ApiNotice(api, "CtlElemIdGetEnum: value=%ld", valueL); - break; - case SND_CTL_ELEM_TYPE_BYTES: - valueL = snd_ctl_elem_value_get_byte(elemData, idx); - AFB_ApiNotice(api, "CtlElemIdGetByte: value=%ld", valueL); - break; - case SND_CTL_ELEM_TYPE_IEC958: - default: - AFB_ApiNotice(api, "CtlElemIdGetInt: Unsupported type=%d", elemType); - break; - } +PUBLIC int CtlElemIdSetLong(AFB_ApiT api, snd_ctl_t *ctlDev, snd_ctl_elem_id_t *elemId, long value) { + snd_ctl_elem_value_t *elemData; + snd_ctl_elem_info_t *elemInfo; + const char* name; + int error, numid; + + snd_ctl_elem_info_alloca(&elemInfo); + snd_ctl_elem_info_set_id(elemInfo, elemId); + if (snd_ctl_elem_info(ctlDev, elemInfo) < 0) goto OnErrorExit; + + if (!snd_ctl_elem_info_is_writable(elemInfo)) goto OnErrorExit; + + int count = snd_ctl_elem_info_get_count(elemInfo); + if (count == 0) goto OnErrorExit; + + snd_ctl_elem_value_alloca(&elemData); + snd_ctl_elem_value_set_id(elemData, elemId); + error = snd_ctl_elem_read(ctlDev, elemData); + if (error) goto OnErrorExit; + + + for (int index = 0; index < count; index++) { + snd_ctl_elem_value_set_integer(elemData, index, value); } + + error = snd_ctl_elem_write(ctlDev, elemData); + if (error) goto OnErrorExit; + + return 0; + +OnErrorExit: + numid = snd_ctl_elem_info_get_numid(elemInfo); + name = snd_ctl_elem_info_get_name(elemInfo); + AFB_ApiError(api, "CtlElemIdSetInt: numid=%d name=%s not writable", numid, name); return -1; } // Clone of AlsaLib snd_card_load2 static function + PUBLIC snd_ctl_card_info_t *AlsaCtlGetInfo(CtlSourceT *source, const char *cardid) { int error; snd_ctl_t *ctlDev; @@ -220,15 +324,34 @@ OnErrorExit: return NULL; } -PUBLIC int AlsaCtlGetNumidValueI(CtlSourceT *source, snd_ctl_t* ctlDev, int numid, long* value) { - - snd_ctl_elem_id_t *elemId = AlsaCtlGetElemId(source, ctlDev, numid); +PUBLIC int AlsaCtlNumidSetLong(CtlSourceT *source, snd_ctl_t* ctlDev, int numid, long value) { + + snd_ctl_elem_id_t *elemId = AlsaCtlGetNumidElemId(source, ctlDev, numid); + if (!elemId) { + AFB_ApiError(source->api, "AlsaCtlNumidSetLong [sndcard=%s] fail to find numid=%d", snd_ctl_name(ctlDev), numid); + goto OnErrorExit; + } + + int error = CtlElemIdSetLong(source->api, ctlDev, elemId, value); + if (error) { + AFB_ApiError(source->api, "AlsaCtlNumidSetLong [sndcard=%s] fail to set numid=%d value=%ld", snd_ctl_name(ctlDev), numid, value); + goto OnErrorExit; + } + + return 0; +OnErrorExit: + return -1; +} + +PUBLIC int AlsaCtlNumidGetLong(CtlSourceT *source, snd_ctl_t* ctlDev, int numid, long* value) { + + snd_ctl_elem_id_t *elemId = AlsaCtlGetNumidElemId(source, ctlDev, numid); if (!elemId) { AFB_ApiError(source->api, "AlsaCtlGetNumValueI [sndcard=%s] fail to find numid=%d", snd_ctl_name(ctlDev), numid); goto OnErrorExit; } - int error = CtlElemIdGetInt(source->api, ctlDev, elemId, value); + int error = CtlElemIdGetLong(source->api, ctlDev, elemId, value); if (error) { AFB_ApiError(source->api, "AlsaCtlGetNumValueI [sndcard=%s] fail to get numid=%d value", snd_ctl_name(ctlDev), numid); goto OnErrorExit; @@ -239,6 +362,79 @@ OnErrorExit: return -1; } +STATIC int AlsaCtlMakeControl(CtlSourceT *source, snd_ctl_t* ctlDev, AlsaPcmInfoT *subdev, const char *ctlName, int ctlCount, int ctlMin, int ctlMax, int ctlStep) { + snd_ctl_elem_type_t ctlType; + snd_ctl_elem_info_t *elemInfo; + int error; + + snd_ctl_elem_info_alloca(&elemInfo); + if (ctlName) snd_ctl_elem_info_set_name(elemInfo, ctlName); + snd_ctl_elem_info_set_interface(elemInfo, SND_CTL_ELEM_IFACE_MIXER); + snd_ctl_elem_info(ctlDev, elemInfo); + + // softvol plugin is bugged and can only map volume to sndcard device+subdev=0 + // snd_ctl_elem_info_set_device(elemInfo, subdev->device); + // snd_ctl_elem_info_set_subdevice(elemInfo, subdev->subdev); + snd_ctl_elem_info_set_device(elemInfo, 0); + snd_ctl_elem_info_set_subdevice(elemInfo, 0); + + // only two types implemented + if (ctlMin == 0 && ctlMax == 1) ctlType = SND_CTL_ELEM_TYPE_BOOLEAN; + else ctlType = SND_CTL_ELEM_TYPE_INTEGER; + + switch (ctlType) { + case SND_CTL_ELEM_TYPE_BOOLEAN: + error = snd_ctl_add_boolean_elem_set(ctlDev, elemInfo, 1, ctlCount); + if (error) goto OnErrorExit; + break; + + case SND_CTL_ELEM_TYPE_INTEGER: + error = snd_ctl_add_integer_elem_set(ctlDev, elemInfo, 1, ctlCount, ctlMin, ctlMax, ctlStep); + if (error) goto OnErrorExit; + break; + + default: + AFB_ApiError(source->api, "AlsaCtlMakeControl:%s(subdev) fail to create %s(control)", subdev->uid, ctlName); + goto OnErrorExit; + } + + // retrieve newly created control numid + int numid = snd_ctl_elem_info_get_numid(elemInfo); + return numid; + +OnErrorExit: + return -1; +} + +PUBLIC int AlsaCtlCreateControl(CtlSourceT *source, snd_ctl_t* ctlDev, AlsaPcmInfoT *subdevs, char* ctlName, int ctlCount, int ctlMin, int ctlMax, int ctlStep, long value) { + int numid = -1; + + // if control does not exist then create + snd_ctl_elem_id_t *elemId = AlsaCtlGetNameElemId(source, ctlDev, ctlName); + if (elemId) { + numid = snd_ctl_elem_id_get_numid(elemId); + } else { + // create or get numid control when already exist + numid = AlsaCtlMakeControl(source, ctlDev, subdevs, ctlName, ctlCount, ctlMin, ctlMax, ctlStep); + if (numid <= 0) { + AFB_ApiError(source->api, "AlsaCtlCreateControl [sndcard=%s] fail to create ctlName=%s", snd_ctl_name(ctlDev), ctlName); + goto OnErrorExit; + } + + elemId = AlsaCtlGetNumidElemId(source, ctlDev, numid); + } + + int error = CtlElemIdSetLong(source->api, ctlDev, elemId, value); + if (error) { + AFB_ApiError(source->api, "AlsaCtlCreateControl [sndcard=%s] fail to set ctlName=%s Numid=%d", snd_ctl_name(ctlDev), ctlName, numid); + goto OnErrorExit; + } + + return numid; +OnErrorExit: + return -1; +} + STATIC int CtlSubscribeEventCB(sd_event_source* src, int fd, uint32_t revents, void* userData) { int error, numid; SubscribeHandleT *subscribeHandle = (SubscribeHandleT*) userData; @@ -270,7 +466,7 @@ STATIC int CtlSubscribeEventCB(sd_event_source* src, int fd, uint32_t revents, v // extract element from event and get value snd_ctl_event_elem_get_id(eventId, elemId); - error = CtlElemIdGetInt(subscribeHandle->api, subscribeHandle->ctlDev, elemId, &value); + error = CtlElemIdGetLong(subscribeHandle->api, subscribeHandle->ctlDev, elemId, &value); if (error) goto OnErrorExit; error = CtlElemIdGetNumid(subscribeHandle->api, subscribeHandle->ctlDev, elemId, &numid); @@ -286,15 +482,15 @@ STATIC int CtlSubscribeEventCB(sd_event_source* src, int fd, uint32_t revents, v } if (idx == AudioStreamHandle.count) { char cardName[32]; - ALSA_CTL_UID(subscribeHandle->ctlDev,cardName); - AFB_ApiWarning(subscribeHandle->api, "CtlSubscribeEventCB:%s/%d card=%s numid=%d (ignored)", subscribeHandle->info, subscribeHandle->tid, cardName, numid); + ALSA_CTL_UID(subscribeHandle->ctlDev, cardName); + AFB_ApiWarning(subscribeHandle->api, "CtlSubscribeEventCB:%s/%d card=%s numid=%d (ignored)", subscribeHandle->info, subscribeHandle->tid, cardName, numid); } - + OnSuccessExit: return 0; OnErrorExit: - AFB_ApiWarning(subscribeHandle->api, "CtlSubscribeEventCB: ignored unsupported event"); + AFB_ApiInfo(subscribeHandle->api, "CtlSubscribeEventCB: ignored unsupported event"); return 0; } @@ -340,7 +536,6 @@ OnErrorExit: return NULL; } - PUBLIC int AlsaCtlSubscribe(CtlSourceT *source, snd_ctl_t * ctlDev) { int error; char string [32]; @@ -389,43 +584,32 @@ OnErrorExit: } PUBLIC int AlsaCtlRegister(CtlSourceT *source, AlsaPcmInfoT *pcm, int numid) { - long value; - int error; - // NumID are attached to sndcard retrieve ctldev from PCM - snd_ctl_t* ctlDev = AlsaCrlFromPcm(source, pcm->handle); - if (!ctlDev) { - AFB_ApiError(source->api, "AlsaCtlRegister [pcm=%s] fail attache sndcard", pcm->cardid); + + int count = AudioStreamHandle.count; + if (count > MAX_AUDIO_STREAMS) { + AFB_ApiError(source->api, "AlsaCtlRegister [pcm=%s] to many audio stream max=%d", pcm->cardid, MAX_AUDIO_STREAMS); goto OnErrorExit; } - // This is the first registration let's subscrive to event - if (AudioStreamHandle.count == 0) { + // If 1st registration then open a dev control channel to recieve events + if (!AudioStreamHandle.ctlDev) { + snd_ctl_t* ctlDev = AlsaCrlFromPcm(source, pcm->handle); + if (!ctlDev) { + AFB_ApiError(source->api, "AlsaCtlRegister [pcm=%s] fail attache sndcard", pcm->cardid); + goto OnErrorExit; + } + AlsaCtlSubscribe(source, ctlDev); } - error = AlsaCtlGetNumidValueI(source, ctlDev, numid, &value); - if (error) goto OnErrorExit; - - AFB_ApiNotice(source->api, "AlsaCtlRegister [pcm=%s] numid=%d value=%ld", pcm->cardid, numid, value); - // store PCM in order to pause/resume depending on event - int count=AudioStreamHandle.count; AudioStreamHandle.stream[count].pcm = pcm; AudioStreamHandle.stream[count].numid = numid; - - // we only need to keep ctldev open for initial registration - if (AudioStreamHandle.count++ > 0) snd_ctl_close(ctlDev); - - // toggle pause/resume (should be done after pcm_start) - if ((error = snd_pcm_pause(pcm->handle, !value)) < 0) { - AFB_ApiError(source->api, "AlsaCtlRegister [pcm=%s] fail to pause", pcm->cardid); - goto OnErrorExit; - } + AudioStreamHandle.count++; return 0; OnErrorExit: - return -1; } diff --git a/plugins/alsa/alsa-core-pcm.c b/plugins/alsa/alsa-core-pcm.c index f143eb6..bc676ef 100644 --- a/plugins/alsa/alsa-core-pcm.c +++ b/plugins/alsa/alsa-core-pcm.c @@ -29,9 +29,6 @@ for the specific language governing permissions and #include - -#define BUFFER_FRAME_COUNT 1024 - typedef struct { snd_pcm_t *pcmIn; snd_pcm_t *pcmOut; @@ -238,7 +235,7 @@ STATIC int AlsaPcmReadCB(sd_event_source* src, int fd, uint32_t revents, void* u // In/Out frames transfer through buffer copy framesOut = snd_pcm_writei(pcmCopyHandle->pcmOut, pcmCopyHandle->buffer, framesIn); if (framesOut < 0 || framesOut != framesIn) { - AFB_ApiNotice(pcmCopyHandle->api, "AlsaPcmReadCB PcmOut=%s UNDERUN/SUSPEND frameOut=%ld", ALSA_PCM_UID(pcmCopyHandle->pcmOut, string), framesOut); + AFB_ApiNotice(pcmCopyHandle->api, "AlsaPcmReadCB PcmOut=%s UNDERUN frame=%ld", ALSA_PCM_UID(pcmCopyHandle->pcmOut, string), (framesIn - framesOut)); goto ExitOnSuccess; } @@ -283,28 +280,26 @@ PUBLIC int AlsaPcmCopy(CtlSourceT *source, AlsaPcmInfoT *pcmIn, AlsaPcmInfoT *pc struct pollfd *pcmInFds; int error; - // prepare PCM for capture and replay - error = AlsaPcmConf(source, pcmIn, opts); + // input and output should match + error = AlsaPcmConf(source, pcmOut, opts); if (error) goto OnErrorExit; // Prepare PCM for usage - if ((error = snd_pcm_start(pcmIn->handle)) < 0) { - AFB_ApiError(source->api, "AlsaPcmCopy: Fail to prepare PCM=%s error=%s", ALSA_PCM_UID(pcmIn->handle, string), snd_strerror(error)); + if ((error = snd_pcm_prepare(pcmOut->handle)) < 0) { + AFB_ApiError(source->api, "AlsaPcmCopy: Fail to prepare PCM=%s error=%s", ALSA_PCM_UID(pcmOut->handle, string), snd_strerror(error)); goto OnErrorExit; }; - - error = AlsaPcmConf(source, pcmOut, opts); + // prepare PCM for capture and replay + error = AlsaPcmConf(source, pcmIn, opts); if (error) goto OnErrorExit; // Prepare PCM for usage - if ((error = snd_pcm_prepare(pcmOut->handle)) < 0) { - AFB_ApiError(source->api, "AlsaPcmCopy: Fail to start PCM=%s error=%s", ALSA_PCM_UID(pcmOut->handle, string), snd_strerror(error)); + if ((error = snd_pcm_start(pcmIn->handle)) < 0) { + AFB_ApiError(source->api, "AlsaPcmCopy: Fail to prepare PCM=%s error=%s", ALSA_PCM_UID(pcmIn->handle, string), snd_strerror(error)); goto OnErrorExit; }; - - AlsaPcmCopyHandleT *pcmCopyHandle = malloc(sizeof (AlsaPcmCopyHandleT)); pcmCopyHandle->info = "pcmCpy"; pcmCopyHandle->pcmIn = pcmIn->handle; @@ -312,7 +307,7 @@ PUBLIC int AlsaPcmCopy(CtlSourceT *source, AlsaPcmInfoT *pcmIn, AlsaPcmInfoT *pc pcmCopyHandle->api = source->api; pcmCopyHandle->channels = opts->channels; pcmCopyHandle->frameSize = opts->channels * opts->sampleSize; - pcmCopyHandle->frameCount = BUFFER_FRAME_COUNT; + pcmCopyHandle->frameCount = ALSA_BUFFER_FRAMES_COUNT; pcmCopyHandle->buffer = malloc(pcmCopyHandle->frameCount * pcmCopyHandle->frameSize); // get FD poll descriptor for capture PCM @@ -350,8 +345,8 @@ PUBLIC int AlsaPcmCopy(CtlSourceT *source, AlsaPcmInfoT *pcmIn, AlsaPcmInfoT *pc return 0; OnErrorExit: - AFB_ApiError(source->api, "AlsaPcmCopy: Fail \n - pcmIn=%s \n - pcmOut=%s", ALSA_PCM_UID(pcmIn->handle, string), ALSA_PCM_UID(pcmOut->handle, string)); - + AFB_ApiError(source->api, "AlsaPcmCopy: - pcmIn=%s" , ALSA_PCM_UID(pcmIn->handle, string)); + AFB_ApiError(source->api, "AlsaPcmCopy: - pcmOut=%s", ALSA_PCM_UID(pcmOut->handle, string)); return -1; } diff --git a/plugins/alsa/alsa-plug-dmix.c b/plugins/alsa/alsa-plug-dmix.c index 22b5117..b06d5f6 100644 --- a/plugins/alsa/alsa-plug-dmix.c +++ b/plugins/alsa/alsa-plug-dmix.c @@ -25,7 +25,7 @@ static int uniqueIpcIndex = 1024; ALSA_PLUG_PROTO(dmix); -PUBLIC AlsaPcmInfoT* AlsaCreateDmix(CtlSourceT *source, const char* pcmName, AlsaPcmInfoT *pcmSlave) { +PUBLIC AlsaPcmInfoT* AlsaCreateDmix(CtlSourceT *source, const char* pcmName, AlsaPcmInfoT *pcmSlave, int open) { snd_config_t *dmixConfig, *slaveConfig, *elemConfig, *pcmConfig; AlsaPcmInfoT *pcmPlug= calloc(1,sizeof(AlsaPcmInfoT)); @@ -57,7 +57,7 @@ PUBLIC AlsaPcmInfoT* AlsaCreateDmix(CtlSourceT *source, const char* pcmName, Als error += snd_config_add(dmixConfig, slaveConfig); if (error) goto OnErrorExit; - error = _snd_pcm_dmix_open(&pcmPlug->handle, pcmPlug->cardid, snd_config, dmixConfig, SND_PCM_STREAM_PLAYBACK , SND_PCM_NONBLOCK); + if (open) error = _snd_pcm_dmix_open(&pcmPlug->handle, pcmPlug->cardid, snd_config, dmixConfig, SND_PCM_STREAM_PLAYBACK , SND_PCM_NONBLOCK); if (error) { AFB_ApiError(source->api, "AlsaCreateDmix: fail to create Dmix=%s Slave=%s Error=%s", pcmPlug->cardid, pcmSlave->cardid, snd_strerror(error)); goto OnErrorExit; diff --git a/plugins/alsa/alsa-plug-multi.c b/plugins/alsa/alsa-plug-multi.c index 6deca96..3e82e1e 100644 --- a/plugins/alsa/alsa-plug-multi.c +++ b/plugins/alsa/alsa-plug-multi.c @@ -22,7 +22,7 @@ ALSA_PLUG_PROTO(multi); -PUBLIC AlsaPcmInfoT* AlsaCreateMulti(CtlSourceT *source, const char *pcmUid) { +PUBLIC AlsaPcmInfoT* AlsaCreateMulti(CtlSourceT *source, const char *pcmUid, int open) { snd_config_t *multiConfig, *elemConfig, *slavesConfig, *slaveConfig, *bindingsConfig, *bindingConfig, *pcmConfig; int error = 0, channelIdx=0; @@ -79,7 +79,7 @@ PUBLIC AlsaPcmInfoT* AlsaCreateMulti(CtlSourceT *source, const char *pcmUid) { // update top config to access previous plugin PCM snd_config_update(); - error = _snd_pcm_multi_open(&pcmPlug->handle, pcmUid, snd_config, multiConfig, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK); + if (open) error = _snd_pcm_multi_open(&pcmPlug->handle, pcmUid, snd_config, multiConfig, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK); if (error) { AFB_ApiError(source->api, "AlsaCreateMulti: fail to create Plug=%s Error=%s", pcmPlug->cardid, snd_strerror(error)); goto OnErrorExit; diff --git a/plugins/alsa/alsa-plug-rate.c b/plugins/alsa/alsa-plug-rate.c new file mode 100644 index 0000000..a9b9e23 --- /dev/null +++ b/plugins/alsa/alsa-plug-rate.c @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2018 "IoT.bzh" + * Author Fulup Ar Foll + * + * 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" + + +ALSA_PLUG_PROTO(rate); + + +PUBLIC AlsaPcmInfoT* AlsaCreateRate(CtlSourceT *source, const char* pcmName, AlsaPcmInfoT *pcmSlave, int open) { + + snd_config_t *rateConfig, *slaveConfig, *elemConfig, *pcmConfig; + AlsaPcmInfoT *pcmPlug= calloc(1,sizeof(AlsaPcmInfoT)); + pcmPlug->uid= strdup(pcmName); + pcmPlug->cardid=pcmPlug->uid; + + int error=0; + + // refresh global alsalib config and create PCM top config + snd_config_update(); + error += snd_config_top(&rateConfig); + error += snd_config_set_id (rateConfig, pcmPlug->cardid); + error += snd_config_imake_string(&elemConfig, "type", "plug"); + error += snd_config_add(rateConfig, elemConfig); + if (error) goto OnErrorExit; + + error += snd_config_make_compound(&slaveConfig, "slave", 0); + error += snd_config_imake_string(&elemConfig, "pcm", pcmSlave->cardid); + if (pcmSlave->params.rate) { + error += snd_config_add(slaveConfig, elemConfig); + // *** error += snd_config_imake_integer(&elemConfig, "rate", pcmSlave->params.rate); + error += snd_config_imake_integer(&elemConfig, "rate", 48000); + } + error += snd_config_add(slaveConfig, elemConfig); + if (error) goto OnErrorExit; + + // add leaf into config + error += snd_config_add(rateConfig, slaveConfig); + if (error) goto OnErrorExit; + + error += snd_config_search(snd_config, "pcm", &pcmConfig); + error += snd_config_add(pcmConfig, rateConfig); + if (error) { + AFB_ApiError(source->api, "AlsaCreateRate: fail to add configRATE=%s", pcmPlug->cardid); + goto OnErrorExit; + } + + if (open) error = _snd_pcm_rate_open(&pcmPlug->handle, pcmPlug->cardid, snd_config, rateConfig, SND_PCM_STREAM_PLAYBACK , SND_PCM_NONBLOCK); + if (error) { + AFB_ApiError(source->api, "AlsaCreateRate: fail to create Rate=%s Slave=%s Error=%s", pcmPlug->cardid, pcmSlave->cardid, snd_strerror(error)); + goto OnErrorExit; + } + + // Debug config & pcm + AlsaDumpCtlConfig (source, "plug-rate", pcmConfig, 1); + //AlsaDumpCtlConfig (source, "plug-rate", rateConfig, 1); + AFB_ApiNotice(source->api, "AlsaCreateRate: %s done\n", pcmPlug->cardid); + return pcmPlug; + +OnErrorExit: + AlsaDumpCtlConfig(source, "plug-rate", rateConfig, 1); + AFB_ApiNotice(source->api, "AlsaCreateRate: OnErrorExit\n"); + return NULL; +} \ No newline at end of file diff --git a/plugins/alsa/alsa-plug-route.c b/plugins/alsa/alsa-plug-route.c index 2c8f3cb..8fa5de8 100644 --- a/plugins/alsa/alsa-plug-route.c +++ b/plugins/alsa/alsa-plug-route.c @@ -47,7 +47,7 @@ OnErrorExit: return -1; } -PUBLIC AlsaPcmInfoT* AlsaCreateRoute(CtlSourceT *source, AlsaSndZoneT *zone) { +PUBLIC AlsaPcmInfoT* AlsaCreateRoute(CtlSourceT *source, AlsaSndZoneT *zone, int open) { snd_config_t *routeConfig, *elemConfig, *slaveConfig, *tableConfig, *pcmConfig; int error = 0; @@ -115,7 +115,7 @@ PUBLIC AlsaPcmInfoT* AlsaCreateRoute(CtlSourceT *source, AlsaSndZoneT *zone) { // update top config to access previous plugin PCM snd_config_update(); - error = _snd_pcm_route_open(&pcmPlug->handle, zone->uid, snd_config, routeConfig, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK); + if (open) error = _snd_pcm_route_open(&pcmPlug->handle, zone->uid, snd_config, routeConfig, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK); if (error) { AFB_ApiError(source->api, "AlsaCreateRoute:zone(%s) fail to create Plug=%s error=%s", zone->uid, pcmPlug->cardid, snd_strerror(error)); goto OnErrorExit; diff --git a/plugins/alsa/alsa-plug-stream.c b/plugins/alsa/alsa-plug-stream.c index e0ef69a..ffb7010 100644 --- a/plugins/alsa/alsa-plug-stream.c +++ b/plugins/alsa/alsa-plug-stream.c @@ -36,11 +36,11 @@ STATIC AlsaPcmInfoT* SlaveZoneByUid(CtlSourceT *source, AlsaPcmInfoT **pcmZones return NULL; } -PUBLIC AlsaPcmInfoT* AlsaCreateStream(CtlSourceT *source, AlsaSndStreamT *stream, AlsaPcmInfoT *ctlControl) { +PUBLIC AlsaPcmInfoT* AlsaCreateStream(CtlSourceT *source, AlsaSndStreamT *stream, AlsaPcmInfoT *ctlControl, const char* ctlName, int open) { snd_config_t *streamConfig, *elemConfig, *slaveConfig, *controlConfig,*pcmConfig; int error = 0; - AlsaPcmInfoT *pcmPlug= malloc(sizeof(AlsaPcmInfoT)); + AlsaPcmInfoT *pcmPlug= calloc(1,sizeof(AlsaPcmInfoT)); // assert static/global softmixer handle get requited info AlsaSndLoopT *ctlLoop = Softmixer->loopCtl; @@ -57,16 +57,19 @@ PUBLIC AlsaPcmInfoT* AlsaCreateStream(CtlSourceT *source, AlsaSndStreamT *stream } // search for target zone uid - pcmPlug->uid= stream->uid; - pcmPlug->cardid= stream->uid; AlsaPcmInfoT *pcmSlave= SlaveZoneByUid (source, pcmZones, stream->zone); if (!pcmSlave || !pcmSlave->uid) { - AFB_ApiError(source->api, "AlsaCreateStream:%s(stream) fail to find Zone=%s", pcmPlug->uid, stream->zone); + AFB_ApiError(source->api, "AlsaCreateStream:%s(stream) fail to find Zone=%s", stream->uid, stream->zone); goto OnErrorExit; } // stream inherit from zone channel count + pcmPlug->uid= strdup(stream->uid); + pcmPlug->cardid= pcmPlug->uid; + pcmPlug->devpath=NULL; pcmPlug->ccount= pcmSlave->ccount; + memcpy (&pcmPlug->params, &stream->params, sizeof(AlsaPcmHwInfoT)); + pcmPlug->params.channels= pcmSlave->ccount; // refresh global alsalib config and create PCM top config snd_config_update(); @@ -74,6 +77,8 @@ PUBLIC AlsaPcmInfoT* AlsaCreateStream(CtlSourceT *source, AlsaSndStreamT *stream error += snd_config_set_id (streamConfig, pcmPlug->cardid); error += snd_config_imake_string(&elemConfig, "type", "softvol"); error += snd_config_add(streamConfig, elemConfig); + error += snd_config_imake_integer(&elemConfig, "resolution", 99); // use 0-100% + error += snd_config_add(streamConfig, elemConfig); if (error) goto OnErrorExit; // add slave leaf @@ -85,7 +90,7 @@ PUBLIC AlsaPcmInfoT* AlsaCreateStream(CtlSourceT *source, AlsaSndStreamT *stream // add control leaf error += snd_config_make_compound(&controlConfig, "control", 0); - error += snd_config_imake_string(&elemConfig, "name", stream->uid); + error += snd_config_imake_string(&elemConfig, "name", ctlName); error += snd_config_add(controlConfig, elemConfig); error += snd_config_imake_integer(&elemConfig, "card", ctlControl->cardidx); error += snd_config_add(controlConfig, elemConfig); @@ -95,7 +100,7 @@ PUBLIC AlsaPcmInfoT* AlsaCreateStream(CtlSourceT *source, AlsaSndStreamT *stream // update top config to access previous plugin PCM snd_config_update(); - error = _snd_pcm_softvol_open(&pcmPlug->handle, stream->uid, snd_config, streamConfig, SND_PCM_STREAM_PLAYBACK , SND_PCM_NONBLOCK); + if (open) error = _snd_pcm_softvol_open(&pcmPlug->handle, stream->uid, snd_config, streamConfig, SND_PCM_STREAM_PLAYBACK , SND_PCM_NONBLOCK); if (error) { AFB_ApiError(source->api, "AlsaCreateStream:%s(stream) fail to create Plug=%s Slave=%s error=%s", stream->uid, pcmPlug->cardid, pcmSlave->cardid, snd_strerror(error)); goto OnErrorExit; diff --git a/plugins/alsa/alsa-softmixer.c b/plugins/alsa/alsa-softmixer.c index bd9ee5e..23a01dd 100644 --- a/plugins/alsa/alsa-softmixer.c +++ b/plugins/alsa/alsa-softmixer.c @@ -33,85 +33,3 @@ CTLP_ONLOAD(plugin, callbacks) { return NULL; } -CTLP_LUA2C(AlsaDmix, source, argsJ, responseJ) { - json_object* subscribeArgsJ = NULL; - - int error = 0; - wrap_json_pack(&subscribeArgsJ, "{ss}", "value", "location"); - AFB_ApiNotice(source->api, "lua2c router with %s", json_object_to_json_string_ext(subscribeArgsJ, JSON_C_TO_STRING_PRETTY)); - - return error; -} - - - -CTLP_LUA2C(AlsaRouter, source, argsJ, responseJ) { - json_object *sndInJ, *sndOutJ, *paramsJ = NULL; - AlsaPcmInfoT *sndIn, *sndOut; - int error; - - // make sndIn/Out a pointer to get cleaner code - sndIn = calloc(1, sizeof (AlsaPcmInfoT)); - sndOut = calloc(1, sizeof (AlsaPcmInfoT)); - - // set pcm options to defaults - AlsaPcmHwInfoT *pcmOpts; - pcmOpts = calloc(1, sizeof (AlsaPcmHwInfoT)); - pcmOpts->format = SND_PCM_FORMAT_UNKNOWN; - pcmOpts->access = SND_PCM_ACCESS_RW_INTERLEAVED; - - error = wrap_json_unpack(argsJ, "{s:o,s:o,s?o}", "devin", &sndInJ, "devout", &sndOutJ, "params", ¶msJ); - if (error) { - AFB_ApiNotice(source->api, "--lua2c-- AlsaRouter ARGS missing devIn|devOut args=%s", json_object_get_string(argsJ)); - goto OnErrorExit; - } - - error = wrap_json_unpack(sndInJ, "{s?s,s?s,s?i,s?i,s?i}", "path", &sndIn->devpath, "id", &sndIn->cardid, "numid", &sndIn->numid, "dev", &sndIn->device, "sub", &sndIn->subdev); - if (error || (!sndIn->devpath && !sndIn->cardid)) { - AFB_ApiNotice(source->api, "--lua2c-- AlsaRouter DEV-IN missing 'path|id|dev|sub|numid' devin=%s", json_object_get_string(sndInJ)); - goto OnErrorExit; - } - - error = wrap_json_unpack(sndOutJ, "{s?s,s?s,s?i,s?i, s?i}", "path", &sndOut->devpath, "id", &sndOut->cardid, "numid", &sndOut->numid, "dev", &sndOut->device, "sub", &sndOut->subdev); - if (error || (!sndOut->devpath && !sndOut->cardid)) { - AFB_ApiNotice(source->api, "--lua2c-- AlsaRouter DEV-OUT missing 'path|id|dev|sub' devout=%s", json_object_get_string(sndOutJ)); - goto OnErrorExit; - } - - if (paramsJ) if ((error = wrap_json_unpack(paramsJ, "{s?i, s?i, s?i, s?i}", "format", &pcmOpts->format, "access", &pcmOpts->access, "rate", &pcmOpts->rate, "channels", &pcmOpts->channels)) != 0) { - AFB_ApiNotice(source->api, "--lua2c-- AlsaRouter PARAMS missing 'format|access|rate|channels' params=%s", json_object_get_string(paramsJ)); - goto OnErrorExit; - } - - AFB_ApiNotice(source->api, "--lua2c-- AlsaRouter devin=%s devout=%s rate=%d channel=%d", sndIn->devpath, sndOut->devpath, pcmOpts->rate, pcmOpts->channels); - - // Check sndOut Exist and build a valid cardid config - error = AlsaByPathDevid(source, sndOut); - if (error) goto OnErrorExit; - - // open capture PCM - AlsaPcmInfoT *pcmIn = AlsaByPathOpenPcm(source, sndIn, SND_PCM_STREAM_CAPTURE); - if (!pcmIn) goto OnErrorExit; - - AlsaPcmInfoT *pcmDmix = AlsaCreateDmix(source, "DmixPlugPcm", sndOut); - if (!pcmDmix) goto OnErrorExit; - - //AlsaPcmInfoT *pcmVol = AlsaCreateVol(source, "SoftVol", sndIn, pcmDmix); - //if (!pcmVol) goto OnErrorExit; - - //error = AlsaPcmCopy(source, pcmIn, pcmVol, pcmOpts); - //if (error) goto OnErrorExit; - - // Registration to event should be done after pcm_start - if (sndIn->numid) { - error = AlsaCtlRegister(source, pcmIn, sndIn->numid); - if (error) goto OnErrorExit; - } - - return 0; - -OnErrorExit: - AFB_ApiNotice(source->api, "--lua2c-- ERROR AlsaRouter sndIn=%s sndOut=%s rate=%d channel=%d", sndIn->devpath, sndOut->devpath, pcmOpts->rate, pcmOpts->channels); - return -1; -} - diff --git a/plugins/alsa/alsa-softmixer.h b/plugins/alsa/alsa-softmixer.h index db499c3..3018402 100644 --- a/plugins/alsa/alsa-softmixer.h +++ b/plugins/alsa/alsa-softmixer.h @@ -33,9 +33,11 @@ #include #define MAINLOOP_WATCHDOG 30000 -#define MAX_AUDIO_STREAMS 8 +#define MAX_AUDIO_STREAMS 8*2 #define ALSA_DEFAULT_PCM_RATE 48000 -#define ALSA_CARDID_MAX_LEN 32 +#define ALSA_DEFAULT_PCM_VOLUME 80 +#define ALSA_BUFFER_FRAMES_COUNT 1024 +#define ALSA_CARDID_MAX_LEN 64 #define ALSA_PLUG_PROTO(plugin) \ @@ -110,11 +112,15 @@ typedef struct { extern SoftMixerHandleT *Softmixer; +// alsa-utils-bypath.c PUBLIC snd_ctl_card_info_t* AlsaByPathInfo(CtlSourceT *source, const char *control); PUBLIC AlsaPcmInfoT* AlsaByPathOpenPcm(CtlSourceT *source, AlsaPcmInfoT *dev, snd_pcm_stream_t direction); PUBLIC snd_ctl_t *AlsaByPathOpenCtl(CtlSourceT *source, AlsaPcmInfoT *dev); PUBLIC int AlsaByPathDevid(CtlSourceT *source, AlsaPcmInfoT *dev); +// alsa-api-*.c +PUBLIC int ProcessSndParams(CtlSourceT *source, const char* uid, json_object *paramsJ, AlsaPcmHwInfoT *params); + // alsa-utils-dump.c PUBLIC void AlsaDumpFormats(CtlSourceT *source, snd_pcm_t *pcmHandle); PUBLIC char *AlsaDumpPcmUid(snd_pcm_t *pcmHandle, char *buffer, size_t len); @@ -130,18 +136,25 @@ PUBLIC char *AlsaDumpCtlUid(snd_ctl_t *ctlHandle, char *buffer, size_t len); // alsa-core-ctl.c PUBLIC snd_ctl_card_info_t *AlsaCtlGetInfo(CtlSourceT *source, const char *cardid); PUBLIC snd_ctl_t *AlsaCtlOpenCtl(CtlSourceT *source, const char *cardid); +PUBLIC snd_ctl_t* AlsaCrlFromPcm(CtlSourceT *source, snd_pcm_t *pcm); PUBLIC int AlsaCtlSubscribe(CtlSourceT *source, snd_ctl_t *ctlDev); PUBLIC int AlsaCtlRegister(CtlSourceT *source, AlsaPcmInfoT *pcm, int numid); -PUBLIC int AlsaCtlGetNumidValueI(CtlSourceT *source, snd_ctl_t* ctlDev, int numid, long* value); +PUBLIC int AlsaCtlNumidGetLong(CtlSourceT *source, snd_ctl_t* ctlDev, int numid, long* value); +PUBLIC int AlsaCtlNumidSetLong(CtlSourceT *source, snd_ctl_t* ctlDev, int numid, long value); +PUBLIC int AlsaCtlCreateControl(CtlSourceT *source, snd_ctl_t* ctlDev, AlsaPcmInfoT *subdevs, char* name, int ctlCount, int ctlMin, int ctlMax, int ctlStep, long value); +PUBLIC snd_ctl_elem_id_t *AlsaCtlGetNameElemId(CtlSourceT *source, snd_ctl_t* ctlDev, const char *ctlName); +PUBLIC int CtlElemIdSetLong(AFB_ApiT api, snd_ctl_t *ctlDev, snd_ctl_elem_id_t *elemId, long value); +PUBLIC int CtlElemIdGetLong(AFB_ApiT api, snd_ctl_t *ctlDev, snd_ctl_elem_id_t *elemId, long *value); // alsa-core-pcm.c PUBLIC int AlsaPcmConf(CtlSourceT *source, AlsaPcmInfoT *pcm, AlsaPcmHwInfoT *opts); PUBLIC int AlsaPcmCopy(CtlSourceT *source, AlsaPcmInfoT *pcmIn, AlsaPcmInfoT *pcmOut, AlsaPcmHwInfoT *opts); // alsa-plug-*.c _snd_pcm_PLUGIN_open_ see macro ALSA_PLUG_PROTO(plugin) -PUBLIC AlsaPcmInfoT* AlsaCreateDmix(CtlSourceT *source, const char* pcmName, AlsaPcmInfoT *pcmSlave); -PUBLIC AlsaPcmInfoT* AlsaCreateMulti(CtlSourceT *source, const char *pcmName); -PUBLIC AlsaPcmInfoT* AlsaCreateStream(CtlSourceT *source, AlsaSndStreamT *stream, AlsaPcmInfoT *pcmControl); -PUBLIC AlsaPcmInfoT* AlsaCreateRoute(CtlSourceT *source, AlsaSndZoneT *zone); +PUBLIC AlsaPcmInfoT* AlsaCreateDmix(CtlSourceT *source, const char* pcmName, AlsaPcmInfoT *pcmSlave, int open); +PUBLIC AlsaPcmInfoT* AlsaCreateMulti(CtlSourceT *source, const char *pcmName, int open); +PUBLIC AlsaPcmInfoT* AlsaCreateRoute(CtlSourceT *source, AlsaSndZoneT *zone, int open); +PUBLIC AlsaPcmInfoT* AlsaCreateStream(CtlSourceT *source, AlsaSndStreamT *stream, AlsaPcmInfoT *ctlControl, const char* ctlName, int open); +PUBLIC AlsaPcmInfoT* AlsaCreateRate(CtlSourceT *source, const char* pcmName, AlsaPcmInfoT *pcmSlave, int open); #endif \ No newline at end of file diff --git a/plugins/alsa/alsa-utils-bypath.c b/plugins/alsa/alsa-utils-bypath.c index 7ceb66f..a857ab1 100644 --- a/plugins/alsa/alsa-utils-bypath.c +++ b/plugins/alsa/alsa-utils-bypath.c @@ -74,7 +74,6 @@ PUBLIC int AlsaByPathDevid(CtlSourceT *source, AlsaPcmInfoT *dev) { dev->cardid=malloc(ALSA_CARDID_MAX_LEN); snprintf((char*)dev->cardid, ALSA_CARDID_MAX_LEN, "hw:%i", dev->cardidx); cardInfo = AlsaCtlGetInfo(source, dev->cardid); - cardInfo = AlsaCtlGetInfo(source, dev->cardid); } if (!cardInfo) { -- cgit 1.2.3-korg