Salta ai contenuti

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.

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.cmake
cd FirstSound

Su 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.

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.

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) follows
cmake --preset default -DJUCE_HOME=<path to JUCE 9.0.3>
cmake --build --preset default

Su 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: yes
ok

Nome 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.

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.cmake
shuffle_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 tasks
net_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 frames
194 device callbacks, 194 engine blocks, load 0.1 % (peak 0.4 %), 0 overruns, 2 of 2 processing threads real-time

Il 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.

  • Cambia 440.0 con un’altra frequenza, oppure il guadagno 0.1f (-20 dB) con 0.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.
  • 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.