Logo Search packages:      
Sourcecode: alsa-utils version File versions  Download package

state.c

/*
 *  Advanced Linux Sound Architecture Control Program
 *  Copyright (c) by Abramo Bagnara <abramo@alsa-project.org>
 *                   Jaroslav Kysela <perex@perex.cz>
 *
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
 *
 */

#include "aconfig.h"
#include "version.h"
#include <getopt.h>
#include <stdarg.h>
#include <stdio.h>
#include <assert.h>
#include <errno.h>
#include <alsa/asoundlib.h>
#include "alsactl.h"


char *id_str(snd_ctl_elem_id_t *id)
{
      static char str[128];
      assert(id);
      sprintf(str, "%i,%i,%i,%s,%i", 
            snd_ctl_elem_id_get_interface(id),
            snd_ctl_elem_id_get_device(id),
            snd_ctl_elem_id_get_subdevice(id),
            snd_ctl_elem_id_get_name(id),
            snd_ctl_elem_id_get_index(id));
      return str;
}

char *num_str(long n)
{
      static char str[32];
      sprintf(str, "%ld", n);
      return str;
}

static int snd_config_integer_add(snd_config_t *father, char *id, long integer)
{
      int err;
      snd_config_t *leaf;
      err = snd_config_make_integer(&leaf, id);
      if (err < 0)
            return err;
      err = snd_config_add(father, leaf);
      if (err < 0) {
            snd_config_delete(leaf);
            return err;
      }
      err = snd_config_set_integer(leaf, integer);
      if (err < 0) {
            snd_config_delete(leaf);
            return err;
      }
      return 0;
}

static int snd_config_integer64_add(snd_config_t *father, char *id, long long integer)
{
      int err;
      snd_config_t *leaf;
      err = snd_config_make_integer64(&leaf, id);
      if (err < 0)
            return err;
      err = snd_config_add(father, leaf);
      if (err < 0) {
            snd_config_delete(leaf);
            return err;
      }
      err = snd_config_set_integer64(leaf, integer);
      if (err < 0) {
            snd_config_delete(leaf);
            return err;
      }
      return 0;
}

static int snd_config_string_add(snd_config_t *father, const char *id, const char *string)
{
      int err;
      snd_config_t *leaf;
      err = snd_config_make_string(&leaf, id);
      if (err < 0)
            return err;
      err = snd_config_add(father, leaf);
      if (err < 0) {
            snd_config_delete(leaf);
            return err;
      }
      err = snd_config_set_string(leaf, string);
      if (err < 0) {
            snd_config_delete(leaf);
            return err;
      }
      return 0;
}

static int snd_config_compound_add(snd_config_t *father, const char *id, int join,
                           snd_config_t **node)
{
      int err;
      snd_config_t *leaf;
      err = snd_config_make_compound(&leaf, id, join);
      if (err < 0)
            return err;
      err = snd_config_add(father, leaf);
      if (err < 0) {
            snd_config_delete(leaf);
            return err;
      }
      *node = leaf;
      return 0;
}

#define MAX_USER_TLV_SIZE     64

static char *tlv_to_str(unsigned int *tlv)
{
      int i, len = tlv[1] / 4 + 2;
      char *s, *p;

      if (len >= MAX_USER_TLV_SIZE)
            return NULL;
      s = malloc(len * 8 + 1);
      if (! s)
            return NULL;
      p = s;
      for (i = 0; i < len; i++) {
            sprintf(p, "%08x", tlv[i]);
            p += 8;
      }
      return s;
}

static int hextodigit(int c)
{
      if (c >= '0' && c <= '9')
            c -= '0';
      else if (c >= 'a' && c <= 'f')
            c = c - 'a' + 10;
      else if (c >= 'A' && c <= 'F')
            c = c - 'A' + 10;
      else
            return -1;
      return c;
}

static unsigned int *str_to_tlv(const char *s)
{
      int i, j, c, len;
      unsigned int *tlv;
                  
      len = strlen(s);
      if (len % 8) /* aligned to 4 bytes (= 8 letters) */
            return NULL;
      len /= 8;
      if (len > MAX_USER_TLV_SIZE)
            return NULL;
      tlv = malloc(sizeof(int) * len);
      if (! tlv)
            return NULL;
      for (i = 0; i < len; i++) {
            tlv[i] = 0;
            for (j = 0; j < 8; j++) {
                  if ((c = hextodigit(*s++)) < 0) {
                        free(tlv);
                        return NULL;
                  }
                  tlv[i] = (tlv[i] << 4) | c;
            }
      }
      return tlv;
}

/*
 * add the TLV string and dB ranges to comment fields
 */
static int add_tlv_comments(snd_ctl_t *handle, snd_ctl_elem_id_t *id,
                      snd_ctl_elem_info_t *info, snd_config_t *comment)
{
      unsigned int tlv[MAX_USER_TLV_SIZE];
      unsigned int *db;
      long dbmin, dbmax;
      int err;

      if (snd_ctl_elem_tlv_read(handle, id, tlv, sizeof(tlv)) < 0)
            return 0; /* ignore error */

      if (snd_ctl_elem_info_is_tlv_writable(info)) {
            char *s = tlv_to_str(tlv);
            if (s) {
                  err = snd_config_string_add(comment, "tlv", s);
                  if (err < 0) {
                        error("snd_config_string_add: %s", snd_strerror(err));
                        return err;
                  }
                  free(s);
            }
      }

      err = snd_tlv_parse_dB_info(tlv, sizeof(tlv), &db);
      if (err <= 0)
            return 0;

      snd_tlv_get_dB_range(db, snd_ctl_elem_info_get_min(info),
                       snd_ctl_elem_info_get_max(info),
                       &dbmin, &dbmax);
      if (err < 0)
            return err;
      snd_config_integer_add(comment, "dbmin", dbmin);
      snd_config_integer_add(comment, "dbmax", dbmax);
      return 0;
}

