Il suono secondo Linux - Uso dei devices sonori
Pubblicato da blAAd! il 18/12/2001
Livello intermedio

Introduzione

Vediamo come utilizzare attraverso la funzione ioctl(...), i devices sonori di cui e' dotato il nostro Kernel. I codici introdotti, li ho testati sotto una Mandrake 8.1 mantenendo un certo standard, quindi bastera' compilarli con un semplice "gcc -o prog prog.c" e lanciarli tramite "./prog". A livello di Kernel non dovrebbero esserci particolari problemi, sia utilizzado la versione 2.2.* che la 2.4.* (non ho avuto la possibilita' di provare sotto la versione 2.0.* pero' visto le fonti da cui e' attinto il tutorial, non dovrebbero esserci problemi). Cio' che troverete qui, e' relativo ad informazioni prese da testi in giro per la rete (e quindi ulteriormente approfondibili) tra cui i "Sound HOW-TO", e in particolare un capitolo on-line di un libro del 1996 "LINUX MULTIMEDIA GUIDE" di Jeff Tranter al sito www.oreally.com (il tutorial che segue ne rappresenta praticamente una traduzione, con qualche aggiunta "attualizzata").

Programmi usati

Iniziamo

I devices che vedremo (alcuni solo brevemente), hanno subito e subiscono ancora profonde modifiche strutturali, quindi cerchero' di spiegarne l'uso attraverso "parametri generali" legati alla funzione ioctl (...). Non rappresentano la completezza sonora di Linux, ma solo (almeno spero) le sue fondamenta applicative.

Una caratteristica essenziale dei sistemi *NIX, e' la "visione" dell'hardware a livello piu' alto come un insieme di files, contenuti nella directory /dev (nel caso di Linux). Sara' possibile quindi aprire, chiudere, leggere e scrivere su questi, tramite le classiche System Calls open(), close(), read(), write() utilizzate per files "normali". Inoltre e' possibile passare "parametri" a questi devs, tramite la funzione ioctl(...) fondamentale per interfacciarci all'interno dei nostri programmi con l'hardware. Daro' per scontato che chi legga questo tutorial, conosca gia' le funzioni citate (eventualmente trovate gia' tutto spiegato sul sito, e comunque un semplice "man 2 funzione" potrebbe esservi di aiuto).

I devs sonori di cui parliamo agiscono come una "idealizzazione" di una eventuale card sonora (SB16, Yamaha, ecc.), gestendo le eventuali differenze hardware in maniera omogenea (anche se spesso per famosi problemi di MONOPOLIO risulta difficile l'eventuale utlizzo di tutte le potenzialita').
Sintetizzatori, A/D converter ecc., sono controllati dalla CPU tramite il bus ISA. Il bus ISA "fa' transitare" segnali di controllo, indirizzi e dati legati alla sound card (tramite la slot su cui e' inserita), verso la CPU (oltre all'ISA esistono anche PCI e EISA). I comuni inputs della scheda sono il microfono (MIC) e il cd-rom (CD), mentre le uscite sono i tradizionali speakers. Un "output mixer" poi, combinera' i vari segnali e li mandera' ai dispositivi esterni tramite un D/A converter, che si occupera' di convertire i segnali digitali in analogici (ad esempio quando suonate un cd tramite cd-rom). A sua volta esiste un "input mixer" che mandera' il tutto verso un dispositivo A/D, per la "trasformazione" dei segnali analogici di ingresso in segnali digitali.

Terminata questa breve parentesi hardware, passiamo ora al lato software che piu' ci interessa.

Programmi che utilizzano sound devices, devono includere il sound header <linux/soundcard.h>. Esiste anche il file <linux/ultrasound.h> che e' legato esclusivamente alla scheda Gravis UltraSound. Mi sto' riferendo ovviamente alla directory "include" dei sorgenti del Kernel.

Uno dei devices sonori piu' importanti e' il /dev/dsp. "Scrivere" verso questo device, significa accedere al D/A converter per produrre del suono, mentre in lettura si attiva l'A/D converter per la registrazione sonora. DSP significa - digital signal processor -, un processore specializzato per l'analisi sonora digitale. Alcune sound card forniscono piu' di un processore. Potreste quindi trovare sotto la directory /dev anche un eventuale /dev/dsp1. E' importante ricordare che solo un processo per volta, puo' "aprire" /dev/dsp. Se tentiamo con un secondo processo avremo un errore con codice relativo a EBUSY.
Bisogna inoltre sottolineare il fatto, che la scheda audio e' legata dal punto di vista della "temporizzazione", non alla velocita' con cui il processo accede al dispositivo, ma alla sincronia imposta dal Kernel. Se il nostro processo "legge" troppo lentamente dalla scheda, allora i dati mandati in eccesso verranno tagliati, e quindi il nostro audio risultera' discontinuo, con dei salti durante l'ascolto. Viceversa, se si legge troppo rapidamente, il Kernel mettera' in attesa il nostro dispositivo, fino a che una nuova quantita' di dati sufficiente, non sara' pronta.

Le impostazioni di default della nostra scheda possono essere cambiate tramite la funzione ioctl(...). Per far cio' prima dovremo aprire il dispositivo, modificare i parametri, e infine passare alle classiche operazioni di read() e write(). Se passiamo un parametro fuori da un range, ad esempio il parametro di campionatura (sampling rate) di 9500Hz, la nostra scheda si portera' sul valore permesso piu' vicino (ad esempio 9000Hz).

