369 lines
12 KiB
C#
369 lines
12 KiB
C#
/*******************************************************************************************
|
|
*
|
|
* raylib [audio] example - stream callback
|
|
*
|
|
* Example complexity rating: [★★★☆] 3/4
|
|
*
|
|
* Example originally created with raylib 6.0, last time updated with raylib 6.0
|
|
*
|
|
* Example created by Dan Hoang (@dan-hoang) and reviewed by Ramon Santamaria (@raysan5)
|
|
*
|
|
* NOTE: Example sends a wave to the audio device,
|
|
* user gets the choice of four waves: sine, square, triangle, and sawtooth
|
|
* A stream is set up to play to the audio device; stream is hooked to a callback that
|
|
* generates a wave, that is determined by user choice
|
|
*
|
|
* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
|
|
* BSD-like license that allows static linking with closed source software
|
|
*
|
|
* Copyright (c) 2026 Dan Hoang (@dan-hoang)
|
|
*
|
|
********************************************************************************************/
|
|
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
|
|
namespace Examples.Audio;
|
|
|
|
[ExcludeFromBrowser("SetAudioStreamCallback is unreliable on the wasm audio backend")]
|
|
public unsafe partial class StreamCallback : IExample
|
|
{
|
|
private const int screenWidth = 800;
|
|
private const int screenHeight = 450;
|
|
|
|
private const int BUFFER_SIZE = 4096;
|
|
private const int SAMPLE_RATE = 44100;
|
|
|
|
// Wave type
|
|
private enum WaveType
|
|
{
|
|
Sine,
|
|
Square,
|
|
Triangle,
|
|
Sawtooth
|
|
}
|
|
|
|
public string Name => "Audio / Stream Callback";
|
|
|
|
public string Title => "raylib [audio] example - stream callback";
|
|
|
|
public int TargetFps => 30;
|
|
|
|
private static int waveFrequency = 440;
|
|
private static int newWaveFrequency = 440;
|
|
private static int waveIndex = 0;
|
|
|
|
// Buffer to keep the last second of uploaded audio,
|
|
// part of which will be drawn on the screen
|
|
private static readonly float[] buffer = new float[SAMPLE_RATE];
|
|
|
|
private static readonly string[] waveTypesAsString = { "sine", "square", "triangle", "sawtooth" };
|
|
|
|
private AudioStream stream;
|
|
private WaveType waveType;
|
|
|
|
public void Init()
|
|
{
|
|
InitAudioDevice();
|
|
|
|
waveFrequency = 440;
|
|
newWaveFrequency = 440;
|
|
waveIndex = 0;
|
|
Array.Clear(buffer, 0, buffer.Length);
|
|
|
|
// Set the number of samples the stream will keep in memory at a time to BUFFER_SIZE
|
|
SetAudioStreamBufferSizeDefault(BUFFER_SIZE);
|
|
|
|
// Init raw audio stream (sample rate: 44100, sample size: 32bit-float, channels: 1-mono)
|
|
stream = LoadAudioStream(SAMPLE_RATE, 32, 1);
|
|
PlayAudioStream(stream);
|
|
|
|
// Configure it so that the callback for waveType is called whenever stream is out of samples
|
|
waveType = WaveType.Sine;
|
|
SetWaveCallback();
|
|
}
|
|
|
|
// Attach the callback matching the current waveType to the stream
|
|
private void SetWaveCallback()
|
|
{
|
|
switch (waveType)
|
|
{
|
|
case WaveType.Sine:
|
|
SetAudioStreamCallback(stream, &SineCallback);
|
|
break;
|
|
case WaveType.Square:
|
|
SetAudioStreamCallback(stream, &SquareCallback);
|
|
break;
|
|
case WaveType.Triangle:
|
|
SetAudioStreamCallback(stream, &TriangleCallback);
|
|
break;
|
|
case WaveType.Sawtooth:
|
|
SetAudioStreamCallback(stream, &SawtoothCallback);
|
|
break;
|
|
}
|
|
}
|
|
|
|
public void Update()
|
|
{
|
|
// Update
|
|
//----------------------------------------------------------------------------------
|
|
if (IsKeyDown(KeyboardKey.Up))
|
|
{
|
|
newWaveFrequency += 10;
|
|
if (newWaveFrequency > 12500)
|
|
{
|
|
newWaveFrequency = 12500;
|
|
}
|
|
}
|
|
|
|
if (IsKeyDown(KeyboardKey.Down))
|
|
{
|
|
newWaveFrequency -= 10;
|
|
if (newWaveFrequency < 20)
|
|
{
|
|
newWaveFrequency = 20;
|
|
}
|
|
}
|
|
|
|
if (IsKeyPressed(KeyboardKey.Left))
|
|
{
|
|
if (waveType == WaveType.Sine)
|
|
{
|
|
waveType = WaveType.Sawtooth;
|
|
}
|
|
else if (waveType == WaveType.Square)
|
|
{
|
|
waveType = WaveType.Sine;
|
|
}
|
|
else if (waveType == WaveType.Triangle)
|
|
{
|
|
waveType = WaveType.Square;
|
|
}
|
|
else
|
|
{
|
|
waveType = WaveType.Triangle;
|
|
}
|
|
|
|
SetWaveCallback();
|
|
}
|
|
|
|
if (IsKeyPressed(KeyboardKey.Right))
|
|
{
|
|
if (waveType == WaveType.Sine)
|
|
{
|
|
waveType = WaveType.Square;
|
|
}
|
|
else if (waveType == WaveType.Square)
|
|
{
|
|
waveType = WaveType.Triangle;
|
|
}
|
|
else if (waveType == WaveType.Triangle)
|
|
{
|
|
waveType = WaveType.Sawtooth;
|
|
}
|
|
else
|
|
{
|
|
waveType = WaveType.Sine;
|
|
}
|
|
|
|
SetWaveCallback();
|
|
}
|
|
//----------------------------------------------------------------------------------
|
|
|
|
// Draw
|
|
//----------------------------------------------------------------------------------
|
|
BeginDrawing();
|
|
|
|
ClearBackground(Color.RayWhite);
|
|
DrawText($"frequency: {newWaveFrequency}", screenWidth - 220, 10, 20, Color.Red);
|
|
DrawText($"wave type: {waveTypesAsString[(int)waveType]}", screenWidth - 220, 30, 20, Color.Red);
|
|
DrawText("Up/down to change frequency", 10, 10, 20, Color.DarkGray);
|
|
DrawText("Left/right to change wave type", 10, 30, 20, Color.DarkGray);
|
|
|
|
// Draw the last 10 ms of uploaded audio
|
|
for (int i = 0; i < screenWidth; i++)
|
|
{
|
|
Vector2 startPos = new Vector2(i, 250 - 50 * buffer[WaveSampleIndex(i)]);
|
|
Vector2 endPos = new Vector2(i + 1, 250 - 50 * buffer[WaveSampleIndex(i + 1)]);
|
|
DrawLineV(startPos, endPos, Color.Red);
|
|
}
|
|
|
|
EndDrawing();
|
|
//----------------------------------------------------------------------------------
|
|
}
|
|
|
|
public void Unload()
|
|
{
|
|
UnloadAudioStream(stream); // Close raw audio stream and delete buffers from RAM
|
|
CloseAudioDevice(); // Close audio device (music streaming is automatically stopped)
|
|
}
|
|
|
|
// Maps a screen column to a sample index in the last 10 ms of the buffer. The final column
|
|
// maps one sample past the end of the buffer; upstream C reads out of bounds there, so we
|
|
// clamp to the last valid sample (visually identical, but safe under C# bounds checking).
|
|
private static int WaveSampleIndex(int column)
|
|
{
|
|
int index = SAMPLE_RATE - SAMPLE_RATE / 100 + column * SAMPLE_RATE / 100 / screenWidth;
|
|
return index < SAMPLE_RATE ? index : SAMPLE_RATE - 1;
|
|
}
|
|
|
|
//------------------------------------------------------------------------------------
|
|
// Module Functions Definition
|
|
//------------------------------------------------------------------------------------
|
|
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
|
|
private static void SineCallback(void* framesOut, uint frameCount)
|
|
{
|
|
int fc = (int)frameCount;
|
|
int wavelength = SAMPLE_RATE / waveFrequency;
|
|
float* frames = (float*)framesOut;
|
|
|
|
// Synthesize the sine wave
|
|
for (int i = 0; i < fc; i++)
|
|
{
|
|
frames[i] = MathF.Sin(2 * MathF.PI * waveIndex / wavelength);
|
|
|
|
waveIndex++;
|
|
|
|
if (waveIndex >= wavelength)
|
|
{
|
|
waveFrequency = newWaveFrequency;
|
|
waveIndex = 0;
|
|
}
|
|
}
|
|
|
|
// Save the synthesized samples for later drawing
|
|
for (int i = 0; i < SAMPLE_RATE - fc; i++)
|
|
{
|
|
buffer[i] = buffer[i + fc];
|
|
}
|
|
|
|
for (int i = 0; i < fc; i++)
|
|
{
|
|
buffer[SAMPLE_RATE - fc + i] = frames[i];
|
|
}
|
|
}
|
|
|
|
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
|
|
private static void SquareCallback(void* framesOut, uint frameCount)
|
|
{
|
|
int fc = (int)frameCount;
|
|
int wavelength = SAMPLE_RATE / waveFrequency;
|
|
float* frames = (float*)framesOut;
|
|
|
|
// Synthesize the square wave
|
|
for (int i = 0; i < fc; i++)
|
|
{
|
|
frames[i] = (waveIndex < wavelength / 2) ? 1 : -1;
|
|
waveIndex++;
|
|
|
|
if (waveIndex >= wavelength)
|
|
{
|
|
waveFrequency = newWaveFrequency;
|
|
waveIndex = 0;
|
|
}
|
|
}
|
|
|
|
// Save the synthesized samples for later drawing
|
|
for (int i = 0; i < SAMPLE_RATE - fc; i++)
|
|
{
|
|
buffer[i] = buffer[i + fc];
|
|
}
|
|
|
|
for (int i = 0; i < fc; i++)
|
|
{
|
|
buffer[SAMPLE_RATE - fc + i] = frames[i];
|
|
}
|
|
}
|
|
|
|
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
|
|
private static void TriangleCallback(void* framesOut, uint frameCount)
|
|
{
|
|
int fc = (int)frameCount;
|
|
int wavelength = SAMPLE_RATE / waveFrequency;
|
|
float* frames = (float*)framesOut;
|
|
|
|
// Synthesize the triangle wave
|
|
for (int i = 0; i < fc; i++)
|
|
{
|
|
frames[i] = (waveIndex < wavelength / 2) ? (-1 + 2.0f * waveIndex / (wavelength / 2)) : (1 - 2.0f * (waveIndex - wavelength / 2) / (wavelength / 2));
|
|
waveIndex++;
|
|
|
|
if (waveIndex >= wavelength)
|
|
{
|
|
waveFrequency = newWaveFrequency;
|
|
waveIndex = 0;
|
|
}
|
|
}
|
|
|
|
// Save the synthesized samples for later drawing
|
|
for (int i = 0; i < SAMPLE_RATE - fc; i++)
|
|
{
|
|
buffer[i] = buffer[i + fc];
|
|
}
|
|
|
|
for (int i = 0; i < fc; i++)
|
|
{
|
|
buffer[SAMPLE_RATE - fc + i] = frames[i];
|
|
}
|
|
}
|
|
|
|
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
|
|
private static void SawtoothCallback(void* framesOut, uint frameCount)
|
|
{
|
|
int fc = (int)frameCount;
|
|
int wavelength = SAMPLE_RATE / waveFrequency;
|
|
float* frames = (float*)framesOut;
|
|
|
|
// Synthesize the sawtooth wave
|
|
for (int i = 0; i < fc; i++)
|
|
{
|
|
frames[i] = -1 + 2.0f * waveIndex / wavelength;
|
|
waveIndex++;
|
|
|
|
if (waveIndex >= wavelength)
|
|
{
|
|
waveFrequency = newWaveFrequency;
|
|
waveIndex = 0;
|
|
}
|
|
}
|
|
|
|
// Save the synthesized samples for later drawing
|
|
for (int i = 0; i < SAMPLE_RATE - fc; i++)
|
|
{
|
|
buffer[i] = buffer[i + fc];
|
|
}
|
|
|
|
for (int i = 0; i < fc; i++)
|
|
{
|
|
buffer[SAMPLE_RATE - fc + i] = frames[i];
|
|
}
|
|
}
|
|
|
|
public static int Main()
|
|
{
|
|
// Initialization
|
|
//--------------------------------------------------------------------------------------
|
|
InitWindow(screenWidth, screenHeight, "raylib [audio] example - stream callback");
|
|
|
|
SetTargetFPS(30);
|
|
//--------------------------------------------------------------------------------------
|
|
|
|
var game = new StreamCallback();
|
|
game.Init();
|
|
|
|
// Main game loop
|
|
while (!WindowShouldClose()) // Detect window close button or ESC key
|
|
{
|
|
game.Update();
|
|
}
|
|
|
|
game.Unload();
|
|
|
|
// De-Initialization
|
|
//--------------------------------------------------------------------------------------
|
|
CloseWindow(); // Close window and OpenGL context
|
|
//--------------------------------------------------------------------------------------
|
|
|
|
return 0;
|
|
}
|
|
}
|