DSP-Programmierung auf dem Falcon, Teil 4: Tips & Tricks mit Programmbeispiel

Waren die letzten drei Teile eher theoretischer Natur, kommen jetzt vom Badewannenheuler bis zum Wiener Sängerknaben alle auf ihre Kosten. Mit einem Karaoke-Effekt läßt sich das Stimmensignal eines Musikstückes löschen, so daß eine Art Playback entsteht.

Egal ob man nun zu einem Musikstück lieber selbst singen will, oder ob man vielleicht sogenannte „objectionable language“ in Liedern (natürlich aus Jugendschutzgründen) ausblenden will, die digitale Technik, und damit der DSP im Falcon, macht’s möglich. Zukünftig werden wohl Discjockeys, wenn zu fortgeschrittener Stunde mal wieder ein alter Ohrwurm des Kalibers „We don’t need no...“ läuft, nicht mehr die Lautstärke zurückschrauben, sondern auf dem daneben stehenden Falcon einfach den Karaoke-Effekt einschalten und somit das ganze Publikum zum Mitsingen auffordern.

Aus heutiger Sicht mag auch der Rap viel zu früh erschienen sein. Zig Tonnen schwarzes Vinyl hätte man vor der sicheren Vernichtung bewahren können, wäre nicht mehr mit dem Plattenteller, sondern mit der Computermaus gescratcht worden. Wer weiß, was auf digitalem Wege noch alles möglich ist und einmal ganz neue Trends in Bewegung setzt! „Where have all the (old) good times gone?“ Soweit wollen wir hier allerdings nicht gehen. Unser Ziel ist es, aus einem Wirrwarr von Frequenzen die der Gesangsstimme herauszufiltern. Dies läßt im ersten Moment einen tiefgreifenden Algorithmus (evtl, sogar mit Spracherkennung) dahinter vermuten. Und - dieses Eingeständnis muß sein: Wenn man es richtig machen wollte, müßte man wohl auch so verfahren. Zum Glück aber gibt es einen einfacheren und trotzdem „fast“ perfekten Weg.

Das Karaoke-Prinzip

Eine zwingende Voraussetzung ist allerdings die Bearbeitung von Stereo-Aufnahmen. Wer über eine gute Stereo-Anlage verfügt und keine, taube oder extrem tolerante Nachbarn hat, dem ist sicherlich schon aufgefallen, daß der Gesang meistens in der Kanalmitte liegt. Über Kopfhörer hinterläßt das den Eindruck, als ob die oder der Sänger genau zwischen den Gehirnwindungen säße. Die verschiedenen Instrumente hingegen liegen meist seitlich oder wechseln ständig die Position. Außerdem werden selbst für Studioaufnahmen keine Stereo-Mikrophone benutzt, womit halbwegs sichergestellt wäre, daß das Stimmensignal im rechten und linken Kanal identisch ist. Und genau dort setzt unser Karaoke-Effekt (Karaoke: jap. ohne Orchester) an. Dazu filtert man das Signal in der Kanalmitte heraus, welches nicht zwangsläufig die Singstimme sein muß. So erwischt dieser Effekt zum Beispiel bei vielen Jazz-Stücken auch die Instrumentensoli bzw. den Baß und teilweise das Schlagzeug (Bass-Drum).

Bleibt noch zu klären, wie man die Kanalmitte herausfiltert. Eine einfache Formel veranschaulicht unseren Fall. Folgendes sei gegeben: Eine Singstimme S in der Kanalmitte und für den linken und rechten Kanal jeweils ein mehr oder weniger wohlklingendes Hintergrundgeräusch LH und RH. Damit ergeben sich folgende Signale für die beiden Kanäle:

Linker Kanal: L = S + LH
Rechter Kanal: R = S + RH

Zieht man nun den rechten vom linken Kanal ab, kommt man mit „etwas höherer“ Mathematik zu folgendem Ergebnis:

L - R = LH - RH

Damit wäre bewiesen, daß sich auf diese Weise die Stimme in der Kanalmitte herausfiltern läßt. Um einem Überlauf durch die Subtraktion vorzubeugen, sollte man vorher noch den Wertebereich beider Kanäle halbieren.

Den Beweis, daß das Ergebnis LH-RH auch noch nach der Hintergrundmusik der beiden Kanäle klingt, liefert unser Programmbeispiel.

Eine so verblüffend einfache Formel mit einem so beeindruckenden Ergebnis verführt förmlich dazu, einfach einmal andere Formeln zu benutzen. Dennoch wird man schnell feststellen: Man kann zwar viele Algorithmen aus dem immensen Repertoire der Mathematiker auf die Sample-Daten hetzen, die, mathematisch betrachtet, durchaus auch „schöne“ Ergebnisse liefern, ob allerdings die weitaus sensibleren Gehörnerven zum selben Schluß kommen, steht auf einem anderen Blatt.

Auf dem Plattenteller

Kommen wir zum technischen Teil: der Implementierung im DSP. Wie in [1] und [2] beschrieben, versorgt die DMA-Matrix den DSP mit den Sounddaten. Dies bedeutet für den DSP im Maximalfall einen Datendurchsatz von vier Stereokanälen (jeweils DMA-Aufnahme und DMA-Wiedergabe) mit 16-Bit-Samples. Bei maximaler Sample-Frequenz sind das etwa 1,6 MByte pro Sekunde. Allerdings besteht die Aufgabe des DSP nicht nur darin, diese Datenmenge hin und her zu schaufeln, sondern vielmehr in der Berechnung der Samples. Noch wichtiger ist dann die „rechtzeitige“ Berechnung.

