Sviluppare un emulatore del Suzuki Omnichord (parte 1)
Sono anni che bramo l'Omnichord, da quando l'ho scoperto una quindicina di anni fa e comprarne uno a un prezzo umano non era impossibile. E ho anche avuto diverse occasioni per farlo, ma alla fine non l'ho mai acquistato e di questo mi sono molto pentito, perché da quando è diventato uno strumento "mainstream", probabilmente grazie a Damon Albarn (su YouTube c'è un video bellissimo in cui si vede Damon che spiega come ha composto 'Clint Eastwood' dei Gorillaz), i prezzi sono saliti in modo sproporzionato.
Per non parlare del costo della reissue proposta dalla Suzuki per la modica cifra di 800 euro, un prezzo davvero popolare, non c'è che dire!
Non ho voglia di spendere tutti questi soldi per un hobby che non mi ha mai fatto guadagnare nulla, anzi, ci ho sempre rimesso!
Essendo principalmente un programmatore, la cosa più logica è realizzare un emulatore software. Certo, qualche emulatore esiste già, ma non mi sembra che copra tutte le possibilità di questo strumento. Forse quello più completo sembra quello realizzato per iOS, C.ARP, però dai video che ho visto sembra più una reinterpretazione in chiave moderna che un emulatore a tutti gli effetti.
Quindi da circa un mese ho deciso di lavorarci su. Dopotutto, perché no?
Ho sempre bisogno di un side project e questo sembra particolarmente divertente.
Lo stack tecnologico
Prima di tutto, ho dovuto scegliere lo stack tecnologico.Essendo uno strumento "touch", la cosa più logica è sfruttare le caratteristiche touch di smartphone e tablet. Non possiedo nulla di Apple e per ora non ho intenzione di possedere nulla, quindi per quanto mi riguarda lo sviluppo sarà per Android.
Da un po' di tempo nel mio lavoro principale utilizzo Flutter, il framework di Google basato su Dart che permette di creare app native per Android, iOS, Linux, ecc., e devo dire che mi ci trovo molto bene.
Ho provato anni fa a sviluppare nativamente per Android con Kotlin ed è stata un'esperienza molto frustrante; usando termini tecnici: l'API di Android è un casino e non si capisce nulla. Kotlin è per me un linguaggio abbastanza ostico e pieno di zucchero sintattico, mi sembra inutilmente complesso e non mi piace particolarmente, specie per me che vengo dal C.
Dart invece lo trovo molto più nelle mie corde, è un linguaggio moderno, per alcuni versi simile a JavaScript, ma si può usare quasi come se fosse C++, se non fosse per la gestione della memoria che avviene tramite Garbage Collector.
Ma su Dart (come su JavaScript) bisogna stare comunque attenti alla gestione della memoria quando, ad esempio, si istanziano degli event listener che poi vanno anche de-allocati quando non più utilizzati. Meglio non divagare troppo, in ogni caso.
Concludo dicendo che ho scelto Flutter perché secondo me ha messo molto ordine nello sviluppo nativo su Android; per quanto mi riguarda, ormai è un vero piacere utilizzare i suoi widget e tutte le utility che mette a disposizione. Inoltre, è un framework molto libero, nel senso che non ti impone un'architettura da usare nel tuo progetto, ma sei libero di fare quello che vuoi.
E questo tipo di libertà mi ricorda molto quella sensazione che si prova quando si inizia un programma in C partendo solo da int main() { return 0; }
Il problema della latenza
Il primo problema per realizzare un'app audio professionale è ridurre il più possibile la latenza, cioè il tempo che intercorre da quando viene premuto lo schermo alla riproduzione del suono. Capirete che se questo tempo è alto, l'app risulterebbe inutilizzabile.Non sono un grande fan delle dipendenze nei progetti, ma per capire cosa potevo avere a disposizione ho provato tutti i pacchetti per l'audio proposti su pub.dev (il repository ufficiale dei pacchetti Dart/Flutter) e avevano tutti il problema della latenza altissima, perché, per quanto Flutter compili nativamente sulla piattaforma target, per quanto riguarda l'audio c'è sempre uno strato di traduzione da Flutter verso l'ambiente target che introduce latenza. E non poca, a quanto ho potuto vedere!
Stavo quasi per abbandonare Flutter e passare mio malgrado allo sviluppo su Android nativo, quando ho trovato un'interessante alternativa: la libreria Google Oboe, che permette di avere la latenza più bassa possibile su Android, senza dover impazzire troppo con le sue API, che, come ho scritto sopra, sono incasinate. In pratica, uno strato di traduzione ottimizzato per diverse versioni di Android, una vera manna dal cielo!
La libreria è scritta in C++ e non ci sono dei package che permettono l'utilizzo in modo semplificato su Flutter. O meglio, qualcosa c'è ma sono più che altro esempi su come collegare Flutter a Oboe, e molti di questi esempi si basano su vecchie versioni di Flutter e di Oboe. Quindi ho deciso di fare da me e sporcarmi un po' le mani, altrimenti dove sarebbe il divertimento?
La soluzione: dart:ffi e un wrapper C++
Quindi ho importato la libreria sul mio progetto, come modulo di git, e ho iniziato a scrivere tramite dart:ffi un connettore verso Oboe.dart:ffi sta per foreign function interface ed è il modo per interfacciarsi con Flutter a un backend scritto in C.
Poiché Oboe è scritto in C++, non sarebbe possibile collegarlo direttamente a dart:ffi.
C'è un trucchetto: scrivere un wrapper contenente delle funzioni extern "C", quindi compatibili con C, che istanziano le classi di Oboe (C++) e si intefacciano ad esso.
Saranno queste le funzioni che verranno poi richiamate tramite dart:ffi.
Occorrono quindi diversi passaggi: Flutter -> dart:ffi -> Wrapper C++ -> Oboe.
Come funziona questo connettore? In primis c'è il main di Flutter che per semplicità al momento è uno StatefulWidget.
Nell'initState del widget richiamo una funzione definita tramite dart:ffi, che a sua volta chiama una funzione scritta con extern "C", che infine chiama la libreria di Oboe e la istanzia.
L'istanziamento di Oboe non è molto diverso da quello proposto nel Getting Started della sua documentazione.
Faccio inoltre lo "start" del AudioStreamDataCallback di Oboe, che non è altro che la funzione che gira in loop, alla quale si danno in pasto i sample da far riprodurre da Oboe. Il sample non è altro che un numero in floating point.
Oltre a Oboe, ai fini del test della latenza, istanzio anche un oscillatore sinusoidale LFO (che ho opportunamente scritto), che mi permetterà di far suonare ad Oboe un tono di test. Questo LFO è inizialmente impostato a 0hz, quindi è muto.
Fatto questo, ho inserito un pulsante nel Main, che quando viene premuto chiama un'altra funzione definita tramite dart:ffi, che richiama un'altra funzione extern "C", che a sua volta richiama l'istanza dell'LFO e ne setta il rate ad una frequenza udibile, per esempio la classica 440hz.
A questo punto, avendo istruito AudioStreamDataCallback a leggere il sample dall'LFO (e a farlo avanzare), il tono a 440hz viene riprodotto.
Ho potuto verificare il tutto direttamente sul mio vecchissimo Samsung Galaxy J6 (il mio telefono che uso dal 2018), la latenza è bassissima, il suono viene riprodotto non appena viene premuto il tasto sullo schermo.
Risolto questo problema, posso finalmente concentrarmi sui suoni.
Questo sarà l'argomento che affronterò nella seconda parte.
È stato inviato e sarà moderato prima della pubblicazione.