Soundsamples für ATARI STE- und TT-Computer - Einladen & Abspielen

Die Rechner der ATARI-STE- und TT-Reihe bieten die einfache Möglichkeit, digitalisierte Geräusche oder Musik in verschiedenen (aber festen) Frequenzlagen abzuspielen. Leider bietet das Betriebssystem keine entsprechende Funktion, welche diese Arbeit erledigt, so daß man darauf angewiesen ist, selber dafür zu sorgen.

Dies erledigt die Unit in Listing 1. Sie stellt einfach zu benutzende Funktionen zum Laden und Abspielen von Soundsamples bereit. In Listing 2 findet sich ein kurzes Programm zum Testen der obigen Unit.

Über Samples ...

Als Samples bezeichnet man im allgemeinen digitalisierte Geräusche oder Musik. Analoge Signale werden über einen A/D-(Analog-Digital)-Konverter in digitale Signale umgewandelt. Da analoge Signale beliebige Amplitudenwerte (Pegel) besitzen können, müssen sie auf ein festes digitales Raster abgebildet werden.

... und ihre Darstellung im ATARI

Im Falle der DMA im ATARI, welche für die Verarbeitung der Samples zuständig ist, wird jedem Analog- ein Digitalwert zwischen -128 (negative Amplitude) und +127 (positive Amplitude) zugeordnet. Allerdings gibt es auch Samples, deren binäre Darstellung derart ist, daß das (negative) Minimum bei 0 und das (positive) Maximum bei 255 liegt. Solche Samples klingen natürlich auf einem ATARI nicht korrekt, können aber durch ein einfaches Programm umgewandelt werden. Es wird also, in beiden Fällen, jeweils ein Byte (8 Bit) zur Kodierung verwendet. (Im Gegensatz z.B. zu CD-Playern. Diese verwenden i.a. 16 Bit.)

Nun müssen diese Daten auch wiedergegeben werden können. Hierzu findet ein zu oben umgekehrter Vorgang Verwendung. Ein Digital-Analog-Konverter berechnet aus den (recht wenigen) 255 möglichen Pegelwerten wieder das analoge Signal. Man erhält also in keinem Fall wieder den Verlauf des originalen analogen Pegels, sondern nur eine (mehr oder weniger) gute Annäherung.

Die Geschwindigkeit, mit welcher diese Umrechnung erfolgt bzw. mit welcher das Sample wiedergegeben wird, läßt sich einstellen. Sie wird als Sample-Rate bezeichnet. In der Analog-Digital-Konvertierung gilt folgendes: Je höher die Sample-Rate ist, desto genauer, d.h. in kleineren, engeren, Zeitintervallen kann ein analoges Signal abgetastet werden, um so eine bessere Kopie des originalen Signals zu erhalten. Bei der Digital-Analog-Konvertierung gibt die Sample-Rate primär die Abspielgeschwindigkeit des Samples an, wobei sie natürlich mit der Sample-Rate der A/D-Konvertierung identisch sein sollte, um eine originalgetreue Wiedergabe zu ermöglichen. Beim ATARI sind dies 6.25,12,25 oder 50 kHz. Zum Beispiel werden bei 12 kHz 12000 digitale Daten (also etwa 12 KBytes) in der Sekunde in ein analoges Signal umgerechnet und wiedergegeben. Dies ist recht viel und erklärt auch, wieso Samples i.a. soviel Speicherplatz benötigen.

Des weiteren gibt es noch die Möglichkeit, die Daten in Stereo wiederzugeben. In diesem Fall wird jeweils immer abwechselnd ein Byte auf dem rechten und dem linken Kanal ausgegeben. Damit verdoppelt sich natürlich der Speicherbedarf für ein einziges Sample.

Als letztes gibt es noch die Möglichkeit ein einzelnes Sample nur ein einziges Mal abzuspielen oder beliebig oft zu wiederholen.

Implementierung

Funktion sam_load (Dateiname): Ergebnis.

Parameter:

Ein Sample wird durch obige Funktion geladen und in einem Speicherbereich, der angefordert wird, abgelegt. Zur Verwaltung aller eingeladenen Samples wird ein Feld-Record benutzt, der jeweils die Länge des Samples und die Anfangsadresse des reservierten Speicherbereiches enthält. Bei erfolgreichem Laden erhält man ein Handle, mit dem das Sample referenziert wird. Sollte nicht genügend Platz für alle zu ladenden Samples in dem Record sein, kann die Konstante max_samples, zu finden im Kopf der Unit, entsprechend hochgesetzt werden.

Prozedur sam_play(ld,Wie_oft, Frequenz, Mono_Stereo);

Parameter:

Die Prozedur übernimmt das Abspielen der Samples. Ihr werden außer der Referenznummer eines Samples die oben erwähnten Charakteristika übergeben (Es empfiehlt sich, die Konstantenvereinbarungen aus dem Unit-Kopf zu verwenden.) Diese Daten werden nun in die entsprechenden DMA-Register des ATARI übertragen. Da die Register in einem geschützten Speicherbereich liegen, muß der Zugriff im Supervisormodus erfolgen. Dies erledigt die erste Anweisung in der Prozedur. Sobald alle Daten in die Register geschrieben wurden, beginnt die DMA mit dem Abspielen des Samples. Dies geschieht im Hintergrund und allein durch die DMA, d.h. der Hauptprozessor wird durch den Vorgang nicht beeinträchtigt.

Prozedur sam_free(Id);

Parameter:

Vor Verlassen eines Programmes, in dem die Unit Verwendung findet, sollten die Speicherbereiche, in denen die Samples abgelegt sind, durch obige Prozedurfreigegeben werden. Andernfalls könnte man plötzlich einige hundert Kilobyte RAM-Speicher vermissen.

Listing 2 stellt eine kleine Testumgebung für die Unit dar. Ein Sample kann ausgewählt werden, und die Abspiel-Charakteristika lassen sich einstellen. Klicken auf Abbruch in der Fileselectorbox führt zum Verlassen des Programmes.

Die Unit ist recht einfach und modular aufgebaut, und es sollte keine zu großen Probleme bereiten, sie für andere Pascal-Versionen oder auf andere Sprachen anzupassen.

DMA-Hardware-Register

Alle in Bild 1 aufgeführten Register sind Wortregister, sie haben also eine Länge von 2 Byte, von denen allerdings immer nur das niederwertige Byte (Registeradresse+1) benutzt wird.


(* Laden und Abspielen von Soundsamples auf *)
(* Atari STE und TT Computer                *)
(* (w) Marco Feikert (10'92) & (1'93)       *)
(* (c) MAXON Computer 1993                  *)
{$X+}
UNIT sample;

INTERFACE

USES tos;

CONST
    max_samples = 150;

    stop = 0; 
    einmal = 1; 
    dauernd = 3;

    mono = 1; 
    stereo= 0;

    fr_6 = 0; 
    fr_12 = 1; 
    fr_25 = 2; 
    fr_50 = 3;

VAR
    i: INTEGER;

FUNCTION sam_load(name: STRING): INTEGER; 
PROCEDURE sam_free(was: INTEGER);
PROCEDURE sam_play(id, wie_oft, frequenz, mono_stereo: INTEGER);

(* -------------------------------------------*)

IMPLEMENTATION

TYPE
    sam_arr = RECORD
        start: POINTER; (* Startadresse *)
        len: LONGINT; (* Länge *)
    END;

VAR
    sam: ARRAY[0 .. max_samples] OF sam_arr;

(* Lädt Sample name, return: Id-Nummer bzw. *)
(* -1: Datei nicht gefunden,                *)
(* -2: Speicherreservierungsfehler          *)
(* -3: Einladefehler                        *)
(* -4: Recordarray vollständig belegt       *)
FUNCTION sam_load(name: STRING): INTEGER;
VAR
    frei, i, fh, return: INTEGER; 
    amount, len: LONGINT; 
    p: POINTER; 
    dat: DTAPtr;
