STE-Soundbox, Teil 3: Der STE als Einfachsynthesizer

Daß man mit dem STE nicht nur Frames ausgeben kann, sondern auch die Möglichkeit hat, auf die Lautstärke und den Klang eines Frames Einfluß zu nehmen, wurde in der letzten Folge besprochen. In der vorliegenden Folge geht es nun darum, diese Funktionen zur Erzeugung von Synthesizerklängen zu nutzen.

Alle älteren Synthesizermodelle - und auch viele der modernen digitalen Synthies - orientieren sich in ihrem Aufbau mehr oder weniger an ihrem Urvater, dem Moog-Synthesizer.

Die klassische Synthesizerstruktur...

...ist in Abb. 1 dargestellt. Ganz links sieht man die Klangerzeugung, welche bei den älteren Synthies aus Tongeneratoren mit unterschiedlicher Kurvenform (z.B. Dreieck-, Sinus- oder Rechteckgeneratoren)be-steht. Die Klangerzeugung vieler moderner Modelle kann bereits fertige Klänge (z.B. Streicher oder Klavier) abrufen. Auch mit dem DMA-Sound des STE ist es möglich, komplexere Klänge wiederzugeben, man kann sich ja seinen Frame beliebig zusammenbasteln.

Die nächste in sich geschlossene Einheit stellen die steuerbaren Filter dar. Sie gestatten eine nachträgliche Veränderung von Klängen, indem bestimmte Frequenzen angehoben bzw. abgesenkt werden. Ein großes Manko der Klangerzeugung besteht darin, daß sie in den meisten Fällen nicht in der Lage ist, den erzeugten Klang nachträglich zu verändern. Doch gerade dies trägt zur Natürlichkeit eines Klanges wesentlich bei. Hier nun kann man sich mit steuerbaren Filtern behelfen, denn sie lassen eine Änderung ihrer Parameter (Filterfrequenz, Stärke der Absenkung bzw. Anhebung) während der Bearbeitung zu. Die Filter im STE besitzen, was die Flexibilität betrifft, nur eingeschränkte Verwendbarkeit als Synthesizerfilter. Es ist zwar möglich, Frequenzen um einen bestimmten Betrag anzuheben bzw. abzusenken, doch beschränkt sich dies nur auf die beiden Frequenzen 15 kHz und 50 Hz.

Nach der Filtersektion durchläuft das Signal einen steuerbaren Verstärker. Er hat zur Aufgabe, den Lautstärkeverlauf (auch Amplitudenverlauf) eines Klanges nachzubilden. Beispiele hierfür sind wieder bei konventionellen Musikinstrumenten zu finden: ein Klavierton beginnt laut und klingt dann allmählich aus, eine gestrichene Violine setzt leise ein und wird immer lauter. Mit den Fähigkeiten des LMC1992 im STE ist es möglich, einen brauchbaren steuerbaren Verstärker zu realisieren.

LFO...

... ist die Abkürzung für ‘Low Frequency Oscillator’. Es handelt sich hier um einen Tongenerator mit einer (zumeist einstellbaren) Frequenz zwischen 0 und 20 Hz. Dieser kann zur Steuerung einer der drei Grundeinheiten verwendet werden.

Angewandt auf die Klangerzeugung bewirkt ein LFO eine langsame, periodische Frequenzänderung, ein sogenanntes Vibrato. Steuert man einen Filter mit dem LFO an, so entstehen periodische Klangänderungen (z.B. Wha-Wha-Effekte). Schließlich kann ein LFO auch den Verstärker steuern. Der pfiffige Leser kann sich natürlich schon denken, was dann passiert. Richtig - es entsteht eine periodische Amplitudenänderung (Tremolo).

Im STE kann man LFOs für alle 3 Einheiten nachbilden. Bei der Klangerzeugung ist dies allerdings ein wenig verzwickt. Man müßte mehrere Frames mit leicht abweichender Frequenz füllen und diese Frames dann periodisch umschalten - ein Thema, auf das ausführlich in der nächsten Folge eingegangen wird.