Zwar besitzt der DSP einen eigenen Timer, da in unserem Fall aber die DMA-Matrix den Takt diktiert, überläßt man die Synchronisation beim Datenaustausch auch weitgehend dem Multiplexer. Beim Senden und Empfangen der Samples löst dieser jeweils einen Interrupt im DSP aus, der sich wiederum um den verbleibenden Rest kümmert. Dafür stehen im DSP sowohl Sende- als auch Empfangsvektoren bereit. Bevor die beiden Vektoren jedoch „anspringen“, ist noch etwas Konfiguration vonnöten. Zuerst bestimmt man, welche Priorität (Interrupt-Level) ein ausgelöster Interrupt erhält.

Da der DSP neben dem hier angesprochenen SSI-Interface noch weitere Ein-/Ausgabekanäle besitzt, die sich ebenfalls interruptsteuern lassen, bietet eine freie Prioritäten wähl der verschiedenen Schnittstellen enorme Vorteile. In unserem Fall, bei dem nur die SSI-Interrupts zum Einsatz kommen, muß die Priorität einfach nur höher sein als außerhalb des Interrupts.

Als nächstes gilt es, das Datenformat dieser seriellen Schnittstelle festzulegen. Zunächst wäre da die Wortbreite in Bits (bzw. hier die Sample-Breite). Da ATARI seine Entwickler mit der Funktion setsndmode() gesegnet hat, mit der man immerhin zwischen 8-Bit-Mono und 8- und 16-Bit-Stereo-Samples wählen kann, könnte man der Versuchung unterliegen, entsprechende Daten auch am DSP zu erwarten. - Leicht daneben - genauer gesagt: zweimal daneben. Zum ersten erreichen die Samples den DSP immer 16 Bit breit und zum zweiten auch immer in Stereo.

Was nun die Funktion setsndmode() anbelangt, bezieht diese sich ausschließlich auf die DMA-Wiedergabe. Das heißt, daß die Samples zwar in diesen Formaten voriiegen dürfen, dann aber auf das 16-Bit-Stereoformat gebracht werden. Innerhalb der Verbindungsmatrix gibt es dann nur noch dieses Format. Entsprechend gelangen die Daten bei der DMA-Aufnahme nur im 16-Bit-Stereoformat in den Speicher.

Zurück zum DSP: dort erleichtert dieser Umstand natürlich die Arbeit, bleibt doch als variabler Faktor nur noch die Anzahl der Spuren (settrack()), wobei der DSP intern zwischen Spuren und Kanälen (Mono oder Stereo) nicht unterscheidet. Er vergibt an jede Spur und/oder jeden Kanal eine Zeitscheibe (time slots). Zwei Stereospuren haben also vier Slots. Die Zeitscheiben verteilen sich gleichmäßig über einen Sample-Takt, wobei jeweils ein Sende- und ein Empfangs-Interrupt pro Slot eintritt. Welcher der beiden Interrupts zuerst eintritt, ist nicht definiert. Dieser Umstand hat weitreichende Folgen.

Tritt zum Beispiel der erste Sende-Interrupt auf, liegen noch nicht alle Eingangs-Samples des gleichen Taktes vor. Diese benötigt man aber eventuell zur Berechnung des Ausgangssignals. Folglich müssen die Samples eines Taktes zwischengespeichert werden. Da die Berechnung der neuen Ausgangs-Samples auch noch einen Sample-Takt benötigen kann und darf, verlassen die Samples also frühestens zwei Takte später den DSP. Bild 1 veranschaulicht diesen Ablauf.

Im SSI Kontrollegister A (Bit 8-12) setzt man also die Samples pro Takt minus eins und bestimmt so, in wieviele Slots sich der Sample-Takt teilt.

Die beiden Interrupt-Routinen sollten jetzt noch erfahren, in welchem Slot sie denn aufgetreten sind. Bit 2 und 3 des SSI-Statusregisters signalisieren hierzu dem Sende- bzw. dem Empfangs-Interrupt eine Taktsynchronisation. Technisch bedingt ist Bit 2 immer im zweiten Slot des Sendeinterrupts gesetzt, Bit 3 hingegen immer im ersten Slot während des Empfangs, unabhängig von der Anzahl der Slots pro Takt. Wie in Bild 1 ersichtlich, findet die eigentliche Sample-Berechnung dann außerhalb der Interrupts in einer Endlosschleife statt. Diese gilt es nun auch im Gleichtakt zu halten. In unserem Beispiel setzt dazu die Empfangsroutine des ersten Slots ein Bit. Ist dieses Bit gesetzt, setzt die Hauptroutine das Bit zurück und beginnt mit der Berechnung aller Samples eines Taktes. Sollte bis zum nächsten Takt etwas Zeit verbleiben, erledigt die Hauptroutine noch die Kommunikation mit dem Host (hier dem Hauptprozessor).

Bild 1: Dieses Diagramm veranschaulicht den zeitlichen Ablauf vom Einlesen der Samples über deren Berechnung bis zur Sample-Ausgabe.

Im SSI Kontrollregister-B schaltet man die SSI-Interrupts ein und versetzt den DSP in den Netzwerkmodus (Bit 11). Der Netzwerkmodus teilt, wie besprochen, den Sample-Takt in gleich lange Slots.