static int get_control(snd_ctl_t *handle, snd_ctl_elem_id_t *id, snd_config_t *top)
{
      snd_ctl_elem_value_t *ctl;
      snd_ctl_elem_info_t *info;
      snd_config_t *control, *comment, *item, *value;
      const char *s;
      char buf[256];
      unsigned int idx;
      int err;
      unsigned int device, subdevice, index;
      const char *name;
      snd_ctl_elem_type_t type;
      unsigned int count;
      snd_ctl_elem_value_alloca(&ctl);
      snd_ctl_elem_info_alloca(&info);
      snd_ctl_elem_info_set_id(info, id);
      err = snd_ctl_elem_info(handle, info);
      if (err < 0) {
            error("Cannot read control info '%s': %s", id_str(id), snd_strerror(err));
            return err;
      }

      if (snd_ctl_elem_info_is_inactive(info) ||
                        !snd_ctl_elem_info_is_readable(info))
            return 0;
      snd_ctl_elem_value_set_id(ctl, id);
      err = snd_ctl_elem_read(handle, ctl);
      if (err < 0) {
            error("Cannot read control '%s': %s", id_str(id), snd_strerror(err));
            return err;
      }

      err = snd_config_compound_add(top, num_str(snd_ctl_elem_info_get_numid(info)), 0, &control);
      if (err < 0) {
            error("snd_config_compound_add: %s", snd_strerror(err));
            return err;
      }
      err = snd_config_compound_add(control, "comment", 1, &comment);
      if (err < 0) {
            error("snd_config_compound_add: %s", snd_strerror(err));
            return err;
      }

      buf[0] = '\0';
      buf[1] = '\0';
      if (snd_ctl_elem_info_is_readable(info))
            strcat(buf, " read");
      if (snd_ctl_elem_info_is_writable(info))
            strcat(buf, " write");
      if (snd_ctl_elem_info_is_inactive(info))
            strcat(buf, " inactive");
      if (snd_ctl_elem_info_is_volatile(info))
            strcat(buf, " volatile");
      if (snd_ctl_elem_info_is_locked(info))
            strcat(buf, " locked");
      if (snd_ctl_elem_info_is_user(info))
            strcat(buf, " user");
      err = snd_config_string_add(comment, "access", buf + 1);
      if (err < 0) {
            error("snd_config_string_add: %s", snd_strerror(err));
            return err;
      }

      type = snd_ctl_elem_info_get_type(info);
      device = snd_ctl_elem_info_get_device(info);
      subdevice = snd_ctl_elem_info_get_subdevice(info);
      index = snd_ctl_elem_info_get_index(info);
      name = snd_ctl_elem_info_get_name(info);
      count = snd_ctl_elem_info_get_count(info);
      s = snd_ctl_elem_type_name(type);
      err = snd_config_string_add(comment, "type", s);
      if (err < 0) {
            error("snd_config_string_add: %s", snd_strerror(err));
            return err;
      }
      err = snd_config_integer_add(comment, "count", count);
      if (err < 0) {
            error("snd_config_integer_add: %s", snd_strerror(err));
            return err;
      }

      switch (type) {
      case SND_CTL_ELEM_TYPE_BOOLEAN:
            break;
      case SND_CTL_ELEM_TYPE_INTEGER:
      {
            long min = snd_ctl_elem_info_get_min(info);
            long max = snd_ctl_elem_info_get_max(info);
            long step = snd_ctl_elem_info_get_step(info);
            if (step)
                  sprintf(buf, "%li - %li (step %li)", min, max, step);
            else
                  sprintf(buf, "%li - %li", min, max);
            err = snd_config_string_add(comment, "range", buf);
            if (err < 0) {
                  error("snd_config_string_add: %s", snd_strerror(err));
                  return err;
            }
            if (snd_ctl_elem_info_is_tlv_readable(info)) {
                  err = add_tlv_comments(handle, id, info, comment);
                  if (err < 0)
                        return err;
            }
            break;
      }
      case SND_CTL_ELEM_TYPE_INTEGER64:
      {
            long long min = snd_ctl_elem_info_get_min64(info);
            long long max = snd_ctl_elem_info_get_max64(info);
            long long step = snd_ctl_elem_info_get_step64(info);
            if (step)
                  sprintf(buf, "%Li - %Li (step %Li)", min, max, step);
            else
                  sprintf(buf, "%Li - %Li", min, max);
            err = snd_config_string_add(comment, "range", buf);
            if (err < 0) {
                  error("snd_config_string_add: %s", snd_strerror(err));
                  return err;
            }
            break;
      }
      case SND_CTL_ELEM_TYPE_ENUMERATED:
      {
            unsigned int items;
            err = snd_config_compound_add(comment, "item", 1, &item);
            if (err < 0) {
                  error("snd_config_compound_add: %s", snd_strerror(err));
                  return err;
            }
            items = snd_ctl_elem_info_get_items(info);
            for (idx = 0; idx < items; idx++) {
                  snd_ctl_elem_info_set_item(info, idx);
                  err = snd_ctl_elem_info(handle, info);
                  if (err < 0) {
                        error("snd_ctl_card_info: %s", snd_strerror(err));
                        return err;
                  }
                  err = snd_config_string_add(item, num_str(idx), snd_ctl_elem_info_get_item_name(info));
                  if (err < 0) {
                        error("snd_config_string_add: %s", snd_strerror(err));
                        return err;
                  }
            }
            break;
      }
      default:
            break;
      }
      s = snd_ctl_elem_iface_name(snd_ctl_elem_info_get_interface(info));
      err = snd_config_string_add(control, "iface", s);
      if (err < 0) {
            error("snd_config_string_add: %s", snd_strerror(err));
            return err;
      }
      if (device != 0) {
            err = snd_config_integer_add(control, "device", device);
            if (err < 0) {
                  error("snd_config_integer_add: %s", snd_strerror(err));
                  return err;
            }
      }
      if (subdevice != 0) {
            err = snd_config_integer_add(control, "subdevice", subdevice);
            if (err < 0) {
                  error("snd_config_integer_add: %s", snd_strerror(err));
                  return err;
            }
      }
      err = snd_config_string_add(control, "name", name);
      if (err < 0) {
            error("snd_config_string_add: %s", snd_strerror(err));
            return err;
      }
      if (index != 0) {
            err = snd_config_integer_add(control, "index", index);
            if (err < 0) {
                  error("snd_config_integer_add: %s", snd_strerror(err));
                  return err;
            }
      }

      switch (type) {
      case SND_CTL_ELEM_TYPE_BYTES:
      case SND_CTL_ELEM_TYPE_IEC958:
      {
            size_t size = type == SND_CTL_ELEM_TYPE_BYTES ?
                  count : sizeof(snd_aes_iec958_t);
            char buf[size * 2 + 1];
            char *p = buf;
            char *hex = "0123456789abcdef";
            const unsigned char *bytes = 
              (const unsigned char *)snd_ctl_elem_value_get_bytes(ctl);
            for (idx = 0; idx < size; idx++) {
                  int v = bytes[idx];
                  *p++ = hex[v >> 4];
                  *p++ = hex[v & 0x0f];
            }
            *p = '\0';
            err = snd_config_string_add(control, "value", buf);
            if (err < 0) {
                  error("snd_config_string_add: %s", snd_strerror(err));
                  return err;
            }
            return 0;
      }
      default:
            break;
      }

      if (count == 1) {
            switch (type) {
            case SND_CTL_ELEM_TYPE_BOOLEAN:
                  err = snd_config_string_add(control, "value", snd_ctl_elem_value_get_boolean(ctl, 0) ? "true" : "false");
                  if (err < 0) {
                        error("snd_config_string_add: %s", snd_strerror(err));
                        return err;
                  }
                  return 0;
            case SND_CTL_ELEM_TYPE_INTEGER:
                  err = snd_config_integer_add(control, "value", snd_ctl_elem_value_get_integer(ctl, 0));
                  if (err < 0) {
                        error("snd_config_integer_add: %s", snd_strerror(err));
                        return err;
                  }
                  return 0;
            case SND_CTL_ELEM_TYPE_INTEGER64:
                  err = snd_config_integer64_add(control, "value", snd_ctl_elem_value_get_integer64(ctl, 0));
                  if (err < 0) {
                        error("snd_config_integer64_add: %s", snd_strerror(err));
                        return err;
                  }
                  return 0;
            case SND_CTL_ELEM_TYPE_ENUMERATED:
            {
                  unsigned int v = snd_ctl_elem_value_get_enumerated(ctl, 0);
                  snd_config_t *c;
                  err = snd_config_search(item, num_str(v), &c);
                  if (err == 0) {
                        err = snd_config_get_string(c, &s);
                        assert(err == 0);
                        err = snd_config_string_add(control, "value", s);
                  } else {
                        err = snd_config_integer_add(control, "value", v);
                  }
                  if (err < 0)
                        error("snd_config add: %s", snd_strerror(err));
                  return 0;
            }
            default:
                  error("Unknown control type: %d\n", type);
                  return -EINVAL;
            }
      }

      err = snd_config_compound_add(control, "value", 1, &value);
      if (err < 0) {
            error("snd_config_compound_add: %s", snd_strerror(err));
            return err;
      }

      switch (type) {
      case SND_CTL_ELEM_TYPE_BOOLEAN:
            for (idx = 0; idx < count; idx++) {
                  err = snd_config_string_add(value, num_str(idx), snd_ctl_elem_value_get_boolean(ctl, idx) ? "true" : "false");
                  if (err < 0) {
                        error("snd_config_string_add: %s", snd_strerror(err));
                        return err;
                  }
            }
            break;
      case SND_CTL_ELEM_TYPE_INTEGER:
            for (idx = 0; idx < count; idx++) {
                  err = snd_config_integer_add(value, num_str(idx), snd_ctl_elem_value_get_integer(ctl, idx));
                  if (err < 0) {
                        error("snd_config_integer_add: %s", snd_strerror(err));
                        return err;
                  }
            }
            break;
      case SND_CTL_ELEM_TYPE_INTEGER64:
            for (idx = 0; idx < count; idx++) {
                  err = snd_config_integer64_add(value, num_str(idx), snd_ctl_elem_value_get_integer64(ctl, idx));
                  if (err < 0) {
                        error("snd_config_integer64_add: %s", snd_strerror(err));
                        return err;
                  }
            }
            break;
      case SND_CTL_ELEM_TYPE_ENUMERATED:
            for (idx = 0; idx < count; idx++) {
                  unsigned int v = snd_ctl_elem_value_get_enumerated(ctl, idx);
                  snd_config_t *c;
                  err = snd_config_search(item, num_str(v), &c);
                  if (err == 0) {
                        err = snd_config_get_string(c, &s);
                        assert(err == 0);
                        err = snd_config_string_add(value, num_str(idx), s);
                  } else {
                        err = snd_config_integer_add(value, num_str(idx), v);
                  }
                  if (err < 0) {
                        error("snd_config add: %s", snd_strerror(err));
                        return err;
                  }
            }
            break;
      default:
            error("Unknown control type: %d\n", type);
            return -EINVAL;
      }
      
      return 0;
}
      
