Inleiding | De eerste schets | Python en Arduino | De lamp aansluiten | Installeren van de hardware | Versie 1.0 | Versie 2.0 | Versie 2.1 | Versie 2.2 | Versie 3.0

Hommage aan Kurt Schwitters

Een project voor museum dr8888

Inleiding

In Museum Drachten (dr8888) is deze zomer een overzichtstentoonstelling van het werk van Kurt Schwitters, de bekende dadaïstische kunstenaar die blijkbaar goed bevriend was met de in Drachten wonende Theo van Doesburg. In het kader van deze tentoonstelling maakte Marten Winters (Heerenveen, 1969) een installatie getiteld 'Hommage aan Kurt Schwitterts'. Uit de catalogus:

In dit omvangrijke kunstwerk gebruikt Winters afgedankte materialen die hij vond op straat, uit containers viste en cadeau kreeg van de inwoners van Drachten. Geïnspireerd door Schwitters die ooit zei 'Waardelooos materiaal bestaat niet'. Door de herkenbare materialen te rangschikken en te bewerken vormt dit bouwwerk een absurdistisch totaalkunstwerk. Zowel de poëzie, de collages én de Merzbau van Schwitters worden verenigd in deze hedendaagse interpretatie.

Het leek Winters een mooi idee deze installatie uit te breiden met een onderdeel waarbij bezoekers van het museum konden meewerken aan het grootste dadaïstische gedicht ooit. Ze werden dan uitgenodigd om in een kleine nis in de installatie plaats te nemen en één woord in te spreken. Uiteindelijk, als er 8888 woorden zouden zijn verzameld, zouden deze op één of andere manier worden samengevoegd tot het gedicht. Via collega B bereikte mij het verzoek om de technische kant van dit idee uit te werken.

Eerste schets

De algemene structuur van het werk was niet heel lastig. Er moest een drukknop komen die als opnameknop fungeerde: zolang deze knop ingedruk was, moest de opname lopen. Een lamp moest duidelijk maken dat er opgenomen werd, en als de knop werd losgelaten moest de opname opgeslagen worden en een dankbaar geluidje afgespeeld worden.

Eén van de eerste en (zo zou blijken) lastigste keuzes was welke software gebruikt zou moeten worden om de daadwerkelijke opname te maken. Initieel waren er twee opties: QuickTime en Audacity, waarbij de eerste de voorkeur genoot omdat deze mogelijk een betere kwaliteit leverde – én omdat deze direct een fraaie visualisatie van het geluid kon leveren. Voor de code zelf besloot ik vrij snel Python te gebruiken: een redelijk arbitraire keuze maar wel eentje waarvan ik wist dat die eenvoudig op een keur aan computers geïnstalleerd kon worden (wat relevant was, omdat ik niet wist welke computer er bij de installatie gebruikt zou worden).

Al tijdens de eerste besprekingen met B had ik een systeem gevonden waarmee ik via Python een opname kon starten, stoppen en opslaan. Blijkbaar kon je Audacity zo africhten dat hij luistert naar een soort file-based message queue. Eenvoudig berichten van en naar dit bestand (of bestanden eigenlijk) lezen en schrijven bleek initieel te werken.

Audacity settings

Hoewel de documentatie van deze technologie nogal mager was, de techniek nogal experimenteel was en Apple sowieso niet graag samenwerkt met open source projecten, ging ik hier toch goedgemutst mee aan de slag: als ik met de hand opnames kon maken, kon ik het ook wel scripten. Totdat ik me realiseerde dat het simpelweg opslaan van het bestand een Audacity-bestand opleverde, en niet een cross-platform audio-bestand (mp4 of wat dan ook). Omdat ik vanuit mijn cassettebandjesproject de ervaring heb dat deze eerste erg makkelijk gecorrumpeerd raken, wilde ik het bestand exporteren.

