Bekanntlich sind die Rechner der STE- (und TT-) Reihe mit einer 8 Bit Stereoklangerzeugung ausgestattet. Die vorliegende Artikelserie ‘STE-Soundbox’ soll es dem Interessierten ermöglichen, sich Schritt für Schritt mit den Features dieser Klangerzeugung vertraut zu machen. Um die trockene Theorie etwas aufzulockern, endet jede Folge mit einem Listing, in dem das behandelte Thema praktisch aufgearbeitet wird. Die erste Folge beschreibt die grundlegende Arbeitsweise des DMA-Soundchips.
Stereo-DMA-Sound. 8-Bit-D/A-Wandler mit bis zu 50 kHz. So oder ähnlich lauten die Schlagwörter, mit denen die STE-Klangerzeugung umschrieben wird. Darin steckt schon eine Menge Information - aber alles der Reihe nach.
DMA ist wieder solch eine neuhochdeutsche Abkürzung und bedeutet ‘Direct-Memory-Access’. Dieser Ausdruck steht für die Fähigkeit des Soundchips, den STE-Speicher direkt zu adressieren. Der Soundchip kann also seine Klangdaten eigenständig aus dem Speicher abrufen, ohne daß die CPU eingreifen muß. Folglich muß sich die CPU nur zu Beginn um die richtige Initialisierung des Soundchips kümmern und kann sich dann - während der Soundchip die Klangdaten abarbeitet - getrost anderen Aufgaben zuwenden.
Das ‘D/A’ steht für Digital/Analog. Analog bedeutet, daß ein Signal zwischen den vorgegebenen Grenzen jeden beliebigen Wert annehmen kann. Digitale Signale sind jedoch diskrete Signale, d.h. sie können nur eine bestimmte Anzahl von diskreten Werten annehmen. So lassen sich z.B. mit 8 Bit 256 verschiedene Werte darstellen. Ein D/A-Wandler stellt nun das Bindeglied zwischen digitaler Welt (Rechner) und analoger Welt (Verstärker und Lautsprecher) dar. Abb. 1 zeigt, wie digitale Daten mit einen D/A-Wandler in analoge Signale gewandelt werden.
Nun ist wahrscheinlich jedem bekannt, daß ein Ton in einem Verstärker nichts anderes ist als eine zeitlich sich verändernde Spannung. In Abb. 2 ist dieser Sachverhalt nochmals am Beispiel einer Dreieckspannung aufgezeigt. Zwischen den Grenzen +1V und -IV nimmt die Spannung jeden möglichen Wert an. Nach 0.25 ms ist sie auf 1V angestiegen, nach 0.75 ms hat sie den Wert -1V erreicht, und nach 1 ms schließlich beträgt die Spannung wieder 0V. Ein solcher Durchlauf des Signalwertes (von 0V ins Positive, danach ins Negative und wieder zurück nach 0V) nennt man eine Periode. Die Periodendauer beträgt in unserem Beispiel genau 1 ms. Aus der Dauer einer Periode läßt sich die Frequenz wie folgt berechnen: Frequenz = 1 / Periodendauer.
Setzt man die Periodendauer aus unserem Beispiel in die Gleichung ein, erhält man als Frequenz 1000 Hz oder 1 kHz. Wie wird nun eine solche analoge Schwingung im Rechner dargestellt?
Die Digitalisierung des Signals wird in zwei Schritten vollzogen:
Die Anzahl der Werte, die dabei pro Sekunde ermittelt werden, heißt Sampling-Rate oder Sampling-Frequenz. Wird beispielsweise alle 62.5 µs ein Wert ermittelt, ergibt dies eine Sampling-Rate von 1/62.5 µs = 16 kHz. Töne werden auf dem STE dadurch erzeugt, daß man entsprechend der gewünschten Schwingung eine Zahlenkolonne erzeugt und an den Soundchip weiterleitet (siehe Abb. 3).
Der DMA-Soundchip bietet hierzu folgende Möglichkeiten:
Nun fragt man sich natürlich, ob denn 256 verschiedene Ausgangsspannungen reichen, um die theoretisch unendlich vielen Spannungswerte einer analogen Schwingung nachzubilden. Die Dreieckspannung aus Abb. 3, die aus einem 3-Bit-D/A-Wandler stammt, sieht ja doch recht eckig aus und erinnert nur noch sehr entfernt an ihr Pendant aus der analogen Welt. Bei 8 Bit sieht das ganze natürlich deutlich besser aus, aber Ecken gibt es trotzdem, und die bedeuten nun mal Verzerrungen und Rauschen.
Leider werden von Atari keine Betriebssystemroutinen für die Programmierung des DMA-Soundchips zur Verfügung gestellt. Deshalb habe ich zu jedem Feature kleine Assembler-Routinen entwickelt, die man in Turbo-C einbinden kann. Diese werden bei der Behandlung des je welligen Themas vorgestellt.
Die Werte für den D/A-Wandler werden im STE-Speicher als vorzeichenbehaftete 8-Bit-Zahl (entspricht ‘char’ in Turbo C) abgelegt. Eine Gruppierung solcher 8-Bit-Zahlen, welche später den Ton ergeben sollen, nennt Atari einen ‘Frame’. Man kann einen Frame einmal abspielen oder ihn theoretisch unendlich oft wiederholen. In Abb. 4 sind die Soundchip-Register aufgeführt. Die ‘Frame Base Address’ ist dabei die Adresse des ersten Bytes des Frames, die ‘Frame End Address’ die Adresse des ersten Bytes nach dem Frame. Ein Frame muß immer eine gerade Anzahl an Bytes beinhalten.
So, jetzt das versprochene Listing: In ‘SOUT.S’ sind die Assembler-Funktionen ‘sndjplay’ und ‘snd_stop’ implementiert. ‘snd_stop’ dient dazu, die Tonausgabe an einer beliebigen Stelle anzuhalten. Dies wird erreicht, indem das ‘Sound DMA Control’-Register mit 0 gefüllt wird. Die Funktion ‘snd_play’, welche für das Abspielen eines Frames zuständig ist, erwartet als Parameter einen Zeiger auf die ‘SOUND’-Struktur. Diese Struktur ist in ‘SOUT.H’ definiert und beinhaltet unter anderem die für die Tonausgabe relevanten Daten. Siehe hierzu auch die Kommentare im Listing.
Bei der Programmierung des Soundchips sollte man folgende Schritte berücksichtigen:
Zu beachten ist noch, daß alle Zugriffe auf die Soundchip-Register im Supervisormodus der CPU erfolgen müssen.
Die Assembler-Funktionen sind jeweils so konzipiert, daß sie von Turbo-C aus aufgerufen werden können. Ein kleines Beispiel für die Ausgabe eines Sinustones ist in ‘KLANG.C’ enthalten. Der Ton wird in Mono mit einer Sampling-Rate von 50 kHz wiedergegeben. Seine Frequenz beträgt 440 Hz. Wer sich die Funktion ‘sinus’ etwas genauer ansieht, wird feststellen, daß in einem Frame genau eine Periode des Sinustones enthalten ist. Es macht natürlich keinen Sinn, solch einen Frame nur einmal abzuspielen, deshalb wird das ‘Sound DMA Control’-Register auf Wiederholung gesetzt.
So, das war’s fürs erste. Viel Spaß beim Experimentieren mit eigenen Sounds.
Literatur:
STE Developer Addendum
/* SOUT.H Headerdatei für das Assemblermodul SOUT.S */
/* Konstanten f. das Sound-DMA-Control Register */
#define SND_STOP 0
#define SND_EINMAL 1
#define SND_IMMER 3
/* Konstanten für das Sound-Mode Register */
#define MOB_FR6K 0x0000 /* Samplingfrequenz: 6258 Hz */
#define MOD_FR12K 0x0001 /* 12517 Hz */
#define MOD_FR25K 0x0002 /* 25033 Hz */
#define MOD_FR50K 0x0003 /* 50066 Hz */
#define MOD_STEREO 0x0000 /* Stereo-Wiedergabe */
#define MOD_MONO 0x0080 /* Mono-Wiedergabe */
/* Typdeklaration */
typedef struct
{
unsigned long anz_bytes; /* Länge des Frames */
unsigned long bytes_pro_sekunde; /* Samplingfrequenz */
int control_reg; /* Wert für Sound-DMA-Control-Register */
int mode_reg; /* Wert für Sound-Mode-Control Register */
int frequenz; /* Was wohl? */
char *s_ptr; /* Zeiger auf den Frame */
} SOUND;
/* Prototypen der Assemblerfunktionen aus sout.s */
void snd_stop( void );
void snd_play( SOUND * );
* ------------------------------------------------------- *
* SOUT.S : Routinen zur Programmierung der DMA-Soundchips *
* *
* Zum Einbinden in Turbo-C 2.0, Peter Engler *
* (c) MAXON Computer 1991 *
* ------------------------------------------------------- *
* Deklaration der Routinen
GLOBL snd_stop
GLOBL snd_play
* Adressen der DMA-Soundchipregister
S_CNTRL EQU $FF8900
F_BASE EQU $FF8903
F_COUNT EQU $FF8908
F_END EQU $FF890F
S_MODE EQU $FF8920
* Daten
DATA
EVEN
* Heap
BSS
EVEN
* Speicher für Parameter reservieren
SND_ADR:
ds.l 1
* Code
TEXT
EVEN
* Aufruf von s_stop im Supervisormodus
snd_stop:
pea s_stop * Adresse von s_stop auf Stack
move.w #$26,-(sp) * Xbios Nr. $26 (->SUPEXEC)
trap #14 * Xbios Aufruf
addq.l #6,sp * Stackpointer korrigieren
rts
* Aufruf von s_play im Supervisormodus
snd_play:
move.l a0,SND_ADR * Adresse der Struktur retten
pea s_play * Adresse von s_stop auf Stack
move.w #$26,-(sp) * Xbios Nr. $26 (->SUPEXEC)
trap #14 * Xbios Aufruf
addq.l #6,sp * Stackpointer korrigieren
rts
* Stoppen der Tonausgabe
s_stop:
move.w #0,S_CNTRL * Sound-Control-Register löschen
rts
* Ton ausgeben: Der Ton wird durch die SOUND-
* Struktur beschrieben
s_play:
movem.l d3/d4,-(sp) * d3 und d4 retten
movea.l SND_ADR,a0 * Adresse der Sound-Struktur in a0
move.l (a0)+,d0 * Anzahl Bytes in d0
lea.l 4(a0),a0 * Auf 'control_reg' positionieren
move.w (a0)+,d3 * 'control_reg' in d3
move.w (a0),d4 * 'mode_reg' in d4
lea.l 4(a0),a0 * Auf 's_ptr' positionieren
move.l (a0),d1 * Adresse der Bytes in d1
move.l d1,d2 * Adresse merken
move.b d1,F_BASE+4 * Low-Byte eintragen
asr.l #8,d1 * Mid-Byte holen
move.b d1,F_BASE+2 * Mid-Byte eintragen
asr.l #8,d1 * High-Byte holen
move.b d1,F_BASE * High-Byte eintragen
add.l d0,d2 * Frame-End berechnen
move.b d2,F_END+4 * Low-Byte eintragen
asr.l #8,d2 * Mid-Byte holen
move.b d2,F_END+2 * Mid-Byte eintragen
asr.l #8,d2 * High-Byte holen
move.b d2,F_END * High-Byte eintragen
move.w d4,S_M0DE * Mode-Register setzen
move.w d3,S_CNTRL * Sound ausgeben (Control-Register)
movem.l (sp)+,d3/d4 * d3 und d4 restaurieren
rts
END
snd_play:
move.l a0,SND_ADR * Adresse der Struktur retten
pea s_play * Adresse von s_stop auf Stack
move.w #$26,-(sp) * Xbios Nr. $26 (->SUPEXEC)
trap #14 * Xbios Aufruf
addq.l #6,sp * Stackpointer korrigieren
rts
* Stoppen der Tonausgabe
s_stop:
move.w #0,S_CNTRL * Sound-Control-Register löschen
rts
* Ton ausgeben Der Ton wird durch die SOUND-
* Struktur beschrieben
s_play:
movem.l d3/d4,-(sp) * d3 und d4 retten
movea.l SND_ADR,a0 * Adresse der Sound-Struktur in a0
move.l (a0)+,d0 * Anzahl Bytes in d0
lea.l 4(a0),a0 * Auf 'control_reg' positionieren
move.w (a0)+,d3 * 'control_reg' in d3
move.w (a0),d4 * 'mode_reg' in d4
lea.l 4(a0),a0 * Auf 's_ptr' positionieren
move.l (a0),d1 * Adresse der Bytes in d1
move.l d1,d2 * Adresse merken
move.b d1,F_BASE+4 * Low-Byte eintragen
asr.l #8,d1 * Mid-Byte holen
move.b d1,F_BASE+2 * Mid-Byte eintragen
asr.l #8,d1 * High-Byte holen
move.b d1,F_BASE * High-Byte eintragen
add.l d0,d2 * Frame-End berechnen
move.b d2,F_END+4 * Low-Byte eintragen
asr.l #8,d2 * Mid-Byte holen
move.b d2,F_END+2 * Mid-Byte eintragen
asr.l #8,d2 * High-Byte holen
move.b d2,F_END * High-Byte eintragen
move.w d4,S_MODE * Mode-Register setzen
move.w d3,S_CNTRL * Sound ausgeben (Control-Register)
movem.l (sp)+,d3/d4 * d3 und d4 restaurieren
rts
END
/* -------------------------------------------- */
/* KLANG1.C: */
/* Erzeugung eines Sinustones auf dem STE */
/* */
/* In Turbo-C 2.0 implementiert v. Peter Engler */
/* (c) MAXON Computer 1991 */
/* -------------------------------------------- */
#include <stdio.h>
#include <stdlib.h>
#include <ext.h>
#include <tos.h>
#include <math.h>
#include "sout.h”
/* Prototypen der im Modul verwendeten Funktionen */
int snd_alloc( SOUND *, unsigned long );
void snd_free( SOUND * );
void sinus( SOUND * );
int main( void ) ;
/* Anlegen des Arrays f. die zu wandelnden Bytes */
int snd_alloc( SOUND *snd, unsigned long anz )
{
/* Speicherplatz belegen */
snd -> s_ptr = (char *) malloc( anz );
/* Fehler aufgetreten */
if (! snd -> s_ptr)
{
snd -> anz_bytes = 0L;
return( -1 );
}
/* Anzahl Bytes des reservierten Bereichs */
snd -> anz_bytes = anz;
/* Anzahl Bytes, die pro Sekunde gewandelt werden */
switch( snd -> mode_reg & 0x000F )
{
case MOD_FR50K :
snd -> bytes_pro_sekunde = 50066L;
break;
case MOD_FR25K :
snd -> bytes_pro_sekunde = 25033L;
break;
case MOD_FR12K :
snd -> bytes_pro_sekunde = 12517L;
break;
case MOD_FR6K
snd -> bytes_pro_sekunde = 6258L;
break;
default :
snd -> bytes_pro_sekunde = 0L;
break;
}
return( 0 );
}
/* Freigeben des Arrays der SOUND-Struktur */
void snd_free( SOUND *snd )
{
free( snd -> s_ptr );
}
/* Generieren der Werte fur den Sinuston */
void sinus( SOUND *snd)
{
unsigned long bytes_pro_periode, index;
char *h_ptr;
h_ptr = snd -> s_ptr;
/* Berechnung der Anzahl Bytes pro Periode */
bytes_pro_periode = snd -> bytes_pro_sekunde / snd -> frequenz;
/* Wert muß gerade sein */
if ( (bytes_pro_periode % 2) == 1) bytes_pro_periode++;
/* Eintragen in SOUND-Struktur */
snd -> anz_bytes = bytes_pro_periode;
/* Berechnung der Werte fur einen Sinuston */
for (index = 0; index < bytes_pro_periode, index++)
{
*h_ptr++ =
(char) (127 * sin( 2.0 * M_PI * ((double) index) / (double) bytes_pro_periode) - 1);
}
}
int main( )
{
SOUND snd;
/* 'mode_reg' immer vor 1. Aufruf von 'snd_alloc' setzen!! */
snd.mode_reg = MOD_FR50K | MOD_MONO;
snd.control_reg = SND_IMMER;
snd.frequenz = 440; /* in Hz */
/* Array für den Frame anlegen */
if (snd_alloc( &snd, 65536L)) return(-1);
/* Sinus in SOUND-Struktur eintragen */
sinus( &snd );
/* ... und abspielen */
snd_play ( &snd );
/* Auf Tastendruck warten */
(void) getch( );
/* Tonerzeugung ausschalten */
snd_stop( );
snd_free( &snd );
return( 0 );
}