robert@493: /* robert@493: * render.cpp robert@493: * robert@493: * Created on: Oct 24, 2014 robert@493: * Author: parallels robert@493: */ robert@493: robert@493: robert@493: #include robert@493: #include "game.h" robert@493: #include robert@493: #include robert@493: #include robert@493: #include robert@493: robert@493: int gAudioFramesPerMatrixFrame = 2; // Ratio in audio to matrix sample rate robert@493: robert@493: int gInputTank1Angle = 0; // Inputs for the cannon angles robert@493: int gInputTank2Angle = 1; robert@493: int gInputLauncher = 2; // Input for launcher FSR robert@493: robert@493: int gOutputX = 0; // Outputs for the scope robert@493: int gOutputY = 1; robert@493: int gOutputPlayer1LED = 2; robert@493: int gOutputPlayer2LED = 3; robert@493: robert@493: int gGameFramesPerSecond = 60; // How often the physics are updated robert@493: int gGameFrameInterval; // ...and in frames robert@493: int gSamplesUntilNextFrame; // Counter until next update robert@493: int gSamplesSinceFinish = 0; // How long since somebody won? robert@493: bool gGameShouldRestart = false;// Whether we need to reinitiliase the game robert@493: robert@493: // Counter for overall number of samples that have elapsed robert@493: unsigned int gSampleCounter = 0; robert@493: robert@493: // 1st-order filter and peak detector for launcher input robert@493: float gLauncherLastSample = 0; robert@493: float gLauncherFilterPole = 0.8; robert@493: float gLauncherPeakValue = 0; robert@493: float gLauncherPeakFilterPole = 0.999; robert@493: float gLauncherNoiseThreshold = 0.01; robert@493: float gLauncherMinimumPeak = 0.1; robert@493: bool gLauncherTriggered = false; robert@493: robert@493: // Screen update rate; affects buffer size. Actual contents of buffer robert@493: // may be smaller than this robert@493: int gScreenWidth = 512; robert@493: int gScreenHeight = 512; robert@493: int gScreenFramesPerSecond = 25; robert@493: robert@493: // Double buffer for rendering screen. Each is an interleaved buffer robert@493: // of XY data. robert@493: float *gScreenBuffer1, *gScreenBuffer2; robert@493: float *gScreenBufferWrite, *gScreenBufferRead; robert@493: int gScreenBufferMaxLength; // What is the total buffer allocated? robert@493: int gScreenBufferReadLength; // How long is the read buffer? robert@493: int gScreenBufferWriteLength; // How long is the write (next) buffer? robert@493: int gScreenBufferReadPointer; // Where are we in the read buffer now? robert@493: int gScreenBufferNextUpdateLocation; // When should we render the next buffer? robert@493: bool gScreenNextBufferReady; // Is the next buffer ready to go? robert@493: robert@493: // Auxiliary (low-priority) task for updating the screen robert@493: AuxiliaryTask gScreenUpdateTask; robert@493: robert@493: // Buffers for music and sound effects robert@493: extern float *gMusicBuffer; robert@493: extern int gMusicBufferLength; robert@493: extern float *gSoundBoomBuffer; robert@493: extern int gSoundBoomBufferLength; robert@493: extern float *gSoundHitBuffer; robert@493: extern int gSoundHitBufferLength; robert@493: robert@493: // Current state for sound and music robert@493: int gMusicBufferPointer = 0; // 0 means start of buffer... robert@493: int gSoundBoomBufferPointer = -1; // -1 means don't play... robert@493: int gSoundHitBufferPointer = -1; robert@493: float gSoundProjectileOscillatorPhase = 0; robert@493: float gSoundProjectileOscillatorGain = 0.2; robert@493: float gOscillatorPhaseScaler = 0; robert@493: robert@493: void screen_update(); robert@493: robert@493: // setup() is called once before the audio rendering starts. robert@493: // Use it to perform any initialisation and allocation which is dependent robert@493: // on the period size or sample rate. robert@493: // robert@493: // userData holds an opaque pointer to a data structure that was passed robert@493: // in from the call to initAudio(). robert@493: // robert@493: // Return true on success; returning false halts the program. robert@493: robert@493: bool setup(BelaContext *context, void *userData) robert@493: { robert@493: srandom(time(NULL)); robert@493: chris@543: // Verify we are running with analog channels enabled chris@543: if(context->analogFrames == 0 || context->analogOutChannels < 4) { chris@543: rt_printf("Error: this example needs least 4 analog output channels\n"); robert@493: return false; robert@493: } robert@493: robert@493: // Initialise audio variables robert@493: gAudioFramesPerMatrixFrame = context->audioFrames / context->analogFrames; robert@493: gOscillatorPhaseScaler = 2.0 * M_PI / context->audioSampleRate; robert@493: robert@493: // Initialise the screen buffers robert@493: gScreenBufferMaxLength = 2 * context->analogSampleRate / gScreenFramesPerSecond; robert@493: gScreenBuffer1 = new float[gScreenBufferMaxLength]; robert@493: gScreenBuffer2 = new float[gScreenBufferMaxLength]; robert@493: if(gScreenBuffer1 == 0 || gScreenBuffer2 == 0) { robert@493: rt_printf("Error initialising screen buffers\n"); robert@493: return false; robert@493: } robert@493: robert@493: gScreenBufferRead = gScreenBuffer1; robert@493: gScreenBufferWrite = gScreenBuffer2; robert@493: gScreenBufferReadLength = gScreenBufferWriteLength = 0; robert@493: gScreenBufferReadPointer = 0; robert@493: gScreenBufferNextUpdateLocation = 0; robert@493: gScreenNextBufferReady = false; robert@493: robert@493: // Initialise the game robert@493: setupGame(gScreenWidth, gScreenHeight); robert@493: gGameFrameInterval = context->analogSampleRate / gGameFramesPerSecond; robert@493: gSamplesUntilNextFrame = gGameFrameInterval; robert@493: robert@493: // Initialise auxiliary tasks robert@493: if((gScreenUpdateTask = Bela_createAuxiliaryTask(&screen_update, 90, robert@493: "bela-screen-update")) == 0) robert@493: return false; robert@493: robert@493: return true; robert@493: } robert@493: robert@493: // Swap buffers on the screen robert@493: void swap_buffers() robert@493: { robert@493: if(gScreenBufferRead == gScreenBuffer1) { robert@493: gScreenBufferRead = gScreenBuffer2; robert@493: gScreenBufferWrite = gScreenBuffer1; robert@493: } robert@493: else { robert@493: gScreenBufferRead = gScreenBuffer1; robert@493: gScreenBufferWrite = gScreenBuffer2; robert@493: } robert@493: robert@493: gScreenBufferReadLength = gScreenBufferWriteLength; robert@493: gScreenBufferReadPointer = 0; robert@493: robert@493: // Schedule next update for 3/4 of the way through the buffer robert@493: gScreenBufferNextUpdateLocation = gScreenBufferReadLength * 0.75; robert@493: gScreenNextBufferReady = false; robert@493: } robert@493: robert@493: // render() is called regularly at the highest priority by the audio engine. robert@493: // Input and output are given from the audio hardware and the other robert@493: // ADCs and DACs (if available). If only audio is available, numMatrixFrames robert@493: // will be 0. robert@493: robert@493: void render(BelaContext *context, void *userData) robert@493: { robert@493: int audioIndex = 0; robert@493: robert@493: for(unsigned int n = 0; n < context->analogFrames; n++) { robert@493: for(int k = 0; k < gAudioFramesPerMatrixFrame; k++) { robert@493: // Render music and sound robert@493: float audioSample = 0; robert@493: robert@493: // Music plays in a loop robert@493: if(gMusicBuffer != 0 && gMusicBufferPointer >= 0) { robert@493: audioSample += gMusicBuffer[gMusicBufferPointer++]; robert@493: if(gMusicBufferPointer >= gMusicBufferLength) robert@493: gMusicBufferPointer = 0; robert@493: } robert@493: robert@493: // Sound effect plays until finished, then stops robert@493: if(gSoundBoomBuffer != 0 && gSoundBoomBufferPointer >= 0) { robert@493: audioSample += gSoundBoomBuffer[gSoundBoomBufferPointer++]; robert@493: if(gSoundBoomBufferPointer >= gSoundBoomBufferLength) robert@493: gSoundBoomBufferPointer = -1; robert@493: } robert@493: robert@493: if(gSoundHitBuffer != 0 && gSoundHitBufferPointer >= 0) { robert@493: audioSample += gSoundHitBuffer[gSoundHitBufferPointer++]; robert@493: if(gSoundHitBufferPointer >= gSoundHitBufferLength) robert@493: gSoundHitBufferPointer = -1; robert@493: } robert@493: robert@493: // Oscillator plays to indicate projectile height robert@493: if(gameStatusProjectileInMotion()) { robert@493: audioSample += gSoundProjectileOscillatorGain * sinf(gSoundProjectileOscillatorPhase); robert@493: robert@493: gSoundProjectileOscillatorPhase += gOscillatorPhaseScaler * constrain(map(gameStatusProjectileHeight(), robert@493: 1.0, 0, 300, 2000), 200, 6000); robert@493: if(gSoundProjectileOscillatorPhase > 2.0 * M_PI) robert@493: gSoundProjectileOscillatorPhase -= 2.0 * M_PI; robert@493: } robert@493: robert@493: context->audioOut[2*audioIndex] = context->audioOut[2*audioIndex + 1] = audioSample; robert@493: audioIndex++; robert@493: } robert@493: robert@493: // First-order lowpass filter to remove noise on launch FSR robert@493: float rawSample = analogRead(context, n, gInputLauncher); robert@493: float launchSample = gLauncherFilterPole * gLauncherLastSample + robert@493: (1.0f - gLauncherFilterPole) * rawSample; robert@493: gLauncherLastSample = launchSample; robert@493: robert@493: // Peak-detect on launch signal robert@493: if(launchSample >= gLauncherPeakValue) { robert@493: gLauncherPeakValue = launchSample; robert@493: gLauncherTriggered = false; robert@493: } robert@493: else { robert@493: if(gLauncherPeakValue - launchSample > gLauncherNoiseThreshold && !gLauncherTriggered) { robert@493: // Detected a peak; is it big enough overall? robert@493: if(gLauncherPeakValue >= gLauncherMinimumPeak) { robert@493: gLauncherTriggered = true; robert@493: // Peak detected-- fire!! robert@493: // Set both cannon strengths but only one will robert@493: // fire depending on whose turn it is robert@493: float strength = map(gLauncherPeakValue, robert@493: gLauncherMinimumPeak, 1.0, robert@493: 0.5f, 10.0f); robert@493: setTank1CannonStrength(strength); robert@493: setTank2CannonStrength(strength); robert@493: fireProjectile(); robert@493: } robert@493: } robert@493: robert@493: gLauncherPeakValue *= gLauncherPeakFilterPole; robert@493: } robert@493: robert@493: if(--gSamplesUntilNextFrame <= 0) { robert@493: // Update game physics and cannon angles robert@493: gSamplesUntilNextFrame = gGameFrameInterval; robert@493: robert@493: setTank1CannonAngle(map(analogRead(context, n, gInputTank1Angle), robert@493: 0, 1.0, M_PI, 0)); robert@493: setTank2CannonAngle(map(analogRead(context, n, gInputTank2Angle), robert@493: 0, 1.0, M_PI, 0)); robert@493: nextGameFrame(); robert@493: robert@493: // Check for collision and start sound accordingly robert@493: if(gameStatusCollisionOccurred()) { robert@493: gSoundBoomBufferPointer = 0; robert@493: } robert@493: robert@493: if(gameStatusTankHitOccurred()) { robert@493: gSoundHitBufferPointer = 0; robert@493: } robert@493: } robert@493: robert@493: if(gScreenBufferReadPointer >= gScreenBufferReadLength - 1 robert@493: && gScreenNextBufferReady) { robert@493: // Got to the end; swap buffers robert@493: swap_buffers(); robert@493: } robert@493: robert@493: // Push current screen buffer to the matrix output robert@493: if(gScreenBufferReadPointer < gScreenBufferReadLength - 1) { robert@493: float x = gScreenBufferRead[gScreenBufferReadPointer++]; robert@493: float y = gScreenBufferRead[gScreenBufferReadPointer++]; robert@493: robert@493: // Rescale screen coordinates to matrix ranges; invert the Y robert@493: // coordinate to go from normal screen coordinates to scope coordinates robert@493: analogWriteOnce(context, n, gOutputX, constrain(map(x, 0, gScreenWidth, 0, 1.0), 0, 1.0)); robert@493: analogWriteOnce(context, n, gOutputY, constrain(map(y, 0, gScreenHeight, 1.0, 0), 0, 1.0)); robert@493: } robert@493: else { robert@493: // Still not ready! Write 0 until something happens robert@493: analogWriteOnce(context, n, gOutputX, 0); robert@493: analogWriteOnce(context, n, gOutputY, 0); robert@493: } robert@493: robert@493: if(gameStatusWinner() != 0) { robert@493: // Blink one LED to show who won robert@493: // Blink both LEDs when projectile is in motion robert@493: float val = (gSampleCounter % 4000 > 2000) ? 1.0 : 0; robert@493: analogWriteOnce(context, n, gOutputPlayer1LED, gameStatusWinner() == 1 ? val : 0); robert@493: analogWriteOnce(context, n, gOutputPlayer2LED, gameStatusWinner() == 2 ? val : 0); robert@493: robert@493: // After 5 seconds, restart the game robert@493: gSamplesSinceFinish++; robert@493: if(gSamplesSinceFinish > 22050*5) robert@493: gGameShouldRestart = true; robert@493: } robert@493: else if(gameStatusProjectileInMotion()) { robert@493: // Blink both LEDs when projectile is in motion robert@493: float val = (gSampleCounter % 2000 > 1000) ? 1.0 : 0; robert@493: analogWriteOnce(context, n, gOutputPlayer1LED, val); robert@493: analogWriteOnce(context, n, gOutputPlayer2LED, val); robert@493: } robert@493: else if(gameStatusPlayer1Turn()) { robert@493: analogWriteOnce(context, n, gOutputPlayer1LED, 1.0); robert@493: analogWriteOnce(context, n, gOutputPlayer2LED, 0); robert@493: } robert@493: else { robert@493: analogWriteOnce(context, n, gOutputPlayer2LED, 1.0); robert@493: analogWriteOnce(context, n, gOutputPlayer1LED, 0); robert@493: } robert@493: robert@493: // Check if we have reached the point where we should next update robert@493: if(gScreenBufferReadPointer >= gScreenBufferNextUpdateLocation && robert@493: !gScreenNextBufferReady) { robert@493: // Update the screen at lower priority than the audio thread robert@493: Bela_scheduleAuxiliaryTask(gScreenUpdateTask); robert@493: } robert@493: robert@493: gSampleCounter++; robert@493: } robert@493: } robert@493: robert@493: void screen_update() robert@493: { robert@493: // If we should restart, reinitialise the game robert@493: if(gGameShouldRestart) { robert@493: restartGame(); robert@493: gGameShouldRestart = false; robert@493: gSamplesSinceFinish = 0; robert@493: } robert@493: robert@493: // Render the game based on the current state robert@493: gScreenBufferWriteLength = drawGame(gScreenBufferWrite, gScreenBufferMaxLength); robert@493: robert@493: // Flag it as ready to go robert@493: gScreenNextBufferReady = true; robert@493: } robert@493: robert@493: // cleanup() is called once at the end, after the audio has stopped. robert@493: // Release any resources that were allocated in setup(). robert@493: robert@493: void cleanup(BelaContext *context, void *userData) robert@493: { robert@493: // Clean up the game state robert@493: cleanupGame(); robert@493: }