Hier bleek echter eens te meer de beperkingen van een grafische gebruikersinterface (we komen het zometeen nog een keer tegen). Om het bestand te exporteren, moest ik het via een dialoogvenster een naam geven. Met geen mogelijkheid kon ik via de message queue dit dialoogvenster benaderen. Een optie zou kunnen zijn om te proberen dit venster via AppleScript (osascript) aan te spreken, maar Audacity bleek geen scriptable application te zijn (dank, Apple). Na een avond vruchteloos experimenteren besloot ik dan maar gebruik te maken van QuickTime, omdat ik dat wel via osascript aan kon spreken.

Het exporteren van een bestand uit Audacity kan alleen via een dialoogvenster

Het bleek relatief eenvoudig om via osascript een nieuwe audio-opname te starten, te stoppen en op te slaan. En ik kon door gebruik te maken van os.popen() dit script vanuit Python aanroepen, dus toen ik dit stukje voor elkaar had was het tijd om het Pythonscript vanuit de Arduino aan te spreken.


#Python code
os.popen('osascript stop.osa')


#Osascript code (stop.osa)
set myDate to do shell script "date +'%Y-%m-%dT%H-%M-%S'"
set theFilePath to POSIX path of (path to movies folder) & myDate & ".m4a"


tell application "QuickTime Player"
    tell document "Audio Recording"
        pause
        save it in POSIX file theFilePath
        stop
        close
    end tell
end tell

Communicatie tussen Python en de Arduino

Bij het project van de recentste editie van onze DAT-Minor had ik veel geleerd over de seriële communicatie tussen een computer en een Arduino. Eén van de belangrijkste lessen was dat het niet handig is om continu data over de lijn te sturen, omdat er dan allerlei buffers lijken vol te lopen. Met deze les in het achterhoofd maakte ik op de Arduino een state machine die bij elke wijziging van status een bericht naar de computer verzond. De states waren IDLE, START en STOP – aan de Python-kant maakte ik corresponderende states.

Omgekeerd zorgde ik ervoor dat de computer een melding naar de Arduino stuurde wanneer de opname gestart en gestopt was; op deze manier kon ik een een LED (later een lamp) met deze opnamestatus laten corresponderen. Een momentbutton en het bijhorende script waren natuurlijk triviaal.

Later breide ik het aantal LEDs uit zodat ik ook kon zien of er stroom op de Arduino stond en of de seriële communicatie in orde was.

Breadboard met drie controle-ledjes

De lamp aansluiten

Nu het softwaredeel van het project in eerste instantie wel afgerond was, was het tijd om aan de hardware te gaan werken. Wanneer de bezoeker op de opnameknop zou drukken, moest er een echte rode lamp gaan branden. Deze lamp liep in principe op 220, dus er moest een constructie komen om dit relatief hoge voltage met een Arduino te kunnen schakelen.

Natuurlijk had ik wel eerder met relais gewerkt (bijvoorbeeld bij de eerste editie van onze DAT-Minor), maar nog nooit echt bij een serieus project. Dus voor de zekerheid maar even informatie ingewonnen bij de vrienden van Okaphone. Daar kocht ik een erg fraai relais (toch mooi dat die techniek al zo lang min of meer onveranderd bestaat) en een wel heel dikke diode (eentje die pieken van vierhonderd volt tegen kan houden).

Op basis van dit voorbeeld ging ik aan de slag. Hoewel ik niet dezelfde transistor had die ze daar gebruikte, wist vriend L me te vertellen dat elke NPN-transistor in principe gebruikt kon worden, zolang de IC maar 55 mA kon hebben (90Ω over 5V is 55 mA). Volgens de specs kon de transistor die ik wel had 600 mA, dus dat was prima.

Eerst even de boel op een breadboard uitproberen, voordat ik er daadwerkelijk licht mee schakel. En het werkende circuit met behulp van fritzing maar even goed gedocumenteerd.

Fritzing-weergave van de opzet van het knipperend relais.