Der Hüllkurvengenerator...

... dient wie der LFO zur Steuerung von Synthesizereinheiten, speziell der Filtersektion und des Verstärkers. Sinn dieses Moduls ist es, nicht wie der LFO periodische, sich immer wiederholende Änderungen einzustellen, sondern Klang oder Amplitude einmal in der Gesamtheit zu beeinflussen. Ein Blick auf Abb. 2 verdeutlicht dies am Beispiel der Amplitudenhüllkurve.

Man erkennt, daß die Hüllkurve in vier Phasen eingeteilt ist. Die Dauer der einzelnen Phasen sowie das Sustainlevel (siehe unten) sind frei einstellbar. In der Attack-phase steigt die Amplitude des Klanges von ihrem minimalen auf ihren maximalen Wert. Eine kurze Attackphase bildet beispielsweise den harten Anschlag eines Klaviers nach, eine lange Attackphase das langsame Anklingen einer gestrichenen Saite. In der Decayphase fällt die Lautstärke auf das Sustainlevel ab. Das Sustainlevel wird während der gesamten Sustain-phase beibehalten. Die Releasephase schließlich dient zum Ausblenden des Klanges.

Abb. 2: Typischer Hüllkurvenverlauf am Beispiel der Amplitudenhüllkurve

Die Implementation...

... sieht bereits folgende Möglichkeiten vor (siehe hierzu das Listing ‘SYNTH.C’ in Verbindung mit den in der letzten Folge vorgestellten Assembler-Routinen):

Das Verfahren der Klangerzeugung dürfte inzwischen hinreichend bekannt sein. Dem Spieltrieb sind hier keine Grenzen gesetzt (im Listing habe ich z.B. noch die Erzeugung eines Rauschsignals implementiert, der Aufruf ist jedoch ausgesternt).

Die Filter und der steuerbare Verstärker sind mit den Assembler-Funktionen der letzten Folgen realisiert. Die drei Hüllkurven wurden in einer Struktur namens HUELLKURVE zusammengefaßt (siehe Kommentare bei der Deklaration in ‘SYNTH.C’)- In diese Struktur werden die gewünschten Werte der drei Hüllkurven eingetragen. Danach kann der Aufruf der Funktion ton_ausga.be erfolgen, welche die komplette Klangsteuerung übernimmt.

Abb. 1: Der grundlegende Aufbau von Synthesizern

Natürlich läßt das vorgestellte Listing noch einige Wünsche offen. Diese zu realisieren, bleibt dem Leser als Übung selbst überlassen. Als da wären :

Literatur: STE Developer Addendum

p:synth.prg = 
tcstart.o 
sout.s 
synth.c 
tcfltlib.lib 
tcstdlib.lib 
tctoslib.lib 
tcextlib.lib
/* -------------------------------------- */
/* --- SYNTH C: Ausgabe eines Tones mit - */
/* --- Amplituden- und Klanghullkurve   - */
/* ---                                --- */
/* --- In Turbo-C 2 0 implementiert von 
       Peter Engler                   --- */
/* -------------------------------------- */

#include <stdio.h>
#include <stdlib.h>
#include <ext.h>

#include "sout.h"

/* --- Typdeklaration --- */

typedef struct 
{
    /* Einheit der Zeitwerte ist 1 ms ! */
    /* Sustainlevel (..._slevel) werden in dB angegeben --- */

    /* --- Amplitudenhüllkurve --- */
    int amp_attack, amp_decay, amp_sustain, 
        amp_slevel, amp_release;
    /* --- Hüllkurve für Höhenfilter --- */
    int hoe_attack, hoe_decay, hoe_sustain, 
        hoe_slevel, hoe_release;
    /* --- Hüllkurve für Tiefenfilter --- */
    int tie_attack, tie_decay, tie_sustain, 
        tie_slevel, tie_release;

} HUELLKURVE;

/* --- Prototypen der im Modul verwendeten Funktionen --- */