BEGIN 
    P:=NIL; 
    len:=0; 
    return:=-4; 
    i: =0;
(* Freien Platz suchen *)
    REPEAT
        IF sam[i].start=NIL THEN 
            return:=i 
        ELSE
            INC(i);
    UNTIL (i>max_samples) OR (return<>-4);
    IF return<>-4 THEN BEGIN (* Dateilänge bestimmen *) 
        i:=FSFIRST(name,0);
        IF i=0 THEN BEGIN 
            dat:=FGETDTA; 
            len:=dat^.d_length;
        END;
(* Dateilänge=0 => nicht gefunden *)
        IF len=0 THEN 
            return:=-1 
        ELSE BEGIN 
            frei:=return;
    (* Speicherplatz reservieren *) 
            p:=Malloc(len);
            IF p=NIL THEN 
                return:=-2 
            ELSE BEGIN 
                return:=-3;
                fh: =Fopen(name,FO_READ);
                IF fh>0 THEN BEGIN 
                    (* Sample einiesen *)
                    amount:=Fread(fh,len.p);
                    IF amount=len THEN BEGIN 
                        sam[frei].start:=p;
                        sam[frei].len:=len;
                        return:=frei;
                    END;
                    Fclose(fh);
                END;
            END;
        END;
    END;
(* Einladefehler, Speicher freigeben *)
    IF return=-3 THEN 
        Mfree(p); 
    sam_load:=return;
END;

(* Speicherplatz von Samples freigeben *)
(* was: >=0: Für ein Sample freigeben *)
(* -1: Für alle Samples freigeben *)
PROCEDURE sam_free(was: INTEGER);
VAR
    i: INTEGER;
BEGIN
    IF was>=0 THEN BEGIN
        IF sam[was].start<>NIL THEN BEGIN 
            Mfree(sam[was].start); 
            sam[was].start:=NIL; 
            sam[was].len:=0;
        END;
    END 
    ELSE BEGIN 
        i:=0;
        REPEAT
            IF sam[i].start<>NIL THEN BEGIN 
                Mfree(sam[i].start); 
                sam[i].start:=NIL; 
                sam[i].len:=0;
            END;
            INC(i);
        UNTIL i>max_samp1es;
    END;
END;

(* 1 Longint in 4 Bytes aufteilen *)
(* b1: Highbyte ... b4: Lowbyte *)

PROCEDURE split_var(v: LONGINT; VAR b1, b2, b3, b4: BYTE);
VAR
    p: POINTER;
BEGIN
    p:=ADDR(v); 
    b1:=BYTE(p^);
    b2:=BYTE(POINTER(LONGINT(p)+1)^); 
    b3:=BYTE(POINTER(LONGINT(p)+2)^); 
    b4:=BYTE(POINTER(LONGINT(p)+3)^);
END;

(* Sample abspielen                         *)
(* id: Samplekennung aus sam„load()         *)
(* wie_oft: 0: Stop, 1: Einmal, 3: Endlos   *)
(* frequenz: 0: 6.25 Khz, 1: 12.5 Khz,      *)
(* 2: 25 Khz, 3: 50 Khz                     *)
(* mono_stereo: 0: Stereo, 1: Mono          *)
(* bzw. Konstantenvereinbarungen aus        *)
(* Programmkopf benutzen                    *)
PROCEDURE sam_play(id, wie_oft, frequenz, mono_stereo: INTEGER);
VAR
    l, Start, ende, os: LONGINT; 
    b1, b2, b3, b4: BYTE;