Über das Port-C-Kontrollregister bestimmt man, welche Signale auf den 9 Leitungen des Port C liegen. An den DSP lassen sich wahlweise ein 9-Bit-paralleler Port oder eine serielle synchrone und eine serielle Kommunikationsschnittstelle anschließen. Wie das Kürzel SSI (Synchronous Serial Interface) verrät, benötigen wir hier die synchrone serielle Schnittstelle zum Multiplexer. Zum Schluß setzt man noch den Interrupt-Level herunter, und das ganze Ding kommt ins Rollen.

Prinzipiell wäre ein Programmaufbau, indem auch die Sample-Berechnung innerhalb der Interrupts läuft, einfacher zu realisieren. Nur ist man dann zeitlich fester gebunden, denn jeder Interrupt muß innerhalb seines Slots abgeschlossen werden, während sich in unserem Beispiel der ganze Sample-Takt nutzen läßt. Wenn dann noch mehrere Spuren zu bearbeiten sind und sich dadurch die Slot-Zeiten verkürzen, muß man früher oder später ohnehin auf dieses Verfahren zurückgreifen.

Das auf den folgenden Seiten abgedruckte Assembler-Listing läßt sich von der Host-Seite so konfigurieren, daß wahlweise eine oder zwei Stereospuren zu einem Stereo-Ausgang gemischt werden. Damit eignet sich das DSP-Programm auch für einen Harddisk-Rekorder mit Ein- und Überblendtechnik. Die Pegel aller vier Kanäle lassen sich vom Host setzen.

Damit neben den Ohren auch noch die Augen auf ihre Kosten kommen, berechnet das DSP-Programm gleich noch die Ausgangspegel der abzuspielenden Samples mit. Über regelmäßige Abfragen der Pegel vom Hauptprozessor stellt dieser die Pegel dann grafisch dar. Stereo - versteht sich - schließlich hat man ja auch zwei Augen.

Um den Quelltext nicht ins Unendliche zu treiben, wurde auf eine logarithmische Ausgabe der Pegel verzichtet.

Es ist angerichtet

Und hier noch ein paar Tips, die je nach Planetenkonstellation dem gestreßten Soundprogrammierer zwischen 5 Minuten und 5 Tagen Zeitersparnis einbringen. Zumeist handelt es sich um die Richtigstellung von zweideutigen Formulierungen in der ATARI-Dokumentation, die oft zu Fehlinterpretationen führen.

So findet man des öfteren falsche Frequenzangaben für die verschiedenen Vorteiler im Zusammenhang mit der Funktion devconnect(). Deshalb sollte man sich ausschließlich an die von ATARI ausgegebene Formel

Sample-Frequenz = ( 25.175 MHz / 256 ) / ( Vorteiler + 1 )

halten (25.175 MHz bei Verwendung des CODEC). Die gültigen Vorteilerwerte müssen zwischen eins und elf (ohne 6, 8 und 10) liegen.

Des weiteren entnimmt man der Beschreibung zur Funktion dsptristate() nur, daß man damit die externen Leitungen von dem Multiplexer abkoppelt. Weitaus bedeutender ist aber der umgekehrte Fall: Ist nämlich keine weitere Hardware am DSP-Ausgang angeschlossen, müssen eben selbige Leitungen an die Matrix angeschlossen werden, sobald der DSP im DMA-Datenpfad eingebunden ist. Zusätzliches Gewicht verleiht diesem Aspekt die Tatsache, daß zumindest das TOS 4.01 vom 2.10.92 bei einem Reset die externen Leitungen von der Matrix abkoppelt. Prinzipiell sollte man nicht ATARIs Beispiel folgen und irgendwelche Grundeinstellungen nach einem Reset erwarten. Das zu obiger TOS-Version mitgelieferte Spiel „Breakout“ gibt nämlich nur Töne von sich, wenn vorher ein anderes Programm im Multiplexer DMAPLAY mit DAC verbunden hat (z.B.: mit „TLKCLOCK“).

# Kontrollregister des DSP

Host Control Register HCR

Bit-Nummer Funktion
0 Host Receive Interrupt Enable
1 Host Transmit Interrupt Enable
2 Host Command Interrupt Enable
3 Host Flag 2
4 Host Flag 3

Host Status Register HSR

Bit-Nummer Funktion
0 Host Receive Data Full
1 Host Transmit Data Empty
2 Host Command Pending
3 Host Flag 0
4 Host Flag 1
7 DMA Status

SSI Control Register A M_CRA