static int get_controls(int cardno, snd_config_t *top)
{
      snd_ctl_t *handle;
      snd_ctl_card_info_t *info;
      snd_config_t *state, *card, *control;
      snd_ctl_elem_list_t *list;
      unsigned int idx;
      int err;
      char name[32];
      unsigned int count;
      const char *id;
      snd_ctl_card_info_alloca(&info);
      snd_ctl_elem_list_alloca(&list);

      sprintf(name, "hw:%d", cardno);
      err = snd_ctl_open(&handle, name, SND_CTL_READONLY);
      if (err < 0) {
            error("snd_ctl_open error: %s", snd_strerror(err));
            return err;
      }
      err = snd_ctl_card_info(handle, info);
      if (err < 0) {
            error("snd_ctl_card_info error: %s", snd_strerror(err));
            goto _close;
      }
      id = snd_ctl_card_info_get_id(info);
      err = snd_config_search(top, "state", &state);
      if (err == 0 &&
          snd_config_get_type(state) != SND_CONFIG_TYPE_COMPOUND) {
            error("config state node is not a compound");
            err = -EINVAL;
            goto _close;
      }
      if (err < 0) {
            err = snd_config_compound_add(top, "state", 1, &state);
            if (err < 0) {
                  error("snd_config_compound_add: %s", snd_strerror(err));
                  goto _close;
            }
      }
      err = snd_config_search(state, id, &card);
      if (err == 0 &&
          snd_config_get_type(card) != SND_CONFIG_TYPE_COMPOUND) {
            error("config state.%s node is not a compound", id);
            err = -EINVAL;
            goto _close;
      }
      if (err < 0) {
            err = snd_config_compound_add(state, id, 0, &card);
            if (err < 0) {
                  error("snd_config_compound_add: %s", snd_strerror(err));
                  goto _close;
            }
      }
      err = snd_config_search(card, "control", &control);
      if (err == 0) {
            err = snd_config_delete(control);
            if (err < 0) {
                  error("snd_config_delete: %s", snd_strerror(err));
                  goto _close;
            }
      }
      err = snd_ctl_elem_list(handle, list);
      if (err < 0) {
            error("Cannot determine controls: %s", snd_strerror(err));
            goto _close;
      }
      count = snd_ctl_elem_list_get_count(list);
      if (count < 0) {
            err = 0;
            goto _close;
      }
      err = snd_config_compound_add(card, "control", count > 0, &control);
      if (err < 0) {
            error("snd_config_compound_add: %s", snd_strerror(err));
            goto _close;
      }
      if (count == 0) {
            err = 0;
            goto _close;
      }
      snd_ctl_elem_list_set_offset(list, 0);
      if (snd_ctl_elem_list_alloc_space(list, count) < 0) {
            error("No enough memory...");
            goto _close;
      }
      if ((err = snd_ctl_elem_list(handle, list)) < 0) {
            error("Cannot determine controls (2): %s", snd_strerror(err));
            goto _free;
      }
      for (idx = 0; idx < count; ++idx) {
            snd_ctl_elem_id_t *id;
            snd_ctl_elem_id_alloca(&id);
            snd_ctl_elem_list_get_id(list, idx, id);
            err = get_control(handle, id, control);
            if (err < 0)
                  goto _free;
      }           
            
      err = 0;
 _free:
      snd_ctl_elem_list_free_space(list);
 _close:
      snd_ctl_close(handle);
      return err;
}

