Tutorial 2: la tua prima app, una sinusoide attraverso l'engine
Generi un progetto, ci colleghi l’engine audio del framework e suoni una sinusoide a 440 Hz sull’uscita audio predefinita per un secondo. Strada facendo scrivi una composition root: l’unico punto in cui un’applicazione crea i suoi oggetti, passa a ciascuno ciò di cui ha bisogno e li distrugge in ordine inverso.
Tempo: circa 30 minuti (JUCE si compila una volta sola). Ti servono gli strumenti del tutorial 1, un checkout di JUCE 9.0.3 (oppure una connessione di rete: la build può scaricarlo) e un checkout di shuffle_juce, il satellite che contiene gli adapter JUCE, clonato accanto a shuffle_engine.
Backend. I passi da 1 a 4 usano l’adapter audio JUCE (
shuffle_juce_audio, da shuffle_juce). Il passo 5 compila lo stesso programma senza JUCE, sull’adapter miniaudio: un progetto C++ semplice, senza scaricare JUCE e senza satellite.
1. Genera il progetto
Sezione intitolata “1. Genera il progetto”Da una cartella qualsiasi (usa i tuoi percorsi):
cmake -D NAME=FirstSound -D BACKEND=juce -D DIR=FirstSound -D ENGINE_DIR=<path to shuffle_engine> -P <path to shuffle_engine>/cmake/ShuffleEngineNewProject.cmakecd FirstSoundSu Windows tieni la cartella del progetto in un percorso breve (per esempio
D:\dev\FirstSound): l’albero di build sta lì dentro e i percorsi generati
da JUCE sono lunghi.
Ottieni CMakeLists.txt, CMakePresets.json, src/main.cpp, README.md
e .gitignore. Questi file sono tuoi (CC0). ENGINE_DIR fa usare al progetto
il tuo checkout; se lo ometti, shuffle_engine viene scaricato da GitHub.
2. Chiedi i moduli che usi
Sezione intitolata “2. Chiedi i moduli che usi”Apri CMakeLists.txt. Dopo il blocco che include shuffle_engine, aggiungi
shuffle_juce, che fornisce i moduli adapter JUCE (trova da solo lo
shuffle_engine già aggiunto dal progetto). Questa app non usa Tracktion,
quindi spegnine gli adapter:
set (SHUFFLE_JUCE_WITH_TRACKTION OFF CACHE BOOL "")add_subdirectory (<path to shuffle_juce> shuffle_juce)Poi cambia l’ultima chiamata in modo che l’app usi l’adapter di piattaforma JUCE (coda dei task, orologio, timer) e l’adapter audio JUCE (dispositivi, callback del dispositivo, thread real-time):
shuffle_engine_add_app (FirstSound BACKEND juce SOURCES src/main.cpp USES juce_platform juce_audio)juce_audio si porta dietro core/engine.
3. Scrivi il programma
Sezione intitolata “3. Scrivi il programma”Sostituisci src/main.cpp con il programma di
examples/juce/play/main.cpp di shuffle_juce,
che la sua CI compila ed esegue. Le parti che contano:
Un nodo. L’engine esegue un grafo di nodi. Un nodo sorgente scrive
l’audio in process, che gira sul thread audio: per questo è noexcept,
non alloca nulla ed è marcato SHUFFLE_RT (vedi
Real-time rules):
class Sine final : public engine::NodeProcessor{public: void prepare (const engine::NodePrepare& spec) override { step_ = 440.0 / spec.sampleRate; }
bool process (engine::ProcessContext& ctx) noexcept SHUFFLE_RT override { auto& out = ctx.audioOut[0]; for (int k = 0; k < ctx.numSamples; ++k) { float sample = 0.0f; SHUFFLE_RT_ALLOW_BEGIN ("std::sin is pure arithmetic: no allocation, lock or system call") sample = static_cast<float> (std::sin (6.283185307179586 * phase_)); phase_ = std::fmod (phase_ + step_, 1.0); SHUFFLE_RT_ALLOW_END for (int c = 0; c < out.numChannels; ++c) out.channels[c][k] = sample; } return true; }
private: double step_ = 440.0 / 48'000.0; double phase_ = 0.0;};La composition root. I membri di una struct vengono costruiti dall’alto in basso e distrutti dal basso in alto: l’ordine in cui li leggi è l’ordine della loro vita.
struct Player final : routing::AudioDeviceSwitch::Listener{ JuceTaskQueue tasks; // the control thread JuceClock clock; JuceProcessingThreads threads { 1 }; // real-time worker threads engine::Engine engine { { clock, &threads } }; // the audio graph EngineAudioCallback callback { engine, tasks, &threads }; // one Engine::process per device callback JuceAudioDevices devices { callback, tasks }; // opens nothing yet ControlTimer timer { [this] { tick(); } }; // ...};Il grafo è fatto di dati semplici. Costruiscilo, consegnalo all’engine e, se lo rifiuta, leggi i problemi:
engine::Graph g;const auto sine = engine.newNodeId();const auto gain = engine.newNodeId();const auto out = engine.newNodeId();g.nodes = { engine::sourceNode (sine, "Sine", std::make_shared<Sine>(), 2), engine::processorNode (gain, "Gain", std::make_shared<engine::GainProcessor> (0.1f), 2, 2), // -20 dB engine::sinkNode (out, "Main Out", std::make_shared<engine::DeviceOutput> (0), 2) };g.edges = { engine::audioEdge (engine.newEdgeId(), sine, gain), engine::audioEdge (engine.newEdgeId(), gain, out) };
const auto graph = engine.setGraph (g);for (const auto& p : graph.problems) std::printf ("graph problem: %s\n", p.message.c_str());I dispositivi si aprono dopo. requestDevice ritorna subito; la risposta
arriva sul thread di controllo attraverso il listener. Niente si blocca, ed è
proprio quello che le piattaforme mobili richiedono:
devices.setListener (this);devices.requestDevice (devices.defaultDeviceKey()); // deviceSwitchFinished (key, opened, reason) follows4. Compila ed esegui
Sezione intitolata “4. Compila ed esegui”cmake --preset default -DJUCE_HOME=<path to JUCE 9.0.3>cmake --build --preset defaultSu Windows con Visual Studio usa il preset windows del progetto generato
(cmake --preset windows ..., cmake --build --preset windows).
shuffle_engine di per sé non compila codice JUCE: i moduli JUCE arrivano da
shuffle_juce, alla versione di JUCE fissata da shuffle_engine. Se ometti
JUCE_HOME, JUCE viene scaricato alla versione fissata.
Eseguilo. JUCE mette il programma in
build/<preset>/FirstSound_artefacts/Release/:
10 audio device(s); requesting "Windows Audio:Speakers (Realtek(R) Audio)"playing a 440 Hz sine at -20 dBFS on "Windows Audio:Speakers (Realtek(R) Audio)": 48000 Hz, blocks of 480, 2 output channel(s)played 48000 samples in 100 callbacks; load peak 0.3 %, 0 xrun(s), denormals flushed: yesokNome del dispositivo, frequenza di campionamento e dimensione del blocco sono quelli della tua macchina. Su una macchina senza uscita audio il programma dice che ha saltato la riproduzione ed esce comunque con 0.
5. Lo stesso senza JUCE
Sezione intitolata “5. Lo stesso senza JUCE”Genera un progetto C++ semplice (senza BACKEND, quindi core) e usa
l’adapter miniaudio, l’engine e un orologio dall’adapter dei socket:
cmake -D NAME=PlainSound -D DIR=PlainSound -D ENGINE_DIR=<path to shuffle_engine> -P <path to shuffle_engine>/cmake/ShuffleEngineNewProject.cmakeshuffle_engine_add_app (PlainSound SOURCES src/main.cpp USES miniaudio engine net_sockets)Copia examples/miniaudio/play/main.cpp
in src/main.cpp. La composition root ha la stessa forma; cambiano solo gli
adapter e, senza il ciclo di messaggi di JUCE, il programma esegue un suo
ciclo di controllo (una TaskQueue i cui task vengono eseguiti da main):
MainLoop loop; // the control thread: main() runs posted tasksnet_sockets::SteadyClock clock;miniaudio::OsProcessingThreads threads (2);engine::Engine engine ({ clock, &threads });miniaudio::EngineAudioCallback callback (engine);auto devices = std::make_unique<miniaudio::MiniaudioDevices> (loop, callback);Compila con cmake --preset default (o windows) e
cmake --build --preset default; l’adapter miniaudio è attivo per default.
playing a 440 Hz sine at -12 dB on 'wasapi:Speakers (Realtek(R) Audio)': 2 outputs, 48000 Hz, blocks of 256 frames194 device callbacks, 194 engine blocks, load 0.1 % (peak 0.4 %), 0 overruns, 2 of 2 processing threads real-timeIl programma stampa il dispositivo, il carico dell’engine e gli overrun; su una macchina senza uscita suona sul backend nullo di miniaudio ed esce comunque con 0. Vedi adapters/miniaudio.
6. Fallo tuo
Sezione intitolata “6. Fallo tuo”- Cambia
440.0con un’altra frequenza, oppure il guadagno0.1f(-20 dB) con0.05f. - Aggiungi un secondo nodo sorgente e collega entrambi al gain: l’engine somma gli ingressi di un pin.
- Stampa
engine.load()a ogni tick per guardare il carico di elaborazione.
Cosa hai imparato
Sezione intitolata “Cosa hai imparato”- un’applicazione è una composition root: crei gli adapter dei backend e i core, e passi i riferimenti verso il basso; non c’è niente di globale da configurare;
- il grafo dell’engine è fatto di dati, validati con messaggi che dicono cosa correggere;
- il thread audio esegue solo codice
noexcept SHUFFLE_RT; tutto il resto gira sul thread di controllo, raggiunto attraverso la coda dei task.
Avanti: Tutorial 3, una scaletta con il pulsante GO in una UI web, oppure Tutorial 4, DSP senza dispositivi se non vuoi nessun dispositivo.
© 2026 Umberto Sorbo. Il software è distribuito con la Apache License, Version 2.0, e fornito “così com’è”, senza garanzie o condizioni di alcun tipo; leggi l’esclusione di responsabilità.
SHUFFLE Framework e il logo di SHUFFLE Framework sono marchi di Umberto Sorbo; la licenza non concede alcun diritto su di essi (marchi). Gli altri nomi appartengono ai rispettivi titolari. Caratteri: IBM Plex Sans e JetBrains Mono, SIL Open Font License 1.1.
Contatti: shuffleframework@gmail.com · Instagram @shuffleframework. Problemi di sicurezza: scrivi allo stesso indirizzo con “Security” nell’oggetto, non in una issue pubblica.