Bit-Nummer Funktion
0-7 Prescale Modulus
8-12 Frame Rate Divider (#slots -1)
13-14 Word Length Control
00 - 8 Bit, 01-12 Bit
10-16 Bit, 11-24 Bit
15 Prescaler Range

SSI Control Register B M_CRB

Bit-Nummer Funktion
0 Serial Output Flag 0
1 Serial Output Flag 1
2 Serial Control 0 Direction
3 Serial Control 1 Direction
4 Serial Control 2 Direction
5 Clock Source Direction
8 Frame Sync Length
9 Sync/Async Control
10 Gated Clock Control
11 Mode Select (0 - normal, 1 - network)
12 SSI Transmit Enable
13 SSI
14 SSI
15 SSI

SSI Status Register M_SR

Bit-Nummer Funktion
0 Serial Input Flag 0
1 Serial Input Flag 1
2 Transmit Frame Sync
3 Receive Frame Sync
4 Transmitter Underrun Error
5 Receiver Overrun Error
6 Transmit Data Register Empty
7 Receive Data Register Full

Port C Control Register M_PCC

Bit-Nummer Funktion
0 0 - Parallel 0 /1 - SCI Receive
1 0 - Parallel 1/1- SCI Transmit
2 0 - Parallel 2 /1 - SCI Clock
3 0 - Parallel 3 /1 - SSI Control 0
4 0 - Parallel 4 /1 - SSI Control 1
5 0 - Parallel 5 /1 - SSI Control 2
6 0 - Parallel 6 /1 - SSI Clock
7 0 - Parallel 7 /1 - SSI Receive
8 0 - Parallel 8 / 1 - SSI Transmit

Interrupt Priority Register M_IPR

Bit-Nummer Funktion
0-1 IRQA Mode Interrupt Priority Level
2 IRQA Mode Trigger Mode
3-4 IRQB Mode Interrupt Priority Level
5 IRQB Mode Trigger Mode
10-11 Host Interrupt Priority Level
12-13 SSI Interrupt Priority Level
00 - locked, 01 - level 0
10 - level 2, 11 - level 3
14-15 SCI Interrupt Priority Level

Tabelle 1: Über diese Kontrollregister läßt sich der DSP an alle äußeren Bedingungen (vorgegeben durch die Falcon-Hardware) anpassen.

Verschiedene Reset-Verhalten bescheinigen die Programmierer von „MUSiCOM“ auch der Funktion sndstatus(1). Diese soll, je nach TOS-Version, die Matrix an die externen Leitungen ab- oder ankoppeln. Außerdem vermisse ich bei der Beschreibung der Funktion buffoperf), den Warnhinweis (mit einem großen roten Dreieck), daß dies sozusagen der Startschuß für die DMA-Aufname bzw. -Wiedergabe ist, auch wenn der Multiplexer noch nicht verbunden wurde oder noch keine Puffer mit setbuffer() gesetzt wurden. Befinden sich die Pufferzeiger noch irgendwo im RAM-Speicher, wird die Sache tragisch, sobald anstelle der Samples eines leisen Solo-Parts der Multiplexer versucht, den Programmcode eines gerade im Speicher befindlichen Debuggers abzuspielen. Dann legen nämlich die über die Stereo-Anlage angeschlossenen Lautsprecherboxen einen rekordverdächtigen Steptanz auf die (Regal-)Bretter. So geschehen!

Dagegen ist die Tatsache, daß die Status von Dsp_Lock() und locksnd() bei Programmbeendigung und natürlich auch bei Programmabsturz nicht zurückgesetzt werden, schon fast belanglos. Weil man aber bei der relativ neuen Materie „DSP-Programmierung“ die Ursache für ein Fehlverhalten überall dort sucht, wo sie nicht ist, und den naheliegendsten Fall außer Betracht läßt, sollte es hier dennoch erwähnt werden.

Vor der Programmbeendigung sollte mit soundcmd(ADCINPUT,3) der Sound-Chip wieder mit dem AD-Wandler verbunden werden, da sonst der Tastaturklick ausbleibt.

In der Sekundärliteratur zum Falcon findet man des öfteren bei der Beschreibung zur Funktion Dsp_BlkHandshake(), daß dort long-Zeiger zu übergeben sind, was dann zu dem falschen Schluß führt, daß hier das gleiche Datenformat wie bei der Funktion Dsp_BlkUnpacked(), nämlich long-Werte, bearbeitet wird. Die Funktion Dsp_BlkHandshake() erwartet aber, wie auch Dsp_DoBlock(), DSP-Wörter, also Drei-Byte-Werte.

Nun noch eine Richtigstellung in eigener Sache:

Leider hat sich in die Header-Datei der DSP-Bibliothek aus [2] ein Fehler eingeschlichen: Bei dem Zielparameter von devconnect() handelt es sich nicht um einen Index, sondern um einen Bit-Vektor. Folglich erhalten die Makros nachstehende Werte und lassen sich beim Aufruf ODER-verknüpfen:

#define DMAREC 0x01 
#define DSPREC 0x02 
#define EXTOUT 0x04 
#define DAC 0x08

Megadisk

Auf der diesmonatigen Megadisk befinden sich, wie immer, alle Sourcen und die ausführbaren Programme zu diesem Teil des DSP-Kurses. Zusätzlich dazu haben wir auch ein kurzes Stereo-Sample mit auf die Diskette gegeben, mit dem der Karaoke-Effekt sehr gut deutlich wird. Wir möchten dabei der westfälischen Rockgruppe „Cabo“ danken, die uns freundlicherweise die Erlaubnis gegeben hat, das Sample aus einem Ihrer Songs zu entnehmen.

Literatur:

[1] Der Verwandlungskünstler, Teil 1, ST-Computer, 10/93

[2] Der Verwandlungskünstler, Teil 2, ST-Computer, 11/93

[3] Der Verwandlungskünstler, Teil 3, ST-Computer, 12/93

[4] DSP56000/DSP56001 Digital Signal Processor User's Manual, Motorola, 1990

[5] Das Buch zum ATARI Falcon030, Dietmar Hendricks, Alexander Herzlinger, Martin Pittelkow, Data Becker, 1. Auflage 1992


; Karaoke-Effekt DSP-Assembler 
; (0)1993 by MAXON-Computer  
; Autor: Jürgen Lietzow 

;--------- INTERRUPT ------------------------
   