static long config_iface(snd_config_t *n)
{
      long i;
      long long li;
      snd_ctl_elem_iface_t idx; 
      const char *str;
      switch (snd_config_get_type(n)) {
      case SND_CONFIG_TYPE_INTEGER:
            snd_config_get_integer(n, &i);
            return i;
      case SND_CONFIG_TYPE_INTEGER64:
            snd_config_get_integer64(n, &li);
            return li;
      case SND_CONFIG_TYPE_STRING:
            snd_config_get_string(n, &str);
            break;
      default:
            return -1;
      }
      for (idx = 0; idx <= SND_CTL_ELEM_IFACE_LAST; idx++) {
            if (strcasecmp(snd_ctl_elem_iface_name(idx), str) == 0)
                  return idx;
      }
      return -1;
}

static int config_bool(snd_config_t *n)
{
      const char *str;
      long val;
      long long lval;

      switch (snd_config_get_type(n)) {
      case SND_CONFIG_TYPE_INTEGER:
            snd_config_get_integer(n, &val);
            if (val < 0 || val > 1)
                  return -1;
            return val;
      case SND_CONFIG_TYPE_INTEGER64:
            snd_config_get_integer64(n, &lval);
            if (lval < 0 || lval > 1)
                  return -1;
            return (int) lval;
      case SND_CONFIG_TYPE_STRING:
            snd_config_get_string(n, &str);
            break;
      case SND_CONFIG_TYPE_COMPOUND:
            if (!force_restore)
                  return -1;
            n = snd_config_iterator_entry(snd_config_iterator_first(n));
            return config_bool(n);
      default:
            return -1;
      }
      if (strcmp(str, "on") == 0 || strcmp(str, "true") == 0)
            return 1;
      if (strcmp(str, "off") == 0 || strcmp(str, "false") == 0)
            return 0;
      return -1;
}

static int config_enumerated(snd_config_t *n, snd_ctl_t *handle,
                       snd_ctl_elem_info_t *info)
{
      const char *str;
      long val;
      long long lval;
      unsigned int idx, items;

      switch (snd_config_get_type(n)) {
      case SND_CONFIG_TYPE_INTEGER:
            snd_config_get_integer(n, &val);
            return val;
      case SND_CONFIG_TYPE_INTEGER64:
            snd_config_get_integer64(n, &lval);
            return (int) lval;
      case SND_CONFIG_TYPE_STRING:
            snd_config_get_string(n, &str);
            break;
      case SND_CONFIG_TYPE_COMPOUND:
            if (!force_restore)
                  return -1;
            n = snd_config_iterator_entry(snd_config_iterator_first(n));
            return config_enumerated(n, handle, info);
      default:
            return -1;
      }
      items = snd_ctl_elem_info_get_items(info);
      for (idx = 0; idx < items; idx++) {
            int err;
            snd_ctl_elem_info_set_item(info, idx);
            err = snd_ctl_elem_info(handle, info);
            if (err < 0) {
                  error("snd_ctl_elem_info: %s", snd_strerror(err));
                  return err;
            }
            if (strcmp(str, snd_ctl_elem_info_get_item_name(info)) == 0)
                  return idx;
      }
      return -1;
}

static int config_integer(snd_config_t *n, long *val)
{
      int err = snd_config_get_integer(n, val);
      if (err < 0 && force_restore) {
            if (snd_config_get_type(n) != SND_CONFIG_TYPE_COMPOUND)
                  return err;
            n = snd_config_iterator_entry(snd_config_iterator_first(n));
            return config_integer(n, val);
      }
      return err;
}

static int config_integer64(snd_config_t *n, long long *val)
{
      int err = snd_config_get_integer64(n, val);
      if (err < 0 && force_restore) {
            if (snd_config_get_type(n) != SND_CONFIG_TYPE_COMPOUND)
                  return err;
            n = snd_config_iterator_entry(snd_config_iterator_first(n));
            return config_integer64(n, val);
      }
      return err;
}

static int is_user_control(snd_config_t *conf)
{
      snd_config_iterator_t i, next;

      snd_config_for_each(i, next, conf) {
            snd_config_t *n = snd_config_iterator_entry(i);
            const char *id, *s;
            if (snd_config_get_id(n, &id) < 0)
                  continue;
            if (strcmp(id, "access") == 0) {
                  if (snd_config_get_string(n, &s) < 0)
                        return 0;
                  if (strstr(s, "user"))
                        return 1;
            }
      }
      return 0;
}

/*
 * get the item type from the given comment config
 */