Natuurlijk kon deze constructie (net als de rest trouwens) niet op een breadboard blijven, dus ik had een stukje PCB gekocht om de boel op te solderen. Alleen waren de poten van die dikke diode wat te dik, dus ik moest een paar gaten een beetje uitboren. Toen ik dat eenmaal gemaakt had, durfde ik het wel aan om daadwerkelijk lichten te schakelen. Hiervoor had ik even op een tweede Arduino een scriptje voor eenvoudig knipperlicht gezet.

De hardware in het kastje

Ik had, eveneens bij onze vrienden van Okaphone, een fijn doosje gekocht waar de hele boel in gemonteerd moest worden. Hiervoor moest natuurlijk wel het één en ander uitgeboord en -gevijld worden. Met name het gat dat voor de beide stroomkabels (de C7-aansluitingen) was wat lastig, maar met wat grof geweld kreeg ik dat toch voor elkaar.

Om de boel goed op de Arduino aan te sluiten, soldeerde ik betreffende kabels op pinheads, zodat ik die direct in de poorten van de Arduino kon steken. Het werd wel een beetje een kabelbende, maar het was uiteindelijk behoorlijk stabiel en robuust – om dat te testen heb ik het een paar keer laten vallen.

Plaatje van de constructie (1/6) Plaatje van de constructie (2/6) Plaatje van de constructie (3/6) Plaatje van de constructie (4/6) Plaatje van de constructie (5/6) Plaatje van de constructie (6/6)

Versie 1.0 in productie op de laptop

Als hardware om de installatie op te draaien had vriend B een oude iMac van zijn buurman geregeld. Helaas bleek het OS van dat ding zo oud dat weinig hierop nog werkte. Ik zou een goeie dag kwijt zijn met het upgraden van deze machine, maar gelukkig kon ik voor de duur van het project van mijn lieftallige echtgenote een oude laptop lenen. Ik had niet helemaal in de gaten hoe het project uiteindelijke geëxposeerd zou worden, dus een laptop (of iMac) leek me op zich prima.

Ik kopieerde de code, installeerde de nodige bibliotheken en startte de boel op. Verrassend genoeg werkte de installatie nagenoeg in één keer. Dit moest het dan maar even zijn, vond ik, dus we brachten alles naar vriend B, die de dag daarna het geheel in het werk van Winters in te bouwen.

Initieel leek alles het goed te doen, maar helaas werd ik halverwege de eerste dag dat het open was gebeld dat de installatie niet meer werkte. Daar was ik al bang voor, want zo'n laptop is toch eigenlijk niet geschikt voor inbouw: hij kan makkelijk dichtklappen en de poorten op deze machine waren sowieso wat brak.

Dus ik sprong op de motor om door de regen naar Drachten te rijden om de boel in ogenschouw te nemen. Onderweg reed ik nog even langs Groningen om een Mac Mini op te halen, die waarschijnlijk wat beter voor dit soort werk geschikt leek. Helaas bleek in Drachten dat er een wachtwoord op de Mac Mini zat, wat de eigenaar niet meer wist te achterhalen. Een beetje zinloze actie dus, maar wel goed dat ik nu een goed beeld had van waar het werk in terecht kwam.

Versie 2.0 op de Mac Mini

Diezelfde avond heb ik besteed om een betere versie op mijn eigen Mac Mini te zetten. Het probleem van de GUI dat ik bij het gebruik van Audactity had ervaren, trad ook op bij het gebruik van QuickTime: op sommige momenten kwam QuickTime met een dialoogvenster met de melding dat het maken van een opname niet mogelijk was. Om het proces dan verder te helpen, moest er in dat dialoogvenster op 'OK' geklikt worden. Maar dat kan natuurlijk niet op een headless Mac Mini.

Ik kon die fout enigszins reproduceren, waardoor ik het optreden hiervan aanzienlijk kon reduceren. Maar hij kwam nog steeds af en toe op, zonder dat ik kon verklaren waarom of wanneer.