I_RESET     EQU     $0000   ; hardware _RESET
I_STACK     EQU     $0002   ; stack error
I_SSIRD     EQU     $0000   ; SSI receive data
I_SSTRDE    EQU     $000E   ; SSI rec exception
I_SSITD     EQU     $0010   ; SSI transmit data
I_SSITDE    EQU     $0012   ; SSI tra exception
  
I_HSTRD     EQU     $0020   ; host receive data
I_HSTTD     EQU     $0022   ; host transmit data
I_HSTCM     EQU     $0024   ; host command

;--------- I/O EQUATES ---------------------

M_BCR       EQU     $FFFE   ; Port A Bus Control Reg
M_PBC       EQU     $FFE0   ; Port B Control Register
M_PCC       EQU     $FFE1   ; Port C Control Register
  
HCR         EQU     $FFE8   ; Host Control Register
HSR         EQU     $FFE9   ; Host Status Register
HRX         EQU     $FFEB   ; Host Receive Data Reg
HTX         EQU     $FFEB   ; Host Transmit Data Reg
  
M_RX        EQU     $FFEF   ; SSI Receive Data Reg
M_TX        EQU     $FFEF   ; SSI Transmit Data Reg
M_CRA       EQU     $FFEC   ; SSI Control Register A
M_CRB       EQU     $FFED   ; SSI Control Register B
M_SR        EQU     $FFEE   ; SSI Status Register
M_TSR       EQU     $FFEE   ; SSI Time Slot Register
M_IPR       EQU     $FFFF   ; Interrupt Priority Reg
M_UU        EQU     $FFEA 
  
VMAIN       EQU     $0040 
  
;---- Effects --  only use upper 16 bits -------
  
tKaraoke    EQU     8 
tLevel      EQU     9 
  
;---- RESET VECTOR -----------------------------

    ORG     P:I_RESET 
    JMP     VMAIN  

;---- STACK ERROR VECTOR -----------------------
  
    ORG     P:I_STACK 
    JMP     VSTACKERR 
  
;---- SSI RCV INTERRUPT VECTORS ----------------
  
    ORG     P:I_SSIRD   ; input 
    JSR     VLRTEST 
  
    ORG     P:I_SSIRDE  ; input error 
    JSR     VELRTEST 

    ORG     P:I_SSITD   ; output 
    JSR     VOUTPUT 
      
    ORG     P:I_SSITDE  ; short output, error 
    MOVEP   X:M_SR,X:M_SR 

;--------------------------------------------- 
; Start of Programm code 
;--------------------------------------------- 
 
    ORG     P:VMAIN 
    JMP     DoMain 
  

;--------------------------------------------- 
; Clear stack pointer and status 
;--------------------------------------------- 

VSTACKERR  
    ORI     #$03,MR     ; mask interrupts 
    MOVE    #>$00,SP 
    JMP     VSTACKERR 


;--------------------------------------------- 
; Receive interrupt routine + error routine  
;--------------------------------------------- 
   
VELRTEST  
    MOVEP   X:M_SR,X:M_UU   ; clear status 
VLRTEST  
    JCLR    #3,X:M_SR,NoFSync 
    MOVEP   X:M_RX,X:tDInFirst ; first in temp 
    MOVE    #tDataIn+1,R3 
    BSET    #0,X:IsSync     ; signal frame sync 
    RTI                     ; to main loop 
NoFSync 
    MOVEP   X:M_RX,X:(R3)+ 
    RTI  
   
;--------------------------------------------- 
; Transmit interrupt routine  
;--------------------------------------------- 

VOUTPUT  
    JSET    #2,X:M_SR,OUT_R     ; frame sync ? 
    MOVEP   X:OutLeft,X:M_TX 
    RTI 
OUT_R  
    MOVEP   X:OutRight,X:M_TX 
    MOVE    X:tRight,Y1         ; load next pair 
    MOVE    Y1,X:OutRight 
    MOVE    X:tLeft,Y1 
    MOVE    Y1,X:OutLeft 
    RTI  


;--------------------------------------------- 
; Init DSP  
;--------------------------------------------- 

DoMain  
    RESET  
    ORI     #$03,MR             ; Mask Interrupts 

    MOVE    #tDataIn,R3 
      
    MOVEP   #$1,X:M_PBC         ; Port B is Host 
    MOVEP   #$3000,X:M_IPR      ; SSI RCV INT level 2 
    MOVEP   #$4100,X:M_CRA      ; Set SSI 16 Bit 
                                ; 2 Frames 
    MOVEP   #$F800,X:M_CRB      ; Set SSI 
    MOVEP   #$0000,X:M_PCC      ; Individual reset / 
                                ; SSI Port 
    MOVEP   #$01F8,X:M_PCC      ; Turn on SSI Port.
    MOVEP   X:M_SR,X:M_UU       ; Clear Status 
    MOVEP   X:M_RX,X:M_SR       ; Clear Status

    ANDI    #$FC,MR             ; Unmask all interrupts

;--------------------------------------------- 
;  Start of forever loop
;  This is looped once for each input frame sync
;--------------------------------------------- 

;  Registers R3, and Y1 used by Interrupts 
;  Do not use them here!

MainLoop