static int get_comment_type(snd_config_t *n)
{
      const char *type;

      if (snd_config_get_string(n, &type) < 0)
            return -EINVAL;
      if (strcmp(type, "BOOLEAN") == 0)
            return SND_CTL_ELEM_TYPE_BOOLEAN;
      else if (strcmp(type, "INTEGER") == 0)
            return SND_CTL_ELEM_TYPE_INTEGER;
      else if (strcmp(type, "ENUMERATED") == 0)
            return SND_CTL_ELEM_TYPE_ENUMERATED;
      else if (strcmp(type, "INTEGER64") == 0)
            return SND_CTL_ELEM_TYPE_INTEGER;
      else if (strcmp(type, "IEC958") == 0)
            return SND_CTL_ELEM_TYPE_IEC958;
      else
            return -EINVAL;
}

/*
 * get the value range from the given comment config
 */
static int get_comment_range(snd_config_t *n, int ctype,
                       long *imin, long *imax, long *istep)
{
      const char *s;
      int err;

      if (snd_config_get_string(n, &s) < 0)
            return -EINVAL;
      switch (ctype) {
      case SND_CTL_ELEM_TYPE_INTEGER:
            err = sscanf(s, "%li - %li (step %li)", imin, imax, istep);
            if (err != 3) {
                  istep = 0;
                  err = sscanf(s, "%li - %li", imin, imax);
                  if (err != 2)
                        return -EINVAL;
            }
            break;
      default:
            return -EINVAL;
      }
      return 0;
}

static int add_user_control(snd_ctl_t *handle, snd_ctl_elem_info_t *info, snd_config_t *conf)
{
      snd_ctl_elem_id_t *id;
      snd_config_iterator_t i, next;
      long imin, imax, istep;
      snd_ctl_elem_type_t ctype;
      unsigned int count;
      int err;
      unsigned int *tlv;

      imin = imax = istep = 0;
      count = 0;
      ctype = SND_CTL_ELEM_TYPE_NONE;
      tlv = NULL;
      snd_config_for_each(i, next, conf) {
            snd_config_t *n = snd_config_iterator_entry(i);
            const char *id;
            if (snd_config_get_id(n, &id) < 0)
                  continue;
            if (strcmp(id, "type") == 0) {
                  err = get_comment_type(n);
                  if (err < 0)
                        return err;
                  ctype = err;
                  continue;
            }
            if (strcmp(id, "range") == 0) {
                  err = get_comment_range(n, ctype, &imin, &imax, &istep);
                  if (err < 0)
                        return err;
                  continue;
            }
            if (strcmp(id, "count") == 0) {
                  long v;
                  if ((err = snd_config_get_integer(n, &v)) < 0)
                        return err;
                  count = v;
                  continue;
            }
            if (strcmp(id, "tlv") == 0) {
                  const char *s;
                  if ((err = snd_config_get_string(n, &s)) < 0)
                        return -EINVAL;
                  if (tlv)
                        free(tlv);
                  if ((tlv = str_to_tlv(s)) == NULL)
                        return -EINVAL;
                  continue;
            }
      }

      snd_ctl_elem_id_alloca(&id);
      snd_ctl_elem_info_get_id(info, id);
      if (count <= 0)
            count = 1;
      switch (ctype) {
      case SND_CTL_ELEM_TYPE_INTEGER:
            if (imin > imax || istep > imax - imin)
                  return -EINVAL;
            err = snd_ctl_elem_add_integer(handle, id, count, imin, imax, istep);
            if (err < 0)
                  goto error;
            if (tlv)
                  snd_ctl_elem_tlv_write(handle, id, tlv);
            break;
      case SND_CTL_ELEM_TYPE_BOOLEAN:
            err = snd_ctl_elem_add_boolean(handle, id, count);
            break;
      case SND_CTL_ELEM_TYPE_IEC958:
            err = snd_ctl_elem_add_iec958(handle, id);
            break;
      default:
            err = -EINVAL;
            break;
      }

 error:
      free(tlv);
      if (err < 0)
            return err;
      return snd_ctl_elem_info(handle, info);
}

/*
 * look for a config node with the given item name
 */
static snd_config_t *search_comment_item(snd_config_t *conf, const char *name)
{
      snd_config_iterator_t i, next;
      snd_config_for_each(i, next, conf) {
            snd_config_t *n = snd_config_iterator_entry(i);
            const char *id;
            if (snd_config_get_id(n, &id) < 0)
                  continue;
            if (strcmp(id, name) == 0)
                  return n;
      }
      return NULL;
}

/*
 * check whether the config item has the same of compatible type
 */
static int check_comment_type(snd_config_t *conf, int type)
{
      snd_config_t *n = search_comment_item(conf, "type");
      int ctype;

      if (!n)
            return 0; /* not defined */
      ctype = get_comment_type(n);
      if (ctype == type)
            return 0;
      if ((ctype == SND_CTL_ELEM_TYPE_BOOLEAN ||
           ctype == SND_CTL_ELEM_TYPE_INTEGER ||
           ctype == SND_CTL_ELEM_TYPE_INTEGER64 ||
           ctype == SND_CTL_ELEM_TYPE_ENUMERATED) &&
          (type == SND_CTL_ELEM_TYPE_BOOLEAN ||
           type == SND_CTL_ELEM_TYPE_INTEGER ||
           type == SND_CTL_ELEM_TYPE_INTEGER64 ||
           type == SND_CTL_ELEM_TYPE_ENUMERATED))
            return 0; /* OK, compatible */
      return -EINVAL;
}

/*
 * convert from an old value to a new value with the same dB level
 */
static int convert_to_new_db(snd_config_t *value, long omin, long omax,
                       long nmin, long nmax,
                       long odbmin, long odbmax,
                       long ndbmin, long ndbmax)
{
      long val;
      if (config_integer(value, &val) < 0)
            return -EINVAL;
      if (val < omin || val > omax)
            return -EINVAL;
      val = ((val - omin) * (odbmax - odbmin)) / (omax - omin) + odbmin;
      if (val < ndbmin)
            val = ndbmin;
      else if (val > ndbmax)
            val = ndbmax;
      val = ((val - ndbmin) * (nmax - nmin)) / (ndbmax - ndbmin) + nmin;
      return snd_config_set_integer(value, val);
}

/*
 * compare the current value range with the old range in comments.
 * also, if dB information is available, try to compare them.
 * if any change occurs, try to keep the same dB level.
 */
