Seit dem Erscheinen des ATARI Falcon030 ist schon viel über Harddiskrecording geschrieben worden. Es war neben der Beschreibung des Soundsubsystems eines der ersten Themen, das von den Autoren aufgegriffen worden ist. Natürlich liegt es auch daran, daß es der Aufbau des Falcon ermöglicht, mit nur wenigen Programmzeilen einen kleinen Harddiskrecorder zu programmieren. Es gibt allerdings Fälle, bei denen man mit der herkömmlichen Programmierweise schnell an Grenzen stößt. Um das zu verdeutlichen, werfen wir doch noch einmal einen Blick auf das System des „Halfbuffers“.
Um ein Lied auf Festplatte aufzunehmen, legt man sich zuerst einen genügend großen Buffer-Speicher an. Nun startet man die Aufnahme-DMA im „Endlos“-Modus und fragt in einer Schleife ständig per XBIOS die aktuelle Schreibposition ab. Überschreitet die DMA gerade die Hälfte des Buffers, speichert man die erste Hälfte ab; erreicht die Schreibposition das Buffer-Ende, sichert man die zweite Hälfte des Buffers auf Festplatte.
Mit diesen wenigen Sätzen ist das Prinzip der Aufnahme auf Festplatte schon beschrieben. Die Wiedergabe funktioniert natürlich genauso. Da die Daten per DMA transportiert werden, muß sich die CPU praktisch um nichts kümmern. Doch stellt man bei genauem Hinsehen fest, daß der Rechner die meiste Zeit mit Festplattenoperationen beschäftigt ist. Je nach Buffer-Größe und Plattengeschwindigkeit stecken bist zu 90% der Prozessorzeit im Fread- bzw. Fwrite-Befehl. Während dieser Zeit ist in TOS 4.0x das eigene Programm und sind unter MiNT (V1.0x) alle anderen Prozesse blockiert! Dieser Umstand kann schnell zum Problem werden. Stellen wir uns einfach einmal vor, wir möchten während des Abspielens per Tastatur Marken in dem Sample setzen, um es später an diesen Stellen zu „zerschneiden“.
Wenn man sich die Dokumentation zum Falcon anschaut, stellt man fest, daß das Betriebssystem in der Lage ist, am Ende des DMA-Buffers einen Interrupt auszulösen. Das allein hilft natürlich noch nicht weiter, denn eine Interrupt-Routine, die nur ein Flag setzt (so wie ATARI es vorschlägt), verkompliziert nur das obige System. Schließlich fragt das Hauptprogramm nun dieses Flag anstatt der Schreib-/Leseposition ab. Man hat also im Prinzip noch gar nichts gewonnen. Außerdem hat sich die Zeit für Festplattenoperationen nicht verkürzt.
Mit einem kleinen Trick kommt man allerdings doch ans Ziel. Normalerweise haben die Buffer für Harddiskrecording eine Größenordnung von einigen 100 KBs. Der Ansatz ist nun, den Buffer in kleine Einheiten (Patterns) von ca. 1 bis 2 KB einzuteilen. Die Start- und Endadresse der Sound-DMA setzen wir auf das erste Pattem und weisen das Betriebssystem an, am Ende des Patterns einen Interrupt auszulösen. Die Interrupt-Routine hat dann folgende Aufgaben:
Auf diese Art und Weise kann man jederzeit eine Marke im Raster der Pattern-Größe setzen, denn der Interrupt wird ja auch während des Abspeicherns und Ladens von Daten ausgelöst! In der Wahl der Pattern-Größe hat man recht große Freiheit, nur zu klein sollte man sie nicht wählen, denn schließlich braucht auch die Interrupt-Auslösung und -Bearbeitung eine gewisse Zeit.
Ein Frage drängt sich geradezu auf: Wenn man genau am Ende des Patterns per Interrupt eine neue Startadresse für die DMA festlegt, kommt es doch zu kurzen, hörbaren Aussetzern in der Wiedergabe! Dieser Einwand ist richtig, und trotzdem funktioniert obiges System. Dies hat folgenden Grund: Der erste Interrupt wird nicht am Ende, sondern zu Beginn des ersten Patterns ausgelöst. D.h., während wir das erste Pattern hören, setzt der Interrupt die Adressen für das zweite Pattern. Diese neuen Positionen übernimmt die Sound-DMA aber erst am Schluß des Frames.
Ob ATARI diesen Umstand beim Design des Falcon geplant hat, möchte ich einmal dahingestellt lassen. Wichtig ist nur. daß die Methode problemlos funktioniert.
Wie bei den meisten einfachen Problemlösungen steckt die Tücke im Detail. Das beginnt mit der ATARI-Dokumentation, die vom MFP-Interrupt „i7“ spricht, was aber nicht „MFP-Interrupt Nr.7“ bedeutet, sondern „MFP-Interrupt, der über Portbit 7 ausgelöst wird“! Wenn man das erkannt hat, sagt einem ein Blick in das Profibuch, daß es sich um den „MFP-Interrupt Nr. 15“ handelt! Die Routine SetUpInt im Beispiel-Listing (Listing 2) zeigt, wie die Initialisierung des Interrupts in Assembler aussieht.
Das nächste Problem ist, daß es zumindest unter MultiTOS nicht möglich ist, aus einem Interrupt heraus einen XBIOS-Aufruf zu starten. Nun muß aber unsere Interrupt-Routine die DMA-Start- und -Endadressen lesen und verändern, und außerdem soll sie auch noch die SHIFT-Taste abfragen. Die Betriebs-Systemaufrufe buffoper, buffptr und Kbshift sind also tabu. Wie kann man diese Funktionen ersetzen, ohne die Lauffähigkeit des Programms auf dem zukünftigen „Vogelvieh“ von ATARI zu verlieren?
Beginnen wir mit Kbshift, denn dieses Problem ist relativ einfach zu lösen. Es existiert eine Systemvariable, die genau die gewünschte Information enthält. Deren Adresse ist auch noch rechner- und betriebssystemunabhängig im ROM-Header eingetragen. Folgende Konstruktion in Pure C löst also dieses Problem:
SYSHDR **sys;
long stack;
long *kbshift;
stack=Super(0L);
sys=0x4f2;
kbshift=(*sys)->kbshift
Super(stack);
Im Interrupt:
shift=kbshift&0x3000000;
Das Problem mit den DMA-Adressen ist etwas kniffliger, denn ATARI hat (zurecht) in den normalen Entwicklerunterlagen keine Adressen von Customchips dokumentiert. Da alte Programme, die eigentlich für den DMA-Sound der STE/ TT-Computer programmiert wurden, auch auf dem Falcon laufen, liegt die Annahme nah, daß sich die Registeradressen nicht verändert haben. Ein Blick mit dem Disassembler in den XBIOS-Bereich des ROM bestätigt dies. Da ATARI wohl auch in Zukunft abwärtskompatibel sein will und sich in den STE-/TT-TOS-Versionen keinerlei DMA-Sound-Unterstützung findet, kann man davon ausgehen, daß sich an den Adressen auch in Zukunft nichts ändern wird.
So entstanden die zwei Routinen my_getpos und my_setbuffer, wobei letztere praktisch identisch mit der original TOS-Routine ist. Beide Routinen müssen im Supervisor-Mode aufgerufen werden, was aber kein Problem ist, denn wir wollen sie ja innerhalb eines Interrupts benutzen.
Listing 1 ist ein kleines Beispiel für die obigen Ausführungen. Das compilierte Programm spielt ein per Fileselector ausgewähltes 16-Bit-Stereo-Sample mit der Pattern-Methode vor. Während des Abspielens kann man mit der SHIFT-Taste Marken setzen, welche am Schluß ausgegeben werden. Das Programm wurde in Pure C geschrieben und benutzt nur Standardbibliotheken. Es besteht aus dem C-Quellcode (Listing 1) mit dem Hauptprogramm und dem kleinen Assembler-Quellcode (Listing 2) mit den Interrupt-Routinen. Um ein lauffähiges Programm zu erstellen, brauchen Sie noch die passende Projektdatei, welche in Listing 3 zu sehen ist.
Natürlich ist die hier vorgestellte Lösung nur eine Anwendungsmöglichkeit von vielen. Um Marken zu setzen, könnte man auch einen anderen, asynchronen Interrupt verwenden. Eine weitere interessante Anwendung ist die Synchronisation von Audio- und Videodaten, was für einen Multimediacomputer sehr wichtig ist. Man berechnet die Pattern-Größe (abhängig von der Sample-Frequenz) so, daß der Interrupt z.B. 12mal pro Sekunde ausgelöst wird. Der Interrupt ist dafür zuständig, daß das nächste Bild auf dem Bildschirm dargestellt wird. Bild und Ton laufen nun absolut synchron. Natürlich bleibt hierbei noch das Problem, wie man die enormen Datenmengen bewältigt - doch das ist ein anderes Thema.
/* Harddiskrecording mit Unterbrechung */
/* Listing 1 */
/* (c)1994 by MAXON-Computer */
/* Autor: Andreas Binner */
#include <tos.h>
#include <stdio.h>
#include <stdlib.h>
#include <ext.h>
/* Buffer- und Patterngröße festlegen */
#define PATTERN 2048L
#define BUFFER PATTERN*50L
#define HALFBUF BUFFER/2L
/* Prototypes */
long file_len(int h);
void play(char *name);
/* Globale Variablen */
/* Zähler und Flags für Interrupt */
long *kbshift;
long l;
long dmapos,loadpos,aktpos,endaddr,begaddr;
int load_flag,int_off, shift;
/* Buffer */
char *sndbuf;
/* Platz für 64 Marken */
long marken[64];
int mz;
/* Externe Assemblerfunktionen */
extern long IntOff();
extern void Inton();
extern char IntFlag;
/* Hauptprogramm (als TTP ausgelegt) */
int main(int argc, const char *argv[])
{
SYSHDR **sys;
long stack;
int curadder,curadc;
int i;
/* Adresse für 'kbshift' holen */
stack=Super(0L);
sys=0x4f2;
kbshift=(*sys)->kbshift;
Super(stack);
/* Sound-Subsystem sperren und initialisieren*/
locksnd();
settracks(0,0);
setmode(1);
settracks(0,0);
curadder=(int)soundcmd(ADDERIN,INQUIRE);
soundcmd(ADDERIN,2);
curadc=(int)soundcmd(ADCINPUT,INQUIRE);
soundcmd( ADCINPUT, 0 );
dsptristate(ENABLE,ENABLE);
/* Speicher für Buffer anfordern */
sndbuf=(char *)Malloc(BUFFER);
/* Falls genug frei war -> Sample Vorspielen */
if (sndbuf)
play(argv[1]);
/* Spicher freigeben */
Mfree(sndbuf);
/* Marken anzeigen */
for (i=0;i<mz;i++)
printf("#%d nach %1d sec\r\n",i,marken[i]/ACT_CLK33K);
/* Sound-Subsystem zurücksetzen und freigeben */
dsptristate(TRISTATE,TRISTATE);
soundcmd(ADDERIN,curadder);
soundcmd(ADCINPUT,curadc);
unlocksnd();
/* ... und raus */
return(0);
}
/* Routine zum Abspielen eines Samples */
void play(char *name)
{
int h;
/* Datei öffnen (Rohsample ohne Header!)*/
h=Fopen(name,0);
if (h<0)
return;
/* Buffer komplett füllen */
Fread(h,BUFFER,sndbuf);
/* DMA initialisieren */
devconnect(DMAPLAY,DAC,CLK_25M,CLK33K,NO_SHAKE);
setbuffer(0,sndbuf,&sndbuf[PATTERN]);
/* Globale Variablen setzen */
begaddr=0;
endaddr=file_len(h);
l=PATTERN;
/* Interrupt einschalten */
int_off=0;
Inton(1);
buffoper(PLAY_ENABLE|PLAY_REPEAT);
delay(10);
mz=0;
shift=0;
load_flag=0;
do
{
/* Auf nächsten Interrupt warten */
do{}
while (!IntFlag);
IntFlag=0;
/* Nachladen ?*/
if (load_flag)
{
if (load_flag==1)
Fread(h,HALFBUF,sndbuf);
else
Fread(h,HALFBUF,&sndbuf[HALFBUF]);
load_flag=0;
}
} while (!int_off);
/* Datei schließen, DMA und Interrupt aus */
Fclose(h);
setinterrupt(1,0);
Supexec(IntOff);
buffoper(0);
}
/* Dateilänge feststellen */
long file_len(int h)
{
long len;
len=Fseek(0,h,2);
Fseek(0,h,0);
return(len);
}
/* Interruptroutine */
void sound_end(void)
{
long apos;
long tcode;
/* Neuer Pattern */
my_setbuffer(0,Ssndbuf[1],&sndbuf[1+PATTERN]);
/* Aktuelle Position berechnen */
apos=(dmapos%PATTERN)+loadpos;
/* Ende erreicht ?*/
if (apos+PATTERN>=endaddr)
{
setinterrupt(1,0);
int_off=1;
return;
}
/* Shift-Taste abfragen */
tcode=*kbshift;
if (tcode&0x3000000)
{
if (!shift)
{
shift=1;
if (mz<64)
marken[mz++]=apos/4L;
}
}
else
shift=0;
/* Load-Flag setzen */
dmapos=my_getpos()-(long)sndbuf;
l+=PATTERN;
loadpos+=PATTERN;
if (l==HALFBUF+PATTERN)
load_flag=1;
if (l==BUFFER)
l = 0L;
if (l==PATTERN)
load_flag=2;
}
/* Harddiskrecording mit Unterbrechung */
/* Listing 2 */
/* (c)1994 by MAXON-Computer */
/* Autor: Andreas Binner */
GLOBL IntOn
GLOBL IntOff
GLOBL IntFlag
GLOBL setinterrupt
GLOBL Supexec
GLOBL sound_end
GLOBL my_setbuffer
GLOBL my_getpos
/* MFP Register */
imra EQU $fffa13
iera EQU $fffa07
ipra EQU $fffa0b
isra EQU $fffa0f
aer EQU $fffa03
mfpints EQU $100
/* Variablen */
IntFlag: ds.b 1
function: ds.w 1
ALIGN
/* Interrupt aus */
IntOff:
bclr #7,iera
bclr #7,imra
bclr #7,ipra
bclr #7,isra
rts
/* Interrupt einschalten */
IntOn:
move d0,function
move.l #SetInt,a0
jsr Supexec
rts
SetInt:
clr.b IntFlag
bsr IntOff
move.l #(15*4),d0
add.l #mfpints,d0
move.l d0,a0
move.l #my_int,(a0)
tst.w function
beq .record
move.w #1,d0
move.w #1,d1
jsr setinterrupt
bra .cont
.record:
move.w #1,d0
move.w #2,d1
jsr setinterrupt
.cont:
bset #7,aer
bclr #7,ipra
bclr #7,isra
bset #7,imra
bset #7,iera
clr.l d0
rts
/* Interrupt Routine */
my_int:
st IntFlag
/* Register sichern und C-Funktion aufrufen */
movem.l a0-a6/d0-d7,-(sp)
jsr sound_end
movem.l (sp)+,a0-a6/d0-d7
/* Nächsten Interrupt ermöglichen */
bclr #7,isra
bset #7,imra
rte
/* DMA Position ermitteln */
my_getpos:
clr.l d0
clr.l d1
move.w $ff8908,d0
rol.l #8,d0
rol.l #8,d0
move.w $ff890a,d1
rol.l #8,d1
add.l d1,d0
move.w $ff890c,d1
add.l d1,d0
rts
/* Neue DMA Position setzen */
my_setbuffer:
tst.w d0
beq .1
bset.b #$0007,$ff8901
bra .2
.1:
bclr.b #$0007,$ff8901
.2:
move.l a0,d0
move.w d0,$ff8906
ror.l #8,d0
move.w d0,$ff8904
ror.l #8,d0
move.w d0,$ff89O2
move.l a1,d0
move.w d0,$ff8912
ror.l #8,d0
move.w d0,$ff8910
ror.l #8,d0
move.w d0,$ff890E
rts
; Harddiskrecording mit Unterbrechung
; Projekt-Datei
*.TTP ; Beispielprogramm als TTP
.C [ -Y ]
.L [ -L -Y ]
.S [ -Y ]
PCSTART.O
SND_INT.S ; Interruptroutinen
*
PCSTDLIB.LIB
PCEXTLIB.LIB
PCTOSLIB.LIB