Another bit of code. This one generates the notes on startup and stores the samples in a memblock for each note + wave type. I also added sawtooth and triangle wave types for fun.
The end result is that loading the buffer is much faster and shouldn't affect the looping as much as doing all the calculations each time. Since this idea is for a plugin, this seems acceptable for testing since the C++ code should be much faster than the tier 1 code.
Despite this, my phone still wouldn't play it without popping even with a buffer size of 44000.
#option_explicit
// Project: AGK Streaming Sound
// Created: 2018-12-27
// show all errors
SetErrorMode(2)
// set window properties
SetWindowTitle("AGK Streaming Sound")
SetWindowSize(1024, 768, 0)
SetWindowAllowResize(1)
// set display properties
SetVirtualResolution(1024, 768)
SetOrientationAllowed(1, 1, 1, 1)
SetSyncRate(30, 0)
SetScissor(0, 0, 0, 0)
UseNewDefaultFonts(1)
#constant TWO_PI 6.28318530718
#constant NEWLINE chr(10)
// Sound information
#constant SOUND_BUFFER_LENGTH 4096 // buffer size in samples
#constant SOUND_BUFFER_COUNT 2 // two buffers
#constant SOUND_HEADER_LENGTH 12
#constant SOUND_CHANNELS 2 // Stereo
#constant SOUND_SAMPLE_RATE 44100 // CD quality
#constant SOUND_SAMPLE_BITS 16 // 16-bit. Be sure to set GetMemblockSample and SetMemblockSample below!
// For 16 bit samples
#constant GetMemblockSample GetMemblockShort
#constant SetMemblockSample SetMemblockShort
// For 8 bit samples
//~ #constant GetMemblockSample GetMemblockByte
//~ #constant SetMemblockSample SetMemblockByte
// Must be 8 or 16
if not (SOUND_SAMPLE_BITS = 8 or SOUND_SAMPLE_BITS = 16)
Message("Invalid SOUND_SAMPLE_BITS value: " + str(SOUND_SAMPLE_BITS) + ". Should be 8 or 16.")
Exit
endif
global soundBytesPerSample as integer
soundBytesPerSample = (SOUND_SAMPLE_BITS / 8)
// Precalculate some values
global soundBytesPerFrame as integer
soundBytesPerFrame = soundBytesPerSample * SOUND_CHANNELS
global soundBufferLengthMS as float
soundBufferLengthMS = (1000.0 * SOUND_BUFFER_LENGTH / SOUND_SAMPLE_RATE)
global maxSampleValue as integer
maxSampleValue = 2 ^ (SOUND_SAMPLE_BITS - 1) - 1 // Subtract 1 so the range is -127 to +127 or -32767 to +32767.
#constant PLAY_BUTTON 1
#constant WAVEFORM_BUTTON 2
#constant CLEAR_BUTTON 3
global waveformNames as string[3] = ["Sine", "Square", "Sawtooth", "Triangle"]
global waveform as integer
AddVirtualButton(PLAY_BUTTON, 500, 50, 100)
SetVirtualButtonText(PLAY_BUTTON, "Play")
AddVirtualButton(WAVEFORM_BUTTON, 600, 50, 100)
SetVirtualButtonText(WAVEFORM_BUTTON, waveformNames[waveform])
AddVirtualButton(CLEAR_BUTTON, 700, 50, 100)
SetVirtualButtonText(CLEAR_BUTTON, "Clear" + NEWLINE + "Status" + NEWLINE + "Text")
global statusTextID as integer
statusTextID = CreateText("")
SetTextPosition(statusTextID, 0, 180)
SetTextSize(statusTextID, 30)
SetTextMaxWidth(statusTextID, 768)
global memblockID as integer
global soundID as integer
global soundInstance as integer
global loadSoundBufferRunTime as integer
GenerateNoteSamples()
InitializeSoundBuffers()
startTime as integer
do
startTime = GetMilliseconds()
Print(ScreenFPS())
Print("Loops: " + str(GetSoundInstanceLoopCount(soundInstance)))
Print("soundBufferLengthMS: " + str(soundBufferLengthMS))
Print("LoadSoundBuffer run time: " + str(loadSoundBufferRunTime) + " ms")
//~ Print(status)
if GetVirtualButtonPressed(PLAY_BUTTON)
if not soundInstance
PlaySoundStream(25)
SetVirtualButtonText(PLAY_BUTTON, "Stop")
else
StopSoundStream()
SetVirtualButtonText(PLAY_BUTTON, "Play")
endif
elseif GetVirtualButtonPressed(WAVEFORM_BUTTON)
inc waveform
if waveform = waveformNames.length + 1
waveform = 0
endif
SetVirtualButtonText(WAVEFORM_BUTTON, waveformNames[waveform])
elseif GetVirtualButtonPressed(CLEAR_BUTTON)
SetTextString(statusTextID, "")
endif
CheckSoundBuffer()
Print("Loop time: " + str(GetMilliseconds() - startTime) + " ms")
Sync()
if GetRawKeyPressed(27)
exit
endif
loop
#constant NOTE_COUNT 8
#constant NOTE_LENGTH 8000
global lastLoopCount as integer
global musicScale as float [7] = [261.63, 293.66, 329.63, 349.23, 392.00, 440.00, 493.88, 523.25]
global noteFrame as integer
global noteCount as integer = 0
global nextBuffer as integer
global loadBufferTime as float
global playVolume as integer
Function PlaySoundStream(volume as integer)
// Reset variables.
nextBuffer = 0
lastLoopCount = 0
noteCount = 0
noteFrame = 0
// Preload all buffers
index as integer
for index = 0 to SOUND_BUFFER_COUNT - 1
LoadSoundBuffer()
next
playVolume = volume
soundInstance = PlaySound(soundID, playVolume, 1)
// Start the buffer timer.
// Multiple soundBufferLengthMS by 1.1 when starting so we don't try to load the buffer EXACTLY when the next one starts player.
// This is an attempt to reduce popping.
loadBufferTime = GetMilliseconds() + soundBufferLengthMS * 1.1
AddStatus("Next loadBufferTime: " + str(loadBufferTime))
EndFunction
Function StopSoundStream()
if not soundInstance
ExitFunction
endif
StopSoundInstance(soundInstance)
soundInstance = 0
EndFunction
Function CheckSoundBuffer()
if not soundInstance
ExitFunction
endif
//~ AddStatus("CheckSoundBuffer")
// Resizing the window can cause looping sounds to stop playing.
if not GetSoundInstancePlaying(soundInstance)
AddStatus("Restarting...")
soundInstance = PlaySound(soundID, playVolume, 1)
loadBufferTime = GetMilliseconds() + soundBufferLengthMS
endif
currentMilliseconds as integer
currentMilliseconds = GetMilliseconds()
if currentMilliseconds > loadBufferTime
inc loadBufferTime, soundBufferLengthMS
// If there was an abnormally long pause between buffer loads, catch up to the current system time.
if loadBufferTime <= currentMilliseconds
AddStatus("have to catch up!")
loadBufferTime = currentMilliseconds + soundBufferLengthMS
endif
LoadSoundBuffer()
AddStatus("Next loadBufferTime: " + str(loadBufferTime))
endif
EndFunction
Function Sgn(value as float)
if value > 0
ExitFunction 1
elseif value < 0
ExitFunction -1
endif
EndFunction 0
global noteData as integer[NOTE_COUNT, 3]
//
// Generates and stores the note wave samples so the calculations don't have to be done in the loop.
//
Function GenerateNoteSamples()
AddStatus("Generating Note Samples")
// 8-bit sound is unsigned and needs to be centered at 127 instead of 0.
// 16-bit is signed and does not need to be adjusted..
unsignedOffset as integer
if SOUND_SAMPLE_BITS = 8
unsignedOffset = maxSampleValue
endif
note as integer
for note = 0 to NOTE_COUNT - 1
// Calculate the balance for the note.
leftBalance as float
rightBalance as float
if SOUND_CHANNELS = 1
leftBalance = 1
else
rightBalance = note / 7.0
leftBalance = 1 - rightBalance
endif
// Prepare a memblock for each waveform.
waveForm as integer
for waveForm = 0 to noteData[note].length
noteData[note, waveform] = CreateMemblock(NOTE_LENGTH * soundBytesPerFrame)
next
// Create the wave forms.
offset as integer = 0
sample as integer
for sample = 0 to NOTE_LENGTH - 1
t as float
t = (musicScale[note] * sample) / SOUND_SAMPLE_RATE
// Calculate the wave form.
sine as float
sine = SinRad(TWO_PI * t) * maxSampleValue
square as float
square = Sgn(sine) * maxSampleValue
sawtooth as float
sawtooth = (t - floor(t + 0.5)) * maxSampleValue
triangle as float
triangle = abs(sawtooth)
// Apply a quick fade in and fade out to eliminate popping when switching notes.
volume as float
if sample < 1000
volume = sample / 1000.0
elseif sample > (NOTE_LENGTH - 1000)
volume = (NOTE_LENGTH - sample) / 1000.0
else
volume = 1
endif
sine = sine * volume
square = square * volume
sawtooth = sawtooth * volume
triangle = triangle * volume
// Write to the memblock.
// Use the #constant lines at the top to set this function definition.
SetMemblockSample(noteData[note, 0], offset, sine * leftBalance + unsignedOffset)
SetMemblockSample(noteData[note, 1], offset, square * leftBalance + unsignedOffset)
SetMemblockSample(noteData[note, 2], offset, sawtooth * leftBalance + unsignedOffset)
SetMemblockSample(noteData[note, 3], offset, triangle * leftBalance + unsignedOffset)
inc offset, soundBytesPerSample
if SOUND_CHANNELS = 2
SetMemblockSample(noteData[note, 0], offset, sine * rightBalance + unsignedOffset)
SetMemblockSample(noteData[note, 1], offset, square * rightBalance + unsignedOffset)
SetMemblockSample(noteData[note, 2], offset, sawtooth * rightBalance + unsignedOffset)
SetMemblockSample(noteData[note, 3], offset, triangle * rightBalance + unsignedOffset)
inc offset, soundBytesPerSample
endif
next
next
EndFunction
//
// This plays an ascending C scale holding each note for 1 second.
// Waveform is set by the user.
// There's a little fade in/fade out on each note to eliminate audio pops when changing notes.
//
Function LoadSoundBuffer()
AddStatus("Loading buffer " + str(nextBuffer))
startTime as integer
startTime = GetMilliseconds()
offset as integer
offset = SOUND_HEADER_LENGTH + nextBuffer * SOUND_BUFFER_LENGTH * soundBytesPerFrame
// This block of code loads the tone memblock as chunks and sends everything to the sound buffer at once.
// Not very flexible, but fast.
//~ length as integer
//~ length = SOUND_BUFFER_LENGTH
//~ while length > 0
//~ noteMemblock as integer
//~ noteMemblock = noteData[noteCount, waveForm]
//~ noteLength as integer
//~ noteLength = (NOTE_LENGTH - noteFrame)
//~ if noteLength > length
//~ noteLength = length
//~ endif
//~ CopyMemblock(noteMemblock, memblockID, noteFrame * soundBytesPerFrame, offset, noteLength * soundBytesPerFrame)
//~ inc offset, noteLength * soundBytesPerFrame
//~ dec length, noteLength
//~ inc noteFrame, noteLength
//~ if noteFrame = NOTE_LENGTH
//~ // Move to next note.
//~ noteFrame = 0
//~ inc noteCount
//~ if noteCount = NOTE_COUNT
//~ noteCount = 0
//~ endif
//~ endif
//~ endwhile
// The code below loads each sample from the tone memblock and sends them to the sound buffer.
// Slower, but more flexible.
sample as integer
noteOffset as integer
noteOffset = noteFrame * soundBytesPerFrame
bufferIndex as integer
for bufferIndex = 0 to SOUND_BUFFER_LENGTH - 1
// Use the #constant lines at the top to set this function definition.
sample = GetMemblockSample(noteData[noteCount, waveform], noteOffset)
SetMemblockSample(memblockID, offset, sample)
inc offset, soundBytesPerSample
inc noteOffset, soundBytesPerSample
if SOUND_CHANNELS = 2
sample = GetMemblockSample(noteData[noteCount, waveform], noteOffset)
SetMemblockSample(memblockID, offset, sample)
inc offset, soundBytesPerSample
inc noteOffset, soundBytesPerSample
endif
inc noteFrame
if noteFrame = NOTE_LENGTH
// Move to the next note.
noteFrame = 0
noteOffset = 0
inc noteCount
if noteCount = NOTE_COUNT
// Go to the first note.
noteCount = 0
endif
endif
next
CreateSoundFromMemblock(soundID, memblockID)
// Prepare for the next buffer.
inc nextBuffer
if nextBuffer = SOUND_BUFFER_COUNT
nextBuffer = 0
endif
loadSoundBufferRunTime = GetMilliseconds() - startTime
EndFunction
Function InitializeSoundBuffers()
memblockID = CreateMemblock(SOUND_HEADER_LENGTH + SOUND_BUFFER_LENGTH * SOUND_BUFFER_COUNT * soundBytesPerFrame)
SetMemblockShort(memblockID, 0, SOUND_CHANNELS)
SetMemblockShort(memblockID, 2, SOUND_SAMPLE_BITS)
SetMemblockInt(memblockID, 4, SOUND_SAMPLE_RATE)
SetMemblockInt(memblockID, 8, SOUND_BUFFER_LENGTH * SOUND_BUFFER_COUNT)
// Initialize the sound buffers to silence.
soundID = CreateSoundFromMemblock(memblockID)
EndFunction
Function AddStatus(text as String)
SetTextString(statusTextID, str(GetMilliseconds()) + ": " + text + NEWLINE + left(GetTextString(statusTextID), 600))
EndFunction