static int check_comment_range(snd_ctl_t *handle, snd_config_t *conf,
                         snd_ctl_elem_info_t *info, snd_config_t *value)
{
      snd_config_t *n;
      long omin, omax, ostep;
      long nmin, nmax;
      long odbmin, odbmax;
      long ndbmin, ndbmax;
      snd_ctl_elem_id_t *id;

      n = search_comment_item(conf, "range");
      if (!n)
            return 0;
      if (get_comment_range(n, SND_CTL_ELEM_TYPE_INTEGER,
                        &omin, &omax, &ostep) < 0)
            return 0;
      nmin = snd_ctl_elem_info_get_min(info);
      nmax = snd_ctl_elem_info_get_max(info);
      if (omin != nmin && omax != nmax) {
            /* Hey, the range mismatches */
            if (!force_restore)
                  return -EINVAL;
      }
      if (omin >= omax || nmin >= nmax)
            return 0; /* invalid values */

      n = search_comment_item(conf, "dbmin");
      if (!n)
            return 0;
      if (config_integer(n, &odbmin) < 0)
            return 0;
      n = search_comment_item(conf, "dbmax");
      if (!n)
            return 0;
      if (config_integer(n, &odbmax) < 0)
            return 0;
      if (odbmin >= odbmax)
            return 0; /* invalid values */
      snd_ctl_elem_id_alloca(&id);
      snd_ctl_elem_info_get_id(info, id);
      if (snd_ctl_get_dB_range(handle, id, &ndbmin, &ndbmax) < 0)
            return 0;
      if (ndbmin >= ndbmax)
            return 0; /* invalid values */
      if (omin == nmin && omax == nmax &&
          odbmin == ndbmin && odbmax == ndbmax)
            return 0; /* OK, identical one */

      /* Let's guess the current value from dB range */
      if (snd_config_get_type(value) == SND_CONFIG_TYPE_COMPOUND) {
            snd_config_iterator_t i, next;
            snd_config_for_each(i, next, value) {
                  snd_config_t *n = snd_config_iterator_entry(i);
                  convert_to_new_db(n, omin, omax, nmin, nmax,
                                odbmin, odbmax, ndbmin, ndbmax);
            }
      } else
            convert_to_new_db(value, omin, omax, nmin, nmax,
                          odbmin, odbmax, ndbmin, ndbmax);
      return 0;
}

static int restore_config_value(snd_ctl_t *handle, snd_ctl_elem_info_t *info,
                        snd_ctl_elem_iface_t type,
                        snd_config_t *value,
                        snd_ctl_elem_value_t *ctl, int idx)
{
      long val;
      long long lval;
      int err;

      switch (type) {
      case SND_CTL_ELEM_TYPE_BOOLEAN:
            val = config_bool(value);
            if (val >= 0) {
                  snd_ctl_elem_value_set_boolean(ctl, idx, val);
                  return 1;
            }
            break;
      case SND_CTL_ELEM_TYPE_INTEGER:
            err = config_integer(value, &val);
            if (err == 0) {
                  snd_ctl_elem_value_set_integer(ctl, idx, val);
                  return 1;
            }
            break;
      case SND_CTL_ELEM_TYPE_INTEGER64:
            err = config_integer64(value, &lval);
            if (err == 0) {
                  snd_ctl_elem_value_set_integer64(ctl, idx, lval);
                  return 1;
            }
            break;
      case SND_CTL_ELEM_TYPE_ENUMERATED:
            val = config_enumerated(value, handle, info);
            if (val >= 0) {
                  snd_ctl_elem_value_set_enumerated(ctl, idx, val);
                  return 1;
            }
            break;
      case SND_CTL_ELEM_TYPE_BYTES:
      case SND_CTL_ELEM_TYPE_IEC958:
            break;
      default:
            error("Unknow control type: %d", type);
            return -EINVAL;
      }
      return 0;
}

static int restore_config_value2(snd_ctl_t *handle, snd_ctl_elem_info_t *info,
                         snd_ctl_elem_iface_t type,
                         snd_config_t *value,
                         snd_ctl_elem_value_t *ctl, int idx,
                         unsigned int numid)
{
      int err = restore_config_value(handle, info, type, value, ctl, idx);
      long val;

      if (err != 0)
            return err;
      switch (type) {
      case SND_CTL_ELEM_TYPE_BYTES:
      case SND_CTL_ELEM_TYPE_IEC958:
            err = snd_config_get_integer(value, &val);
            if (err < 0 || val < 0 || val > 255) {
                  error("bad control.%d.value.%d content", numid, idx);
                  return force_restore ? 0 : -EINVAL;
            }
            snd_ctl_elem_value_set_byte(ctl, idx, val);
            return 1;
            break;
      default:
            break;
      }
      return 0;
}