Le chiamate ioctl(...) per DSP hanno nomi che iniziano con SOUND_PCM_READ_XXX, se vogliamo conoscere il valore corrente di un parametro, e SOUND_PCM_WRITE_XXX se vogliamo modificarlo.

Vediamo alcuni tra i parametri piu' importanti inclusi nel file <linux/soundcard.h> utilizzabili tramite ioctl(...): SOUND_PCM_WRITE_BITS
Setta la misura in bits della campionatura. I valori tipici sono 8 e 16.
SOUND_PCM_READ_BITS
Ritorna il valore sopra.
SOUND_PCM_WRITE_CHANNELS
Setta il numero di canali,1 per mono, 2 per stereo. In modalita' stereo, i dati sono intervallati sia in scrittua che in lettura come sinistro-destro-sinistro-destro-...
SOUND_PCM_READ_CHANNELS
Ritorna il numero di canali.
SOUND_PCM_WRITE_RATE
Setta la frequenza di campionamento, in campioni al secondo. Eventuali valori fuori range vengono arrotondati intorno ad un valore minimo di 4kHz, a valori superiori di 13, 15, 22, o 44 kHz.
SOUND_PCM_READ_RATE
Ritona il valore sopra. Potra'o' essere diverso dal nostro setting, proprio a causa di arrotondamenti.

Ora vediamo un semplice programma che registrera' alcuni secondi di sonoro, passati attraverso l'input di default che nella maggior parte dei casi e' rappresentato dal microfono (MIC) e li risuonera' automaticamente:


/*
 * parrot.c
 * Program to illustrate /dev/dsp device
 * Records several seconds of sound, then echoes it back.
 * Runs until Control-C is pressed.
 */

#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <stdio.h>
#include <linux/soundcard.h>

#define LENGTH 3    /* how many seconds of speech to store */
#define RATE 8000   /* the sampling rate */
#define SIZE 8      /* sample size: 8 or 16 bits */
#define CHANNELS 1  /* 1 = mono 2 = stereo */

/* this buffer holds the digitized audio */
unsigned char buf[LENGTH*RATE*SIZE*CHANNELS/8];

int main()
{
  int fd;       /* sound device file descriptor */
  int arg;      /* argument for ioctl calls */
  int status;   /* return status of system calls */

  /* open sound device */
  fd = open("/dev/dsp", O_RDWR);
  if (fd < 0) {
    perror("open of /dev/dsp failed");
    exit(1);
  }

  /* set sampling parameters */
  arg = SIZE;      /* sample size */
  status = ioctl(fd, SOUND_PCM_WRITE_BITS, &arg);
  if (status == -1)
    perror("SOUND_PCM_WRITE_BITS ioctl failed");
  if (arg != SIZE)
    perror("unable to set sample size");

  arg = CHANNELS;  /* mono or stereo */
  status = ioctl(fd, SOUND_PCM_WRITE_CHANNELS, &arg);
  if (status == -1)
    perror("SOUND_PCM_WRITE_CHANNELS ioctl failed");
  if (arg != CHANNELS)
    perror("unable to set number of channels");

  arg = RATE;      /* sampling rate */
  status = ioctl(fd, SOUND_PCM_WRITE_RATE, &arg);
  if (status == -1)
    perror("SOUND_PCM_WRITE_WRITE ioctl failed");

  while (1) { /* loop until Control-C */
    printf("Say something:\n");
    status = read(fd, buf, sizeof(buf)); /* record some sound */
    if (status != sizeof(buf))
      perror("read wrong number of bytes");
    printf("You said:\n");
    status = write(fd, buf, sizeof(buf)); /* play it back */
    if (status != sizeof(buf))
      perror("wrote wrong number of bytes");
    /* wait for playback to complete before recording again */
    status = ioctl(fd, SOUND_PCM_SYNC, 0); 
  if (status == -1)
    perror("SOUND_PCM_SYNC ioctl failed");
  }
}
Facciamo una breve parentesi sul dispositivo /dev/audio. Esso e' simile a /dev/dsp, ma e' legato alla compatibilita' con le workstations della Sun MicroSystems. L'uso di questo dispositivo e' sconsigliato sotto Linux, a cui e' preferito /dev/dsp. Usa il sistema di encoding mu-law. Un esempio di utilizzo sotto piattaforma Sun e' il comando "cat file.au >/dev/audio".

Vediamo ora l'importante /dev/mixer suddiviso in input e output mixer.

L'input mixer accetta segnali analogici da fonti diverse. Per sorgenti si intendono i canali e i dispositivi del mixer. Un controllo del guadagno elettronico ed un software per il controllo del volume regolano il livello del segnale prima dell'input vero e proprio al mixer. L'input del mixer e' invece regolato da un guadagno finale chiamato Reclev (recording level). Il tutto e' mandato infine al convertitore analogico/digitale (A/D).

L'output mixer funziona in maniera simile. Sostanzialmente vengono annullati i precedenti guadagni, cosi' che i segnali escano verso il convertitore digitale/analogico (D/A) e infine verso l'output analogico (ad esempio gli speakers).

Programmare il mixer significa sostanzialmente definire i livelli di guadagno e le abilitazioni delle sorgenti. Non e' pero' il tipico device *NIX, in quanto non e' possibile operare su di esso con read() e write(), ma solo con open(), close() e ioctl(...). Diversamente dal DSP, piu' di un processo puo' accedere al mixer contemporaneamente, e anche se il mixer device viene chiuso, le modifiche ai parametri restano effettive. In genere non c'e' bisogno di aprire il /dev/mixer se si e' gia' aperto il /dev/dsp, si potra' cioe' usare lo stesso file descriptor di /dev/dsp. I comandi per ioctl sono prefissati con SOUND_MIXER e MIXER_.
Ecco una lista dei principali comandi:

SOUND_MIXER_VOLUME         master output level

SOUND_MIXER_BASS           bass tone control

SOUND_MIXER_TREBLE         treble tone control

SOUND_MIXER_SYNTH          FM synthesizer

SOUND_MIXER_PCM            D/A converter

SOUND_MIXER_SPEAKER        PC speaker output level

SOUND_MIXER_LINE           line input

SOUND_MIXER_MIC            microphone input

SOUND_MIXER_CD             audio CD input

SOUND_MIXER_IMIX           playback volume from recording source

SOUND_MIXER_ALTPCM         secondary D/A converter

SOUND_MIXER_RECLEV         master recording level

SOUND_MIXER_IGAIN          input gain level

SOUND_MIXER_OGAIN          output gain level

SOUND_MIXER_LINE1          card-specific input #1

SOUND_MIXER_LINE2          card-specific input #2

SOUND_MIXER_LINE3          card-specific input #3
La macro MIXER_READ e' usata quando si vuole leggere a che livello e' stato settato un canale, con valori espressi in percentuali, cioe' da 0 a 100. Accetta un parametro corrispondente al bitmask del canale in questione. Ad esempio, per leggere il livello di input del microfono, possiamo scrivere il seguente codice:

int vol;
ioctl(fd, MIXER_READ(SOUND_MIXER_MIC), &vol);
printf("Mic gain is at %d %%\n", vol);
Se il canale e' stereo i valori ritornati potrebbero essere 2, uno per canale. Il byte meno significativo rappresenterebbe il canale sinistro, e l'altro byte il destro. Una eventuale codifica potrebbe essere:

int left, right;
left  =  vol & 0xff;
right = (vol & 0xff00) >> 8;
printf("Left gain is %d %%, Right gain is %d %%\n", left, right);
Per dispositivi mono abbiamo solo il byte meno significativo.
I livelli di guadagno possono essere settati usando la macro MIXER_WRITE. Ad esempio per settare il volume:

vol = (right << 8) + left;
ioctl(fd, MIXER_WRITE(SOUND_MIXER_MIC), &vol);
I valori possono essere variati dal nostro settaggio, come gia' detto in precedenza, in base alle vere caratteristiche dell'hardware. ioctl(...) ritorna il valore attualmente usato.
Il sound driver header, da' per ogni canale un nome simbolico, e il loro numero totale sara' dato dal valore SOUND_MIXER_NRDEVICES (da 0 a SOUND_MIXER_NRDEVICES-1). Puoi ottenere i nomi simbolici di questi canali tramite:

const char *labels[] = SOUND_DEVICE_LABELS;
const char *names[]  = SOUND_DEVICE_NAMES;
Queste sono quelle che ho trovato sotto il mio Kernel nel file soundcard.h (mi riferisco alla Mandrake 8.1 con kernel 2.4.8 patchato):

#define SOUND_DEVICE_LABELS {"Vol  ", "Bass ", "Trebl", "Synth", "Pcm  ", "Spkr ", "Line ", \
                             "Mic  ", "CD   ", "Mix  ", "Pcm2 ", "Rec  ", "IGain", "OGain", \
                             "Line1", "Line2", "Line3", "Digital1", "Digital2", "Digital3", \
                             "PhoneIn", "PhoneOut", "Video", "Radio", "Monitor"}

#define SOUND_DEVICE_NAMES  {"vol", "bass", "treble", "synth", "pcm", "speaker", "line", \
                             "mic", "cd", "mix", "pcm2", "rec", "igain", "ogain", \
                             "line1", "line2", "line3", "dig1", "dig2", "dig3", \
                             "phin", "phout", "video", "radio", "monitor"}
Con "labels" ci riferiamo direttamente al nome dei canali, mentre con "names", ad eventuali comandi in linea.

Tramite ioctl(...) e' possibile inoltre ottenere informazioni sul mixer in generale. La funzione ritornera' un valore pari ad una bitmask dove ogni bit si riferisce ad un canale. La macro usata in questo caso e' SOUND_MIXER_READ_DEVMASK, mentre con la macro SOUND_MIXER_READ_RECMASK possiamo settare sempre tramite bitmask ogni canale come sorgente di registrazione. Per controllare ad esempio che l'input del CD sia un valido canale di input possiamo usare il codice:


ioctl(fd, SOUND_MIXER_READ_DEVMASK, &devmask);
if (devmask & SOUND_MIXER_CD)
  printf("The CD input is supported");
o per vedere se e' un valido sorgente per la registrazione:

ioctl(fd, SOUND_MIXER_READ_RECMASK, &recmask);
if (recmask & SOUND_MIXER_CD)
  printf("The CD input can be a recording source");
SOUND_MIXER_READ_RECSRC indica quali canali sono settati come sorgenti (se la scheda lo permette per piu' di uno), mentre SOUND_MIXER_READ_STEREODEVS ha dei bits settati se ci troviamo in modalita' stereo, viceversa da' 0 (cioe' mono).

SOUND_MIXER_READ_CAPS si riferisce alle capacita' della scheda (cercate nei files del Kernel) mentre SOUND_MIXER_WRITE_RECSRC setta il corrente canale di registrazione come nel codice:


devmask = SOUND_MIXER_CD;
ioctl(fd, SOUND_MIXER_WRITE_DEVMASK, &devmask);
Ecco ora un semplice programma per individuare tutte le informazioni della nostra scheda sullo stato dei canali, input, mono, stereo, ecc.:

/*
 * mixer_info.c
 * Example program to display mixer settings
 */

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <linux/soundcard.h>

/* utility function for printing status */
void yes_no(int condition)
{
  condition ? printf("  yes      ") : printf("  no       ");
}

int main(int argc, char *argv[])
{
  int fd;       /* file descriptor for mixer device */
  int i;        /* loop counter */
  int level;    /* volume setting */
  char *device; /* name of device to report on */
  int status;   /* status of system calls */
  /* various device settings */
  int recsrc, devmask, recmask, stereodevs, caps;
  /* names of available mixer channels */
  const char *sound_device_names[] = SOUND_DEVICE_LABELS;

  /* get device name from command line or use default */  
  if (argc == 2)
    device = argv[1];
  else
    device = "/dev/mixer";

  /* open mixer, read only */
  fd = open(device, O_RDONLY);
  if (fd == -1) {
    fprintf(stderr, "%s: unable to open `%s', ", argv[0], device);
    perror("");
    return 1;
  }

  /* get all of the information about the mixer */
  status = ioctl(fd, SOUND_MIXER_READ_RECSRC, &recsrc);
  if (status == -1)
    perror("SOUND_MIXER_READ_RECSRC ioctl failed");
  status = ioctl(fd, SOUND_MIXER_READ_DEVMASK, &devmask);
  if (status == -1)
    perror("SOUND_MIXER_READ_DEVMASK ioctl failed");
  status = ioctl(fd, SOUND_MIXER_READ_RECMASK, &recmask);
  if (status == -1)
    perror("SOUND_MIXER_READ_RECMASK ioctl failed");
  status = ioctl(fd, SOUND_MIXER_READ_STEREODEVS, &stereodevs);
  if (status == -1)
    perror("SOUND_MIXER_READ_STEREODEVS ioctl failed");
  status = ioctl(fd, SOUND_MIXER_READ_CAPS, &caps);
  if (status == -1)
    perror("SOUND_MIXER_READ_CAPS ioctl failed");

  /* print results in a table */
  printf(
         "Status of %s:\n\n"
         "Mixer      Device     Recording  Active     Stereo     Current\n"
         "Channel    Available  Source     Source     Device     Level\n"
         "---------  ---------  ---------  --------   ---------  ---------\n",
         device
         );

  /* loop over all devices */
  for (i = 0 ; i < SOUND_MIXER_NRDEVICES ; i++) {
    /* print number and name */
    printf("%2d %-7s", i, sound_device_names[i]);
    /* print if available */
    yes_no((1 << i) & devmask);
    /* can it be used as a recording source? */
    yes_no((1 << i) & recmask);
    /* it it an active recording source? */
    yes_no((1 << i) & recsrc);
    /* does it have stereo capability? */
    yes_no((1 << i) & stereodevs);
    /* if available, display current level */
    if ((1 << i) & devmask) { 
      /* if stereo, show both levels */
      if ((1 << i) & stereodevs) {
        status = ioctl(fd, MIXER_READ(i), &level);
        if (status == -1)
          perror("SOUND_MIXER_READ ioctl failed");
        printf("  %d%% %d%%", level & 0xff, (level & 0xff00) >> 8);
      } else { /* only one channel */
        status = ioctl(fd, MIXER_READ(i), &level);
        if (status == -1)
          perror("SOUND_MIXER_READ ioctl failed");
        printf("  %d%%", level & 0xff);
      }
  } 
    printf("\n");
  }
  printf("\n");
  /* are recording sources exclusive? */
  printf("Note: Choices for recording source are ");
  if (!(caps & SOUND_CAP_EXCL_INPUT))
    printf("not ");
  printf("exclusive.\n");
  /* close mixer device */
  close(fd);
  return 0;
}
Qui sotto ecco l'output del codice sopra sul mio sistema:
Mixer      Device     Recording  Active     Stereo     Current
Channel    Available  Source     Source     Device     Level
---------  ---------  ---------  --------   ---------  ---------
 0 Vol      yes        no         no         yes        75% 82%
 1 Bass     no         no         no         no
 2 Trebl    no         no         no         no
 3 Synth    no         no         no         no
 4 Pcm      yes        no         no         yes        100% 100%
 5 Spkr     yes        no         no         no         65%
 6 Line     yes        yes        yes        yes        64% 64%
 7 Mic      yes        yes        no         no         68%
 8 CD       yes        yes        no         yes        80% 80%
 9 Mix      no         no         no         no
10 Pcm2     no         no         no         yes
11 Rec      no         no         no         no
12 IGain    yes        yes        no         yes        83% 83%
13 OGain    no         no         no         no
14 Line1    yes        yes        no         yes        39% 39%
15 Line2    no         no         no         no
16 Line3    no         no         no         no
17 Digital1 no         no         no         no
18 Digital2 no         no         no         no
19 Digital3 no         no         no         no
20 PhoneIn  yes        yes        no         no         62%
21 PhoneOut yes        no         no         no         64%
22 Video    yes        yes        no         yes        64% 64%
23 Radio    no         no         no         no
24 Monitor  no         no         no         no

Note: Choices for recording source are exclusive.
Vediamo ora un programma semplice che ci aiutera' a settare il mixer:

/*
 * mixer.c
 * Example of a simple mixer program
 */

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <linux/soundcard.h>