; NOTE: we have to reach this point before 
;     start of second time slot.

    ORI     #$03,MR             ; Mask Interrupts 

    MOVE    X:Right,X0          ; transmit samples
    MOVE    X0,X:tRight         ; from last loop
    MOVE    X:Left,X0           ; to output 
    MOVE    X0,X:tLeft          ; interrupt handler
        
    MOVE    #DataLvls,R2        ; weight 2 channel
    MOVE    #tDataIn,R1         ; stereo input 
    
    MOVE    X:(R2)+,X0          ; mix 2 channels to 
                                ; one channel  
    MOVE    X:(R1)+,X1 
    MPY        X0,X1,A      X:(R2)+,X0 ; left 1
    MOVE    X:(R1)+,X1
    MPY        X0,XI,B      X:(R2)+,X0 ; right
    MOVE    X:(R1)+,X1   
    MACR    X0,X1,A     X:(R2)+,X0  ; left 2
    MOVE    X:(R1)+,X1
    MACR    X0,X1,B     A1,X Left   ; right 2
    
    MOVE    B1,X;Right   
    
    MOVE    X:tDInFirst,X0  ; we got already 
    MOVE    X0,X:tDataIn    ; first data 
    
    BCLR    #0,X:IsSync     ; free for next sync
    
    ANDI    #$FC,MR         ; Unmask all interrupts

; NOTE: we have to reach this point before 
;     end of second time slot.

;--------------------------------------------- 
;  Here you may include more Effects
;--------------------------------------------- 

    JSSET   #tKaraoke,X:Effect,Karaoke

;--------------------------------------------- 
;  Do we send peek levels to Host ?
;--------------------------------------------- 

    JSSET   #tLevel,X:Effect,Level

;--------------------------------------------- 
; While waiting for next frame sync, we 
; check if we get commands from Host
;--------------------------------------------- 

Wait
    JCLR    #0,X:«HSR,NoHostCommand
    MOVEP   X:«HRX,X0       ; Get Host 24 bit

    JSET    #7,X0,ReadXmem  ; read/write ?
    MOVE    #>$7F,A         ; lower 7 bits
    AND        X0,A        #>$7F,XI
    CMP        X1,A        A1,R1
    JNE        XLocation       ; #$7F is SSI CRA
    MOVE    #>$FF00,A 
    AND        X0,A
    MOVEP   A1,X:M_CRA 
    JMP        NoHostCommand
XLocation
    MOVE    #>$FFFF00,A     ; mask out
    AND        X0,A         ; 7 bit address
    MOVE    A1,X:(R1)       ; host value
    JMP        NoHostCommand
ReadXmem
    MOVE    #>$7F,A         ; lower 7 bits 
    AND        X0,A         ; is address 
    MOVE    A1,R1  
XMIT   
    JCLR    #1,X:«HSR,XMIT  ; wait for host 
    MOVEP   X:(R1),X:«HTX   ; send x memory 
NoHostCommand 
    JSET    #0,X:IsSync,MainLoop  
    JMP     Wait  
    
;--------------------------------------------- 
;  Karaoke Effect   
;--------------------------------------------- 

Karaoke   
    MOVE    X:Left,A  
    MOVE    X:Right,X0  
    SUB     X0,A            ; left - right 
    MOVE    A1,X:Left       ; left and right 
    MOVE    A1,X:Right      ; chanal is same 
    RTS

;--------------------------------------------- 
; Calculate Peek Level   
;--------------------------------------------- 
    
Level   
    MOVE    X:Counter,A     ; get peek of 
    MOVE    #$100,X0        ; #Counter samples 
    SUB     X0,A            ; Counter is in 
    JNE     LCNotZero       ; upper 16 bits 

    MOVE    X:tPeekRight,A1 ; ready for host 
    MOVE    A1,X:PeekRight  
    MOVE    X:tPeekLeft,A1  
    MOVE    A1,X:PeekLeft  
    MOVE    X:CountMax,A1  
    MOVE    #0,X0  
    MOVE    X0,X:tPeekLeft  
    MOVE    X0,X:tPeekRight  
   
LCNotZero   
    MOVE    A1,X:Counter    ; store new Counter 

    MOVE    X:tPeekLeft,X0  
    MOVE    X:Left,A  
    CMPM    X0,A X:tPeekRight,X0  
    JLE        LNext1
    ABS        A  
    MOVE    A1,X:tPeekLeft  
LNext1
    MOVE    X:Right,A  
    CMPM    X0,A  
    JLE        LNext 2  
    ABS        A  
    MOVE    A1,X;tPeekRight  
LNext2   
    RTS   

;--------------------------------------------- 
;  Data for Host I/O   
;--------------------------------------------- 
    ORG     X: $0  
   
DataLvls    DC      $3FFFO0  
            DC      $3FFF00  
            DC      $3FFF00  
            DC      $3FFF00  
Effect      DC      $000300 ; Bit map 
                            ; Karaoke + Levels 
CountMax    DC      $020000 ; sample number 
                            ; only upper 16 bits 
PeekLeft    DC      0       ; Peek left  
PeekRight   DC      0       ; Peek right
   
;--------------------------------------------- 
;  Internal Datas   
;--------------------------------------------- 
   
IsSync      DC      0       ; frame sync indicator  
Left        DC      0       ; current left  
Right       DC      0       ; current right  

                            ; the ones by interrupt 
OutLeft     DC      0       ; left output channel  
OutRight    DC      0       ; right output channel  
tLeft       DC      0       ; temp left output  
tRight      DC      0       ; temp right output  

tPeekLeft   DC      0       ; temp left peek
tPeekRight  DC      0       ; temp right peek  
Counter     DC      $000100 ; current counter  
                        ; only upper 16 bits 