static int set_control(snd_ctl_t *handle, snd_config_t *control)
{
      snd_ctl_elem_value_t *ctl;
      snd_ctl_elem_info_t *info;
      snd_config_iterator_t i, next;
      unsigned int numid1;
      snd_ctl_elem_iface_t iface = -1;
      int iface1;
      const char *name1;
      unsigned int numid;
      snd_ctl_elem_type_t type;
      unsigned int count;
      long device = -1;
      long device1;
      long subdevice = -1;
      long subdevice1;
      const char *name = NULL;
      long index1;
      long index = -1;
      snd_config_t *value = NULL;
      snd_config_t *comment = NULL;
      unsigned int idx;
      int err;
      char *set;
      const char *id;
      snd_ctl_elem_value_alloca(&ctl);
      snd_ctl_elem_info_alloca(&info);
      if (snd_config_get_type(control) != SND_CONFIG_TYPE_COMPOUND) {
            error("control is not a compound");
            return -EINVAL;
      }
      err = snd_config_get_id(control, &id);
      if (err < 0) {
            error("unable to get id");
            return -EINVAL;
      }
      numid = atoi(id);
      snd_config_for_each(i, next, control) {
            snd_config_t *n = snd_config_iterator_entry(i);
            const char *fld;
            if (snd_config_get_id(n, &fld) < 0)
                  continue;
            if (strcmp(fld, "comment") == 0) {
                  if (snd_config_get_type(n) != SND_CONFIG_TYPE_COMPOUND) {
                        error("control.%d.%s is invalid", numid, fld);
                        return -EINVAL;
                  }
                  comment = n;
                  continue;
            }
            if (strcmp(fld, "iface") == 0) {
                  iface = (snd_ctl_elem_iface_t)config_iface(n);
                  if (iface < 0) {
                        error("control.%d.%s is invalid", numid, fld);
                        return -EINVAL;
                  }
                  continue;
            }
            if (strcmp(fld, "device") == 0) {
                  if (snd_config_get_type(n) != SND_CONFIG_TYPE_INTEGER) {
                        error("control.%d.%s is invalid", numid, fld);
                        return -EINVAL;
                  }
                  snd_config_get_integer(n, &device);
                  continue;
            }
            if (strcmp(fld, "subdevice") == 0) {
                  if (snd_config_get_type(n) != SND_CONFIG_TYPE_INTEGER) {
                        error("control.%d.%s is invalid", numid, fld);
                        return -EINVAL;
                  }
                  snd_config_get_integer(n, &subdevice);
                  continue;
            }
            if (strcmp(fld, "name") == 0) {
                  if (snd_config_get_type(n) != SND_CONFIG_TYPE_STRING) {
                        error("control.%d.%s is invalid", numid, fld);
                        return -EINVAL;
                  }
                  snd_config_get_string(n, &name);
                  continue;
            }
            if (strcmp(fld, "index") == 0) {
                  if (snd_config_get_type(n) != SND_CONFIG_TYPE_INTEGER) {
                        error("control.%d.%s is invalid", numid, fld);
                        return -EINVAL;
                  }
                  snd_config_get_integer(n, &index);
                  continue;
            }
            if (strcmp(fld, "value") == 0) {
                  value = n;
                  continue;
            }
            error("unknown control.%d.%s field", numid, fld);
      }
      if (!value) {
            error("missing control.%d.value", numid);
            return -EINVAL;
      }
      if (device < 0)
            device = 0;
      if (subdevice < 0)
            subdevice = 0;
      if (index < 0)
            index = 0;

      err = -EINVAL;
      if (! force_restore) {
            snd_ctl_elem_info_set_numid(info, numid);
            err = snd_ctl_elem_info(handle, info);
      }
      if (err < 0) {
            if (iface >= 0 && name) {
                  snd_ctl_elem_info_set_numid(info, 0);
                  snd_ctl_elem_info_set_interface(info, iface);
                  snd_ctl_elem_info_set_device(info, device);
                  snd_ctl_elem_info_set_subdevice(info, subdevice);
                  snd_ctl_elem_info_set_name(info, name);
                  snd_ctl_elem_info_set_index(info, index);
                  err = snd_ctl_elem_info(handle, info);
                  if (err < 0 && comment && is_user_control(comment)) {
                        err = add_user_control(handle, info, comment);
                        if (err < 0) {
                              error("failed to add user control #%d (%s)",
                                    numid, snd_strerror(err));
                              return err;
                        }
                  }
            }
      }
      if (err < 0) {
            error("failed to obtain info for control #%d (%s)", numid, snd_strerror(err));
            return -ENOENT;
      }
      numid1 = snd_ctl_elem_info_get_numid(info);
      iface1 = snd_ctl_elem_info_get_interface(info);
      device1 = snd_ctl_elem_info_get_device(info);
      subdevice1 = snd_ctl_elem_info_get_subdevice(info);
      name1 = snd_ctl_elem_info_get_name(info);
      index1 = snd_ctl_elem_info_get_index(info);
      count = snd_ctl_elem_info_get_count(info);
      type = snd_ctl_elem_info_get_type(info);
      if (err |= numid != numid1 && ! force_restore)
            error("warning: numid mismatch (%d/%d) for control #%d", 
                  numid, numid1, numid);
      if (err |= iface != iface1)
            error("warning: iface mismatch (%d/%d) for control #%d", iface, iface1, numid);
      if (err |= device != device1)
            error("warning: device mismatch (%ld/%ld) for control #%d", device, device1, numid);
      if (err |= subdevice != subdevice1)
            error("warning: subdevice mismatch (%ld/%ld) for control #%d", subdevice, subdevice1, numid);
      if (err |= strcmp(name, name1))
            error("warning: name mismatch (%s/%s) for control #%d", name, name1, numid);
      if (err |= index != index1)
            error("warning: index mismatch (%ld/%ld) for control #%d", index, index1, numid);
      if (err < 0) {
            error("failed to obtain info for control #%d (%s)", numid, snd_strerror(err));
            return -ENOENT;
      }

      if (comment) {
            if (check_comment_type(comment, type) < 0)
                  error("incompatible field type for control #%d", numid);
            if (type == SND_CTL_ELEM_TYPE_INTEGER) {
                  if (check_comment_range(handle, comment, info, value) < 0) {
                        error("value range mismatch for control #%d",
                              numid);
                        return -EINVAL;
                  }
            }
      }

      if (snd_ctl_elem_info_is_inactive(info) ||
                        !snd_ctl_elem_info_is_writable(info))
            return 0;
      snd_ctl_elem_value_set_numid(ctl, numid1);

      if (count == 1) {
            err = restore_config_value(handle, info, type, value, ctl, 0);
            if (err < 0)
                  return err;
            if (err > 0)
                  goto _ok;
      }
      switch (type) {
      case SND_CTL_ELEM_TYPE_BYTES:
      case SND_CTL_ELEM_TYPE_IEC958:
      {
            const char *buf;
            err = snd_config_get_string(value, &buf);
            if (err >= 0) {
                  int c1 = 0;
                  int len = strlen(buf);
                  unsigned int idx = 0;
                  int size = type == SND_CTL_ELEM_TYPE_BYTES ?
                        count : sizeof(snd_aes_iec958_t);
                  if (size * 2 != len) {
                        error("bad control.%d.value contents\n", numid);
                        return -EINVAL;
                  }
                  while (*buf) {
                        int c = *buf++;
                        if ((c = hextodigit(c)) < 0) {
                              error("bad control.%d.value contents\n", numid);
                              return -EINVAL;
                        }
                        if (idx % 2 == 1)
                              snd_ctl_elem_value_set_byte(ctl, idx / 2, c1 << 4 | c);
                        else
                              c1 = c;
                        idx++;
                  }
                  goto _ok;
            }
      }
      default:
            break;
      }
      if (snd_config_get_type(value) != SND_CONFIG_TYPE_COMPOUND) {
            if (!force_restore) {
                  error("bad control.%d.value type", numid);
                  return -EINVAL;
            }
            for (idx = 0; idx < count; ++idx) {
                  err = restore_config_value2(handle, info, type, value,
                                        ctl, idx, numid);
                  if (err < 0)
                        return err;
            }
            goto _ok;
      }

      set = (char*) alloca(count);
      memset(set, 0, count);
      snd_config_for_each(i, next, value) {
            snd_config_t *n = snd_config_iterator_entry(i);
            const char *id;
            if (snd_config_get_id(n, &id) < 0)
                  continue;
            idx = atoi(id);
            if (idx < 0 || idx >= count || 
                set[idx]) {
                  error("bad control.%d.value index", numid);
                  if (!force_restore)
                        return -EINVAL;
                  continue;
            }
            err = restore_config_value2(handle, info, type, n,
                                  ctl, idx, numid);
            if (err < 0)
                  return err;
            if (err > 0)
                  set[idx] = 1;
      }
      for (idx = 0; idx < count; ++idx) {
            if (!set[idx]) {
                  error("control.%d.value.%d is not specified", numid, idx);
                  if (!force_restore)
                        return -EINVAL;
            }
      }

 _ok:
      err = snd_ctl_elem_write(handle, ctl);
      if (err < 0) {
            error("Cannot write control '%d:%ld:%ld:%s:%ld' : %s", (int)iface, device, subdevice, name, index, snd_strerror(err));
            return err;
      }
      return 0;
}