Ik besteedde een goed uur aan het uitzoeken of je met osascript een dialoogvenster kon benaderen, maar dat kreeg ik niet voor elkaar. Aansluitend heb ik even geëxperimenteerd met een audio-bibliotheek voor Python, maar dat ding bleek zo achterhaald dat er zelfs nog string.format() syntax in zat. Uiteindelijk heb ik besloten dat het zo weinig voor kwam dat een eenvoudige reboot van de machine de beste oplossing was. Dat zou het museum dan maar af en toe moeten doen.

De volgende dag ging ik weer naar Drachten om de nieuwe versie, en de Mac Mini, daar te installeren. Gelukkig kon de Mini eenvoudig online worden gebracht, zodat ik er via Remote Desktop bij kon.

Het klaarmaken van de Mac Mini in het museum De plek waar de computer uiteindelijk in terecht kwam

Versie 2.1 op de Mac Mini

Eén van de vervelende dingen was dat ik niet vanaf huis (of waar dan ook) kon zien of er woorden waren ingesproken en zo ja, hoeveel dan. Om dit op te lossen, paste ik het script aan zodat er telkens wanneer er iets gebeurde (opname starten, opname stoppen, installatie herstarten, dat soort werk) een post werd gedaan naar een kort script dat ik op mijn eigen server had gezet. Dan kon ik in ieder geval een soort logfile bijhouden.

Opnieuw toog ik naar Drachten om deze nieuwe versie op de Mac Mini te zetten. Daar aangekomen bleek dat ding al twee dagen met een kapotte QuickTime Player te staan - en dan in zo'n setting dat je dat vanaf de installatie niet merkte: alles leek het gewoon te doen. Toch nog maar even kijken of ik het niet wat robuuster kon maken.

Maar eerst die logging. Het leek in eerste instantie allemaal goed te werken, maar nadat ik de Mini hard had gereset (door de stroom eraf te halen), bleek het script niet op te starten. Een korte inventarisatie leverde op dat het script opgestart werd vóórdat de netwerkverbinding er was. Zeer irritant... Na een goed uurtje in de installatie zelf dingen uitgeprobeerd en onderzocht te hebben (natuurlijk inclusief gezeur met verkeerde versies van dependencies), kwam ik met het onderstaande pythonscriptje dat de boel blokkeert totdat er netwerkverbinding is.


import requests

while True:
    try:
        requests.get('https://google.com', timeout=1)
        break
    except:
        pass

print ('netwerk is check')

Dat werken nabij de installatie zelf had echter een onbedoeld maar zeer gunstig bij-effect, namelijk dat ik mooi kon observeren hoe bezoekers met het werk omgingen. Het bleek dat slehts weinigen de instructies (die toch zo mooi waren opgehangen) goed las, of dat ze het in ieder geval verkeerd begrepen. Mensen drukte op de knop en lieten die gelijk weer los – dus vóórdat ze hun woord hadden ingesproken. Dit was in ieder geval één van de redenen waardoor het script in een verkeerde state terecht kwam: als er te kort op de knop wordt gedrukt, start QuickTime op, maar die krijgt nagelijk gelijk weer een melding om af te sluiten. Dat begrijpt -ie niet, en dan komt er zo'n dialoogvenster.

De flow van de installatie zoals we die hadden bedacht De flow van de installatie zoals die dikwijls optrad

Ik schreef met potlood nog extra instructie bij de knop en ging weer naar huis om 's avonds nog aan dit probleem te werken. Na lang denken bedacht ik me dat er eigenlijk een nieuwe state bij moest komen: het script moest eerst QuickTime opstarten, vervolgens loggen en daarna in een State.READY komen. In een volgende lus zou dan QuickTime pas worden aangesproken wanneer deze state bereikt was. In diezelfde lus kon ik dan checken of de knop weer was losgelaten en in dat geval de hele boel resetten.