/* names of available mixer devices */
const char *sound_device_names[] = SOUND_DEVICE_NAMES;

int fd;                  /* file descriptor for mixer device */
int devmask, stereodevs; /* bit masks of mixer information */
char *name;              /* program name */

/* display command usage and exit with error status */
void usage()
{
  int i;

  fprintf(stderr, "usage: %s <device> <left-gain%%> <right-gain%%>\n"
          "       %s <device> <gain%%>\n\n"
          "Where <device> is one of:\n", name, name);
  for (i = 0 ; i < SOUND_MIXER_NRDEVICES ; i++)
    if ((1 << i) & devmask) /* only display valid devices */
      fprintf(stderr, "%s ", sound_device_names[i]);
  fprintf(stderr, "\n");
  exit(1);
}

int main(int argc, char *argv[])
{
  int left, right, level;  /* gain settings */
  int status;              /* return value from system calls */
  int device;              /* which mixer device to set */
  int i;                   /* general purpose loop counter */
  char *dev;               /* mixer device name */

  /* save program name */
  name = argv[0];

  /* open mixer, read only */
  fd = open("/dev/mixer", O_RDONLY);
  if (fd == -1) {
    perror("unable to open /dev/mixer");
    exit(1);
  }
  
  /* get needed information about the mixer */
  status = ioctl(fd, SOUND_MIXER_READ_DEVMASK, &devmask);
  if (status == -1)
    perror("SOUND_MIXER_READ_DEVMASK ioctl failed");
  status = ioctl(fd, SOUND_MIXER_READ_STEREODEVS, &stereodevs);
  if (status == -1)
    perror("SOUND_MIXER_READ_STEREODEVS ioctl failed");

  /* check that user passed two or three arguments on command line */
  if (argc != 3 && argc != 4)
    usage();

  /* save mixer device name */
  dev = argv[1];

  /* figure out which device to use */
  for (i = 0 ; i < SOUND_MIXER_NRDEVICES ; i++)
    if (((1 << i) & devmask) && !strcmp(dev, sound_device_names[i]))
      break;
  if (i == SOUND_MIXER_NRDEVICES) { /* didn't find a match */
    fprintf(stderr, "%s is not a valid mixer device\n", dev);
    usage();
  }

  /* we have a valid mixer device */
  device = i;

  /* get gain values */
  if (argc == 4) {
    /* both left and right values given */
    left  = atoi(argv[2]);
    right = atoi(argv[3]);
  } else {
    /* left and right are the same */
    left  = atoi(argv[2]);
    right = atoi(argv[2]);
  }
  
  /* display warning if left and right gains given for non-stereo device */
  if ((left != right) && !((1 << i) & stereodevs)) {
    fprintf(stderr, "warning: %s is not a stereo device\n", dev);
  }
  
  /* encode both channels into one value */
  level = (right << 8) + left;
  
  /* set gain */
  status = ioctl(fd, MIXER_WRITE(device), &level);
  if (status == -1) {
    perror("MIXER_WRITE ioctl failed");
    exit(1);
  }

  /* unpack left and right levels returned by sound driver */
  left  = level & 0xff;
  right = (level & 0xff00) >> 8;

  /* display actual gain setting */
  fprintf(stderr, "%s gain set to %d%% / %d%%\n", dev, left, right);

  /* close mixer device and exit */
  close(fd);
  return 0;
}
Possiamo utilizzare il codice sopra ad esempio per settare i volumi del canale sinistro e destro con:

./mixer vol 60 70
Ora che abbiamo terminato la spiegazione del funzionamento del mixer, passiamo (molto brevemente) al /dev/sequencer. Quest'ultimo permette di programmare il sintetizzatore FM o wavetable o dispositivi esterni sul MIDI bus. In genere viene utilizzato nelle applicazioni per la creazione di musica.
La parola "sequencer" si riferisce a una applicazione che controlla devices MIDI, mandandogli in sequenza messaggi per la creazione di musica. L'interfaccia legata a questi dispositivi e' abbastanza complicata da utilizzare. Prima di utilizzare questo sintetizzatore on-board devi avere delle patches relative a dei singoli strumenti (chitarra, piano, ecc.).
Ecco un programma legato a quanto appena detto. Restituisce informazioni sui dispositivi sequencer della scheda:

/*
 * seq_info.c
 * Example program for /dev/sequencer
 */
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>

#include <sys/soundcard.h>