int snd_alloc( SOUND *, unsigned long ); 
void snd_free( SOUND * ); 
void rauschen( SOUND * ); 
void rechteck( SOUND * );
void ton_ausgabe( SOUND *, HUELLKURVE * ); 
int main( void );

/* --- Anlegen des Arrays für 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 für ein Rechteck --- */

void rechteck ( SOUND *snd )
{
    unsigned long bytes_pro_halbperiode, index;
    char *h_ptr;

    h_ptr = snd -> s_ptr;

    /* --- Berechnen der Bytes, die pro Halbperiode ausgegeben werden --- */
    bytes_pro_halbperiode = (snd -> bytes_pro_sekunde / snd -> frequenz) / 2L;

    if ((bytes_pro_halbperiode % 2) == 1) 
        bytes_pro_halbperiode++;

    snd -> anz_bytes = bytes_pro_halbperiode * 2L;

    /* --- Eintragen der Werte für das Rechteck in die SOUND-Struktur --- */
    for (index = 0; index < bytes_pro_halbperiode; index++)
    {
        *h_ptr++ = (char) 127;
    }

    for (index = bytes_pro_halbperiode; index > 0 ; index—-)
    {
        *h_ptr++ = (char) -128;
    }

}



/* --- Generieren der Werte für das Rauschen --- */

void rauschen( SOUND *snd )
{
    unsigned long bytes_pro_periode, index; 
    char *h_ptr; 
    time_t ticks;

    h_ptr = snd -> s_ptr;

    /* --- Berechnen der Bytes, die pro Periode ausgegeben werden --- */
    bytes_pro_periode = snd -> bytes_pro_sekunde / snd -> frequenz;

    if ((bytes_pro_periode % 2) == 1) 
        bytes_pro_periode++;


    snd -> anz_bytes = bytes_pro_periode; 
    srand ((unsigned int) time(&ticks) );

    /* --- Eintragen der zufälligen Werte in die SOUND-Struktur --- */
    for (index = 0; index < bytes_pro_periode; index += 2)
    {
        *h_ptr++ = (char) ( (rand( ) % 256) * (rand( ) % 256) ); 
        *h_ptr++ = (char) 0;
    }

}