Dat leek wel wat te doen; thuis was het allemaal behoorlijk idempotent en stabiel, dus ik wilde deze versie weer in productie zetten. Helaas had ik de volgende dag afgesproken om te gaan fietsen, dus dat werd pas donderdag.

Versie 2.2 op de Mac Mini

Eenmaal in het museum had ik die nieuwe versie er redelijk snel opgezet, maar toen bleek dat de WiFi binnen de installatie zó traag was, dat het de hele boel ophing. Dus maar besloten om die logging weer uit te zetten. Dat is wel erg jammer, met name omdat er in het museum blijkbaar niemand was die ik kon vertellen hoe het ding remote gemonitord kon worden.

De dag hierna gingen we een week door Nederland fietsen, dus kon ik eigenlijk niets doen. Het probleem van de race condition die ik had ontdekt was dat het aan de voorkant leek alsof alles prima werkte: de lamp ging aan en uit en het geluidje werd afgespeeld. Het enige was dat de opname niet werd opgeslagen, omdat QuickTime de hele tijd een error-venster aan het tonen was...

Versie 3.0 op de Mac Mini

Na de fietsvakantie ging in zondagmiddag naar Dachten om de Mac op te halen met het idee om die maandag (wanneer het museum toch dicht is) er een behoorlijke update op te zetten. Hoewel het inderdaad leek alsof alles het deed, bleek er toch sinds een week geen enkele opname opgeslagen te zijn. QuickTime moest er echt uit.

Met behulp van ChatGPT kwamen we (inmiddels had ik ook F gevraagd mee te denken) op een library die het mogelijk maakte om met Python zelf audio-opnamen te maken. Binnen een uurtje had ik een script dat inderdaad een audio-opname maakte en opsloeg. Dit leek een heel vruchtbaar pad.

Maar natuurlijk ontstond hier ook weer een uitdaging. De library maakt een stream aan en neemt stukken (met de grootte van CHUNCK) op. Dit script draait totdat het een CTRL-C binnenkrijgt. Het idee was natuurlijk om die CTRL-C te vervangen door een puls van de Arduino. Maar door de opzet van de library kon het script niks anders doen tijdens het opnemen dan opnemen. Wanneer ik in deze loop een check bouwde om te kijken of er een bitje van de Arduino was binnengekomen om te stoppen, liep die buffer direct out of sync.

Ik probeerde om het script in een eigen thread te laten lopen, waarbij ik gebruikt maakte van een queue voor de communicatie tussen de main tread en de opname-thread. Maar zelfs het luisteren naar deze message queue binnen de loop van de opname leverde een buffer-overflow error op...

Na hier de rest van de maandag mee bezig geweest te zijn (met groeiende irritatie natuurlijk) besloot ik dat ik gewoon het opname-script via het arduino-communicatie-script moest aanroepen. Ik kon vanuit Python redelijk eenvoudig een CTRL-C naar dit eerste script sturen. Net binnen de deadline (ik moest iemand van het station in Groningen halen) was de eerste opname met deze architectuur gelukt. Uiteindelijk was het script relatief eenvoudig:


proc = subprocess.Popen(['python3', 'recorder.py'], shell=False)
while True:
    data = arduino.readline()
    if b'stop' in data:
        proc.send_signal(signal.SIGINT)
        proc.wait()
        arduino.write(bytes('0;', 'utf-8'))
        state = State.idle
        break

Daarna was het weer testen, testen, testen. Stress-testen, endurance-testen, cold-restart testen... Alles leek te werken. Dus deze versie heb ik opgestoond en uiteindelijk op de Mac Mini gezet en dinsdagochtend naar Drachten gebracht. Gelukkig was daar nu ook iemand die ik kon vertellen hoe het script werkte en de computer gemonitord kon worden.

Het lijkt nu wel redelijk te werken; ik sprak na een paar weken terug de curator en die vertelde dat er in ieder geval opnamen gemaakt waren. Het waren er op dat moment wel wat weinig, maar dat is geen softwareprobleem...

Overleg met de curator in Drachten