static int set_controls(int card, snd_config_t *top)
{
      snd_ctl_t *handle;
      snd_ctl_card_info_t *info;
      snd_config_t *control;
      snd_config_iterator_t i, next;
      int err;
      char name[32], tmpid[16];
      const char *id;
      snd_ctl_card_info_alloca(&info);

      sprintf(name, "hw:%d", card);
      err = snd_ctl_open(&handle, name, 0);
      if (err < 0) {
            error("snd_ctl_open error: %s", snd_strerror(err));
            return err;
      }
      err = snd_ctl_card_info(handle, info);
      if (err < 0) {
            error("snd_ctl_card_info error: %s", snd_strerror(err));
            goto _close;
      }
      id = snd_ctl_card_info_get_id(info);
      err = snd_config_searchv(top, &control, "state", id, "control", 0);
      if (err < 0) {
            if (force_restore) {
                  sprintf(tmpid, "card%d", card);
                  err = snd_config_searchv(top, &control, "state", tmpid, "control", 0);
                  if (! err)
                        id = tmpid;
            }
            if (err < 0) {
                  fprintf(stderr, "No state is present for card %s\n", id);
                  goto _close;
            }
            id = tmpid;
      }
      if (snd_config_get_type(control) != SND_CONFIG_TYPE_COMPOUND) {
            error("state.%s.control is not a compound\n", id);
            return -EINVAL;
      }
      snd_config_for_each(i, next, control) {
            snd_config_t *n = snd_config_iterator_entry(i);
            err = set_control(handle, n);
            if (err < 0 && ! force_restore)
                  goto _close;
      }

 _close:
      snd_ctl_close(handle);
      return err;
}

int save_state(const char *file, const char *cardname)
{
      int err;
      snd_config_t *config;
      snd_input_t *in;
      snd_output_t *out;
      int stdio;

      err = snd_config_top(&config);
      if (err < 0) {
            error("snd_config_top error: %s", snd_strerror(err));
            return err;
      }
      stdio = !strcmp(file, "-");
      if (!stdio && (err = snd_input_stdio_open(&in, file, "r")) >= 0) {
            err = snd_config_load(config, in);
            snd_input_close(in);
#if 0
            if (err < 0) {
                  error("snd_config_load error: %s", snd_strerror(err));
                  return err;
            }
#endif
      }

      if (!cardname) {
            int card, first = 1;

            card = -1;
            /* find each installed soundcards */
            while (1) {
                  if (snd_card_next(&card) < 0)
                        break;
                  if (card < 0) {
                        if (first) {
                              error("No soundcards found...");
                              return -ENODEV;
                        }
                        break;
                  }
                  first = 0;
                  if ((err = get_controls(card, config)))
                        return err;
            }
      } else {
            int cardno;

            cardno = snd_card_get_index(cardname);
            if (cardno < 0) {
                  error("Cannot find soundcard '%s'...", cardname);
                  return cardno;
            }
            if ((err = get_controls(cardno, config))) {
                  return err;
            }
      }
      
      if (stdio)
            err = snd_output_stdio_attach(&out, stdout, 0);
      else
            err = snd_output_stdio_open(&out, file, "w");
      if (err < 0) {
            error("Cannot open %s for writing: %s", file, snd_strerror(err));
            return -errno;
      }
      err = snd_config_save(config, out);
      snd_output_close(out);
      if (err < 0)
            error("snd_config_save: %s", snd_strerror(err));
      return 0;
}

int load_state(const char *file, const char *cardname)
{
      int err;
      snd_config_t *config;
      snd_input_t *in;
      int stdio;

      err = snd_config_top(&config);
      if (err < 0) {
            error("snd_config_top error: %s", snd_strerror(err));
            return err;
      }
      stdio = !strcmp(file, "-");
      if (stdio)
            err = snd_input_stdio_attach(&in, stdin, 0);
      else
            err = snd_input_stdio_open(&in, file, "r");
      if (err >= 0) {
            err = snd_config_load(config, in);
            snd_input_close(in);
            if (err < 0) {
                  error("snd_config_load error: %s", snd_strerror(err));
                  return err;
            }
      } else {
            error("Cannot open %s for reading: %s", file, snd_strerror(err));
            return err;
      }

      if (!cardname) {
            int card, first = 1;

            card = -1;
            /* find each installed soundcards */
            while (1) {
                  if (snd_card_next(&card) < 0)
                        break;
                  if (card < 0) {
                        if (first) {
                              error("No soundcards found...");
                              return -ENODEV;
                        }
                        break;
                  }
                  first = 0;
                  if ((err = set_controls(card, config)) && ! force_restore)
                        return err;
            }
      } else {
            int cardno;

            cardno = snd_card_get_index(cardname);
            if (cardno < 0) {
                  error("Cannot find soundcard '%s'...", cardname);
                  return -ENODEV;
            }
            if ((err = set_controls(cardno, config)) && ! force_restore) {
                  return err;
            }
      }
      return 0;
}

Generated by  Doxygen 1.6.0   Back to index