BEGIN
    IF sam[id].len>0 THEN BEGIN (* Gültige Id ? *) 
        (* Supervisormodus an *) 
        os:=Super(NIL);
        (* Startadresse eintragen *)
        l:=$ff8902;
        start:=LONGINT(sam[id].start); 
        split_var(start,b1,b2,b3,b4);
        BYTE(POINTER(l+1)^):=b2;
        BYTE(POINTER(l+3)^):=b3;
        BYTE(POINTER(l+5)^):=b4;
        (* Endadresse eintragen *) 
        l:=$ff890e;
        ende:=LONGINT(sam[id].start)+sam[id].len; 
        split_var(ende,b1,b2,b3,b4);
        BYTE(POINTER(l+1)^):=b2;
        BYTE(POINTER(l+3)^):=b3;
        BYTE(POINTER(l+5)^):=b4;
        (* Frequenz eintragen *)
        1:=$ff8920;
        IF mono_stereo=1 THEN 
            INC(frequenz, 128);
        WORD(POINTER(l)^):=frequenz;
        (* Abspielen starten/stoppen *)
        l:=$ff8900;
        WORD(POINTER(1)^):=wie_oft;
        (* Supervisormodus aus *)
        Super(POINTER(os));
    END;
END;

BEGIN
    (* Record initialisieen *) 
    i:=0;
    REPEAT
        sam[i].start:=NIL; 
        sam[i].len:=0;
        INC(i);
    UNTIL i>maxsamples;
END.

(* Kurzes Beispielprogram zum Testen *)
(* der Sample-Unit *)
{$X+}
PROGRAM test_sample;

USES gem,sample;

VAR
    s1, s2, s3: STRING; 
    len, i, x: INTEGER; 
    freq, m_s: INTEGER;

BEGIN 
    REPEAT 
        s1:=''; 
        s2:='';
(* Sample auswählen *) 
        fsel_input(s1,s2,i);
        IF (i<>0) AND (s2<>'') THEN BEGIN 
            len:=LENGTH(s1);
            WHILE COPY(s1,len,1)<>'\' DO 
                DEC(len); 
            s2:=COPY(s1,1,len)+s2;
(* Laden *)
            x:=sam_load(s2);
            IF x>=0 THEN BEGIN (* Laden erfolgreich *) 
(* ACHTUNG: Bei Verwendung von Write's in der   *) 
(* niedrigen oder mittleren Auflösung gibt es   *) 
(* einen Runtimeerror in Pure Pascal (Version   *) 
(* 13.Aug) in Verbindung mit NVDI 2.1 !         *)
                write('Frequenz: (0-3):'); 
                readln(freq);
                write('Stereo/Mono (0-1):'); 
                readln(m_s);
(* und abspielen *)
                sam_play(x,einmal,freq,m_s); 
                sam_free(x);
            END;
        END;
    UNTIL (i=0) OR (s2='');
END.
Registeradresse Bit-Belegung Erklärung
$ff8900 **** **** **** **BA A=0, B=0: DMA-Sound ausschalten
A=0, B=1: DMA-Sound einschalten
A=1, B=1: DMA-Sound einschalten, Sample endlos wiederholen
$ff8920 **** **** A*** **CD C=0, D=0: Sample-Rate: 6,25 kHz
C=0, D=1: Sample-Rate: 12,50 kHz
C=1, D=0: Sample-Rate: 25,00 kHz
C=1, D=1: Sample-Rate: 50,00 kHz
A=0: Stereo
A=1: mono
$ff8902 **** **** 00xx xxxx Sample-Startadresse, Highbyte
$ff8904 **** **** xxxx xxxx Sample-Startadresse, Middlebyte
$ff8906 **** **** xxxx xxxx Sample-Startadresse, Lowbyte
$ff890E **** **** 00xx xxxx Sample-Endadresse, Highbyte
$ff8910 **** **** xxxx xxxx Sample-Endadresse, Middlebyte
$ff8912 **** **** xxxx xxxx Sample-Endadresse, Lowbyte
$ff8908 **** **** 00xx xxxx Hier findet sich die aktuelle Position
$ff890A **** **** xxxx xxxx im Sample, an welcher sich die DMA
$ff890C **** **** xxxx xxxx gerade während des Abspielens befindet. (High-, Middle- und Lowbyte)

Marco Feikert
Aus: ST-Computer 03 / 1993, Seite 70

Links

Copyright-Bestimmungen: siehe Über diese Seite