Fun met functies: geluid

Er zit muziek in functies! In een tijd waarin functioneel programmeren een hoge buzzword index heeft is het wellicht leuk om te kijken wat voor rare dingen je kunt doen met zogenaamde pure functies (functies waarbij de uitvoer alleen wordt bepaald door de invoer). In dit artikel gaan we muziek dan wel geluid maken met functies.

Auteur: Erik Hooijmeijer

Als je gaat experimenteren, zet het volume dan laag; sommige van deze signalen zijn onvriendelijk voor luidsprekers en versterkers. Daarnaast zullen de kat en je collega’s het ook weten te waarderen.

Geluid en Java: Java Sound API

Als je webapplicaties maakt heb je de Java Sound API meestal niet nodig, maar toch bestaat deze al sinds Java 1.3; naast het afspelen van samples, wat we hier gaan doen, kan je er ook geluid mee opnemen en er MIDI-apparaten mee aansturen. Als je samples gaat afspelen, zijn er twee dingen belangrijk: het aantal samples per seconde (de samplefrequentie) en het aantal bits van je sample. Met 8 bits heb je 28 = 256 verschillende signaalniveaus. Hoe meer bits, hoe preciezer je het signaal kunt definiëren. CD-kwaliteit audio heeft een samplefrequentie van 44100 Hertz (Hz) en 16-bits samples. Echter, wij gaan de lo-fi route met 8000 Hertz en 8-bits samples, oh en ook nog mono.

Als je dat wil uitdrukken met de Java Sound API, dan heb je een AudioFormat nodig waarmee je de samplefrequentie, het aantal bits en het aantal kanalen kunt aangeven. Daarnaast zijn er nog twee parameters waarmee aangegeven wordt hoe de bytes geïnterpreteerd worden. Heb je het AudioFormat dan kan daarmee om een passende zogenaamde SourceDataLine  gevraagd worden. Deze kan geopend en gestart worden en vervolgens kun je byte voor byte je samples aanleveren. Wanneer de samples op zijn, moet je nog even wachten totdat ze daadwerkelijk allemaal gespeeld zijn alvorens de line gestopt en gesloten kan worden. Onderstaande functie implementeert dit: als invoer de samplefrequentie, de duur van het geluid en .. de functie die de samples gaat maken!

import javax.sound.sampled.*

public static void play(int frequency, int durationInMs, Function<Integer, Integer> fun) throws LineUnavailableException {
    AudioFormat af = new AudioFormat((float) frequency, 8, 1, true, false);
    try (SourceDataLine sdl = AudioSystem.getSourceDataLine(af)) {
        sdl.open();
        sdl.start();
        byte[] buf = new byte[1];
        for (int t = 0; t < durationInMs * frequency / 1000; t++) {
            buf[0] = (byte) (fun.apply(t) & 0xFF);
            sdl.write(buf, 0, 1);
        }
        sdl.drain();
        sdl.stop();
    }
}

De definitie van de functie zegt dat er een integer in gaat. Dit is het nummer van de sample waar om gevraagd wordt; de uitvoerwaarde (deze integer moet tussen -128 en +127 liggen) is de sample zelf.

Basis golfvormen

De basisvorm van al het geluid is de wiskundige sinus. Niet zo zeer omdat hij zo mooi klinkt, maar meer omdat je alle andere golfvormen kan uitdrukken in een verzameling bij elkaar opgetelde sinusgolfvormen. Meneer Fourier heeft dat ooit uitgevonden. Als je een sinusgolfvorm wilt maken met een functie, dan kan dat zoals in het onderstaande voorbeeld. De functie meegegeven aan de methode play berekent de sinus op basis van een fractie van de gevraagde sample (hiermee kan je de toonhoogte variëren). Dit levert een waarde tussen de -1 en de 1 op waar je niet zo veel mee kunt als sample, daarom wordt deze vermenigvuldigd met 127 om optimaal van onze 8 bit sample gebruik te kunnen maken (een byte in Java kan je gebruiken om een waarde tussen de -128 en +127 aan te duiden).

De sinusgolfvorm

public static void main(String[] args) throws LineUnavailableException {
    play(8000, 1000, (t) -> (int)(127.0* Math.sin(t/4.0)));
}

Hoe klinkt dat dan? Dat kan je alleen maar zelf uit proberen 🙂 Ik kan je hoogstens een plaatje laten zien:

Een enkele sinus klinkt niet zo heel spannend. Met een blokgolf kan je veel meer. In het volgende voorbeeld gaan we een reeks van sinussen bij elkaar optellen om zo een blokgolf te benaderen, gewoon omdat het kan! Door naast de basistoon ook de oneven harmonischen (een woord voor tonen die een frequentie hebben die een veelvoud zijn van de basistoon) voor een fractie bij elkaar op te tellen kan je een blokgolf maken. Als functie kan je ‘m zo opschrijven:

play(8000,1000, (t) -> {
    double result = 0;
    for (int h=1; h<=9; h+=2) {
        result += 1.0/h * Math.sin(t/4.0*h);
    }
    return (int)(127 * result);
});

De extra loop voegt de fractie van de harmonischen toe. Door meer iteraties toe te voegen benadert de resulterende golfvorm steeds beter een blokgolf:

Het leuke is dat met meer iteraties de klank ook verandert. Je zou kunnen proberen om het aantal iteraties afhankelijk te maken van het samplenummer, bijvoorbeeld t/150. Uiteraard is het berekenen van een reeks van sinussen best wel een rekenintensieve operatie. Een shortcut is dan toch veel makkelijker; in onderstaand voorbeeld gebruiken we de modulo (rest van een deling) operator om iedere 32 samples de helft van de tijd de maximale waarden en de andere helft de minimale samplewaarde terug te sturen.

play(8000, 1000, (t) -> (t % 32 < 16 ? 127 : -128));

Het geluid van binaire operaties

In Java hebben we naast de binaire or (|), and (&) en exclusive-or (^) operaties ook schuifoperaties (<< en >>) welke gebruikt kunnen worden om interessant klinkende geluiden te maken. Deze operatoren hebben altijd twee invoerwaarden nodig, echter als input heb je alleen het samplenummer. Dus moet je eerst een afgeleide waarde maken alvorens er een operator op los te laten. Gelukkig is dat niet moeilijk 🙂 Onderstaand voorbeeld maakt de afgeleide waarde met een schuifoperatie (een vermenigvuldiging met 2) welke vervolgens wordt ge-exord met de originele waarde. Dit levert een leuk plaatje op (maar qua geluid valt het wat tegen):

play(8000, 1000, (t) -> t ^ t << 1);

Een stuk leuker wordt het wanneer de tijd het verloop van de golfvorm gaat beïnvloeden. Dat kan bijvoorbeeld heel leuk met de % operator. In het volgende voorbeeld doen we een & operatie op het samplenummer en de % 255 van het samplenummer. Omdat 255 exact één scheelt met de volgende macht van 2, 256, ontstaat een schuivend effect waardoor de & telkens een iets andere golfvorm produceert. Het duurt even voordat het effect goed te horen is, vandaar de langere speelduur van 5 seconden.

play(8000, 5000, (t) -> (t & t % 255));

Weer anders wordt het als je een hoge toon en een lage met elkaar laat interfereren. Een hoge toon maak je door t te vermenigvuldigen; je drukt de tijd samen. Je kan dit uitrekenen, normaal heeft een zaagtandgolfvorm ( f(t) = t ) een cyclus van 256 samples, dat wil zeggen na 256 samples is de waarde terug waar hij startte omdat we 8-bit samples gebruiken. Met 8000 Hz is dat dus een frequentie van 8000 / 256 = 31,25 Hz. Door met 12 te vermenigvuldigen is de cyclus rond in 256/12 = 21,33 samples en dus een frequentie van 8000 / 21,33 = 342 Hz. Omgekeerd levert delen een expansie van de tijd op en dus een lagere toon.

In onderstaand voorbeeld laten we de hoge en de lage toon via de &-operator met elkaar interfereren. Er ontstaat dan een grappig alarm-achtig geluid. Probeer ook de andere binaire operatoren voor soms dramatische verschillen!

play(8000, 4000, (t) -> t * 12 & t / 36);

Zoals je ziet, er zijn genoeg mogelijkheden tot rare en interessante geluiden te komen. Wil je meer variaties dan kan je ook formules afwisselen met de ternary operator. Of misschien wel door functies aan elkaar te koppelen. Je zou er bijna een framework van kunnen maken 🙂

Meer inspiratie

Geluid maken met functies is natuurlijk niet nieuw (maar wel obscuur en leuk!) en er is dan ook het nodig online over te vinden. Googlen op ‘bytebeat algorithmic music’ werkt goed.  De volgende links kan ik je in ieder geval aanbevelen:

Heel veel plezier met het maken van rare geluiden met een minimale hoeveelheid code!

De broncode behorende bij dit artikel is te vinden op Github: https://github.com/42BV/algosound

BIO

Erik Hooijmeijer is Principal Developer en Ethisch Hacker bij 42 BV. Hij heeft 28 jaar ervaring met het ontwerpen en bouwen van software in vele domeinen. Daarnaast zijn er de hobby projecten; vele daarvan zijn te vinden op http://www.ctrl-alt-dev.nl.