void ton_ausgabe( SOUND *snd, HUELLKURVE * hlk )
{
    int max_index, index; 
        float amp_attack_mc, amp_decay_dec, amp_release_dec, 
        hoe_attack_inc, hoe_decay_dec, hoe_release_dec,
        tie_attack_inc, tie_decay_dec, tie_release_dec,
        amplitude, tiefen, hoehen;

    /* --- Lautstärke auf -80 dB --- */
    snd_laut( -80 ); 
    snd_hoehen( -12 ); 
    snd_tiefen( -12 );

    /* --- Lange des Tones berechnen (in Einheiten zu 10ms) --- */
    max_index = hlk -> amp_attack + hlk -> amp_decay +
                hlk -> amp_sustain + hlk -> amp_release;


    /* --- Wert eines Amplitudenschrittes in der Attackphase --- */
    if (! hlk -> amp_attack) 
        amp_attack_inc = 0.0;
    else
        amp_attack_inc = 80.0 / (float) hlk -> amp_attack;

    /* --- Wert eines Amplitudenschrittes in der Decayphase --- */
    if (! hlk -> amp_decay) 
        amp_decay_dec = 0.0;
    else
        amp_decay_dec = ( (float) hlk -> amp_slevel ) / (float) hlk -> amp_decay;

    /* --- Wert eines Amplitudenschrittes in der Releasephase ---- */
    if (! hlk -> amp_release) 
        amp_release_dec = 0.0; 
    else
        amp_release_dec = ( (float) ( -80.0 -h lk -> amp_slevel )) / (float) hlk -> amp_release;

    /* --- Wert eines Höhenschrittes in der Attackphase --- */
    if (! hlk -> hoe_attack) 
        hoe_attack_inc = 0.0;
    else
        hoe_attack_inc = 24.0 / (float) hlk -> hoe_attack;

    /* --- Wert eines Hoehenschrittes in der Decayphase --- */
    if (! hlk -> hoe_decay) 
        hoe_decay_dec = 0.0;
    else
        hoe_decay_dec = ( -12.0 + (float) hlk -> hoe_slevel ) / (float) hlk -> hoe_decay;

    /* --- Wert eines Hoehenschrittes in der Releasephase --- */
    if (! hlk -> hoe_release) 
        hoe_release_dec = 0.0; 
    else
        hoe_release_dec = ( (float) ( -12.0 - hlk -> hoe_slevel )) / (float) hlk -> hoe_release;

    /* --- Wert eines Tiefenschrittes in der Attackphase --- */
    if (! hlk -> tie_attack) 
        tie_attack_inc = 0.0; 
    else
        tie_attack_inc = 24.0 / (float) hlk -> tie_attack;

    /* --- Wert eines Amplitudenschrittes in der Decayphase --- */
    if (! hlk -> tie_decay) 
        tie_decay_dec = 0.0;
    else
        tie_decay_dec = ( -12.0 + (float) hlk -> tie_slevel ) / (float) hlk -> tie_decay;

    /* --- Wert eines Amplitudenschrittes in der Releasephase --- */
    if (! hlk -> tie_release) 
        tie_release_dec = 0.0; 
    else
        tie_release_dec = ( (float) ( -12.0 - hlk -> tie_slevel )) / (float) hlk -> tie_release;

    /* --- Ton ausgeben --- */
    snd_play( snd );

    /* --- Startwerte fur Amplitude, Hoehen und Tiefen --- */
    amplitude = -80.0; 
    tiefen = hoehen = -12.0;

    /* --- Amplituden - Attackphase 0 ms? --- */ 
    if (! amp_attack_inc)
    {
        /* --- Zusätzlich Decayphase 0 ms ? 
               dann gleich auf Sustainlevel einstellen --- */
        if (! amp_decay_dec)
        {
            snd_laut( hlk -> amp_slevel ); 
            amplitude = (float) hlk -> amp_slevel;
        }
        else
        {
            snd_laut( 0 ); 
            amplitude = 0.0;
        }
    }

    /* --- Hoehen - Attackphase 0 ms ? --- */
    if (! hoe_attack_inc)
    {
        /* --- Zusätzlich Decayphase 0 ms ?
               dann gleich auf Sustainlevel einstellen --- */
        if (! hoe_decay_dec)
        {
            snd_hoehen( hlk -> hoe_slevel ); 
            hoehen = (float) hlk -> hoe_slevel;
        }
        else
        {
            snd_hoehen( 12 ); 
            hoehen = 12.0;
        }
    }


    /* --- Tiefen - Attackphase 0 ms ? --- */
    if (! tie_attack_inc)
    {
        /* --- Zusätzlich Decayphase 0 ms ?
               dann gleich auf Sustainlevel einstellen --- */
        if (! tie_decay_dec)
        {
            snd_tiefen( hlk -> tie_slevel ); 
            tiefen = (float) hlk -> tie_slevel;
        }
        else
        {
            snd_tiefen( 12 ); 
            tiefen = 12.0;
        }
    }

    /* --- Amplituden- und Klanghüllkurve ausgeben --- */
    for ( index = 0;index < max_index; index++)
    {
        /*   Bereichsüberschreitungen abfangen --- */
        if (amplitude >0.0) 
            amplitude = 0.0; 
        else
            if (amplitude < -80.0) 
                amplitude = -80.0;
        if (tiefen > 12.0) 
            tiefen = 12.0;
        else
            if (tiefen < -12.0) 
                tiefen = -12.0;

        if (hoehen > 12.0) 
            hoehen = 12.0; 
        else
            if (hoehen < -12.0) 
                hoehen = -12.0;

        /* --- Lautstärke, Hoehen und Tiefen setzen --- */
        snd_laut( (int) amplitude ); 
        snd_hoehen( (int) hoehen ); 
        snd_tiefen( (int) tiefen );

        /* --- 1 ms Verzögerung --- */
        delay( 1 );

        /* --- Verändern der Amplitude gemäss der Hüllkurve --- */
        /* --- Attackphase ? --- */
        if (index < hlk -> amp_attack)
        {
            amplitude += amp_attack_inc;
        }
        else
        /* --- Decayphase ? --- */
        if (index < (hlk -> amp_decay + hlk -> amp_attack))
        {
            amplitude += amp_decay_dec;
        }
        else
        /* --- Releasephase ? --- */
        if (index > (hlk -> amp_decay + hlk -> amp_attack + hlk -> amp_sustain))
        {
            amplitude += amp_release_dec;
        }

        /* --- Verändern der Höhen gemäss der Hüllkurve --- */
        /* --- Attackphase ? --- */
        if (index < hlk -> hoe_attack)
        {
            hoehen += hoe_attack_inc;
        }
        else
        /* --- Decayphase ? --- */
        if (index < (hlk -> hoe_decay + hlk -> hoe_attack))
        {
            hoehen += hoe_decay_dec;
        }
        else
        /* --- Releasephase ? --- */
        if (index > (hlk -> hoe_decay + hlk -> hoe_attack + hlk -> hoe_sustain))
        {
            hoehen += hoe_release_dec;
        }

        /* --- Verändern der Tiefen gemäss der Hüllkurve --- */
        /* --- Attackphase ? --- */
        if (index < hlk -> tie_attack)
        {
            tiefen += tie_attack_inc;
        }
        else
        /* --- Decayphase ? --- */
        if (index < (hlk -> tie_decay + hlk -> tie_attack))
        {
            tiefen += tie_decay_dec;
        }
        else
        /* --- Releasephase ? --- */
        if (index > (hlk -> tie_decay + hlk -> tie_attack + hlk -> tie_sustain))
        {
            tiefen += tie_release_dec;
        }



    }

    /* --- Ton beenden --- */
    snd_stop( );

}