tOutLeft    DC      0       ; only for transmit  
tOutRight   DC      0       ; interrupt.  
tDInFirst   DC      0 
tDataIn     DS      8       ; max. 8 time slots  
  
  END

/* Karaoke-Effekt Rahmenprogramm */
/* (c)1993 by MAXON-Computer /* Autor: Jurgen Lietzow

#include <stdlib.h> 
#include <stdio.h> 
#include <string.h> 
#include <ctype.h> 
#include <time.h>

#ifndef __GNU__
    #include <tos.h>
    #define _read(h,b,l)    read(h,b,l)
    #define fflush(a) 
#else
    #include <osbind.h> 
    #include <fcntl.h> 
    #include <unistd.h>
#endif

#include "dsplib.h"

/* füllt char buffer mit 3 Byte Worten */

#define Fill3(p,l,v) \ 
         (p[(l)*3+0]=(char)((v)»16),\
          p[(l)*3+1]=(char)((v)»8), \ 
          p[(l)*3+2]=(char)(v) )

/* DSP Kommandos */

#define     HOST_SEND    0x000000L
#define     HOST_RECEIVE 0x000080L

/* DSP Speicherplätze */

#define     LEVEL_L1 0x000000L
#define     LEVEL_R1 0x000001L
#define     LEVEL_L2 0x000002L
#define     LEVEL_R2 0x000003L
#define     EFFECT   0x000004L
#define     SMPL_CNT 0x000005L
#define     PEEK_LEFT  0x000006L
#define     PEEK_RIGHT 0x000007L

    /* CRA ist Pseudo-Speicher */
    /* immer 16 Bit samples */

#define     SSI_CRA  0x00407FL

/* Bit-Maske für Effekte */

#define     KARAOKE     0x000100L 
#define     LEVEL_MTR   0x000200L

/* SSI CRA Werte (frames per sync) */

#define     FRAMES_2    0x000100L
#define     FRAMES 4    0x000300L

void PrintAt( int c, int l, char *t ) 
{
    char buf[16] = "\33Y";
    
    buf[2] = ' ' + l; 
    buf[3] = ' ' + c; 
    buf[4] = '\0';

    Cconws( buf );
    Cconws( t );
} 

int     RunDSP( char *dsp_lod ) 
{
    long xmem, ymem, x, y;
    int  nblocks; 
    void *buf; 
    long len;
    int  ability;

    /* Starte ein DSP-Programm */

    ability = Dsp_RequestUniqueAbility(); 
    Dsp_Available( &xmem, &ymem );

    nblocks =50;
    x = 1000;
    y = 2000;
    if ( xmem < x || ymem < y )
    {
        Dsp_FlushSubroutines();
        Dsp_Available( &xmem, &ymem ); 
        if ( xmem < x || ymem < y )
            return ( -1 );
    }

    if ( Dsp_Reserve( x, y ) != 0 ) 
        return ( -1 );

    len = 3* (x+y+(3*nblocks)); 

    if ( ( buf = malloc( len ) ) == NULL ) 
        return ( -1 );

    len = Dsp_LodToBinary( dsp_lod, buf ); 
    Dsp_ExecProg( buf, len, ability );

    return ( ability );
}

long effect = KARAOKE|LEVEL_MTR; 
long left = 0x3fff00L;
long right = 0x3fff00L;
char host_io[16];
int  lleft = 0;
int  lright = 0;

