blob: 32b93bd033a89f473d2b8607fad3cba58ce3222b [file] [log] [blame]
// SPDX-License-Identifier: GPL-2.0-only
/*
* Google Whitechapel AoC ALSA Driver
*
* Copyright (c) 2019 Google LLC
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*/
#include <linux/module.h>
#include <linux/string.h>
#include "aoc_alsa_drv.h"
#include "aoc_alsa.h"
#define AOC_ALSA_NAME "aoc_alsa"
/* Driver methods */
static int aoc_alsa_probe(struct aoc_service_dev *dev);
static int aoc_alsa_remove(struct aoc_service_dev *dev);
struct aoc_service_resource {
const char *name;
struct aoc_service_dev *dev;
int ref;
bool waiting;
wait_queue_head_t wait_head;
service_event_cb_t event_callback;
void *prvdata;
};
/* TODO: audio_haptics should be added, capture1-3 needs to be determined */
static const char *const audio_service_names[] = {
"audio_output_control",
"audio_input_control",
"audio_playback0",
"audio_playback1",
"audio_playback2",
"audio_playback3",
"audio_playback4",
"audio_playback5",
"audio_playback6",
"audio_haptics",
"audio_capture0",
"audio_capture1",
"audio_capture2",
"audio_capture3",
"audio_voip_rx",
"audio_voip_tx",
"audio_incall_pb_0",
"audio_incall_pb_1",
"audio_incall_cap_0",
"audio_incall_cap_1",
"audio_incall_cap_2",
"decoder_eof",
"audio_raw",
"audio_hifiin",
"audio_hifiout",
"audio_android_aec",
NULL,
};
static struct aoc_service_resource
service_lists[ARRAY_SIZE(audio_service_names) - 1];
static spinlock_t service_lock;
static int8_t n_services;
static bool drv_registered;
static bool aoc_audio_online;
static wait_queue_head_t aoc_audio_state_wait_head;
static void compressed_offload_isr(struct aoc_service_dev *dev)
{
aoc_compr_offload_isr(dev);
}
int8_t aoc_audio_service_num(void)
{
return n_services;
}
EXPORT_SYMBOL_GPL(aoc_audio_service_num);
int alloc_aoc_audio_service(const char *name, struct aoc_service_dev **dev, service_event_cb_t cb,
void *cookies)
{
int i, err = -EINVAL;
if (!name || !dev)
return -EINVAL;
*dev = NULL;
spin_lock(&service_lock);
for (i = 0; i < ARRAY_SIZE(service_lists); i++) {
if (strcmp(name, service_lists[i].name) == 0)
break;
}
if (i == ARRAY_SIZE(service_lists))
goto done;
/* the AoC is not up yet, retrun -EPROBE_DEFER */
if (!service_lists[i].dev) {
err = -EPROBE_DEFER;
goto done;
}
/* Only can be allocated by one client? */
if (service_lists[i].ref != 0) {
pr_err("%s has been allocated %d\n", name,
service_lists[i].ref);
goto done;
}
*dev = service_lists[i].dev;
service_lists[i].event_callback = cb;
service_lists[i].prvdata = cookies;
service_lists[i].ref++;
err = 0;
done:
spin_unlock(&service_lock);
return err;
}
EXPORT_SYMBOL_GPL(alloc_aoc_audio_service);
int free_aoc_audio_service(const char *name, struct aoc_service_dev *dev)
{
int i, err = -EINVAL;
if (!name || !dev)
return -EINVAL;
spin_lock(&service_lock);
for (i = 0; i < ARRAY_SIZE(service_lists); i++) {
/*
* In normal case, we can find the service by testing
* the address of dev, but in AoC crash case, we should
* check the service name as well since the dev might
* have been set to NULL
*/
if (service_lists[i].dev) {
if (dev == service_lists[i].dev)
break;
} else if (strcmp(name, service_lists[i].name) == 0)
break;
}
if (i == ARRAY_SIZE(service_lists))
goto done;
if (service_lists[i].ref <= 0) {
pr_err("ERR: %s ref = %d abnormal\n", name,
service_lists[i].ref);
goto done;
}
service_lists[i].ref--;
service_lists[i].event_callback = NULL;
service_lists[i].prvdata = NULL;
/* Wake up the remove thread if necessary */
if (service_lists[i].ref == 0 &&
service_lists[i].waiting)
wake_up(&service_lists[i].wait_head);
err = 0;
done:
spin_unlock(&service_lock);
if (err != 0)
pr_err("ERR: %s can't free audio service\n", name);
return err;
}
EXPORT_SYMBOL_GPL(free_aoc_audio_service);
__poll_t aoc_audio_state_poll(struct file *f, poll_table *wait,
struct aoc_state_client_t *client)
{
if (!f || !wait || !client || !client->inuse)
return POLLERR;
poll_wait(f, &aoc_audio_state_wait_head, wait);
if (client->exit ||
client->online != aoc_audio_online)
return EPOLLIN | EPOLLRDNORM;
return 0;
}
EXPORT_SYMBOL_GPL(aoc_audio_state_poll);
bool aoc_audio_current_state(void)
{
return aoc_audio_online;
}
EXPORT_SYMBOL_GPL(aoc_audio_current_state);
struct aoc_state_client_t *alloc_audio_state_client(void)
{
struct aoc_state_client_t *client;
client = kmalloc(sizeof(struct aoc_state_client_t), GFP_KERNEL);
if (!client)
return NULL;
client->online = aoc_audio_online;
client->exit = false;
client->inuse = false;
return client;
}
EXPORT_SYMBOL_GPL(alloc_audio_state_client);
void free_audio_state_client(struct aoc_state_client_t *client)
{
kfree(client);
}
EXPORT_SYMBOL_GPL(free_audio_state_client);
static int snd_aoc_alsa_probe(void)
{
int err;
err = aoc_pcm_init();
if (err) {
pr_err("ERR:fail to init aoc pcm\n");
goto out;
}
err = aoc_voice_init();
if (err) {
pr_err("ERR: fail to init aoc voice\n");
goto out;
}
err = aoc_compr_init();
if (err) {
pr_err("ERR:%d failed to init aoc compress offload\n", err);
goto out;
}
err = aoc_path_init();
if (err) {
pr_err("ERR: fail to init aoc path\n");
goto out;
}
err = aoc_nohost_init();
if (err) {
pr_err("ERR: fail to init aoc nohost driver\n");
goto out;
}
err = aoc_incall_init();
if (err) {
pr_err("ERR: fail to init aoc incall driver\n");
goto out;
}
err = aoc_voip_init();
if (err) {
pr_err("ERR: fail to init aoc voip driver\n");
goto out;
}
return 0;
out:
return err;
}
static int snd_aoc_alsa_remove(void)
{
aoc_voip_exit();
aoc_incall_exit();
aoc_nohost_exit();
aoc_path_exit();
aoc_compr_exit();
aoc_voice_exit();
aoc_pcm_exit();
return 0;
}
static int aoc_alsa_probe(struct aoc_service_dev *dev)
{
int i;
int8_t nservices;
spin_lock(&service_lock);
/* Put the aoc service devices in order */
for (i = 0; i < ARRAY_SIZE(service_lists); i++) {
if (strcmp(service_lists[i].name, dev_name(&dev->dev)) == 0)
break;
}
if (i == ARRAY_SIZE(service_lists)) {
pr_err("%s: invalid dev %s", __func__, dev_name(&dev->dev));
spin_unlock(&service_lock);
return -EINVAL;
}
service_lists[i].dev = dev;
service_lists[i].ref = 0;
service_lists[i].event_callback = NULL;
service_lists[i].prvdata = NULL;
service_lists[i].waiting = false;
pr_notice("services %d: %s vs. %s\n", n_services,
service_lists[i].name, dev_name(&dev->dev));
n_services++;
nservices = n_services;
if (nservices == ARRAY_SIZE(service_lists)) {
if (!aoc_audio_online) {
aoc_audio_online = true;
wake_up(&aoc_audio_state_wait_head);
}
}
spin_unlock(&service_lock);
if (nservices == ARRAY_SIZE(service_lists) && !drv_registered) {
snd_aoc_alsa_probe();
drv_registered = true;
pr_notice("alsa-aoc communication is ready!\n");
}
if (strcmp(dev_name(&dev->dev), AOC_COMPR_OFFLOAD_SERVICE) == 0)
dev->handler = compressed_offload_isr;
return 0;
}
static int aoc_alsa_remove(struct aoc_service_dev *dev)
{
int i = 0;
spin_lock(&service_lock);
for (i = 0; i < ARRAY_SIZE(service_lists); i++) {
if (strcmp(service_lists[i].name, dev_name(&dev->dev)) == 0)
break;
}
if (i == ARRAY_SIZE(service_lists)) {
pr_err("%s: invalid dev %s\n", __func__, dev_name(&dev->dev));
spin_unlock(&service_lock);
return -EINVAL;
}
service_lists[i].dev = NULL;
if (aoc_audio_online) {
aoc_audio_online = false;
wake_up(&aoc_audio_state_wait_head);
}
if (service_lists[i].event_callback)
service_lists[i].event_callback(AOC_SERVICE_EVENT_DOWN, service_lists[i].prvdata);
if (service_lists[i].ref < 0) {
pr_warn("%s: invalid ref %d for %s\n", __func__,
service_lists[i].ref, dev_name(&dev->dev));
service_lists[i].ref = 0;
}
/*
* All clients have released the resource
*/
if (service_lists[i].ref == 0)
goto done;
service_lists[i].waiting = true;
spin_unlock(&service_lock);
pr_info("alsa wait %s\n", dev_name(&dev->dev));
/*
* We should block the remove function until the ref
* is 0, otherwise, it might cause "use after free".
*/
wait_event(service_lists[i].wait_head, service_lists[i].ref == 0);
pr_info("alsa wait %s done\n", dev_name(&dev->dev));
spin_lock(&service_lock);
service_lists[i].waiting = false;
done:
n_services--;
spin_unlock(&service_lock);
pr_notice("remove service %s\n", dev_name(&dev->dev));
return 0;
}
/*TODO: ? */
static void cleanup_resources(void)
{
}
static struct aoc_driver aoc_alsa_driver = {
.drv = {
.name = AOC_ALSA_NAME,
},
.service_names = audio_service_names,
.probe = aoc_alsa_probe,
.remove = aoc_alsa_remove,
};
static int __init aoc_alsa_init(void)
{
int i;
pr_debug("aoc alsa driver init\n");
aoc_audio_online = false;
drv_registered = false;
spin_lock_init(&service_lock);
init_waitqueue_head(&aoc_audio_state_wait_head);
for (i = 0; i < ARRAY_SIZE(service_lists); i++) {
service_lists[i].name = audio_service_names[i];
init_waitqueue_head(&service_lists[i].wait_head);
}
aoc_driver_register(&aoc_alsa_driver);
return 0;
}
static void __exit aoc_alsa_exit(void)
{
pr_debug("aoc alsa driver exit\n");
if (drv_registered) {
snd_aoc_alsa_remove();
drv_registered = false;
}
aoc_driver_unregister(&aoc_alsa_driver);
cleanup_resources();
}
module_init(aoc_alsa_init);
module_exit(aoc_alsa_exit);
MODULE_DESCRIPTION("Whitechapel AoC ALSA Driver");
MODULE_AUTHOR("Xinhui Zhou and Carter Hsu (Google)");
MODULE_LICENSE("GPL v2");