int main( )
{
    SOUND snd;
    HUELLKURVE hlk;

    /* --- Setzen von 'mode_reg' vor 1.
       Aufruf von 'snd_alloc' !! ---- */
    snd.mode_reg = MOD_FR50K | MOD MONO; 
    snd.control_reg = SND_IMMER;

    /* --- Array anlegen --- */
    if (snd_alloc( Ssnd, 65536L)) return(-1);

    snd.frequenz = 110; 
    snd_init( );

    /* --- Werte fur Rechteck in den Frame eintragen --- */
    /* rauschen( &snd ); */ 
    rechteck( &snd );

    /* —-- Amplitudenhüllkurve festlegen —-- */
    hlk.amp_attack = 20;    /* --- in ms --- */
    hlk.amp_decay  = 20;    /* --- in ms --- */
    hlk.amp_slevel = -10;   /* --- in dB --- */
    hlk.amp_sustain = 200;  /* --- in ms --- */
    hlk.amp_release = 100;  /* --- in ms --- */

    /* --- Hüllkurve fur Höhenfilter festlegen (15 kHz) --- */
    hlk.hoe_attack  = 5;    /* --- in ms --- */
    hlk.hoe_decay   = 10;   /* --- in ms --- */
    hlk.hoe_slevel  = 0;    /* --- in dB --- */
    hlk.hoe_sustain = 320;  /* --- in ms --- */
    hlk.hoe_release = 0,    /* --- in ms --- */

    /* --- Hüllkurve für Tiefenfilter festlegen (50 Hz) --- */
    hlk.tie_attack  = 100;  /* --- in ms --- */
    hlk.tie_decay   = 100;  /* --- in ms --- */
    hlk.tie_slevel  = -6;   /* --- in dB --- */
    hlk tie_sustain = 100;  /* --- in ms --- */
    hlk.tie_release = 5;    /* --- in ms --- */

    while ( getch( ) != 27 )
    {
        ton_ausgabe( &snd, &hlk );
    }

    snd_init( ); 
    snd_free( &snd );

    return( 0 );
}

Peter Engler
Aus: ST-Computer 11 / 1991, Seite 128

Links

Copyright-Bestimmungen: siehe Über diese Seite