int main( void )
{
    long curadder, curadc; 
    char *sndbuf;
    char c;
    int handle;
    int prescale;
    long length;
    long l;

    /* Ist überhaupt ein DSP vorhanden, */
    /* und ist es ein DSP mit Wortgröße 3 ? */

    if ( Dsp_GetWordSize() != 3 ) 
        return ( -1 );

    /* sperre sound system und DSP */

    if ( locksnd() != 1 ) 
        return ( -1 );

    if ( Dsp_Lock() != 0 ) 
    {
        unlocksnd();
        return ( -1 );
    }

    /* resette CODEC wenn Fehler */

    if ( sndstatus( 0 ) )
        sndstatus( 1 );

    /* keine externe Hardware */

    dsptristate(ENABLE, ENABLE );
    if ( RunDSP( "KARAOKE.LOD" ) == -1 ) 
    {
        Dsp_Unlock();
        unlocksnd();
        return ( -1 );
    }

    /* ein Stereo-Kanal (2 frames) */

    Fill3 ( host_io, 0, SSI_CRA|FRAMES_2 );
    Dsp_BlkHandshake ( host_io, 1, NULL, 0 );
    Fill3( host_io, 0, HOST_SEND|EFFECT|KARAOKE|LEVEL_MTR );
    Dsp_BlkHandshake( host_io, 1, NULL, 0 );

    soundcmd( LTATTEN, 0x80 ); /* Ausgabekanal */
    soundcmd( RTATTEN, 0x80 );

    soundcmd( LTGAIN, 0x80 ); /* Eingabekanal */
    soundcmd( RTGAIN, 0x80 );

    /* Addierereingang nur von Matrix */

    curadder = soundcmd( ADDERIN, INQUIRE );
    soundcmd( ADDERIN, 2 );

    /* setze ADC Eingang auf Mikrofon */

    curado = soundcmd( ADCINPUT, INQUIRE );
    soundcmd( ADCINPUT, 0 );

    setmontrack(0);/* Lautsprecher = erste Spur */

    printf( "\nEingang von \"CABO.HSNV (M)"
            " oder über Mikrofon (F)?" );
    fflush( stdout );
    if ( toupper( (char)Bconin( 2 ) ) == 'M' )
    {
        /* lade Sample-Datei */

        prescale = 11;
        length = 214794L;
        if ( ( sndbuf = malloc( length ) ) == NULL )
            return ( -1 );

        if ( (handle = open( "CABO.HSN", O_RDONLY )) <= 0 )
            return ( -1 );

        /* ohne header */

        if ( _read( handle, sndbuf, 128 ) < 128 )
            return ( -1 );

        if (_read(handle, sndbuf, length) != length)
            return ( -1 );
        close( handle );

        Fill3(host,io,0, HOST_SEND|SMPL_CNT|((16000L/40)«8));
        Dsp_BlkHandshake ( host_io, 1, NULL, 0 );

        setsndmode( STEREO8 );

        /* setze DMA Abspielpuffer */

        setbuffer( 0, &sndbuf[0], &sndbuf[length] );
        settrack( 1, 0 ); /* eine Wiedergabespur */

        /* verbinde DMA-Quelle mit DSP-Eingang */

        devconnect( DMAPLAY,DSPREC, CLK25M, prescale, NO_SHAKE );

        /* verbinde DSP-Ausgang mit DAC */

        devconnect( DSPXMIT,DAC, CLK25M, prescale, NO_SHAKE );

        buffoper( PLAY_ENABLE | PLAY_REPEAT );
    }
    else
    {
        setsndmode{ STEREO16 );
        Fill3(host_io,0, HOST_SEND | SMPL_CNT|((50000L/40)«8 ) );
        Dsp_BlkHandshake ( host_io, 1, NULL, 0 );
        /* verbinde ADC-Eingang mit DSP */

        devconnect( ADC,DSPREC, CLK25M, 1,NO_SHAKE );

        /* verbinde DSP-Ausgang mit DAC */

        devconnect( DSPXMIT,DAC,CLK25M, 1,NO_SHAKE );
    }
/* warte auf Abbruchbedingung */
    PrintAt( 0, 1, "\33f" ); /* Cursor aus */

    printf(
    "\nTaste K für Karaoke an/aus"
    "\nTaste < für linken Kanal verstärken"
    "\nTaste > für rechten Kanal verstärken"
    "\nTaste M für Kanalmitte"
    "\nTaste Return für Abbruch" );
    PrintAt(0,8, "Links  :");
    PrintAt(0,9, "Rechts :");

    fflush( stdout );
    c = 1;
    while ( c )
    {
        if ( effect & LEVEL_MTR )
        {
            Fill3(host_io,0,HOST_RECEIVE|PEEK_LEFT) ;
            Dsp_BlkHandshake( host_io, 1, host_io, 1 );
            host_io[0] »= 1;
            if ( host_io[0] > lleft )
                while ( host_io[0] != lleft )
                    PrintAt (9 + ++lleft,8,"-");
            else
                while ( host_io[0] != lleft )
                    PrintAt(9 + lleft--,8," ");
            Fill3(host_io,0,HOST_RECEIVE|PEEK_RIGHT);
            Dsp_BlkHandshake ( host_io, 1, host_io, 1 );
            host_io[0] »= 1;
            if ( host_io[0] > lright )
                while ( host_io[0] != lright )
                    PrintAt(9 + ++lright,9,"-");
            else
                while ( host_io[0) != lright )
                    PrintAt(9 + lright--,9," ");
            fflush( stdout );
        }
        for ( l = 0; l < 60; l++ )
            if ( Bconstat( 2 ) )
                switch ( toupper((char) Bconin( 2 )) )
                {
                    case '\r': c = 0; break;
                    case 'K' : effect ^= KARAOKE;
                        Fill3 (host_io, 0, HOST_SEND|effect|EFFECT);
                        Dsp_BlkHandshake( host_io, 1, NULL, 0 );
                        break;
                    case '<' : if ( right - 0x80O0L > 0 )
                                {
                                    left += 0x8000L;
                                    right -= 0x8000L;
                                    goto send_level;
                                }
                        break;
                    case '>' : if ( left - 0x8000L > 0 )
                                {
                                    left -= 0x8000L;
                                    right += 0x8000L;
            send_level:             Fill3(host_io,0,HOST_SEND|left|LEVEL_L1);
                                    Fill3(host_io,1,HOST_SEND|right|LEVEL_R1);
                                    Dsp_BlkHandshake( host_io, 2,NULL, 0 );
                                }
                                break;
                    case 'M' : left = right = 0x3fff00L;
                        goto send_level;
                    default : break;
                }
    }

    /* setze die Drsprungsdaten */

    buffoper( 0 );
    soundcmd( ADDERIN, (int) curadder );
    soundcmd( ADCINPUT, (int; curadc );

    Dsp_Unlock();
    unlocksnd();

    return ( 0 );
}

Übersicht DSP-Programmierkurs

Teil 1: Register und Befehlssatz des DSP 56001

Teil 2: I/O-Schnittstellen und I/O-Programmierung des DSP 56001 und des MC68030

Teil 3: Programmier-Tools des DSP 56001 (Assembler, Linker, Debugger)

Teil 4: Tips und Tricks mit Programmbeispiel


Jürgen Lietzow
Aus: ST-Computer 01 / 1994, Seite 108

Links

Copyright-Bestimmungen: siehe Über diese Seite