/* return string given a MIDI device type */
char *midi_device_type(int type)
{
  switch (type) {
  case SNDCARD_ADLIB:      return("Adlib");
  case SNDCARD_SB:         return("SoundBlaster");
  case SNDCARD_PAS:        return("ProAudioSpectrum");
  case SNDCARD_GUS:        return("GravisUltraSound");
  case SNDCARD_MPU401:     return("MPU401");
  case SNDCARD_SB16:       return("SoundBlaster16");
  case SNDCARD_SB16MIDI:   return("SoundBlaster16 MIDI");
  case SNDCARD_UART6850:   return("6850 UART");
  case SNDCARD_GUS16:      return("Gravis UltraSound 16");
  case SNDCARD_MSS:        return("Microsoft Sound System");
  case SNDCARD_PSS:        return("Personal Sound System");
  case SNDCARD_SSCAPE:     return("Ensoniq SoundScape");
/* these require a more recent version of the sound driver */
#if SOUND_VERSION >= 301
  case SNDCARD_PSS_MPU:    return("Personal Sound System + MPU");
  case SNDCARD_PSS_MSS:    return("Personal Sound System/Microsoft Sound 
                                   System");
  case SNDCARD_TRXPRO_MPU: return("MediaTrix Pro + MPU");
  case SNDCARD_MAD16:      return("MAD16");
  case SNDCARD_MAD16_MPU:  return("MAD16 + MPU");
  case SNDCARD_CS4232:     return("CS4232");
  case SNDCARD_CS4232_MPU: return("CS4232 + MPU");
  case SNDCARD_MAUI:       return("Maui");
  case SNDCARD_PSEUDO_MSS: return("Pseudo-MSS");
#endif /* SOUND_VERSION >= 301 */
#if SOUND_VERSION >= 350
  case SNDCARD_GUSPNP:     return ("Gravis UltraSound PlugNPlay");
#endif /* SOUND_VERSION >= 301 */
  default:                 return("unknown");
  }
}
/* return string given a synthesizer device type */
char *synth_device_type(int type)
{
  switch (type) {
  case SYNTH_TYPE_FM:     return("FM");
  case SYNTH_TYPE_SAMPLE: return("Sampling");
  case SYNTH_TYPE_MIDI:   return("MIDI");
  default:                return("unknown");
  }
}

/* return string given a synthesizer device subtype */
char *synth_device_subtype(int type)
{
  switch (type) {
  case FM_TYPE_ADLIB:   return("Adlib");
  case FM_TYPE_OPL3:    return("OPL-3");
  case SAMPLE_TYPE_GUS: return("GUS");
  default:              return("unknown");
  }
}

int main(int argc, char *argv[])
{
  int i, status, numdevs;
  struct synth_info sinfo;
  struct midi_info minfo;
  int fd;        /* file descriptor for /dev/sequencer */
  char *device; /* name of device to report on */

  /* get device name from command line or use default */  
  if (argc == 2)
    device = argv[1];
  else
    device = "/dev/sequencer";

  /* try to open device */
  fd = open(device, O_WRONLY);
  if (fd == -1) {
    fprintf(stderr, "%s: unable to open `%s', ", argv[0], device);
    perror("");
    return 1;
  }

  status = ioctl(fd, SNDCTL_SEQ_NRSYNTHS, &numdevs);
  if (status == -1) {
    perror("ioctl failed");
    exit(1);
  }
  printf("%s:\n%d synthesizer device%s installed\n", device, numdevs,
         numdevs == 1 ? "" : "s");

  for (i = 0 ; i < numdevs ; i++) {
    sinfo.device = i;
    status = ioctl(fd, SNDCTL_SYNTH_INFO, &sinfo);
    if (status == -1) {
      perror("ioctl failed");
      exit(1);
    }
    printf("Device %d: `%s' type: `%s' subtype: `%s' voices: %d\n",
           i,
           sinfo.name,
           synth_device_type(sinfo.synth_type),
           synth_device_subtype(sinfo.synth_subtype),
           sinfo.nr_voices);
  }
  status = ioctl(fd, SNDCTL_SEQ_NRMIDIS, &numdevs);
  if (status == -1) {
    perror("ioctl failed");
    exit(1);
  }
  printf("%d MIDI device%s installed\n", numdevs,
         numdevs == 1 ? "" : "s");
  for (i = 0 ; i < numdevs ; i++) {
    minfo.device = i;
    status = ioctl(fd, SNDCTL_MIDI_INFO, &minfo);
    if (status == -1) {
      perror("ioctl failed");
      exit(1);
    }
    printf("Device %d: `%s' type: `%s'\n",
           i,
           minfo.name,
           midi_device_type(minfo.dev_type));
  }
  /* close file and exit */
  close(fd);
  return 0;
}
Continuiamo, analizzando alcune particolarita' sonore piu' raramente utilizzate.
/dev/dsp usa unsigned data a 8 e 16 bit, /dev/audio mu-law data. E' possibile cambiare i formati a un device usando SOUND_PCM_SETFMT ioctl call. Un numero di questi formati sono trovati sotto "soundcard.h" con prefisso AFMT_ come nel mio sorgente Kernel:

#define AFMT_QUERY               0x00000000      /* Return current fmt */
#define AFMT_MU_LAW              0x00000001
#define AFMT_A_LAW               0x00000002
#define AFMT_IMA_ADPCM           0x00000004
#define AFMT_U8                  0x00000008
#define AFMT_S16_LE              0x00000010      /* Little endian signed 16*/
#define AFMT_S16_BE              0x00000020      /* Big endian signed 16 */
#define AFMT_S8                  0x00000040
#define AFMT_U16_LE              0x00000080      /* Little endian U16 */
#define AFMT_U16_BE              0x00000100      /* Big endian U16 */
#define AFMT_MPEG                0x00000200      /* MPEG (2) audio */
#define AFMT_AC3                 0x00000400      /* Dolby Digital AC3 */
Per settare ad esempio il formato di codifica a mu-law usiamo il codice:

fmt = AFMT_MU_LAW;
ioctl(fd, SOUND_PCM_SETFMT, &fmt);
ioctl(...) ritornera' il formato di codifica selezionato dal Kernel, che risultera' essere lo stesso selezionato da noi, a meno che il dispositivo non lo supporti. AFMT_QUERY ritorna invece il formato usato in quel momento dal device. Con SOUND_PCM_GETFMTS possiamo invece conoscere tutti i formati supportati dal dispositivo (ritorndando una bitmask, con i bits settati relativamente ad un dato formato).
SNDCTL_DSP_GETBLKSIZE ritorna la misura del blocco che il sound driver usa per trasferire i dati. Il valore riportato e' un intero, indicante il numero di bytes. Cio' puo' esser utile in un programma, per selezionare la misura di un buffer, assicurando che tutto il blocco e' stato passato.
SNDCTL_DSP_GETCAPS ritorna una bitmask con tutte le "capacita'" di un dispositivo DSP. Sono scritti sotto "soundcard.h" con prefisso DSP_CAP. Ad esempio DSP_CAP_DUPLEX e' un flag che segnala il "full duplex mode" del dispositivo (playback e registrazione contemporanea).
I DSP_CAP trovati nel mio "soundcard.h" sono:

#define DSP_CAP_REVISION         0x000000ff      /* Bits for revision level (0 to 255) */
#define DSP_CAP_DUPLEX           0x00000100      /* Full duplex record/playback */
#define DSP_CAP_REALTIME         0x00000200      /* Real time capability */
#define DSP_CAP_BATCH            0x00000400      /* Device has some kind of */
                                                 /* internal buffers which may */
                                                 /* cause some delays and */
                                                 /* decrease precision of timing */
#define DSP_CAP_COPROC           0x00000800      /* Has a coprocessor */
                                                 /* Sometimes it's a DSP */
                                                 /* but usually not */
#define DSP_CAP_TRIGGER          0x00001000      /* Supports SETTRIGGER */
#define DSP_CAP_MMAP             0x00002000      /* Supports mmap() */
#define DSP_CAP_MULTI            0x00004000      /* support multiple open */
#define DSP_CAP_BIND             0x00008000      /* channel binding to front/rear/cneter/lfe */
Per concludere, vediamo un programma che restituisce in output le "capacita'" del nostro DSP:

/*
 * dsp_info.c
 * Example program to display sound device capabilities
 */

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <linux/soundcard.h>

/* utility function for displaying boolean status */
static char *yes_no(int condition)
{
  if (condition) return "yes"; else return "no";
}

/*
 * Set sound device parameters to given values. Return -1 if
 * values not valid. Sampling rate is returned.
 */
static int set_dsp_params(int fd, int channels, int bits, int *rate) {
  int status, val = channels;

  status = ioctl(fd, SOUND_PCM_WRITE_CHANNELS, &val);
  if (status == -1)
    perror("SOUND_PCM_WRITE_CHANNELS ioctl failed");
  if (val != channels) /* not valid, so return */
    return -1;
  val = bits;
  status = ioctl(fd, SOUND_PCM_WRITE_BITS, &val);
  if (status == -1)
    perror("SOUND_PCM_WRITE_BITS ioctl failed");
  if (val != bits)
    return -1;
  status = ioctl(fd, SOUND_PCM_WRITE_RATE, rate);
  if (status == -1)
    perror("SOUND_PCM_WRITE_RATE ioctl failed");
  return 0;
}

int main(int argc, char *argv[])
{
  int rate;
  int channels;            /* number of channels */
  int bits;                /* sample size */
  int blocksize;           /* block size */
  int formats;             /* data formats */
  int caps;                /* capabilities */
  int deffmt;              /* default format */
  int min_rate, max_rate;  /* min and max sampling rates */
  char *device;            /* name of device to report on */
  int fd;                  /* file descriptor for device */
  int status;              /* return value from ioctl */

  /* get device name from command line or use default */  
  if (argc == 2)
    device = argv[1];
  else
    device = "/dev/dsp";

  /* try to open device */
  fd = open(device, O_RDWR);
  if (fd == -1) {
    fprintf(stderr, "%s: unable to open `%s', ", argv[0], device);
    perror("");
    return 1;
  }
  
  status = ioctl(fd, SOUND_PCM_READ_RATE, &rate);
  if (status ==  -1)
    perror("SOUND_PCM_READ_RATE ioctl failed");
  status = ioctl(fd, SOUND_PCM_READ_CHANNELS, &channels);
  if (status ==  -1)
    perror("SOUND_PCM_READ_CHANNELS ioctl failed");
  status = ioctl(fd, SOUND_PCM_READ_BITS, &bits);
  if (status ==  -1)
    perror("SOUND_PCM_READ_BITS ioctl failed");
  status = ioctl(fd, SNDCTL_DSP_GETBLKSIZE, &blocksize);
  if (status ==  -1)
    perror("SNFCTL_DSP_GETBLKSIZE ioctl failed");
  
  printf(
         "Information on %s:\n\n"
         "Defaults:\n"
         "  sampling rate: %d Hz\n"
         "  channels: %d\n"
         "  sample size: %d bits\n"
         "  block size: %d bytes\n",
         device, rate, channels, bits, blocksize
         );

/* this requires a more recent version of the sound driver */
#if SOUND_VERSION >= 301
  printf("\nSupported Formats:\n");
  deffmt = AFMT_QUERY;
  status = ioctl(fd, SOUND_PCM_SETFMT, &deffmt);
  if (status ==  -1)
    perror("SOUND_PCM_SETFMT ioctl failed");
  status = ioctl(fd, SOUND_PCM_GETFMTS, &formats);
  if (status ==  -1)
    perror("SOUND_PCM_GETFMTS ioctl failed");
  if (formats & AFMT_MU_LAW) {
    printf("  mu-law");
    (deffmt == AFMT_MU_LAW) ? printf(" (default)\n") : printf("\n");
  }
  if (formats & AFMT_A_LAW) {
    printf("  A-law");
    (deffmt == AFMT_A_LAW) ? printf(" (default)\n") : printf("\n");
  }
  if (formats & AFMT_IMA_ADPCM) {
    printf("  IMA ADPCM");
    (deffmt == AFMT_IMA_ADPCM) ? printf(" (default)\n") : printf("\n");
  }
  if (formats & AFMT_U8) {
    printf("  unsigned 8-bit");
    (deffmt == AFMT_U8) ? printf(" (default)\n") : printf("\n");
  }
  if (formats & AFMT_S16_LE) {
    printf("  signed 16-bit little-endian");
    (deffmt == AFMT_S16_LE) ? printf(" (default)\n") : printf("\n");
  }
  if (formats & AFMT_S16_BE) {
    printf("  signed 16-bit big-endian");
    (deffmt == AFMT_S16_BE) ? printf(" (default)\n") : printf("\n");
  }
  if (formats & AFMT_S8) {
    printf("  signed 8-bit");
    (deffmt == AFMT_S8) ? printf(" (default)\n") : printf("\n");
  }
  if (formats & AFMT_U16_LE) {
    printf("  unsigned 16-bit little-endian");
    (deffmt == AFMT_U16_LE) ? printf(" (default)\n") : printf("\n");
  }
  if (formats & AFMT_U16_BE) {
    printf("  unsigned 16-bit big-endian");
    (deffmt == AFMT_U16_BE) ? printf(" (default)\n") : printf("\n");
  }
  if (formats & AFMT_MPEG) {
    printf("  MPEG 2");
    (deffmt == AFMT_MPEG) ? printf(" (default)\n") : printf("\n");
  }
  
  printf("\nCapabilities:\n");
  status = ioctl(fd, SNDCTL_DSP_GETCAPS, &caps);
  if (status ==  -1)
    perror("SNDCTL_DSP_GETCAPS ioctl failed");
  printf(
         "  revision: %d\n"
         "  full duplex: %s\n"
         "  real-time: %s\n"
         "  batch: %s\n"
         "  coprocessor: %s\n" 
         "  trigger: %s\n"
         "  mmap: %s\n",
         caps & DSP_CAP_REVISION,
         yes_no(caps & DSP_CAP_DUPLEX),
         yes_no(caps & DSP_CAP_REALTIME),
         yes_no(caps & DSP_CAP_BATCH),
         yes_no(caps & DSP_CAP_COPROC),
         yes_no(caps & DSP_CAP_TRIGGER),
         yes_no(caps & DSP_CAP_MMAP));

#endif /* SOUND_VERSION >= 301 */
  
  /* display table heading */
  printf(
         "\nModes and Limits:\n"
         "Device    Sample    Minimum   Maximum\n"
         "Channels  Size      Rate      Rate\n"
         "--------  --------  --------  --------\n"
         );
  
  /* do mono and stereo */  
  for (channels = 1; channels <= 2 ; channels++) {
    /* do 8 and 16 bits */
    for (bits = 8; bits <= 16 ; bits += 8) {
      /* To find the minimum and maximum sampling rates we rely on
         the fact that the kernel sound driver will round them to
         the closest legal value. */
      min_rate = 1;
      if (set_dsp_params(fd, channels, bits, &min_rate) == -1)
        continue;
      max_rate = 100000;
      if (set_dsp_params(fd, channels, bits, &max_rate) == -1)
        continue;
      /* display the results */
      printf("%8d  %8d  %8d  %8d\n", channels, bits, min_rate, max_rate);
    }
  }
  close(fd);
  return 0;
}
L'output che ho ottenuto e' stato:
Information on /dev/dsp:

Defaults:
  sampling rate: 8000 Hz
  channels: 1
  sample size: 8 bits
  block size: 64 bytes

Supported Formats:
  unsigned 8-bit (default)
  signed 16-bit little-endian

Capabilities:
  revision: 0
  full duplex: yes
  real-time: yes
  batch: no
  coprocessor: no
  trigger: yes
  mmap: yes

Modes and Limits:
Device    Sample    Minimum   Maximum
Channels  Size      Rate      Rate
--------  --------  --------  --------
       1         8      4000     48000
       1        16      4000     48000
       2         8      4000     48000
       2        16      4000     48000
Chiudiamo questo tutorial accennando al PC Speaker Sound Driver.
Si puo' accedere all'uso di questi, quando ad esempio non si ha una sound card installata (a dire il vero i devices che richiamero' brevemente sotto, non sono proprio presenti nel mio sistema, un po' forse per la "datatura", un po' per l'"inutilita'").
/dev/pcsp emula /dev/dsp per l'output. Leggere da questo device riportera' l'errore EINVAL. Accetta tutti gli standard ioctl(...) di dsp (alcuni sono ignorati in realta') e include le funzioni tipiche del mixer.
/dev/pcsp16 e' un dispositivo che simula una sound card a 16 bits (prime versioni di DOOM).
/dev/pcaudio e' l'analogo di /dev/audio. Ha le stesse limitazioni di /dev/pcsp.
/dev/pcmixer accetta le stesse chiamate ioctl(...) di /dev/mixer. Non ci sono sorgenti di registrazione e permette un controllo generale del volume.
L'uso diretto degli speakers puo' essere comunque un ottima prova di "hack", di cui troverete esempio in un articolo apparso su LINUX GAZETTE di Cherry George Mathew dal titolo "Creating a Kernel Driver for the PC Speaker".

Conclusioni

Saluto tutti quelli della mailing list RACL e chi ama Linux;)

Attenzione

Non vedo perche' qualcuno debba dirmi qualcosa:) ThinkGeek <--> ThinkLinux