Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
AVI file format
#1
I've been wanting this for a really long time. A program that creates a valid AVI format with sound. The following program only creates (does not play) an uncompressed AVI file, which also contains uncompressed stereo audio. For this reason, prepare about 180 megabytes of free disk space for the test. I think it's already quite complicated, so if I add compression to it, it would be much more complicated. I will deal with compression, but I don't know how far I will get, because I encounter many obstacles.
I must warn you that Windows Media Player blurs the image due to the player's internal filters, VLC player has the best outputs. MPC-HC plays it too, but it rotates the image. I can rotate the image in AVI, but then it is rotated again in Windows Media Player and in VLC. It's kind of a beginning... and also an extreme. First I deal with compression in GIF and save every byte and now tens of MegaBytes are no big deal...

It's uncompressed, so expect a data rate somewhere around 60 megabytes per second. QB64PE can handle that speed just fine.

Code: (Select All)

' Program creates an uncompressed AVI file with uncompressed animation and uncompressed audio.
' The uncompressed format is easier to understand, though I discovered that the AVI file itself
' is a topic suitable for psychiatry... Smile

' -------------------------
' Structure Definitions
' -------------------------
Type AVIMainHeader
    microSecPerFrame As Long '    Microseconds per frame
    maxBytesPerSec As Long '      Maximum data rate
    paddingGranularity As Long '  Padding granularity
    flags As Long '              AVI flags (e.g., HASINDEX)
    totalFrames As Long '        Total number of frames in the video
    initialFrames As Long '      Initial frames (typically 0)
    streams As Long '            Number of streams (video, audio, etc.)
    suggestedBufferSize As Long ' Suggested buffer size for playback
    width As Long '              Video width in pixels
    height As Long '              Video height in pixels
    reserved As String * 16 '    Reserved bytes (set to 0)
End Type

Type AVIStreamHeader
    fccType As String * 4 '      Stream type (e.g., "vids" or "auds")
    fccHandler As String * 4 '    Codec handler (e.g., "DIB " for uncompressed video)
    flags As Long '              Stream flags
    priority As Integer '        Priority (unused)
    language As Integer '        Language code (unused)
    initialFrames As Long '      Initial frames (unused)
    scale As Long '              Time scale for the stream
    rate As Long '                Data rate (scale/rate = FPS for video)
    start As Long '              Start time (usually 0)
    length As Long '              Length of the stream (in time units)
    suggestedBufferSize As Long ' Suggested buffer size for the stream
    quality As Long '            Quality indicator (-1 for default)
    sampleSize As Long '          Sample size (0 for video, nonzero for audio)
    frameLeft As Long '          For video: left coordinate of the source rectangle
    frameTop As Long '            For video: top coordinate of the source rectangle
    frameRight As Long '          For video: right coordinate of the source rectangle
    frameBottom As Long '        For video: bottom coordinate of the source rectangle
End Type

Type BITMAPINFOHEADER
    size As Long '                Size of this header (40 bytes)
    width As Long '              Width of the image in pixels
    height As Long '              Height of the image (negative for top–down)
    planes As Integer '          Number of color planes (must be 1)
    bitCount As Integer '        Bits per pixel (24 for 24-bit, 32 for 32-bit)
    compression As Long '        Compression method (0 = BI_RGB for uncompressed)
    sizeImage As Long '          Size of the image data in bytes
    xPelsPerMeter As Long '      Horizontal resolution (pixels per meter)
    yPelsPerMeter As Long '      Vertical resolution (pixels per meter)
    clrUsed As Long '            Number of colors used (0 = all)
    clrImportant As Long '        Number of important colors (0 = all)
End Type

Type WAVEFORMATEX
    wFormatTag As Integer '      Audio format code (1 = PCM)
    nChannels As Integer '        Number of audio channels (2 for stereo)
    nSamplesPerSec As Long '      Sample rate in Hz
    nAvgBytesPerSec As Long '    Average bytes per second
    nBlockAlign As Integer '      Block alignment in bytes
    wBitsPerSample As Integer '  Bits per sample (16 for 16-bit PCM)
    cbSize As Integer '          Extra size (0 for PCM)
End Type

' -------------------------
' Video Constants
' -------------------------
Const vidFPS = 30 '              Frames per second for the video
Const bytesPerPixel = 4 '        Each pixel in 32-bit image (BGRA) takes 4 bytes

' -------------------------
' Video Parameters
' -------------------------
Dim numFrames As Long: numFrames = 90 '                                      90 frames = 3 seconds of video
Dim frameWidth As Long: frameWidth = 800 '                                    Video width in pixels
Dim frameHeight As Long: frameHeight = 600 '                                  Video height in pixels
Dim frameSize As Long: frameSize = frameWidth * frameHeight * bytesPerPixel ' Total size of one 32-bit frame

' -------------------------
' Create Test Frame Array
' -------------------------
Dim Frames(1 To numFrames) As String
Dim i As Long, s As String, sm As _MEM
s = Space$(frameSize)

$If WIN Then
    pad$ = "\"
$Else
    pad$ = "/"
$End If


fn$ = _CWD$ + pad$ + "VideoTest.avi"
Print "Generating file "; fn$

preDest = _Dest '                                                Save current destination settings

For i = 1 To numFrames
    Dim img As Long, img2 As Long
    img2 = _NewImage(frameWidth, frameHeight, 32)
    _Dest img2 '                                                  Set current destination to img2
    Cls '                                                        Set not transparent background in frame
    _PrintMode _KeepBackground
    ' Draw 100 random colored lines
    For x = 1 To 100
        Line (Rnd * 50, Rnd * 50)-(Rnd * 750, Rnd * 550), _RGB32(Rnd * 255, Rnd * 255, Rnd * 255), B
    Next
    '                                                            Print frame number text at a fixed position
    _PrintString (350, 250), "Video Frame " + Str$(i), img2
    img = _NewImage(frameWidth, frameHeight, 32)

    ' For VLC Player use this line:
    ' _PutImage (0, 0), img2, img, (0, frameHeight)-(frameWidth, 0)

    ' For WMP, MPC-HC Player use:
    _PutImage (0, 0), img2, img

    _FreeImage img2
    sm = _MemImage(img)
    _MemGet sm, sm.OFFSET, s
    _MemFree sm
    _FreeImage img
    Frames(i) = s ' Store frame data as string
Next i
_Dest preDest '    Restore previous destination

' -------------------------
' Simulate Audio
' -------------------------
Dim sampleRate As Long: sampleRate = 44100 '                                          Audio sample rate in Hz
Dim totalSeconds As Long: totalSeconds = numFrames \ vidFPS '                        Duration of video in seconds
Dim totalSamplesPerChannel As Long: totalSamplesPerChannel = totalSeconds * sampleRate
Dim totalSamples As Long: totalSamples = totalSamplesPerChannel * 2 '                Stereo: two channels
ReDim audioSamples(1 To totalSamples) As Integer

'                                                                            Generate a tone with panning effect (from left to right)
Dim freq As Single: freq = 500 '                                            Starting frequency in Hz
Dim As Integer SampleL, SampleR '                                            Variables for left and right channel samples
Dim t As Single, normAmp As Single: normAmp = 0.9
For i = 1 To totalSamplesPerChannel - 1
    t = i / sampleRate
    '                                                                        Calculate pan value from 0 (left) to 1 (right)
    pan = i / (totalSamplesPerChannel - 1)
    '                                                                        Left channel amplitude = 0.9*(1 - pan), right channel amplitude = 0.9*pan
    If Rnd * 10 > 5 Then f = 5 Else f = -5
    freq = freq + f
    SampleL = 32767 * 0.9 * (1 - pan) * Sin(2 * 3.14159 * freq * t)
    SampleR = 32767 * 0.9 * pan * Sin(2 * 3.14159 * freq * t)
    audioSamples(2 * i + 1) = SampleL
    audioSamples(2 * i + 2) = SampleR
Next i

' ---------------------------
' Create AVI file with audio
' ---------------------------
If _FileExists("VideoTest.avi") Then Kill "VideoTest.avi"
CreateAVI Frames(), frameWidth, frameHeight, numFrames, audioSamples(), sampleRate

Print "File created. Open it with your media player program. Recommended is VLC Player."

' -------------------------------------------------------------
' Function to write an AVI file with video + audio (16-bit PCM)
' -------------------------------------------------------------

Sub CreateAVI (Frames() As String, frameWidth As Long, frameHeight As Long, _
              numFrames As Long, audioSamples() As Integer, sampleRate As Long)
    Dim frameSize As Long: frameSize = frameWidth * frameHeight * bytesPerPixel
    Dim fileNum As Integer: fileNum = FreeFile
    Open "VideoTest.avi" For Binary As #fileNum

    Dim videoDuration As Long
    videoDuration = numFrames \ vidFPS

    Dim totalAudioSamples As Long
    totalAudioSamples = UBound(audioSamples)
    Dim stereoFrames As Long
    stereoFrames = totalAudioSamples \ 2

    ' -------------------------------------------
    ' Define chunk index structure for AVI index
    ' -------------------------------------------
    Type ChunkIndex
        chunkID As String * 4 ' Chunk identifier (e.g., "00db" for video, "01wb" for audio)
        flags As Long '        Flags (e.g., keyframe flag)
        offset As Long '        Offset of chunk data from start of 'movi' section
        size As Long '          Size of chunk data in bytes
    End Type

    Dim maxChunks As Long
    maxChunks = numFrames + videoDuration + 10
    Dim idxArr(1 To maxChunks) As ChunkIndex
    Dim idxCount As Long: idxCount = 0

    ' --------------------------------------------------------
    ' RIFF and AVI chunks
    ' --------------------------------------------------------
    Dim riffHeader As String: riffHeader = "RIFF"
    Dim tmpStr As String: tmpStr = riffHeader
    Put #fileNum, , tmpStr

    Dim riffSizePos As Long: riffSizePos = LOF(fileNum) + 1
    Dim dummyLong As Long: dummyLong = 0
    Put #fileNum, , dummyLong '  placeholder for RIFF size

    Dim aviSig As String: aviSig = "AVI "
    tmpStr = aviSig
    Put #fileNum, , tmpStr

    ' --------------------------------------------------------
    ' LIST hdrl chunk (header list)
    ' --------------------------------------------------------
    Dim listHdrl As String: listHdrl = "LIST"
    tmpStr = listHdrl
    Put #fileNum, , tmpStr

    Dim hdrlSizePos As Long: hdrlSizePos = LOF(fileNum) + 1
    Put #fileNum, , dummyLong '  placeholder for hdrl size

    Dim hdrlSig As String: hdrlSig = "hdrl"
    tmpStr = hdrlSig
    Put #fileNum, , tmpStr

    ' --------------------------------------------------------
    ' avih chunk (Main AVI header)
    ' --------------------------------------------------------
    Dim avihChunkID As String: avihChunkID = "avih"
    tmpStr = avihChunkID
    Put #fileNum, , tmpStr

    Dim avihChunkSize As Long: avihChunkSize = 56
    Put #fileNum, , avihChunkSize

    Dim mainHdr As AVIMainHeader
    mainHdr.microSecPerFrame = 1000000 \ vidFPS
    mainHdr.maxBytesPerSec = frameSize * vidFPS
    mainHdr.paddingGranularity = 0
    mainHdr.flags = &H110 '            AVIF_HASINDEX flag
    mainHdr.totalFrames = numFrames
    mainHdr.initialFrames = 0
    mainHdr.streams = 2 '              2 streams: video + audio (stereo is 1 stream and mono also)
    mainHdr.suggestedBufferSize = frameSize
    mainHdr.width = frameWidth
    mainHdr.height = frameHeight
    mainHdr.reserved = String$(16, Chr$(0))
    Put #fileNum, , mainHdr

    ' --------------------------------------------------------
    ' LIST strl chunk for video stream
    ' --------------------------------------------------------
    Dim listStrlVid As String: listStrlVid = "LIST"
    tmpStr = listStrlVid
    Put #fileNum, , tmpStr

    Dim strlVidSizePos As Long: strlVidSizePos = LOF(fileNum) + 1
    Put #fileNum, , dummyLong

    Dim strlVidSig As String: strlVidSig = "strl"
    tmpStr = strlVidSig
    Put #fileNum, , tmpStr

    ' -- strh (video stream header)
    Dim strhVidID As String: strhVidID = "strh"
    tmpStr = strhVidID
    Put #fileNum, , tmpStr

    Dim strhVidSize As Long: strhVidSize = 64
    Put #fileNum, , strhVidSize

    Dim streamHdrVid As AVIStreamHeader
    streamHdrVid.fccType = "vids"
    streamHdrVid.fccHandler = "DIB " ' Uncompressed video (DIB)
    streamHdrVid.flags = 0
    streamHdrVid.priority = 0
    streamHdrVid.language = 0
    streamHdrVid.initialFrames = 0
    streamHdrVid.scale = 1
    streamHdrVid.rate = vidFPS
    streamHdrVid.start = 0
    streamHdrVid.length = numFrames
    streamHdrVid.suggestedBufferSize = frameSize
    streamHdrVid.quality = -1
    streamHdrVid.sampleSize = 0
    streamHdrVid.frameLeft = 0
    streamHdrVid.frameTop = 0
    streamHdrVid.frameRight = frameWidth
    streamHdrVid.frameBottom = frameHeight
    Put #fileNum, , streamHdrVid

    ' --------------------------------------------------------
    ' -- strf (video format chunk) -> BITMAPINFOHEADER
    ' --------------------------------------------------------
    Dim strfVidID As String: strfVidID = "strf"
    tmpStr = strfVidID
    Put #fileNum, , tmpStr

    Dim strfVidSize As Long: strfVidSize = 40
    Put #fileNum, , strfVidSize

    Dim bmpInfo As BITMAPINFOHEADER
    bmpInfo.size = 40
    bmpInfo.width = frameWidth
    bmpInfo.height = -frameHeight ' Negative height indicates top-down format
    bmpInfo.planes = 1
    bmpInfo.bitCount = 32
    bmpInfo.compression = 0 '      BI_RGB = uncompressed
    bmpInfo.sizeImage = frameSize
    bmpInfo.xPelsPerMeter = 0
    bmpInfo.yPelsPerMeter = 0
    bmpInfo.clrUsed = 0
    bmpInfo.clrImportant = 0
    Put #fileNum, , bmpInfo

    Dim currPos As Long
    currPos = LOF(fileNum) + 1
    Dim calcStrlVidSize As Long: calcStrlVidSize = currPos - strlVidSizePos - 4
    Seek #fileNum, strlVidSizePos
    Put #fileNum, , calcStrlVidSize
    Seek #fileNum, currPos

    ' --------------------------------------------------------
    ' LIST strl chunk for audio stream
    ' --------------------------------------------------------
    Dim listStrlAud As String: listStrlAud = "LIST"
    tmpStr = listStrlAud
    Put #fileNum, , tmpStr

    Dim strlAudSizePos As Long: strlAudSizePos = LOF(fileNum) + 1
    Put #fileNum, , dummyLong

    Dim strlAudSig As String: strlAudSig = "strl"
    tmpStr = strlAudSig
    Put #fileNum, , tmpStr

    ' --------------------------------------------------------
    ' -- strh (audio stream header)
    ' --------------------------------------------------------
    Dim strhAudID As String: strhAudID = "strh"
    tmpStr = strhAudID
    Put #fileNum, , tmpStr

    Dim strhAudSize As Long: strhAudSize = 64
    Put #fileNum, , strhAudSize

    Dim streamHdrAud As AVIStreamHeader
    streamHdrAud.fccType = "auds"
    streamHdrAud.fccHandler = String$(4, 0) ' For PCM audio, this is typically empty
    streamHdrAud.flags = 0
    streamHdrAud.priority = 0
    streamHdrAud.language = 0
    streamHdrAud.initialFrames = 0
    streamHdrAud.scale = 1
    streamHdrAud.rate = sampleRate
    streamHdrAud.start = 0
    streamHdrAud.length = stereoFrames
    streamHdrAud.suggestedBufferSize = sampleRate * 4 ' 2 channels x 2 bytes per sample (is used 16bit stereo waveform sound, 1 integer to 1 sample)
    streamHdrAud.quality = -1
    streamHdrAud.sampleSize = 4 ' 4 bytes per stereo sample (2 bytes left + 2 bytes right)
    streamHdrAud.frameLeft = 0
    streamHdrAud.frameTop = 0
    streamHdrAud.frameRight = 0
    streamHdrAud.frameBottom = 0
    Put #fileNum, , streamHdrAud

    ' --------------------------------------------------------
    ' -- strf (audio format chunk) -> WAVEFORMATEX (16 bytes)
    ' --------------------------------------------------------
    Dim strfAudID As String: strfAudID = "strf"
    tmpStr = strfAudID
    Put #fileNum, , tmpStr

    Dim strfAudSize As Long: strfAudSize = 16
    Put #fileNum, , strfAudSize

    Dim wf As WAVEFORMATEX
    wf.wFormatTag = 1
    wf.nChannels = 2
    wf.nSamplesPerSec = sampleRate
    wf.wBitsPerSample = 16
    wf.nBlockAlign = wf.nChannels * (wf.wBitsPerSample \ 8)
    wf.nAvgBytesPerSec = wf.nSamplesPerSec * wf.nBlockAlign
    wf.cbSize = 0
    Put #fileNum, , wf

    currPos = LOF(fileNum) + 1
    Dim calcStrlAudSize As Long: calcStrlAudSize = currPos - strlAudSizePos - 4
    Seek #fileNum, strlAudSizePos
    Put #fileNum, , calcStrlAudSize
    Seek #fileNum, currPos

    currPos = LOF(fileNum) + 1
    Dim hdrlSize As Long: hdrlSize = currPos - hdrlSizePos - 4
    Seek #fileNum, hdrlSizePos
    Put #fileNum, , hdrlSize
    Seek #fileNum, currPos

    ' ----------------------------------------------------------
    ' LIST movi chunk (contains the actual video and audio data)
    ' ----------------------------------------------------------
    Dim listMovi As String: listMovi = "LIST"
    tmpStr = listMovi
    Put #fileNum, , tmpStr

    Dim moviSizePos As Long: moviSizePos = LOF(fileNum) + 1
    Put #fileNum, , dummyLong

    Dim moviSig As String: moviSig = "movi"
    tmpStr = moviSig
    Put #fileNum, , tmpStr

    Dim moviDataStart As Long: moviDataStart = LOF(fileNum) + 1

    ' --------------------------------------------------------------
    ' Interleaved writing of video and audio chunks:
    ' For each second: 30 video frames and 1 audio chunk (1 second)  - this solution MUST NOT BE ALWAYS OPTIMAL! is possible writing sound data after every 5 th frame...
    ' --------------------------------------------------------------
    Dim framesPerSec As Long: framesPerSec = vidFPS
    Dim videoIndex As Long: videoIndex = 1
    Dim sec As Long
    For sec = 1 To videoDuration
        ' Write video chunks
        Dim f As Long
        For f = 1 To framesPerSec
            If videoIndex > numFrames Then Exit For
            Dim chunkOffset As Long
            chunkOffset = LOF(fileNum) - moviDataStart + 1
            Dim frameChunkID As String: frameChunkID = "00db"
            tmpStr = frameChunkID
            Put #fileNum, , tmpStr
            Dim currFrameSize As Long: currFrameSize = frameSize
            Put #fileNum, , currFrameSize
            Put #fileNum, , Frames(videoIndex)
            If (currFrameSize Mod 2) <> 0 Then
                Dim padByte As String: padByte = Chr$(0)
                Put #fileNum, , padByte
            End If
            idxCount = idxCount + 1
            idxArr(idxCount).chunkID = frameChunkID
            idxArr(idxCount).flags = &H10 ' Keyframe flag
            idxArr(idxCount).offset = chunkOffset
            idxArr(idxCount).size = currFrameSize
            videoIndex = videoIndex + 1
        Next f

        ' Write audio chunk for the current second
        Dim chunkOffsetAud As Long
        chunkOffsetAud = LOF(fileNum) - moviDataStart + 1
        Dim audioChunkID As String: audioChunkID = "01wb"
        tmpStr = audioChunkID
        Put #fileNum, , tmpStr
        Dim audioChunkSize As Long
        audioChunkSize = sampleRate * 2 * 2 ' 1 second = sampleRate * (2 channels x 2 bytes)
        Put #fileNum, , audioChunkSize

        Dim startAud As Long: startAud = (sec - 1) * (sampleRate * 2) + 1
        Dim endAud As Long: endAud = startAud + (sampleRate * 2) - 1
        If endAud > UBound(audioSamples) Then endAud = UBound(audioSamples)

        Dim k As Long
        Dim audioBuffer As String
        audioBuffer = ""
        Dim sampleValue As Integer
        Dim uVal As Long
        For k = startAud To endAud
            sampleValue = audioSamples(k)
            uVal = sampleValue And &HFFFF ' Convert to 16-bit two's complement representation
            audioBuffer = audioBuffer + Chr$(uVal And &HFF) + Chr$((uVal \ 256) And &HFF)
        Next k
        Put #fileNum, , audioBuffer

        If (audioChunkSize Mod 2) <> 0 Then
            Dim padByteAud As String: padByteAud = Chr$(0)
            Put #fileNum, , padByteAud
        End If

        idxCount = idxCount + 1
        idxArr(idxCount).chunkID = audioChunkID
        idxArr(idxCount).flags = 0
        idxArr(idxCount).offset = chunkOffsetAud
        idxArr(idxCount).size = audioChunkSize
    Next sec

    currPos = LOF(fileNum) + 1
    Dim moviSize As Long
    moviSize = currPos - moviSizePos - 4
    Seek #fileNum, moviSizePos
    Put #fileNum, , moviSize
    Seek #fileNum, currPos

    Dim riffSize As Long
    riffSize = LOF(fileNum) - 8 + 1
    Seek #fileNum, riffSizePos
    Put #fileNum, , riffSize
    Seek #fileNum, LOF(fileNum) + 1

    Dim idxHeader As String: idxHeader = "idx1"
    tmpStr = idxHeader
    Put #fileNum, , tmpStr
    Dim idxSize As Long: idxSize = idxCount * 16
    Put #fileNum, , idxSize

    Dim n As Long
    For n = 1 To idxCount
        Put #fileNum, , idxArr(n).chunkID
        Put #fileNum, , idxArr(n).flags
        Put #fileNum, , idxArr(n).offset
        Put #fileNum, , idxArr(n).size
    Next n

    Close #fileNum
End Sub


Reply
#2
Wow! I can't wait to give this a try!
Reply
#3
Good work ... the demo works for me.  

I used Handbrake to reduce the AVI filesize to 1.6 MB at 1080 30fps

Stereo works well ( panning from left to right ) 

No video blurriness using WMP, VLC, or MPC.
Reply
#4
(03-22-2025, 08:00 PM)Petr Wrote: I've been wanting this for a really long time. A program that creates a valid AVI format with sound. The following program only creates (does not play) an uncompressed AVI file, which also contains uncompressed stereo audio. For this reason, prepare about 180 megabytes of free disk space for the test. I think it's already quite complicated, so if I add compression to it, it would be much more complicated. I will deal with compression, but I don't know how far I will get, because I encounter many obstacles.
I must warn you that Windows Media Player blurs the image due to the player's internal filters, VLC player has the best outputs. MPC-HC plays it too, but it rotates the image. I can rotate the image in AVI, but then it is rotated again in Windows Media Player and in VLC. It's kind of a beginning... and also an extreme. First I deal with compression in GIF and save every byte and now tens of MegaBytes are no big deal...
It's uncompressed, so expect a data rate somewhere around 60 megabytes per second. QB64PE can handle that speed just fine.
Code: (Select All)

' Program creates an uncompressed AVI file with uncompressed animation and uncompressed audio.
' The uncompressed format is easier to understand, though I discovered that the AVI file itself
' is a topic suitable for psychiatry... Smile
' -------------------------
' Structure Definitions
' -------------------------
Type AVIMainHeader
    microSecPerFrame As Long '    Microseconds per frame
    maxBytesPerSec As Long '      Maximum data rate
    paddingGranularity As Long '  Padding granularity
    flags As Long '              AVI flags (e.g., HASINDEX)
    totalFrames As Long '        Total number of frames in the video
    initialFrames As Long '      Initial frames (typically 0)
    streams As Long '            Number of streams (video, audio, etc.)
    suggestedBufferSize As Long ' Suggested buffer size for playback
    width As Long '              Video width in pixels
    height As Long '              Video height in pixels
    reserved As String * 16 '    Reserved bytes (set to 0)
End Type
Type AVIStreamHeader
    fccType As String * 4 '      Stream type (e.g., "vids" or "auds")
    fccHandler As String * 4 '    Codec handler (e.g., "DIB " for uncompressed video)
    flags As Long '              Stream flags
    priority As Integer '        Priority (unused)
    language As Integer '        Language code (unused)
    initialFrames As Long '      Initial frames (unused)
    scale As Long '              Time scale for the stream
    rate As Long '                Data rate (scale/rate = FPS for video)
    start As Long '              Start time (usually 0)
    length As Long '              Length of the stream (in time units)
    suggestedBufferSize As Long ' Suggested buffer size for the stream
    quality As Long '            Quality indicator (-1 for default)
    sampleSize As Long '          Sample size (0 for video, nonzero for audio)
    frameLeft As Long '          For video: left coordinate of the source rectangle
    frameTop As Long '            For video: top coordinate of the source rectangle
    frameRight As Long '          For video: right coordinate of the source rectangle
    frameBottom As Long '        For video: bottom coordinate of the source rectangle
End Type
Type BITMAPINFOHEADER
    size As Long '                Size of this header (40 bytes)
    width As Long '              Width of the image in pixels
    height As Long '              Height of the image (negative for top–down)
    planes As Integer '          Number of color planes (must be 1)
    bitCount As Integer '        Bits per pixel (24 for 24-bit, 32 for 32-bit)
    compression As Long '        Compression method (0 = BI_RGB for uncompressed)
    sizeImage As Long '          Size of the image data in bytes
    xPelsPerMeter As Long '      Horizontal resolution (pixels per meter)
    yPelsPerMeter As Long '      Vertical resolution (pixels per meter)
    clrUsed As Long '            Number of colors used (0 = all)
    clrImportant As Long '        Number of important colors (0 = all)
End Type
Type WAVEFORMATEX
    wFormatTag As Integer '      Audio format code (1 = PCM)
    nChannels As Integer '        Number of audio channels (2 for stereo)
    nSamplesPerSec As Long '      Sample rate in Hz
    nAvgBytesPerSec As Long '    Average bytes per second
    nBlockAlign As Integer '      Block alignment in bytes
    wBitsPerSample As Integer '  Bits per sample (16 for 16-bit PCM)
    cbSize As Integer '          Extra size (0 for PCM)
End Type
' -------------------------
' Video Constants
' -------------------------
Const vidFPS = 30 '              Frames per second for the video
Const bytesPerPixel = 4 '        Each pixel in 32-bit image (BGRA) takes 4 bytes
' -------------------------
' Video Parameters
' -------------------------
Dim numFrames As Long: numFrames = 90 '                                      90 frames = 3 seconds of video
Dim frameWidth As Long: frameWidth = 800 '                                    Video width in pixels
Dim frameHeight As Long: frameHeight = 600 '                                  Video height in pixels
Dim frameSize As Long: frameSize = frameWidth * frameHeight * bytesPerPixel ' Total size of one 32-bit frame
' -------------------------
' Create Test Frame Array
' -------------------------
Dim Frames(1 To numFrames) As String
Dim i As Long, s As String, sm As _MEM
s = Space$(frameSize)
$If WIN Then
    pad$ = "\"
$Else
    pad$ = "/"
$End If
fn$ = _CWD$ + pad$ + "VideoTest.avi"
Print "Generating file "; fn$
preDest = _Dest '                                                Save current destination settings
For i = 1 To numFrames
    Dim img As Long, img2 As Long
    img2 = _NewImage(frameWidth, frameHeight, 32)
    _Dest img2 '                                                  Set current destination to img2
    Cls '                                                        Set not transparent background in frame
    _PrintMode _KeepBackground
    ' Draw 100 random colored lines
    For x = 1 To 100
        Line (Rnd * 50, Rnd * 50)-(Rnd * 750, Rnd * 550), _RGB32(Rnd * 255, Rnd * 255, Rnd * 255), B
    Next
    '                                                            Print frame number text at a fixed position
    _PrintString (350, 250), "Video Frame " + Str$(i), img2
    img = _NewImage(frameWidth, frameHeight, 32)
    ' For VLC Player use this line:
    ' _PutImage (0, 0), img2, img, (0, frameHeight)-(frameWidth, 0)
    ' For WMP, MPC-HC Player use:
    _PutImage (0, 0), img2, img
    _FreeImage img2
    sm = _MemImage(img)
    _MemGet sm, sm.OFFSET, s
    _MemFree sm
    _FreeImage img
    Frames(i) = s ' Store frame data as string
Next i
_Dest preDest '    Restore previous destination
' -------------------------
' Simulate Audio
' -------------------------
Dim sampleRate As Long: sampleRate = 44100 '                                          Audio sample rate in Hz
Dim totalSeconds As Long: totalSeconds = numFrames \ vidFPS '                        Duration of video in seconds
Dim totalSamplesPerChannel As Long: totalSamplesPerChannel = totalSeconds * sampleRate
Dim totalSamples As Long: totalSamples = totalSamplesPerChannel * 2 '                Stereo: two channels
ReDim audioSamples(1 To totalSamples) As Integer
'                                                                            Generate a tone with panning effect (from left to right)
Dim freq As Single: freq = 500 '                                            Starting frequency in Hz
Dim As Integer SampleL, SampleR '                                            Variables for left and right channel samples
Dim t As Single, normAmp As Single: normAmp = 0.9
For i = 1 To totalSamplesPerChannel - 1
    t = i / sampleRate
    '                                                                        Calculate pan value from 0 (left) to 1 (right)
    pan = i / (totalSamplesPerChannel - 1)
    '                                                                        Left channel amplitude = 0.9*(1 - pan), right channel amplitude = 0.9*pan
    If Rnd * 10 > 5 Then f = 5 Else f = -5
    freq = freq + f
    SampleL = 32767 * 0.9 * (1 - pan) * Sin(2 * 3.14159 * freq * t)
    SampleR = 32767 * 0.9 * pan * Sin(2 * 3.14159 * freq * t)
    audioSamples(2 * i + 1) = SampleL
    audioSamples(2 * i + 2) = SampleR
Next i
' ---------------------------
' Create AVI file with audio
' ---------------------------
If _FileExists("VideoTest.avi") Then Kill "VideoTest.avi"
CreateAVI Frames(), frameWidth, frameHeight, numFrames, audioSamples(), sampleRate
Print "File created. Open it with your media player program. Recommended is VLC Player."
' -------------------------------------------------------------
' Function to write an AVI file with video + audio (16-bit PCM)
' -------------------------------------------------------------
Sub CreateAVI (Frames() As String, frameWidth As Long, frameHeight As Long, _
              numFrames As Long, audioSamples() As Integer, sampleRate As Long)
    Dim frameSize As Long: frameSize = frameWidth * frameHeight * bytesPerPixel
    Dim fileNum As Integer: fileNum = FreeFile
    Open "VideoTest.avi" For Binary As #fileNum
    Dim videoDuration As Long
    videoDuration = numFrames \ vidFPS
    Dim totalAudioSamples As Long
    totalAudioSamples = UBound(audioSamples)
    Dim stereoFrames As Long
    stereoFrames = totalAudioSamples \ 2
    ' -------------------------------------------
    ' Define chunk index structure for AVI index
    ' -------------------------------------------
    Type ChunkIndex
        chunkID As String * 4 ' Chunk identifier (e.g., "00db" for video, "01wb" for audio)
        flags As Long '        Flags (e.g., keyframe flag)
        offset As Long '        Offset of chunk data from start of 'movi' section
        size As Long '          Size of chunk data in bytes
    End Type
    Dim maxChunks As Long
    maxChunks = numFrames + videoDuration + 10
    Dim idxArr(1 To maxChunks) As ChunkIndex
    Dim idxCount As Long: idxCount = 0
    ' --------------------------------------------------------
    ' RIFF and AVI chunks
    ' --------------------------------------------------------
    Dim riffHeader As String: riffHeader = "RIFF"
    Dim tmpStr As String: tmpStr = riffHeader
    Put #fileNum, , tmpStr
    Dim riffSizePos As Long: riffSizePos = LOF(fileNum) + 1
    Dim dummyLong As Long: dummyLong = 0
    Put #fileNum, , dummyLong '  placeholder for RIFF size
    Dim aviSig As String: aviSig = "AVI "
    tmpStr = aviSig
    Put #fileNum, , tmpStr
    ' --------------------------------------------------------
    ' LIST hdrl chunk (header list)
    ' --------------------------------------------------------
    Dim listHdrl As String: listHdrl = "LIST"
    tmpStr = listHdrl
    Put #fileNum, , tmpStr
    Dim hdrlSizePos As Long: hdrlSizePos = LOF(fileNum) + 1
    Put #fileNum, , dummyLong '  placeholder for hdrl size
    Dim hdrlSig As String: hdrlSig = "hdrl"
    tmpStr = hdrlSig
    Put #fileNum, , tmpStr
    ' --------------------------------------------------------
    ' avih chunk (Main AVI header)
    ' --------------------------------------------------------
    Dim avihChunkID As String: avihChunkID = "avih"
    tmpStr = avihChunkID
    Put #fileNum, , tmpStr
    Dim avihChunkSize As Long: avihChunkSize = 56
    Put #fileNum, , avihChunkSize
    Dim mainHdr As AVIMainHeader
    mainHdr.microSecPerFrame = 1000000 \ vidFPS
    mainHdr.maxBytesPerSec = frameSize * vidFPS
    mainHdr.paddingGranularity = 0
    mainHdr.flags = &H110 '            AVIF_HASINDEX flag
    mainHdr.totalFrames = numFrames
    mainHdr.initialFrames = 0
    mainHdr.streams = 2 '              2 streams: video + audio (stereo is 1 stream and mono also)
    mainHdr.suggestedBufferSize = frameSize
    mainHdr.width = frameWidth
    mainHdr.height = frameHeight
    mainHdr.reserved = String$(16, Chr$(0))
    Put #fileNum, , mainHdr
    ' --------------------------------------------------------
    ' LIST strl chunk for video stream
    ' --------------------------------------------------------
    Dim listStrlVid As String: listStrlVid = "LIST"
    tmpStr = listStrlVid
    Put #fileNum, , tmpStr
    Dim strlVidSizePos As Long: strlVidSizePos = LOF(fileNum) + 1
    Put #fileNum, , dummyLong
    Dim strlVidSig As String: strlVidSig = "strl"
    tmpStr = strlVidSig
    Put #fileNum, , tmpStr
    ' -- strh (video stream header)
    Dim strhVidID As String: strhVidID = "strh"
    tmpStr = strhVidID
    Put #fileNum, , tmpStr
    Dim strhVidSize As Long: strhVidSize = 64
    Put #fileNum, , strhVidSize
    Dim streamHdrVid As AVIStreamHeader
    streamHdrVid.fccType = "vids"
    streamHdrVid.fccHandler = "DIB " ' Uncompressed video (DIB)
    streamHdrVid.flags = 0
    streamHdrVid.priority = 0
    streamHdrVid.language = 0
    streamHdrVid.initialFrames = 0
    streamHdrVid.scale = 1
    streamHdrVid.rate = vidFPS
    streamHdrVid.start = 0
    streamHdrVid.length = numFrames
    streamHdrVid.suggestedBufferSize = frameSize
    streamHdrVid.quality = -1
    streamHdrVid.sampleSize = 0
    streamHdrVid.frameLeft = 0
    streamHdrVid.frameTop = 0
    streamHdrVid.frameRight = frameWidth
    streamHdrVid.frameBottom = frameHeight
    Put #fileNum, , streamHdrVid
    ' --------------------------------------------------------
    ' -- strf (video format chunk) -> BITMAPINFOHEADER
    ' --------------------------------------------------------
    Dim strfVidID As String: strfVidID = "strf"
    tmpStr = strfVidID
    Put #fileNum, , tmpStr
    Dim strfVidSize As Long: strfVidSize = 40
    Put #fileNum, , strfVidSize
    Dim bmpInfo As BITMAPINFOHEADER
    bmpInfo.size = 40
    bmpInfo.width = frameWidth
    bmpInfo.height = -frameHeight ' Negative height indicates top-down format
    bmpInfo.planes = 1
    bmpInfo.bitCount = 32
    bmpInfo.compression = 0 '      BI_RGB = uncompressed
    bmpInfo.sizeImage = frameSize
    bmpInfo.xPelsPerMeter = 0
    bmpInfo.yPelsPerMeter = 0
    bmpInfo.clrUsed = 0
    bmpInfo.clrImportant = 0
    Put #fileNum, , bmpInfo
    Dim currPos As Long
    currPos = LOF(fileNum) + 1
    Dim calcStrlVidSize As Long: calcStrlVidSize = currPos - strlVidSizePos - 4
    Seek #fileNum, strlVidSizePos
    Put #fileNum, , calcStrlVidSize
    Seek #fileNum, currPos
    ' --------------------------------------------------------
    ' LIST strl chunk for audio stream
    ' --------------------------------------------------------
    Dim listStrlAud As String: listStrlAud = "LIST"
    tmpStr = listStrlAud
    Put #fileNum, , tmpStr
    Dim strlAudSizePos As Long: strlAudSizePos = LOF(fileNum) + 1
    Put #fileNum, , dummyLong
    Dim strlAudSig As String: strlAudSig = "strl"
    tmpStr = strlAudSig
    Put #fileNum, , tmpStr
    ' --------------------------------------------------------
    ' -- strh (audio stream header)
    ' --------------------------------------------------------
    Dim strhAudID As String: strhAudID = "strh"
    tmpStr = strhAudID
    Put #fileNum, , tmpStr
    Dim strhAudSize As Long: strhAudSize = 64
    Put #fileNum, , strhAudSize
    Dim streamHdrAud As AVIStreamHeader
    streamHdrAud.fccType = "auds"
    streamHdrAud.fccHandler = String$(4, 0) ' For PCM audio, this is typically empty
    streamHdrAud.flags = 0
    streamHdrAud.priority = 0
    streamHdrAud.language = 0
    streamHdrAud.initialFrames = 0
    streamHdrAud.scale = 1
    streamHdrAud.rate = sampleRate
    streamHdrAud.start = 0
    streamHdrAud.length = stereoFrames
    streamHdrAud.suggestedBufferSize = sampleRate * 4 ' 2 channels x 2 bytes per sample (is used 16bit stereo waveform sound, 1 integer to 1 sample)
    streamHdrAud.quality = -1
    streamHdrAud.sampleSize = 4 ' 4 bytes per stereo sample (2 bytes left + 2 bytes right)
    streamHdrAud.frameLeft = 0
    streamHdrAud.frameTop = 0
    streamHdrAud.frameRight = 0
    streamHdrAud.frameBottom = 0
    Put #fileNum, , streamHdrAud
    ' --------------------------------------------------------
    ' -- strf (audio format chunk) -> WAVEFORMATEX (16 bytes)
    ' --------------------------------------------------------
    Dim strfAudID As String: strfAudID = "strf"
    tmpStr = strfAudID
    Put #fileNum, , tmpStr
    Dim strfAudSize As Long: strfAudSize = 16
    Put #fileNum, , strfAudSize
    Dim wf As WAVEFORMATEX
    wf.wFormatTag = 1
    wf.nChannels = 2
    wf.nSamplesPerSec = sampleRate
    wf.wBitsPerSample = 16
    wf.nBlockAlign = wf.nChannels * (wf.wBitsPerSample \ 8)
    wf.nAvgBytesPerSec = wf.nSamplesPerSec * wf.nBlockAlign
    wf.cbSize = 0
    Put #fileNum, , wf
    currPos = LOF(fileNum) + 1
    Dim calcStrlAudSize As Long: calcStrlAudSize = currPos - strlAudSizePos - 4
    Seek #fileNum, strlAudSizePos
    Put #fileNum, , calcStrlAudSize
    Seek #fileNum, currPos
    currPos = LOF(fileNum) + 1
    Dim hdrlSize As Long: hdrlSize = currPos - hdrlSizePos - 4
    Seek #fileNum, hdrlSizePos
    Put #fileNum, , hdrlSize
    Seek #fileNum, currPos
    ' ----------------------------------------------------------
    ' LIST movi chunk (contains the actual video and audio data)
    ' ----------------------------------------------------------
    Dim listMovi As String: listMovi = "LIST"
    tmpStr = listMovi
    Put #fileNum, , tmpStr
    Dim moviSizePos As Long: moviSizePos = LOF(fileNum) + 1
    Put #fileNum, , dummyLong
    Dim moviSig As String: moviSig = "movi"
    tmpStr = moviSig
    Put #fileNum, , tmpStr
    Dim moviDataStart As Long: moviDataStart = LOF(fileNum) + 1
    ' --------------------------------------------------------------
    ' Interleaved writing of video and audio chunks:
    ' For each second: 30 video frames and 1 audio chunk (1 second)  - this solution MUST NOT BE ALWAYS OPTIMAL! is possible writing sound data after every 5 th frame...
    ' --------------------------------------------------------------
    Dim framesPerSec As Long: framesPerSec = vidFPS
    Dim videoIndex As Long: videoIndex = 1
    Dim sec As Long
    For sec = 1 To videoDuration
        ' Write video chunks
        Dim f As Long
        For f = 1 To framesPerSec
            If videoIndex > numFrames Then Exit For
            Dim chunkOffset As Long
            chunkOffset = LOF(fileNum) - moviDataStart + 1
            Dim frameChunkID As String: frameChunkID = "00db"
            tmpStr = frameChunkID
            Put #fileNum, , tmpStr
            Dim currFrameSize As Long: currFrameSize = frameSize
            Put #fileNum, , currFrameSize
            Put #fileNum, , Frames(videoIndex)
            If (currFrameSize Mod 2) <> 0 Then
                Dim padByte As String: padByte = Chr$(0)
                Put #fileNum, , padByte
            End If
            idxCount = idxCount + 1
            idxArr(idxCount).chunkID = frameChunkID
            idxArr(idxCount).flags = &H10 ' Keyframe flag
            idxArr(idxCount).offset = chunkOffset
            idxArr(idxCount).size = currFrameSize
            videoIndex = videoIndex + 1
        Next f
        ' Write audio chunk for the current second
        Dim chunkOffsetAud As Long
        chunkOffsetAud = LOF(fileNum) - moviDataStart + 1
        Dim audioChunkID As String: audioChunkID = "01wb"
        tmpStr = audioChunkID
        Put #fileNum, , tmpStr
        Dim audioChunkSize As Long
        audioChunkSize = sampleRate * 2 * 2 ' 1 second = sampleRate * (2 channels x 2 bytes)
        Put #fileNum, , audioChunkSize
        Dim startAud As Long: startAud = (sec - 1) * (sampleRate * 2) + 1
        Dim endAud As Long: endAud = startAud + (sampleRate * 2) - 1
        If endAud > UBound(audioSamples) Then endAud = UBound(audioSamples)
        Dim k As Long
        Dim audioBuffer As String
        audioBuffer = ""
        Dim sampleValue As Integer
        Dim uVal As Long
        For k = startAud To endAud
            sampleValue = audioSamples(k)
            uVal = sampleValue And &HFFFF ' Convert to 16-bit two's complement representation
            audioBuffer = audioBuffer + Chr$(uVal And &HFF) + Chr$((uVal \ 256) And &HFF)
        Next k
        Put #fileNum, , audioBuffer
        If (audioChunkSize Mod 2) <> 0 Then
            Dim padByteAud As String: padByteAud = Chr$(0)
            Put #fileNum, , padByteAud
        End If
        idxCount = idxCount + 1
        idxArr(idxCount).chunkID = audioChunkID
        idxArr(idxCount).flags = 0
        idxArr(idxCount).offset = chunkOffsetAud
        idxArr(idxCount).size = audioChunkSize
    Next sec
    currPos = LOF(fileNum) + 1
    Dim moviSize As Long
    moviSize = currPos - moviSizePos - 4
    Seek #fileNum, moviSizePos
    Put #fileNum, , moviSize
    Seek #fileNum, currPos
    Dim riffSize As Long
    riffSize = LOF(fileNum) - 8 + 1
    Seek #fileNum, riffSizePos
    Put #fileNum, , riffSize
    Seek #fileNum, LOF(fileNum) + 1
    Dim idxHeader As String: idxHeader = "idx1"
    tmpStr = idxHeader
    Put #fileNum, , tmpStr
    Dim idxSize As Long: idxSize = idxCount * 16
    Put #fileNum, , idxSize
    Dim n As Long
    For n = 1 To idxCount
        Put #fileNum, , idxArr(n).chunkID
        Put #fileNum, , idxArr(n).flags
        Put #fileNum, , idxArr(n).offset
        Put #fileNum, , idxArr(n).size
    Next n
    Close #fileNum
End Sub
This is pretty awesome code.    I remember recently struggling with just the WAV & BMP file formats.    I bet this one took some hair pulling.      I'm really trying to figure out how I might use this.    My current QB64PE project is a Video convertor to turn MP4, AVI, MOV, etc...   Into a format that's playable on the Neo retro Commander X16 platform.    I didn't do any Video parsing on the source side in my own code, I let ffmpeg do that for me.    The output format I invented myself.     https://cx16forum.com/forum/viewtopic.php?t=8226
Reply
#5
I ran it and it works! Brilliant! 

Then I tried modifying it to generate a longer video (3 minutes) at standard DV resolution (720x480), and added a simple progress counters to the loops. 

And removed any DIM statements from inside the loops, because it seems inefficient to be redimming over and over again inside a loop.
(I notice a lot of folks just put DIM statements willy nilly inside the code, I was always taught to put them all up top, nice and neat!)

And I moved the file name into a parameter for CreateAvi and moved the erasing the file if it alread exists inside of that too.

But anyway, it started working, creating frames, and then the program just dies when it gets to 1799 frames. 

I wonder why? (modified code below)

Code: (Select All)
' AVI file format
' https://qb64phoenix.com/forum/showthread.php?tid=3550

' From: Petr, Mini-Mod
' Date: 3/22/2025 4:42 AM
' I've been wanting this for a really long time. A program that creates a valid
' AVI format with sound. The following program only creates (does not play) an
' uncompressed AVI file, which also contains uncompressed stereo audio. For this
' reason, prepare about 180 megabytes of free disk space for the test. I think
' it's already quite complicated, so if I add compression to it, it would be much
' more complicated. I will deal with compression, but I don't know how far I will
' get, because I encounter many obstacles.
' I must warn you that Windows Media Player blurs the image due to the player's
' internal filters, VLC player has the best outputs. MPC-HC plays it too, but it
' rotates the image. I can rotate the image in AVI, but then it is rotated again
' in Windows Media Player and in VLC. It's kind of a beginning... and also an
' extreme. First I deal with compression in GIF and save every byte and now tens
' of MegaBytes are no big deal...
' It's uncompressed, so expect a data rate somewhere around 60 megabytes per
' second. QB64PE can handle that speed just fine.

' -----------------------------------------------------------------------------

' Program creates an uncompressed AVI file with uncompressed animation and uncompressed audio.
' The uncompressed format is easier to understand, though I discovered that the AVI file itself
' is a topic suitable for psychiatry...

' -------------------------
' Video Constants
' -------------------------
Const vidFPS = 30 '              Frames per second for the video
Const bytesPerPixel = 4 '        Each pixel in 32-bit image (BGRA) takes 4 bytes
Const cFileName$ = "VideoTest2.avi"


' -------------------------
' Structure Definitions
' -------------------------
Type AVIMainHeader
    microSecPerFrame As Long '    Microseconds per frame
    maxBytesPerSec As Long '      Maximum data rate
    paddingGranularity As Long '  Padding granularity
    flags As Long '              AVI flags (e.g., HASINDEX)
    totalFrames As Long '        Total number of frames in the video
    initialFrames As Long '      Initial frames (typically 0)
    streams As Long '            Number of streams (video, audio, etc.)
    suggestedBufferSize As Long ' Suggested buffer size for playback
    width As Long '              Video width in pixels
    height As Long '              Video height in pixels
    reserved As String * 16 '    Reserved bytes (set to 0)
End Type

Type AVIStreamHeader
    fccType As String * 4 '      Stream type (e.g., "vids" or "auds")
    fccHandler As String * 4 '    Codec handler (e.g., "DIB " for uncompressed video)
    flags As Long '              Stream flags
    priority As Integer '        Priority (unused)
    language As Integer '        Language code (unused)
    initialFrames As Long '      Initial frames (unused)
    scale As Long '              Time scale for the stream
    rate As Long '                Data rate (scale/rate = FPS for video)
    start As Long '              Start time (usually 0)
    length As Long '              Length of the stream (in time units)
    suggestedBufferSize As Long ' Suggested buffer size for the stream
    quality As Long '            Quality indicator (-1 for default)
    sampleSize As Long '          Sample size (0 for video, nonzero for audio)
    frameLeft As Long '          For video: left coordinate of the source rectangle
    frameTop As Long '            For video: top coordinate of the source rectangle
    frameRight As Long '          For video: right coordinate of the source rectangle
    frameBottom As Long '        For video: bottom coordinate of the source rectangle
End Type

Type BITMAPINFOHEADER
    size As Long '                Size of this header (40 bytes)
    width As Long '              Width of the image in pixels
    height As Long '              Height of the image (negative for top–down)
    planes As Integer '          Number of color planes (must be 1)
    bitCount As Integer '        Bits per pixel (24 for 24-bit, 32 for 32-bit)
    compression As Long '        Compression method (0 = BI_RGB for uncompressed)
    sizeImage As Long '          Size of the image data in bytes
    xPelsPerMeter As Long '      Horizontal resolution (pixels per meter)
    yPelsPerMeter As Long '      Vertical resolution (pixels per meter)
    clrUsed As Long '            Number of colors used (0 = all)
    clrImportant As Long '        Number of important colors (0 = all)
End Type

Type WAVEFORMATEX
    wFormatTag As Integer '      Audio format code (1 = PCM)
    nChannels As Integer '        Number of audio channels (2 for stereo)
    nSamplesPerSec As Long '      Sample rate in Hz
    nAvgBytesPerSec As Long '    Average bytes per second
    nBlockAlign As Integer '      Block alignment in bytes
    wBitsPerSample As Integer '  Bits per sample (16 for 16-bit PCM)
    cbSize As Integer '          Extra size (0 for PCM)
End Type


' Video Parameters
Dim numSeconds As Long ' how long video should carry on
Dim numFrames As Long ' 90 frames = 3 seconds of video
Dim frameWidth As Long '  Video width in pixels
Dim frameHeight As Long '  Video height in pixels
Dim frameSize As Long

' Create Test Frame Array
ReDim Frames(-1 To -1) As String
Dim i As Long
Dim s As String
Dim sm As _MEM

' -------------------------
' Video Parameters
' -------------------------
'numFrames = 90
numSeconds = 60 * 3 ' 3 minutes
numFrames = vidFPS * numSeconds
'frameWidth = 800: frameHeight = 600
frameWidth = 720: frameHeight = 480 ' standard DV resolution
frameSize = frameWidth * frameHeight * bytesPerPixel ' Total size of one 32-bit frame
ReDim Frames(1 To numFrames) As String

' -------------------------
' Create Test Frame Array
' -------------------------
s = Space$(frameSize)
$If WIN Then
    pad$ = "\"
$Else
    pad$ = "/"
$End If


fn$ = _CWD$ + pad$ + cFileName$
Print "Generating file "; fn$

preDest = _Dest ' Save current destination settings

Dim img As Long, img2 As Long

_Dest 0
Locate 2, 1: Print "numFrames=" + _Trim$(Str$(numFrames))
Locate 3, 1: Print "lbound(Frames) = " + _Trim$(Str$(LBound(Frames)))
Locate 4, 1: Print "ubound(Frames) = " + _Trim$(Str$(UBound(Frames)))

For i = 1 To numFrames
   
    ' SHOW PROGRESS
    _Dest 0
    Locate 5, 1
    Print Right$("      " + _Trim$(Str$(i)), 6) + " of " + _Trim$(Str$(numFrames));

    img2 = _NewImage(frameWidth, frameHeight, 32)
    _Dest img2 'Set current destination to img2
    Cls ' Set not transparent background in frame
    _PrintMode _KeepBackground
    ' Draw 100 random colored lines
    For x = 1 To 100
        Line (Rnd * 50, Rnd * 50)-(Rnd * 750, Rnd * 550), _RGB32(Rnd * 255, Rnd * 255, Rnd * 255), B
    Next x
    ' Print frame number text at a fixed position
    _PrintString (350, 250), "Video Frame " + Str$(i), img2
    img = _NewImage(frameWidth, frameHeight, 32)

    ' For VLC Player use this line:
    ' _PutImage (0, 0), img2, img, (0, frameHeight)-(frameWidth, 0)

    ' For WMP, MPC-HC Player use:
    _PutImage (0, 0), img2, img

    _FreeImage img2
    sm = _MemImage(img)
    _MemGet sm, sm.OFFSET, s
    _MemFree sm
    _FreeImage img
    Frames(i) = s ' Store frame data as string
Next i

_Dest preDest ' Restore previous destination

' -------------------------
' Simulate Audio
' -------------------------
Dim sampleRate As Long: sampleRate = 44100 ' Audio sample rate in Hz
Dim totalSeconds As Long: totalSeconds = numFrames \ vidFPS ' Duration of video in seconds
Dim totalSamplesPerChannel As Long: totalSamplesPerChannel = totalSeconds * sampleRate
Dim totalSamples As Long: totalSamples = totalSamplesPerChannel * 2 ' Stereo: two channels
ReDim audioSamples(1 To totalSamples) As Integer

' Generate a tone with panning effect (from left to right)
Dim freq As Single: freq = 500 ' Starting frequency in Hz
Dim As Integer SampleL, SampleR ' Variables for left and right channel samples
Dim t As Single, normAmp As Single: normAmp = 0.9
Dim total2 As Long
total2 = totalSamplesPerChannel - 1
For i = 1 To total2
   
    ' SHOW PROGRESS
    _Dest 0
    Locate 6, 1
    Print Right$("      " + _Trim$(Str$(i)), 6) + " of " + _Trim$(Str$(total2));

    t = i / sampleRate
    ' Calculate pan value from 0 (left) to 1 (right)
    pan = i / (totalSamplesPerChannel - 1)
    ' Left channel amplitude = 0.9*(1 - pan), right channel amplitude = 0.9*pan
    If Rnd * 10 > 5 Then f = 5 Else f = -5
    freq = freq + f
    SampleL = 32767 * 0.9 * (1 - pan) * Sin(2 * 3.14159 * freq * t)
    SampleR = 32767 * 0.9 * pan * Sin(2 * 3.14159 * freq * t)
    audioSamples(2 * i + 1) = SampleL
    audioSamples(2 * i + 2) = SampleR
Next i

' ---------------------------
' Create AVI file with audio
' ---------------------------
CreateAVI cFileName$, Frames(), frameWidth, frameHeight, numFrames, audioSamples(), sampleRate

Print "File created. Open it with your media player program. Recommended is VLC Player."

' -------------------------------------------------------------
' Function to write an AVI file with video + audio (16-bit PCM)
' -------------------------------------------------------------

Sub CreateAVI (FileName$, Frames() As String, frameWidth As Long, frameHeight As Long, _
              numFrames As Long, audioSamples() As Integer, sampleRate As Long)
   
    If _FileExists(FileName$) Then Kill FileName$
   
    Dim frameSize As Long: frameSize = frameWidth * frameHeight * bytesPerPixel
    Dim fileNum As Integer: fileNum = FreeFile
    Open FileName$ For Binary As #fileNum

    Dim videoDuration As Long
    videoDuration = numFrames \ vidFPS

    Dim totalAudioSamples As Long
    totalAudioSamples = UBound(audioSamples)
    Dim stereoFrames As Long
    stereoFrames = totalAudioSamples \ 2

    ' -------------------------------------------
    ' Define chunk index structure for AVI index
    ' -------------------------------------------
    Type ChunkIndex
        chunkID As String * 4 ' Chunk identifier (e.g., "00db" for video, "01wb" for audio)
        flags As Long '        Flags (e.g., keyframe flag)
        offset As Long '        Offset of chunk data from start of 'movi' section
        size As Long '          Size of chunk data in bytes
    End Type

    Dim maxChunks As Long
    maxChunks = numFrames + videoDuration + 10
    Dim idxArr(1 To maxChunks) As ChunkIndex
    Dim idxCount As Long: idxCount = 0

    ' --------------------------------------------------------
    ' RIFF and AVI chunks
    ' --------------------------------------------------------
    Dim riffHeader As String: riffHeader = "RIFF"
    Dim tmpStr As String: tmpStr = riffHeader
    Put #fileNum, , tmpStr

    Dim riffSizePos As Long: riffSizePos = LOF(fileNum) + 1
    Dim dummyLong As Long: dummyLong = 0
    Put #fileNum, , dummyLong '  placeholder for RIFF size

    Dim aviSig As String: aviSig = "AVI "
    tmpStr = aviSig
    Put #fileNum, , tmpStr

    ' --------------------------------------------------------
    ' LIST hdrl chunk (header list)
    ' --------------------------------------------------------
    Dim listHdrl As String: listHdrl = "LIST"
    tmpStr = listHdrl
    Put #fileNum, , tmpStr

    Dim hdrlSizePos As Long: hdrlSizePos = LOF(fileNum) + 1
    Put #fileNum, , dummyLong ' placeholder for hdrl size

    Dim hdrlSig As String: hdrlSig = "hdrl"
    tmpStr = hdrlSig
    Put #fileNum, , tmpStr

    ' --------------------------------------------------------
    ' avih chunk (Main AVI header)
    ' --------------------------------------------------------
    Dim avihChunkID As String: avihChunkID = "avih"
    tmpStr = avihChunkID
    Put #fileNum, , tmpStr

    Dim avihChunkSize As Long: avihChunkSize = 56
    Put #fileNum, , avihChunkSize

    Dim mainHdr As AVIMainHeader
    mainHdr.microSecPerFrame = 1000000 \ vidFPS
    mainHdr.maxBytesPerSec = frameSize * vidFPS
    mainHdr.paddingGranularity = 0
    mainHdr.flags = &H110 ' AVIF_HASINDEX flag
    mainHdr.totalFrames = numFrames
    mainHdr.initialFrames = 0
    mainHdr.streams = 2 ' 2 streams: video + audio (stereo is 1 stream and mono also)
    mainHdr.suggestedBufferSize = frameSize
    mainHdr.width = frameWidth
    mainHdr.height = frameHeight
    mainHdr.reserved = String$(16, Chr$(0))
    Put #fileNum, , mainHdr

    ' --------------------------------------------------------
    ' LIST strl chunk for video stream
    ' --------------------------------------------------------
    Dim listStrlVid As String: listStrlVid = "LIST"
    tmpStr = listStrlVid
    Put #fileNum, , tmpStr

    Dim strlVidSizePos As Long: strlVidSizePos = LOF(fileNum) + 1
    Put #fileNum, , dummyLong

    Dim strlVidSig As String: strlVidSig = "strl"
    tmpStr = strlVidSig
    Put #fileNum, , tmpStr

    ' -- strh (video stream header)
    Dim strhVidID As String: strhVidID = "strh"
    tmpStr = strhVidID
    Put #fileNum, , tmpStr

    Dim strhVidSize As Long: strhVidSize = 64
    Put #fileNum, , strhVidSize

    Dim streamHdrVid As AVIStreamHeader
    streamHdrVid.fccType = "vids"
    streamHdrVid.fccHandler = "DIB " ' Uncompressed video (DIB)
    streamHdrVid.flags = 0
    streamHdrVid.priority = 0
    streamHdrVid.language = 0
    streamHdrVid.initialFrames = 0
    streamHdrVid.scale = 1
    streamHdrVid.rate = vidFPS
    streamHdrVid.start = 0
    streamHdrVid.length = numFrames
    streamHdrVid.suggestedBufferSize = frameSize
    streamHdrVid.quality = -1
    streamHdrVid.sampleSize = 0
    streamHdrVid.frameLeft = 0
    streamHdrVid.frameTop = 0
    streamHdrVid.frameRight = frameWidth
    streamHdrVid.frameBottom = frameHeight
    Put #fileNum, , streamHdrVid

    ' --------------------------------------------------------
    ' -- strf (video format chunk) -> BITMAPINFOHEADER
    ' --------------------------------------------------------
    Dim strfVidID As String: strfVidID = "strf"
    tmpStr = strfVidID
    Put #fileNum, , tmpStr

    Dim strfVidSize As Long: strfVidSize = 40
    Put #fileNum, , strfVidSize

    Dim bmpInfo As BITMAPINFOHEADER
    bmpInfo.size = 40
    bmpInfo.width = frameWidth
    bmpInfo.height = -frameHeight ' Negative height indicates top-down format
    bmpInfo.planes = 1
    bmpInfo.bitCount = 32
    bmpInfo.compression = 0 '      BI_RGB = uncompressed
    bmpInfo.sizeImage = frameSize
    bmpInfo.xPelsPerMeter = 0
    bmpInfo.yPelsPerMeter = 0
    bmpInfo.clrUsed = 0
    bmpInfo.clrImportant = 0
    Put #fileNum, , bmpInfo

    Dim currPos As Long
    currPos = LOF(fileNum) + 1
    Dim calcStrlVidSize As Long: calcStrlVidSize = currPos - strlVidSizePos - 4
    Seek #fileNum, strlVidSizePos
    Put #fileNum, , calcStrlVidSize
    Seek #fileNum, currPos

    ' --------------------------------------------------------
    ' LIST strl chunk for audio stream
    ' --------------------------------------------------------
    Dim listStrlAud As String: listStrlAud = "LIST"
    tmpStr = listStrlAud
    Put #fileNum, , tmpStr

    Dim strlAudSizePos As Long: strlAudSizePos = LOF(fileNum) + 1
    Put #fileNum, , dummyLong

    Dim strlAudSig As String: strlAudSig = "strl"
    tmpStr = strlAudSig
    Put #fileNum, , tmpStr

    ' --------------------------------------------------------
    ' -- strh (audio stream header)
    ' --------------------------------------------------------
    Dim strhAudID As String: strhAudID = "strh"
    tmpStr = strhAudID
    Put #fileNum, , tmpStr

    Dim strhAudSize As Long: strhAudSize = 64
    Put #fileNum, , strhAudSize

    Dim streamHdrAud As AVIStreamHeader
    streamHdrAud.fccType = "auds"
    streamHdrAud.fccHandler = String$(4, 0) ' For PCM audio, this is typically empty
    streamHdrAud.flags = 0
    streamHdrAud.priority = 0
    streamHdrAud.language = 0
    streamHdrAud.initialFrames = 0
    streamHdrAud.scale = 1
    streamHdrAud.rate = sampleRate
    streamHdrAud.start = 0
    streamHdrAud.length = stereoFrames
    streamHdrAud.suggestedBufferSize = sampleRate * 4 ' 2 channels x 2 bytes per sample (is used 16bit stereo waveform sound, 1 integer to 1 sample)
    streamHdrAud.quality = -1
    streamHdrAud.sampleSize = 4 ' 4 bytes per stereo sample (2 bytes left + 2 bytes right)
    streamHdrAud.frameLeft = 0
    streamHdrAud.frameTop = 0
    streamHdrAud.frameRight = 0
    streamHdrAud.frameBottom = 0
    Put #fileNum, , streamHdrAud

    ' --------------------------------------------------------
    ' -- strf (audio format chunk) -> WAVEFORMATEX (16 bytes)
    ' --------------------------------------------------------
    Dim strfAudID As String: strfAudID = "strf"
    tmpStr = strfAudID
    Put #fileNum, , tmpStr

    Dim strfAudSize As Long: strfAudSize = 16
    Put #fileNum, , strfAudSize

    Dim wf As WAVEFORMATEX
    wf.wFormatTag = 1
    wf.nChannels = 2
    wf.nSamplesPerSec = sampleRate
    wf.wBitsPerSample = 16
    wf.nBlockAlign = wf.nChannels * (wf.wBitsPerSample \ 8)
    wf.nAvgBytesPerSec = wf.nSamplesPerSec * wf.nBlockAlign
    wf.cbSize = 0
    Put #fileNum, , wf

    currPos = LOF(fileNum) + 1
    Dim calcStrlAudSize As Long: calcStrlAudSize = currPos - strlAudSizePos - 4
    Seek #fileNum, strlAudSizePos
    Put #fileNum, , calcStrlAudSize
    Seek #fileNum, currPos

    currPos = LOF(fileNum) + 1
    Dim hdrlSize As Long: hdrlSize = currPos - hdrlSizePos - 4
    Seek #fileNum, hdrlSizePos
    Put #fileNum, , hdrlSize
    Seek #fileNum, currPos

    ' ----------------------------------------------------------
    ' LIST movi chunk (contains the actual video and audio data)
    ' ----------------------------------------------------------
    Dim listMovi As String: listMovi = "LIST"
    tmpStr = listMovi
    Put #fileNum, , tmpStr

    Dim moviSizePos As Long: moviSizePos = LOF(fileNum) + 1
    Put #fileNum, , dummyLong

    Dim moviSig As String: moviSig = "movi"
    tmpStr = moviSig
    Put #fileNum, , tmpStr

    Dim moviDataStart As Long: moviDataStart = LOF(fileNum) + 1

    ' --------------------------------------------------------------
    ' Interleaved writing of video and audio chunks:
    ' For each second: 30 video frames and 1 audio chunk (1 second)  - this solution MUST NOT BE ALWAYS OPTIMAL! is possible writing sound data after every 5 th frame...
    ' --------------------------------------------------------------
    Dim framesPerSec As Long: framesPerSec = vidFPS
    Dim videoIndex As Long: videoIndex = 1
    Dim sec As Long
   
    Dim f As Long
    Dim chunkOffset As Long
    Dim frameChunkID As String
    Dim currFrameSize As Long
    Dim padByte As String
    Dim chunkOffsetAud As Long
    Dim audioChunkID As String
    Dim audioChunkSize As Long
    Dim startAud As Long
    Dim endAud As Long
    Dim k As Long
    Dim audioBuffer As String
    Dim sampleValue As Integer
    Dim uVal As Long
    Dim padByteAud As String
    Dim moviSize As Long
    Dim riffSize As Long
    Dim idxHeader As String
    Dim idxSize As Long
    Dim n As Long
   
    For sec = 1 To videoDuration
       
        ' Write video chunks
        For f = 1 To framesPerSec
            ' SHOW PROGRESS
            _Dest 0
            Locate 6, 1
            Print Right$("      " + _Trim$(Str$(sec)), 7) + " of " + _Trim$(Str$(videoDuration)) + _
                ", " + _
                Right$("      " + _Trim$(Str$(f)), 7) + " of " + _Trim$(Str$(framesPerSec)) + _
                "                    ";
       
            If videoIndex > numFrames Then Exit For
           
            chunkOffset = LOF(fileNum) - moviDataStart + 1
            frameChunkID = "00db"
            tmpStr = frameChunkID
            Put #fileNum, , tmpStr
            currFrameSize = frameSize
            Put #fileNum, , currFrameSize
            Put #fileNum, , Frames(videoIndex)
            If (currFrameSize Mod 2) <> 0 Then
                padByte = Chr$(0)
                Put #fileNum, , padByte
            End If
            idxCount = idxCount + 1
            idxArr(idxCount).chunkID = frameChunkID
            idxArr(idxCount).flags = &H10 ' Keyframe flag
            idxArr(idxCount).offset = chunkOffset
            idxArr(idxCount).size = currFrameSize
            videoIndex = videoIndex + 1
        Next f

        ' Write audio chunk for the current second
       
        chunkOffsetAud = LOF(fileNum) - moviDataStart + 1
        audioChunkID = "01wb"
        tmpStr = audioChunkID
        Put #fileNum, , tmpStr
        audioChunkSize = sampleRate * 2 * 2 ' 1 second = sampleRate * (2 channels x 2 bytes)
        Put #fileNum, , audioChunkSize

        startAud = (sec - 1) * (sampleRate * 2) + 1
       
        endAud = startAud + (sampleRate * 2) - 1
        If endAud > UBound(audioSamples) Then endAud = UBound(audioSamples)

        audioBuffer = ""
        For k = startAud To endAud
            ' SHOW PROGRESS
            _Dest 0
            Locate 6, 1
            Print Right$("      " + _Trim$(Str$(sec)), 7) + " of " + _Trim$(Str$(videoDuration)) + _
                ", " + _
                Right$("      " + _Trim$(Str$(k)), 7) + " of " + _Trim$(Str$(endAud)) + _
                "                    ";
           
            sampleValue = audioSamples(k)
            uVal = sampleValue And &HFFFF ' Convert to 16-bit two's complement representation
            audioBuffer = audioBuffer + Chr$(uVal And &HFF) + Chr$((uVal \ 256) And &HFF)
        Next k
        Put #fileNum, , audioBuffer

        If (audioChunkSize Mod 2) <> 0 Then
            padByteAud = Chr$(0)
            Put #fileNum, , padByteAud
        End If

        idxCount = idxCount + 1
        idxArr(idxCount).chunkID = audioChunkID
        idxArr(idxCount).flags = 0
        idxArr(idxCount).offset = chunkOffsetAud
        idxArr(idxCount).size = audioChunkSize
    Next sec

    currPos = LOF(fileNum) + 1
    moviSize = currPos - moviSizePos - 4
    Seek #fileNum, moviSizePos
    Put #fileNum, , moviSize
    Seek #fileNum, currPos

    riffSize = LOF(fileNum) - 8 + 1
    Seek #fileNum, riffSizePos
    Put #fileNum, , riffSize
    Seek #fileNum, LOF(fileNum) + 1

    idxHeader = "idx1"
    tmpStr = idxHeader
    Put #fileNum, , tmpStr
    idxSize = idxCount * 16
    Put #fileNum, , idxSize

    For n = 1 To idxCount
        ' SHOW PROGRESS
        _Dest 0
        Locate 7, 1
        Print Right$("      " + _Trim$(Str$(n)), 7) + " of " + _Trim$(Str$(idxCount)) + _
            "                                        ";
   
        Put #fileNum, , idxArr(n).chunkID
        Put #fileNum, , idxArr(n).flags
        Put #fileNum, , idxArr(n).offset
        Put #fileNum, , idxArr(n).size
    Next n

    Close #fileNum
End Sub ' CreateAVI
Reply
#6
Yes, you're right about the DIM commands, that's from development. It was a problem to get it working, because once it worked without sound, then again without video... I assume that the problem you describe is caused by running out of free RAM. Saving to AVI must be continuous, so that the contents of RAM are written to disk in a flash. Practically compressing video on the fly via QB64PE seems unthinkable to me, because just do the math, if you wanted to compress frames on the fly, it must be done in less than 1/30 of a second. So uncompressed AVI, when you really need to capture everything in real time, is the right solution (because after saving to a file, you no longer have a limited time for compression and can deal with it for longer). But my program doesn't do that at the beginning. It generates a few frames in memory and then saves them with the generated sound. So - I haven't looked at your version yet - but - this is probably the problem. So what does that mean? Good, but we will work on a version that will solve it on the fly. This will also test the elimination of the problem you are writing about. Now to verify my theory, launch the task manager and then your version of the program. I assume that the program will crash at some point when you run out of free memory.
Thank you all for the feedback, I mainly focused on the principle of saving so that it works and I somehow didn't think about this obvious thing... Smile I will work on it.


Reply
#7
That makes sense - I imagine that it would help if we could create the initial video file with header information & maybe a few frames, and then just append the additional frames to the file as they are created? That way we aren't filling up the working memory because as we generate the video it's just saved to disk and we only ever store a few frames at a time in RAM.
Reply
#8
I used your code as a basis but refactored several things.
There is now one big AVI header type that is rewritten in the end.
Also image and audiosamples are now generated inline so no mem limit anymore.
You can now create AVI's up to 4GB (I think that is some AVI limit?)

Code: (Select All)
CreateAVI "VideoTest.avi", 3, 800, 600, 30, 44100

Sub CreateAVI (avi$, videoDuration As Integer, frameWidth As Long, frameHeight As Long, vidFPS As Integer, sampleRate As Long)
Const bytesPerPixel = 4

Type AVIheader
riffHeader As String * 4
riffSize As Long
aviSig As String * 4
listHdrl As String * 4
hdrlSize As Long ' Size from hdrlSig to cbSize (74*4 + 7*2)
hdrlSig As String * 4
avihChunkID As String * 4
avihChunkSize As Long
microSecPerFrame As Long ' Microseconds per frame
maxBytesPerSec As Long ' Maximum data rate
paddingGranularity As Long ' Padding granularity
flags As Long ' AVI flags (e.g., HASINDEX)
totalFrames As Long ' Total number of frames in the video
initialFrames As Long ' Initial frames (typically 0)
streams As Long ' Number of streams (video, audio, etc.)
suggestedBufferSize As Long ' Suggested buffer size for playback
vwidth As Long ' Video width in pixels
vheight As Long ' Video height in pixels
reserved As String * 16 ' Reserved bytes (set to 0)
listStrlVid As String * 4
strlVidSize As Long ' Size from strlVidSig to clrImportant (=31*4)
strlVidSig As String * 4
strhVidID As String * 4
strhVidSize As Long
fccType As String * 4 ' Stream type (e.g., "vids" or "auds")
fccHandler As String * 4 ' Codec handler (e.g., "DIB " for uncompressed video)
streamflags As Long ' Stream flags
priority As Integer ' Priority (unused)
language As Integer ' Language code (unused)
initialFrames0 As Long ' Initial frames (unused)
scale As Long ' Time scale for the stream
rate As Long ' Data rate (scale/rate = FPS for video)
start As Long ' Start time (usually 0)
length As Long ' Length of the stream (in time units)
streamSuggestedBufferSize As Long ' Suggested buffer size for the stream
quality As Long ' Quality indicator (-1 for default)
sampleSize As Long ' Sample size (0 for video, nonzero for audio)
frameLeft As Long ' For video: left coordinate of the source rectangle
frameTop As Long ' For video: top coordinate of the source rectangle
frameRight As Long ' For video: right coordinate of the source rectangle
frameBottom As Long ' For video: bottom coordinate of the source rectangle
strfVidID As String * 4
strfVidSize As Long
size As Long ' Size of this header (40 bytes)
iwidth As Long ' Width of the image in pixels
iheight As Long ' Height of the image (negative for top–down)
planes As Integer ' Number of color planes (must be 1)
bitCount As Integer ' Bits per pixel (24 for 24-bit, 32 for 32-bit)
compression As Long ' Compression method (0 = BI_RGB for uncompressed)
sizeImage As Long ' Size of the image data in bytes
xPelsPerMeter As Long ' Horizontal resolution (pixels per meter)
yPelsPerMeter As Long ' Vertical resolution (pixels per meter)
clrUsed As Long ' Number of colors used (0 = all)
clrImportant As Long ' Number of important colors (0 = all)
listStrlAud As String * 4
strlAudSize As Long ' Size from strlAudSig to cbSize (22*4 + 7*2)
strlAudSig As String * 4
strhAudID As String * 4
strhAudSize As Long
afccType As String * 4 ' AUDIO Stream type (e.g., "vids" or "auds")
afccHandler As String * 4 ' AUDIO Codec handler (e.g., "DIB " for uncompressed video)
astreamflags As Long ' AUDIO Stream flags
apriority As Integer ' AUDIO Priority
alanguage As Integer ' AUDIO Language code
ainitialFrames As Long ' AUDIO Initial frames
ascale As Long ' AUDIO Time scale for the stream
arate As Long ' AUDIO Data rate (scale/rate = FPS for video)
astart As Long ' AUDIO Start time (usually 0)
alength As Long ' AUDIO Length of the stream (in time units)
astreamSuggestedBufferSize As Long ' AUDIO Suggested buffer size for the stream
aquality As Long ' AUDIO Quality indicator (-1 for default)
asampleSize As Long ' AUDIO Sample size (0 for video, nonzero for audio)
aframeLeft As Long ' AUDIO For video: left coordinate of the source rectangle
aframeTop As Long ' AUDIO For video: top coordinate of the source rectangle
aframeRight As Long ' AUDIO For video: right coordinate of the source rectangle
aframeBottom As Long ' AUDIO For video: bottom coordinate of the source rectangle
strfAudID As String * 4
strfAudSize As Long
wFormatTag As Integer ' Audio format code (1 = PCM)
nChannels As Integer ' Number of audio channels (2 for stereo)
nSamplesPerSec As Long ' Sample rate in Hz
nAvgBytesPerSec As Long ' Average bytes per second
nBlockAlign As Integer ' Block alignment in bytes
wBitsPerSample As Integer ' Bits per sample (16 for 16-bit PCM)
cbSize As Integer ' Extra size (0 for PCM)
listMovi As String * 4
moviSize As Long
moviSig As String * 4
End Type

Type ChunkIndex
chunkID As String * 4 ' Chunk identifier (e.g., "00db" for video, "01wb" for audio)
flags As Long ' Flags (e.g., keyframe flag)
offset As Long ' Offset of chunk data from start of 'movi' section
size As Long ' Size of chunk data in bytes
End Type

Dim header As AVIheader
Dim numFrames As Long: numFrames = vidFPS * videoDuration
Dim frameSize As Long: frameSize = frameWidth * frameHeight * bytesPerPixel ' Total size of one 32-bit frame
Dim totalSamplesPerChannel As Long: totalSamplesPerChannel = videoDuration * sampleRate
Dim idxArr(1 To numFrames + videoDuration) As ChunkIndex
Dim idxCount As Long: idxCount = 0

If _FileExists(avi$) Then Kill avi$
Dim fileNum As Integer: fileNum = FreeFile: Open avi$ For Binary As #fileNum

header.riffHeader = "RIFF"
header.riffSize = 0
header.aviSig = "AVI "
header.listHdrl = "LIST"
header.hdrlSize = 74 * 4 + 7 * 2
header.hdrlSig = "hdrl"
header.avihChunkID = "avih"
header.avihChunkSize = 56
header.microSecPerFrame = 1000000 \ vidFPS
header.maxBytesPerSec = frameSize * vidFPS
header.paddingGranularity = 0
header.flags = &H110 ' AVIF_HASINDEX flag
header.totalFrames = numFrames
header.initialFrames = 0
header.streams = 2 ' 2 streams: video + audio (stereo is 1 stream and mono also)
header.suggestedBufferSize = frameSize
header.vwidth = frameWidth
header.vheight = frameHeight
header.reserved = String$(16, Chr$(0))
header.listStrlVid = "LIST"
header.strlVidSize = 29 * 4 + 4 * 2
header.strlVidSig = "strl"
header.strhVidID = "strh"
header.strhVidSize = 64
header.fccType = "vids"
header.fccHandler = "DIB " ' Uncompressed video (DIB)
header.streamflags = 0
header.priority = 0
header.language = 0
header.initialFrames = 0
header.scale = 1
header.rate = vidFPS
header.start = 0
header.length = numFrames
header.streamSuggestedBufferSize = frameSize
header.quality = -1
header.sampleSize = 0
header.frameLeft = 0
header.frameTop = 0
header.frameRight = frameWidth
header.frameBottom = frameHeight
header.strfVidID = "strf"
header.strfVidSize = 40
header.size = 40
header.iwidth = frameWidth
header.iheight = -frameHeight ' Negative height indicates top-down format
header.planes = 1
header.bitCount = 32
header.compression = 0 ' BI_RGB = uncompressed
header.sizeImage = frameSize
header.xPelsPerMeter = 0
header.yPelsPerMeter = 0
header.clrUsed = 0
header.clrImportant = 0
header.listStrlAud = "LIST"
header.strlAudSize = 22 * 4 + 7 * 2
header.strlAudSig = "strl"
header.strhAudID = "strh"
header.strhAudSize = 64
header.afccType = "auds"
header.afccHandler = String$(4, 0) ' For PCM audio, this is typically empty
header.astreamflags = 0
header.apriority = 0
header.alanguage = 0
header.ainitialFrames = 0
header.ascale = 1
header.arate = sampleRate
header.astart = 0
header.alength = totalSamplesPerChannel
header.astreamSuggestedBufferSize = sampleRate * 4 ' 2 channels x 2 bytes per sample (is used 16bit stereo waveform sound, 1 integer to 1 sample)
header.aquality = -1
header.asampleSize = 4 ' 4 bytes per stereo sample (2 bytes left + 2 bytes right)
header.aframeLeft = 0
header.aframeTop = 0
header.aframeRight = 0
header.aframeBottom = 0
header.strfAudID = "strf"
header.strfAudSize = 16
header.wFormatTag = 1
header.nChannels = 2
header.nSamplesPerSec = sampleRate
header.wBitsPerSample = 16
header.nBlockAlign = header.nChannels * (header.wBitsPerSample \ 8)
header.nAvgBytesPerSec = header.nSamplesPerSec * header.nBlockAlign
header.cbSize = 0
header.listMovi = "LIST"
header.moviSize = 0
header.moviSig = "movi"
Put #fileNum, , header
Dim moviDataStart As Long: moviDataStart = Seek(fileNum)

' --------------------------------------------------------------
' Interleaved writing of video and audio chunks:
' For each second: 30 video frames and 1 audio chunk (1 second) - this solution MUST NOT BE ALWAYS OPTIMAL! is possible writing sound data after every 5 th frame...
' --------------------------------------------------------------
Dim chunkOffset As Long
Dim frameChunkID As String: frameChunkID = "00db"
Dim padByte As String: padByte = Chr$(0)
Dim chunkOffsetAud As Long
Dim audioChunkID As String: audioChunkID = "01wb"
Dim audioChunkSize As Long
Dim As Integer SampleL, SampleR 'Variables for left and right channel samples

Dim sm As _MEM
Dim img As Long: img = _NewImage(frameWidth, frameHeight, 32)
'Dim img2 As Long: img2 = _NewImage(frameWidth, frameHeight, 32)
Dim freq As Single: freq = 500 'Starting frequency in Hz
Dim videoIndex As Long: videoIndex = 0
Dim frame As String: frame = Space$(frameSize)
For sec% = 1 To videoDuration
Locate , 1, 0: Print sec%; '@@
' Write video chunks
For i% = 1 To vidFPS
chunkOffset = Seek(fileNum) - moviDataStart
Put #fileNum, , frameChunkID
Put #fileNum, , frameSize
videoIndex = videoIndex + 1

_Dest img '@@
Cls: _PrintMode _KeepBackground
For j% = 1 To 100
Line (Rnd * frameWidth, Rnd * frameHeight)-(Rnd * frameWidth, Rnd * frameHeight), _RGB32(Rnd * 255, Rnd * 255, Rnd * 255), B
Next j%
_PrintString (350, 250), "Video Frame " + _ToStr$(videoIndex)
_Dest 0
'_PutImage (0, 0), img, img2, (0, frameHeight)-(frameWidth, 0) ' For VLC Player
'_PutImage (0, 0), img, img2 ' For WMP, MPC-HC Player
sm = _MemImage(img)
frame = Space$(frameSize)
_MemGet sm, sm.OFFSET, frame
_MemFree sm
Put #fileNum, , frame
If (frameSize Mod 2) <> 0 Then Put #fileNum, , padByte

idxCount = idxCount + 1
idxArr(idxCount).chunkID = frameChunkID
idxArr(idxCount).flags = &H10 ' Keyframe flag
idxArr(idxCount).offset = chunkOffset
idxArr(idxCount).size = frameSize
Next i%

' Write audio chunk for the current second
chunkOffsetAud = Seek(fileNum) - moviDataStart
Put #fileNum, , audioChunkID
audioChunkSize = sampleRate * 2 * 2 ' 1 second = sampleRate * (2 channels x 2 bytes)
Put #fileNum, , audioChunkSize

audioBuffer$ = String$(sampleRate * 4, 0)
For l& = 0 To sampleRate - 1
t! = l& / sampleRate: pan! = l& / totalSamplesPerChannel
freq = freq + 5 - 10 * Int(Rnd * 2)
SampleL = (32767 * 0.9 * (1 - pan!) * Sin(2 * 3.14159 * freq * t!)) And &HFFFF
SampleR = (32767 * 0.9 * pan! * Sin(2 * 3.14159 * freq * t!)) And &HFFFF
Asc(audioBuffer$, l& * 4 + 1) = SampleL And &HFF
Asc(audioBuffer$, l& * 4 + 2) = (SampleL \ 256) And &HFF
Asc(audioBuffer$, l& * 4 + 3) = SampleR And &HFF
Asc(audioBuffer$, l& * 4 + 4) = (SampleR \ 256) And &HFF
Next l&
Put #fileNum, , audioBuffer$

idxCount = idxCount + 1
idxArr(idxCount).chunkID = audioChunkID
idxArr(idxCount).flags = 0
idxArr(idxCount).offset = chunkOffsetAud
idxArr(idxCount).size = audioChunkSize
Next sec%
_Dest 0
_FreeImage img

currPos = Seek(fileNum)
header.moviSize = currPos - moviSizePos - 4
header.riffSize = Seek(fileNum) - 8

Dim idxHeader As String: idxHeader = "idx1": Put #fileNum, , idxHeader
Dim idxSize As Long: idxSize = idxCount * 16: Put #fileNum, , idxSize
Dim n As Long
For n = 1 To idxCount
Put #fileNum, , idxArr(n).chunkID
Put #fileNum, , idxArr(n).flags
Put #fileNum, , idxArr(n).offset
Put #fileNum, , idxArr(n).size
Next n
Put #fileNum, 1, header
Close #fileNum
Print
End Sub
45y and 2M lines of MBASIC>BASICA>QBASIC>QBX>QB64 experience
Reply
#9
Nice work @mdikjens! I increased the length to 60 seconds, and it worked.

I tweaked the code slightly to create a video file using the current timestamp, so as not to overwrite the previous file, and at the end it shows the filename in an inputbox that you can copy to the clipboard.

Code: (Select All)
' #############################################################################
' AVI file format
' https://qb64phoenix.com/forum/showthread.php?tid=3550
' #############################################################################

' From: Petr, Mini-Mod
' Date: 3/22/2025 4:42 AM
' I've been wanting this for a really long time. A program that creates a valid
' AVI format with sound. The following program only creates (does not play) an
' uncompressed AVI file, which also contains uncompressed stereo audio. For this
' reason, prepare about 180 megabytes of free disk space for the test. I think
' it's already quite complicated, so if I add compression to it, it would be much
' more complicated. I will deal with compression, but I don't know how far I will
' get, because I encounter many obstacles.
' I must warn you that Windows Media Player blurs the image due to the player's
' internal filters, VLC player has the best outputs. MPC-HC plays it too, but it
' rotates the image. I can rotate the image in AVI, but then it is rotated again
' in Windows Media Player and in VLC. It's kind of a beginning... and also an
' extreme. First I deal with compression in GIF and save every byte and now tens
' of MegaBytes are no big deal...
' It's uncompressed, so expect a data rate somewhere around 60 megabytes per
' second. QB64PE can handle that speed just fine.
' -----------------------------------------------------------------------------
' Reply #8
' From: mdijkens, Member
' Date: 3/23/2025 12:51 PM (This post was last modified: 3/23/2025 12:56 PM by mdijkens.)
' I used your code as a basis but refactored several things.
' There is now one big AVI header type that is rewritten in the end.
' Also image and audiosamples are now generated inline so no mem limit anymore.
' You can now create AVI's up to 4GB (I think that is some AVI limit?)

' =============================================================================
' GLOBAL VARIABLES
' =============================================================================
Dim Shared m_ProgramPath$: m_ProgramPath$ = Left$(Command$(0), _InStrRev(Command$(0), "\"))
Dim Shared m_ProgramName$: m_ProgramName$ = Mid$(Command$(0), _InStrRev(Command$(0), "\") + 1)

' =============================================================================
' GENERATE VIDEO
' =============================================================================
' FILENAME = EXE NAME + TIMESTAMP + ".AVI"
Dim sFileName As String
sFileName = NoExt$(m_ProgramName$) + "." + GetTimestamp$ + ".avi"

'CreateAVI sFileName, 3, 800, 600, 30, 44100
CreateAVI sFileName, 60, 720, 480, 30, 44100

' /////////////////////////////////////////////////////////////////////////////

Sub CreateAVI (fn$, videoDuration As Integer, _
    frameWidth As Long, frameHeight As Long, _
    vidFPS As Integer, sampleRate As Long)
   
    Const bytesPerPixel = 4

    Type AVIheader
        riffHeader As String * 4
        riffSize As Long
        aviSig As String * 4
        listHdrl As String * 4
        hdrlSize As Long '            Size from hdrlSig to cbSize (74*4 + 7*2)
        hdrlSig As String * 4
        avihChunkID As String * 4
        avihChunkSize As Long
        microSecPerFrame As Long '    Microseconds per frame
        maxBytesPerSec As Long '      Maximum data rate
        paddingGranularity As Long '  Padding granularity
        flags As Long '               AVI flags (e.g., HASINDEX)
        totalFrames As Long '         Total number of frames in the video
        initialFrames As Long '       Initial frames (typically 0)
        streams As Long '             Number of streams (video, audio, etc.)
        suggestedBufferSize As Long ' Suggested buffer size for playback
        vwidth As Long '               Video width in pixels
        vheight As Long '              Video height in pixels
        reserved As String * 16 '     Reserved bytes (set to 0)
        listStrlVid As String * 4
        strlVidSize As Long '         Size from strlVidSig to clrImportant (=31*4)
        strlVidSig As String * 4
        strhVidID As String * 4
        strhVidSize As Long
        fccType As String * 4 '       Stream type (e.g., "vids" or "auds")
        fccHandler As String * 4 '    Codec handler (e.g., "DIB " for uncompressed video)
        streamflags As Long '         Stream flags
        priority As Integer '         Priority (unused)
        language As Integer '         Language code (unused)
        initialFrames0 As Long '      Initial frames (unused)
        scale As Long '               Time scale for the stream
        rate As Long '                Data rate (scale/rate = FPS for video)
        start As Long '               Start time (usually 0)
        length As Long '              Length of the stream (in time units)
        streamSuggestedBufferSize As Long ' Suggested buffer size for the stream
        quality As Long '             Quality indicator (-1 for default)
        sampleSize As Long '          Sample size (0 for video, nonzero for audio)
        frameLeft As Long '           For video: left coordinate of the source rectangle
        frameTop As Long '            For video: top coordinate of the source rectangle
        frameRight As Long '          For video: right coordinate of the source rectangle
        frameBottom As Long '         For video: bottom coordinate of the source rectangle
        strfVidID As String * 4
        strfVidSize As Long
        size As Long '                Size of this header (40 bytes)
        iwidth As Long '              Width of the image in pixels
        iheight As Long '             Height of the image (negative for top–down)
        planes As Integer '           Number of color planes (must be 1)
        bitCount As Integer '         Bits per pixel (24 for 24-bit, 32 for 32-bit)
        compression As Long '         Compression method (0 = BI_RGB for uncompressed)
        sizeImage As Long '           Size of the image data in bytes
        xPelsPerMeter As Long '       Horizontal resolution (pixels per meter)
        yPelsPerMeter As Long '       Vertical resolution (pixels per meter)
        clrUsed As Long '             Number of colors used (0 = all)
        clrImportant As Long '        Number of important colors (0 = all)
        listStrlAud As String * 4
        strlAudSize As Long '         Size from strlAudSig to cbSize (22*4 + 7*2)
        strlAudSig As String * 4
        strhAudID As String * 4
        strhAudSize As Long
        afccType As String * 4 '       AUDIO Stream type (e.g., "vids" or "auds")
        afccHandler As String * 4 '    AUDIO Codec handler (e.g., "DIB " for uncompressed video)
        astreamflags As Long '         AUDIO Stream flags
        apriority As Integer '         AUDIO Priority
        alanguage As Integer '         AUDIO Language code
        ainitialFrames As Long '       AUDIO Initial frames
        ascale As Long '               AUDIO Time scale for the stream
        arate As Long '                AUDIO Data rate (scale/rate = FPS for video)
        astart As Long '               AUDIO Start time (usually 0)
        alength As Long '              AUDIO Length of the stream (in time units)
        astreamSuggestedBufferSize As Long ' AUDIO Suggested buffer size for the stream
        aquality As Long '             AUDIO Quality indicator (-1 for default)
        asampleSize As Long '          AUDIO Sample size (0 for video, nonzero for audio)
        aframeLeft As Long '           AUDIO For video: left coordinate of the source rectangle
        aframeTop As Long '            AUDIO For video: top coordinate of the source rectangle
        aframeRight As Long '          AUDIO For video: right coordinate of the source rectangle
        aframeBottom As Long '         AUDIO For video: bottom coordinate of the source rectangle
        strfAudID As String * 4
        strfAudSize As Long
        wFormatTag As Integer '      Audio format code (1 = PCM)
        nChannels As Integer '        Number of audio channels (2 for stereo)
        nSamplesPerSec As Long '      Sample rate in Hz
        nAvgBytesPerSec As Long '    Average bytes per second
        nBlockAlign As Integer '      Block alignment in bytes
        wBitsPerSample As Integer '  Bits per sample (16 for 16-bit PCM)
        cbSize As Integer '          Extra size (0 for PCM)
        listMovi As String * 4
        moviSize As Long
        moviSig As String * 4
    End Type

    Type ChunkIndex
        chunkID As String * 4 ' Chunk identifier (e.g., "00db" for video, "01wb" for audio)
        flags As Long '        Flags (e.g., keyframe flag)
        offset As Long '        Offset of chunk data from start of 'movi' section
        size As Long '          Size of chunk data in bytes
    End Type

    Dim header As AVIheader
    Dim numFrames As Long: numFrames = vidFPS * videoDuration
    Dim frameSize As Long: frameSize = frameWidth * frameHeight * bytesPerPixel ' Total size of one 32-bit frame
    Dim totalSamplesPerChannel As Long: totalSamplesPerChannel = videoDuration * sampleRate
    Dim idxArr(1 To numFrames + videoDuration) As ChunkIndex
    Dim idxCount As Long: idxCount = 0

    Dim avi$
    avi$ = m_ProgramPath$ + fn$
    If _FileExists(avi$) Then Kill avi$
    Dim fileNum As Integer: fileNum = FreeFile: Open avi$ For Binary As #fileNum

    header.riffHeader = "RIFF"
    header.riffSize = 0
    header.aviSig = "AVI "
    header.listHdrl = "LIST"
    header.hdrlSize = 74 * 4 + 7 * 2
    header.hdrlSig = "hdrl"
    header.avihChunkID = "avih"
    header.avihChunkSize = 56
    header.microSecPerFrame = 1000000 \ vidFPS
    header.maxBytesPerSec = frameSize * vidFPS
    header.paddingGranularity = 0
    header.flags = &H110 '            AVIF_HASINDEX flag
    header.totalFrames = numFrames
    header.initialFrames = 0
    header.streams = 2 '              2 streams: video + audio (stereo is 1 stream and mono also)
    header.suggestedBufferSize = frameSize
    header.vwidth = frameWidth
    header.vheight = frameHeight
    header.reserved = String$(16, Chr$(0))
    header.listStrlVid = "LIST"
    header.strlVidSize = 29 * 4 + 4 * 2
    header.strlVidSig = "strl"
    header.strhVidID = "strh"
    header.strhVidSize = 64
    header.fccType = "vids"
    header.fccHandler = "DIB " ' Uncompressed video (DIB)
    header.streamflags = 0
    header.priority = 0
    header.language = 0
    header.initialFrames = 0
    header.scale = 1
    header.rate = vidFPS
    header.start = 0
    header.length = numFrames
    header.streamSuggestedBufferSize = frameSize
    header.quality = -1
    header.sampleSize = 0
    header.frameLeft = 0
    header.frameTop = 0
    header.frameRight = frameWidth
    header.frameBottom = frameHeight
    header.strfVidID = "strf"
    header.strfVidSize = 40
    header.size = 40
    header.iwidth = frameWidth
    header.iheight = -frameHeight ' Negative height indicates top-down format
    header.planes = 1
    header.bitCount = 32
    header.compression = 0 '      BI_RGB = uncompressed
    header.sizeImage = frameSize
    header.xPelsPerMeter = 0
    header.yPelsPerMeter = 0
    header.clrUsed = 0
    header.clrImportant = 0
    header.listStrlAud = "LIST"
    header.strlAudSize = 22 * 4 + 7 * 2
    header.strlAudSig = "strl"
    header.strhAudID = "strh"
    header.strhAudSize = 64
    header.afccType = "auds"
    header.afccHandler = String$(4, 0) ' For PCM audio, this is typically empty
    header.astreamflags = 0
    header.apriority = 0
    header.alanguage = 0
    header.ainitialFrames = 0
    header.ascale = 1
    header.arate = sampleRate
    header.astart = 0
    header.alength = totalSamplesPerChannel
    header.astreamSuggestedBufferSize = sampleRate * 4 ' 2 channels x 2 bytes per sample (is used 16bit stereo waveform sound, 1 integer to 1 sample)
    header.aquality = -1
    header.asampleSize = 4 ' 4 bytes per stereo sample (2 bytes left + 2 bytes right)
    header.aframeLeft = 0
    header.aframeTop = 0
    header.aframeRight = 0
    header.aframeBottom = 0
    header.strfAudID = "strf"
    header.strfAudSize = 16
    header.wFormatTag = 1
    header.nChannels = 2
    header.nSamplesPerSec = sampleRate
    header.wBitsPerSample = 16
    header.nBlockAlign = header.nChannels * (header.wBitsPerSample \ 8)
    header.nAvgBytesPerSec = header.nSamplesPerSec * header.nBlockAlign
    header.cbSize = 0
    header.listMovi = "LIST"
    header.moviSize = 0
    header.moviSig = "movi"
    Put #fileNum, , header
    Dim moviDataStart As Long: moviDataStart = Seek(fileNum)

    ' --------------------------------------------------------------
    ' Interleaved writing of video and audio chunks:
    ' For each second: 30 video frames and 1 audio chunk (1 second)  - this solution MUST NOT BE ALWAYS OPTIMAL! is possible writing sound data after every 5 th frame...
    ' --------------------------------------------------------------
    Dim chunkOffset As Long
    Dim frameChunkID As String: frameChunkID = "00db"
    Dim padByte As String: padByte = Chr$(0)
    Dim chunkOffsetAud As Long
    Dim audioChunkID As String: audioChunkID = "01wb"
    Dim audioChunkSize As Long
    Dim As Integer SampleL, SampleR 'Variables for left and right channel samples

    Dim sm As _MEM
    Dim img As Long: img = _NewImage(frameWidth, frameHeight, 32)
    'Dim img2 As Long: img2 = _NewImage(frameWidth, frameHeight, 32)
    Dim freq As Single: freq = 500 'Starting frequency in Hz
    Dim videoIndex As Long: videoIndex = 0
    Dim frame As String: frame = Space$(frameSize)
   
    Dim sTotal As String
   
    Locate 1, 1: Print "Generating video file " + Chr$(34) + fn$ + Chr$(34)
    sTotal = " of " + _Trim$(Str$(videoDuration))
    For sec% = 1 To videoDuration
        ' SHOW PROGRESS
        Locate 3, 1: Print _Trim$(Str$(sec%)) + sTotal + "          ";
       
        ' Write video chunks
        For i% = 1 To vidFPS
            chunkOffset = Seek(fileNum) - moviDataStart
            Put #fileNum, , frameChunkID
            Put #fileNum, , frameSize
            videoIndex = videoIndex + 1

            _Dest img '@@
            Cls: _PrintMode _KeepBackground
            For j% = 1 To 100
                Line (Rnd * frameWidth, Rnd * frameHeight)-(Rnd * frameWidth, Rnd * frameHeight), _RGB32(Rnd * 255, Rnd * 255, Rnd * 255), B
            Next j%
            _PrintString (350, 250), "Video Frame " + _ToStr$(videoIndex)
            _Dest 0
            '_PutImage (0, 0), img, img2, (0, frameHeight)-(frameWidth, 0) ' For VLC Player
            '_PutImage (0, 0), img, img2 ' For WMP, MPC-HC Player
            sm = _MemImage(img)
            frame = Space$(frameSize)
            _MemGet sm, sm.OFFSET, frame
            _MemFree sm
            Put #fileNum, , frame
            If (frameSize Mod 2) <> 0 Then Put #fileNum, , padByte

            idxCount = idxCount + 1
            idxArr(idxCount).chunkID = frameChunkID
            idxArr(idxCount).flags = &H10 ' Keyframe flag
            idxArr(idxCount).offset = chunkOffset
            idxArr(idxCount).size = frameSize
        Next i%

        ' Write audio chunk for the current second
        chunkOffsetAud = Seek(fileNum) - moviDataStart
        Put #fileNum, , audioChunkID
        audioChunkSize = sampleRate * 2 * 2 ' 1 second = sampleRate * (2 channels x 2 bytes)
        Put #fileNum, , audioChunkSize

        audioBuffer$ = String$(sampleRate * 4, 0)
        For l& = 0 To sampleRate - 1
            t! = l& / sampleRate: pan! = l& / totalSamplesPerChannel
            freq = freq + 5 - 10 * Int(Rnd * 2)
            SampleL = (32767 * 0.9 * (1 - pan!) * Sin(2 * 3.14159 * freq * t!)) And &HFFFF
            SampleR = (32767 * 0.9 * pan! * Sin(2 * 3.14159 * freq * t!)) And &HFFFF
            Asc(audioBuffer$, l& * 4 + 1) = SampleL And &HFF
            Asc(audioBuffer$, l& * 4 + 2) = (SampleL \ 256) And &HFF
            Asc(audioBuffer$, l& * 4 + 3) = SampleR And &HFF
            Asc(audioBuffer$, l& * 4 + 4) = (SampleR \ 256) And &HFF
        Next l&
        Put #fileNum, , audioBuffer$

        idxCount = idxCount + 1
        idxArr(idxCount).chunkID = audioChunkID
        idxArr(idxCount).flags = 0
        idxArr(idxCount).offset = chunkOffsetAud
        idxArr(idxCount).size = audioChunkSize
    Next sec%
    _Dest 0
    _FreeImage img

    currPos = Seek(fileNum)
    header.moviSize = currPos - moviSizePos - 4
    header.riffSize = Seek(fileNum) - 8

    Dim idxHeader As String: idxHeader = "idx1": Put #fileNum, , idxHeader
    Dim idxSize As Long: idxSize = idxCount * 16: Put #fileNum, , idxSize
    Dim n As Long
    For n = 1 To idxCount
        Put #fileNum, , idxArr(n).chunkID
        Put #fileNum, , idxArr(n).flags
        Put #fileNum, , idxArr(n).offset
        Put #fileNum, , idxArr(n).size
    Next n
    Put #fileNum, 1, header
    Close #fileNum
   
    Print
   
    ' Display file name/path in inputbox so user can copy to clipboard.
    Dim in$
    in$ = _INPUTBOX$(m_ProgramName$, _
        "File created." + chr$(13) + _
            "Open it with your media player program" + chr$(13) + _
            "(VLC player recommended).", _
        avi$) ' title, message, default
   
End Sub ' CreateAVI

' /////////////////////////////////////////////////////////////////////////////
' Simple timestamp function
' In the format yyyy-mm-dd hh:mm:ss
' in 24-hour time.

Function CurrentDateTime$
    CurrentDateTime$ = Mid$(Date$, 7, 4) + "-" + _
        Mid$(Date$, 1, 5) + " " + _
        Time$
End Function ' CurrentDateTime$

' /////////////////////////////////////////////////////////////////////////////
' Return filename sFile without file extension (anything after last ".")

Function NoExt$ (sFile As String)
    Dim iPos As Integer
    iPos = _InStrRev(sFile, ".")
    If iPos > 0 Then
        If Len(sFile) > 1 Then
            If iPos > 1 Then
                NoExt$ = Left$(sFile, iPos - 1)
            Else
                ' dot is first character, removing it returns blank!
                ' our version will just return the name unchanged
                ' but you can return blank if you prefer
                NoExt$ = sFile
            End If
        Else
            ' file only has one character, the dot, removing it returns blank!
            ' our version will just return the name unchanged
            ' but you can return blank if you prefer
            NoExt$ = sFile
        End If
    Else
        ' no dot found
        ' return the name unchanged
        NoExt$ = sFile
    End If
End Function ' NoExt$

' /////////////////////////////////////////////////////////////////////////////
' Get timestamp for filename
' In the format yyyymmdd_hhmmss
' in 24-hour time.

Function GetTimestamp$
    Dim MyDate$
    Dim MyTime$
    MyDate$ = Date$
    MyTime$ = Time$
    MyDate$ = Right$(MyDate$, 4) + Left$(MyDate$, 2) + Mid$(MyDate$, 4, 2)
    MyTime$ = Replace$(MyTime$, ":", "")
    GetTimestamp$ = MyDate$ + "_" + MyTime$
End Function ' GetTimestamp$

' /////////////////////////////////////////////////////////////////////////////
' Generate random value between Min and Max inclusive.

' Before using be sure to initialize random seed with:
' Randomize Timer

Function RandomNumber% (Min%, Max%)
    Dim NumSpread%
    NumSpread% = (Max% - Min%) + 1
    RandomNumber% = Int(Rnd * NumSpread%) + Min% ' GET RANDOM # BETWEEN Max% AND Min%
End Function ' RandomNumber%

' /////////////////////////////////////////////////////////////////////////////
' FROM: String Manipulation
' found at abandoned, outdated and now likely malicious qb64 dot net website
' http://www.qb64.[net]/forum/index_topic_5964-0/
'
'SUMMARY:
'   Purpose:  A library of custom functions that transform strings.
'   Author:   Dustinian Camburides (dustinian@gmail.com)
'   Platform: QB64 (www.qb64.org)
'   Revision: 1.6
'   Updated:  5/28/2012

'SUMMARY:
'[Replace$] replaces all instances of the [Find] sub-string with the [Add] sub-string within the [Text] string.
'INPUT:
'Text: The input string; the text that's being manipulated.
'Find: The specified sub-string; the string sought within the [Text] string.
'Add: The sub-string that's being added to the [Text] string.

' Usage:
' NewString$ = Replace$(OldString$, FindThis$, ReplaceWithThis$)

Function Replace$ (Text1 As String, Find1 As String, Add1 As String)
    ' VARIABLES:
    Dim Text2 As String
    Dim Find2 As String
    Dim Add2 As String
    Dim lngLocation As Long ' The address of the [Find] substring within the [Text] string.
    Dim strBefore As String ' The characters before the string to be replaced.
    Dim strAfter As String ' The characters after the string to be replaced.

    ' INITIALIZE:
    ' MAKE COPIESSO THE ORIGINAL IS NOT MODIFIED (LIKE ByVal IN VBA)
    Text2 = Text1
    Find2 = Find1
    Add2 = Add1

    lngLocation = InStr(1, Text2, Find2)

    ' PROCESSING:
    ' While [Find2] appears in [Text2]...
    While lngLocation
        ' Extract all Text2 before the [Find2] substring:
        strBefore = Left$(Text2, lngLocation - 1)

        ' Extract all text after the [Find2] substring:
        strAfter = Right$(Text2, ((Len(Text2) - (lngLocation + Len(Find2) - 1))))

        ' Return the substring:
        Text2 = strBefore + Add2 + strAfter

        ' Locate the next instance of [Find2]:
        lngLocation = InStr(1, Text2, Find2)

        ' Next instance of [Find2]...
    Wend

    ' OUTPUT:
    Replace$ = Text2
End Function ' Replace$
Reply
#10
We can have all sorts of fun doing animation, since it's frame by frame, you're not limited by the speed of the computer (hard drive space on the other hand...)

Here's a simple example:

Code: (Select All)
' #############################################################################
' AVI file format
' https://qb64phoenix.com/forum/showthread.php?tid=3550
' #############################################################################

' From: Petr, Mini-Mod
' Date: 3/22/2025 4:42 AM
' I've been wanting this for a really long time. A program that creates a valid
' AVI format with sound. The following program only creates (does not play) an
' uncompressed AVI file, which also contains uncompressed stereo audio. For this
' reason, prepare about 180 megabytes of free disk space for the test. I think
' it's already quite complicated, so if I add compression to it, it would be much
' more complicated. I will deal with compression, but I don't know how far I will
' get, because I encounter many obstacles.
' I must warn you that Windows Media Player blurs the image due to the player's
' internal filters, VLC player has the best outputs. MPC-HC plays it too, but it
' rotates the image. I can rotate the image in AVI, but then it is rotated again
' in Windows Media Player and in VLC. It's kind of a beginning... and also an
' extreme. First I deal with compression in GIF and save every byte and now tens
' of MegaBytes are no big deal...
' It's uncompressed, so expect a data rate somewhere around 60 megabytes per
' second. QB64PE can handle that speed just fine.
' -----------------------------------------------------------------------------
' Reply #8
' From: mdijkens, Member
' Date: 3/23/2025 12:51 PM (This post was last modified: 3/23/2025 12:56 PM by mdijkens.)
' I used your code as a basis but refactored several things.
' There is now one big AVI header type that is rewritten in the end.
' Also image and audiosamples are now generated inline so no mem limit anymore.
' You can now create AVI's up to 4GB (I think that is some AVI limit?)

' =============================================================================
' GLOBAL VARIABLES
' =============================================================================
Dim Shared m_ProgramPath$: m_ProgramPath$ = Left$(Command$(0), _InStrRev(Command$(0), "\"))
Dim Shared m_ProgramName$: m_ProgramName$ = Mid$(Command$(0), _InStrRev(Command$(0), "\") + 1)

' =============================================================================
' GENERATE VIDEO
' =============================================================================
' FILENAME = EXE NAME + TIMESTAMP + ".AVI"
Dim sFileName As String
sFileName = NoExt$(m_ProgramName$) + "." + GetTimestampForFilename$ + ".avi"

'CreateAVI sFileName, 3, 800, 600, 30, 44100
CreateAVI sFileName, 60, 720, 480, 60, 44100

' /////////////////////////////////////////////////////////////////////////////

Sub CreateAVI (fn$, videoDuration As Integer, _
    frameWidth As Long, frameHeight As Long, _
    vidFPS As Integer, sampleRate As Long)
   
    Const bytesPerPixel = 4

    Type AVIheader
        riffHeader As String * 4
        riffSize As Long
        aviSig As String * 4
        listHdrl As String * 4
        hdrlSize As Long '            Size from hdrlSig to cbSize (74*4 + 7*2)
        hdrlSig As String * 4
        avihChunkID As String * 4
        avihChunkSize As Long
        microSecPerFrame As Long '    Microseconds per frame
        maxBytesPerSec As Long '      Maximum data rate
        paddingGranularity As Long '  Padding granularity
        flags As Long '               AVI flags (e.g., HASINDEX)
        totalFrames As Long '         Total number of frames in the video
        initialFrames As Long '       Initial frames (typically 0)
        streams As Long '             Number of streams (video, audio, etc.)
        suggestedBufferSize As Long ' Suggested buffer size for playback
        vwidth As Long '               Video width in pixels
        vheight As Long '              Video height in pixels
        reserved As String * 16 '     Reserved bytes (set to 0)
        listStrlVid As String * 4
        strlVidSize As Long '         Size from strlVidSig to clrImportant (=31*4)
        strlVidSig As String * 4
        strhVidID As String * 4
        strhVidSize As Long
        fccType As String * 4 '       Stream type (e.g., "vids" or "auds")
        fccHandler As String * 4 '    Codec handler (e.g., "DIB " for uncompressed video)
        streamflags As Long '         Stream flags
        priority As Integer '         Priority (unused)
        language As Integer '         Language code (unused)
        initialFrames0 As Long '      Initial frames (unused)
        scale As Long '               Time scale for the stream
        rate As Long '                Data rate (scale/rate = FPS for video)
        start As Long '               Start time (usually 0)
        length As Long '              Length of the stream (in time units)
        streamSuggestedBufferSize As Long ' Suggested buffer size for the stream
        quality As Long '             Quality indicator (-1 for default)
        sampleSize As Long '          Sample size (0 for video, nonzero for audio)
        frameLeft As Long '           For video: left coordinate of the source rectangle
        frameTop As Long '            For video: top coordinate of the source rectangle
        frameRight As Long '          For video: right coordinate of the source rectangle
        frameBottom As Long '         For video: bottom coordinate of the source rectangle
        strfVidID As String * 4
        strfVidSize As Long
        size As Long '                Size of this header (40 bytes)
        iwidth As Long '              Width of the image in pixels
        iheight As Long '             Height of the image (negative for top–down)
        planes As Integer '           Number of color planes (must be 1)
        bitCount As Integer '         Bits per pixel (24 for 24-bit, 32 for 32-bit)
        compression As Long '         Compression method (0 = BI_RGB for uncompressed)
        sizeImage As Long '           Size of the image data in bytes
        xPelsPerMeter As Long '       Horizontal resolution (pixels per meter)
        yPelsPerMeter As Long '       Vertical resolution (pixels per meter)
        clrUsed As Long '             Number of colors used (0 = all)
        clrImportant As Long '        Number of important colors (0 = all)
        listStrlAud As String * 4
        strlAudSize As Long '         Size from strlAudSig to cbSize (22*4 + 7*2)
        strlAudSig As String * 4
        strhAudID As String * 4
        strhAudSize As Long
        afccType As String * 4 '       AUDIO Stream type (e.g., "vids" or "auds")
        afccHandler As String * 4 '    AUDIO Codec handler (e.g., "DIB " for uncompressed video)
        astreamflags As Long '         AUDIO Stream flags
        apriority As Integer '         AUDIO Priority
        alanguage As Integer '         AUDIO Language code
        ainitialFrames As Long '       AUDIO Initial frames
        ascale As Long '               AUDIO Time scale for the stream
        arate As Long '                AUDIO Data rate (scale/rate = FPS for video)
        astart As Long '               AUDIO Start time (usually 0)
        alength As Long '              AUDIO Length of the stream (in time units)
        astreamSuggestedBufferSize As Long ' AUDIO Suggested buffer size for the stream
        aquality As Long '             AUDIO Quality indicator (-1 for default)
        asampleSize As Long '          AUDIO Sample size (0 for video, nonzero for audio)
        aframeLeft As Long '           AUDIO For video: left coordinate of the source rectangle
        aframeTop As Long '            AUDIO For video: top coordinate of the source rectangle
        aframeRight As Long '          AUDIO For video: right coordinate of the source rectangle
        aframeBottom As Long '         AUDIO For video: bottom coordinate of the source rectangle
        strfAudID As String * 4
        strfAudSize As Long
        wFormatTag As Integer '      Audio format code (1 = PCM)
        nChannels As Integer '        Number of audio channels (2 for stereo)
        nSamplesPerSec As Long '      Sample rate in Hz
        nAvgBytesPerSec As Long '    Average bytes per second
        nBlockAlign As Integer '      Block alignment in bytes
        wBitsPerSample As Integer '  Bits per sample (16 for 16-bit PCM)
        cbSize As Integer '          Extra size (0 for PCM)
        listMovi As String * 4
        moviSize As Long
        moviSig As String * 4
    End Type

    Type ChunkIndex
        chunkID As String * 4 ' Chunk identifier (e.g., "00db" for video, "01wb" for audio)
        flags As Long '        Flags (e.g., keyframe flag)
        offset As Long '        Offset of chunk data from start of 'movi' section
        size As Long '          Size of chunk data in bytes
    End Type

    Dim header As AVIheader
    Dim numFrames As Long: numFrames = vidFPS * videoDuration
    Dim frameSize As Long: frameSize = frameWidth * frameHeight * bytesPerPixel ' Total size of one 32-bit frame
    Dim totalSamplesPerChannel As Long: totalSamplesPerChannel = videoDuration * sampleRate
    Dim idxArr(1 To numFrames + videoDuration) As ChunkIndex
    Dim idxCount As Long: idxCount = 0

    Dim avi$
    avi$ = m_ProgramPath$ + fn$
    If _FileExists(avi$) Then Kill avi$
    Dim fileNum As Integer: fileNum = FreeFile: Open avi$ For Binary As #fileNum

    header.riffHeader = "RIFF"
    header.riffSize = 0
    header.aviSig = "AVI "
    header.listHdrl = "LIST"
    header.hdrlSize = 74 * 4 + 7 * 2
    header.hdrlSig = "hdrl"
    header.avihChunkID = "avih"
    header.avihChunkSize = 56
    header.microSecPerFrame = 1000000 \ vidFPS
    header.maxBytesPerSec = frameSize * vidFPS
    header.paddingGranularity = 0
    header.flags = &H110 '            AVIF_HASINDEX flag
    header.totalFrames = numFrames
    header.initialFrames = 0
    header.streams = 2 '              2 streams: video + audio (stereo is 1 stream and mono also)
    header.suggestedBufferSize = frameSize
    header.vwidth = frameWidth
    header.vheight = frameHeight
    header.reserved = String$(16, Chr$(0))
    header.listStrlVid = "LIST"
    header.strlVidSize = 29 * 4 + 4 * 2
    header.strlVidSig = "strl"
    header.strhVidID = "strh"
    header.strhVidSize = 64
    header.fccType = "vids"
    header.fccHandler = "DIB " ' Uncompressed video (DIB)
    header.streamflags = 0
    header.priority = 0
    header.language = 0
    header.initialFrames = 0
    header.scale = 1
    header.rate = vidFPS
    header.start = 0
    header.length = numFrames
    header.streamSuggestedBufferSize = frameSize
    header.quality = -1
    header.sampleSize = 0
    header.frameLeft = 0
    header.frameTop = 0
    header.frameRight = frameWidth
    header.frameBottom = frameHeight
    header.strfVidID = "strf"
    header.strfVidSize = 40
    header.size = 40
    header.iwidth = frameWidth
    header.iheight = -frameHeight ' Negative height indicates top-down format
    header.planes = 1
    header.bitCount = 32
    header.compression = 0 '      BI_RGB = uncompressed
    header.sizeImage = frameSize
    header.xPelsPerMeter = 0
    header.yPelsPerMeter = 0
    header.clrUsed = 0
    header.clrImportant = 0
    header.listStrlAud = "LIST"
    header.strlAudSize = 22 * 4 + 7 * 2
    header.strlAudSig = "strl"
    header.strhAudID = "strh"
    header.strhAudSize = 64
    header.afccType = "auds"
    header.afccHandler = String$(4, 0) ' For PCM audio, this is typically empty
    header.astreamflags = 0
    header.apriority = 0
    header.alanguage = 0
    header.ainitialFrames = 0
    header.ascale = 1
    header.arate = sampleRate
    header.astart = 0
    header.alength = totalSamplesPerChannel
    header.astreamSuggestedBufferSize = sampleRate * 4 ' 2 channels x 2 bytes per sample (is used 16bit stereo waveform sound, 1 integer to 1 sample)
    header.aquality = -1
    header.asampleSize = 4 ' 4 bytes per stereo sample (2 bytes left + 2 bytes right)
    header.aframeLeft = 0
    header.aframeTop = 0
    header.aframeRight = 0
    header.aframeBottom = 0
    header.strfAudID = "strf"
    header.strfAudSize = 16
    header.wFormatTag = 1
    header.nChannels = 2
    header.nSamplesPerSec = sampleRate
    header.wBitsPerSample = 16
    header.nBlockAlign = header.nChannels * (header.wBitsPerSample \ 8)
    header.nAvgBytesPerSec = header.nSamplesPerSec * header.nBlockAlign
    header.cbSize = 0
    header.listMovi = "LIST"
    header.moviSize = 0
    header.moviSig = "movi"
    Put #fileNum, , header
    Dim moviDataStart As Long: moviDataStart = Seek(fileNum)

    ' --------------------------------------------------------------
    ' Interleaved writing of video and audio chunks:
    ' For each second: 30 video frames and 1 audio chunk (1 second)  - this solution MUST NOT BE ALWAYS OPTIMAL! is possible writing sound data after every 5 th frame...
    ' --------------------------------------------------------------
    Dim chunkOffset As Long
    Dim frameChunkID As String: frameChunkID = "00db"
    Dim padByte As String: padByte = Chr$(0)
    Dim chunkOffsetAud As Long
    Dim audioChunkID As String: audioChunkID = "01wb"
    Dim audioChunkSize As Long
    Dim As Integer SampleL, SampleR 'Variables for left and right channel samples

    Dim sm As _MEM
    Dim img As Long: img = _NewImage(frameWidth, frameHeight, 32)
    'Dim img2 As Long: img2 = _NewImage(frameWidth, frameHeight, 32)
    Dim freq As Single: freq = 500 'Starting frequency in Hz
    Dim videoIndex As Long: videoIndex = 0
    Dim frame As String: frame = Space$(frameSize)


    ' --------------------------------------------------------------
    ' LET'S HAVE SOME FUN WITH ANIMATION
    ' --------------------------------------------------------------
    Type ObjectType
        x As Integer
        y As Integer
        size As Integer
        minSize As Integer
        maxSize As Integer
        ds As Integer
        ss As Integer
        cs As Integer
        color1 As Integer
        color2 As Integer
        dx As Integer
        dy As Integer
        sx As Integer
        sy As Integer
        cx As Integer
        cy As Integer
        width As Integer
        height As Integer
    End Type
    ReDim ColorArray(-1) As _Unsigned Long
    Dim iColor1 As Integer
    Dim iColor2 As Integer
    ReDim arrObject(1 To 100) As ObjectType
    Dim iLoop As Integer
    Dim iMinSize As Integer
    Dim iMaxSize As Integer
    Dim iCircleCount As Integer
    AddSpectrumColors ColorArray() ' INIT SPECTRUM COLOR ARRAY
    If frameWidth < frameHeight Then
        iMinSize = frameWidth \ 100
        iMaxSize = frameWidth \ 15
    Else
        iMinSize = frameHeight \ 100
        iMaxSize = frameHeight \ 15
    End If
    Randomize Timer
    iCircleCount = RandomNumber%(UBound(arrObject) \ 2, UBound(arrObject))
    For iLoop = 1 To iCircleCount
        arrObject(iLoop).x = RandomNumber%(1, frameWidth)
        arrObject(iLoop).y = RandomNumber%(1, frameHeight)
        arrObject(iLoop).minSize = RandomNumber%(10, iMinSize)
        arrObject(iLoop).maxSize = RandomNumber%(10, iMaxSize)
        arrObject(iLoop).size = arrObject(iLoop).minSize
        arrObject(iLoop).ds = 1
        arrObject(iLoop).ss = RandomNumber%(1, 3)
        arrObject(iLoop).cs = 0
        Do
            arrObject(iLoop).color1 = RandomNumber%(LBound(ColorArray), UBound(ColorArray))
            arrObject(iLoop).color2 = RandomNumber%(LBound(ColorArray), UBound(ColorArray))
            If arrObject(iLoop).color1 <> arrObject(iLoop).color2 Then Exit Do
        Loop
        Do
            arrObject(iLoop).dx = RandomNumber%(-1, 1)
            arrObject(iLoop).dy = RandomNumber%(-1, 1)
            If arrObject(iLoop).dx <> 0 Or arrObject(iLoop).dy <> 0 Then Exit Do
        Loop
        arrObject(iLoop).sx = RandomNumber%(1, 4)
        arrObject(iLoop).sy = RandomNumber%(1, 4)
        arrObject(iLoop).cx = 0
        arrObject(iLoop).cy = 0
    Next iLoop
   
    ' RANDOM BOXES
    ReDim GrayArray(-1) As _Unsigned Long
    Dim arrBox(1 To 100) As ObjectType
    Dim iBoxCount As Integer
    AddGrayscaleColors GrayArray() ' INIT GRAYSCALE COLOR ARRAY
    iBoxCount = RandomNumber%((UBound(arrBox) * .75), UBound(arrBox))
    For iLoop = 1 To iBoxCount
        arrBox(iLoop).x = RandomNumber%(1, frameWidth)
        arrBox(iLoop).y = RandomNumber%(1, frameHeight)
        arrBox(iLoop).width = RandomNumber%(iMinSize * 2, iMaxSize * 2)
        arrBox(iLoop).height = RandomNumber%(iMinSize, iMaxSize \ 2)
        arrBox(iLoop).dx = 1
        arrBox(iLoop).dy = 0
        arrBox(iLoop).sx = RandomNumber%(1, 3)
        arrBox(iLoop).sy = RandomNumber%(1, 3)
        arrBox(iLoop).cx = 0
        arrBox(iLoop).cy = 0
        arrBox(iLoop).color1 = RandomNumber%(LBound(GrayArray), UBound(GrayArray))
    Next iLoop
   
    ' --------------------------------------------------------------
    ' GENERATE THE VIDEO
    ' --------------------------------------------------------------
    Dim sTotal As String
   
    Locate 1, 1: Print "Generating video file " + Chr$(34) + fn$ + Chr$(34)
    sTotal = " of " + _Trim$(Str$(videoDuration))
    For sec% = 1 To videoDuration
        ' SHOW PROGRESS
        Locate 3, 1: Print _Trim$(Str$(sec%)) + sTotal + "          ";
       
        ' Write video chunks
        For i% = 1 To vidFPS
            chunkOffset = Seek(fileNum) - moviDataStart
            Put #fileNum, , frameChunkID
            Put #fileNum, , frameSize
            videoIndex = videoIndex + 1
           
            ' GENERATE NEXT FRAME
            _Dest img '@@
            Cls: _PrintMode _KeepBackground
           
            ' DRAW BOXES
            'For j% = 1 To 100
            '    Line (Rnd * frameWidth, Rnd * frameHeight)-(Rnd * frameWidth, Rnd * frameHeight), _RGB32(Rnd * 255, Rnd * 255, Rnd * 255), B
            'Next j%
            For iLoop = 1 To iBoxCount
                ' MOVE X-AXIS
                If arrBox(iLoop).dx <> 0 Then
                    'arrBox(iLoop).cx = arrBox(iLoop).cx + 1
                    'If arrBox(iLoop).cx >= arrBox(iLoop).sx Then
                    '    arrBox(iLoop).cx = 0
                    arrBox(iLoop).x = arrBox(iLoop).x + arrBox(iLoop).dx
                    If arrBox(iLoop).x > frameWidth Then
                        arrBox(iLoop).x = 1
                    ElseIf arrBox(iLoop).x < 1 Then
                        arrBox(iLoop).x = frameWidth
                    End If
                    'End If
                End If
                ' MOVE Y-AXIS
                If arrBox(iLoop).dy <> 0 Then
                    'arrBox(iLoop).cy = arrBox(iLoop).cy + 1
                    'If arrBox(iLoop).cy >= arrBox(iLoop).sy Then
                    '    arrBox(iLoop).cy = 0
                    arrBox(iLoop).y = arrBox(iLoop).y + arrBox(iLoop).dy
                    If arrBox(iLoop).y > frameHeight Then
                        arrBox(iLoop).y = 1
                    ElseIf arrBox(iLoop).y < 1 Then
                        arrBox(iLoop).y = frameHeight
                    End If
                    'End If
                End If
                ' DRAW IT
                _Dest img
                'DrawRectOutline arrBox(iLoop).x, arrBox(iLoop).y, _
                '   arrBox(iLoop).width, arrBox(iLoop).height, GrayArray(arrBox(iLoop).color1)
                DrawRectSolid arrBox(iLoop).x, arrBox(iLoop).y, _
                    arrBox(iLoop).width, arrBox(iLoop).height, GrayArray(arrBox(iLoop).color1)
            Next iLoop
           
            ' DRAW CIRCLES
            For iLoop = 1 To iCircleCount
                ' MOVE X-AXIS
                If arrObject(iLoop).dx <> 0 Then
                    'arrObject(iLoop).cx = arrObject(iLoop).cx + 1
                    'If arrObject(iLoop).cx >= arrObject(iLoop).sx Then
                    '    arrObject(iLoop).cx = 0
                    arrObject(iLoop).x = arrObject(iLoop).x + arrObject(iLoop).dx
                    If arrObject(iLoop).x > frameWidth Then
                        arrObject(iLoop).x = 1
                    ElseIf arrObject(iLoop).x < 1 Then
                        arrObject(iLoop).x = frameWidth
                    End If
                    'End If
                End If
                ' MOVE Y-AXIS
                If arrObject(iLoop).dy <> 0 Then
                    'arrObject(iLoop).cy = arrObject(iLoop).cy + 1
                    'If arrObject(iLoop).cy >= arrObject(iLoop).sy Then
                    '    arrObject(iLoop).cy = 0
                    arrObject(iLoop).y = arrObject(iLoop).y + arrObject(iLoop).dy
                    If arrObject(iLoop).y > frameHeight Then
                        arrObject(iLoop).y = 1
                    ElseIf arrObject(iLoop).y < 1 Then
                        arrObject(iLoop).y = frameHeight
                    End If
                    'End If
                End If
                ' CHANGE SIZE
                'arrObject(iLoop).cs = arrObject(iLoop).cs + 1
                'If arrObject(iLoop).cs >= arrObject(iLoop).ss Then
                '   arrObject(iLoop).cs = 0
                arrObject(iLoop).size = arrObject(iLoop).size + arrObject(iLoop).ds
                If arrObject(iLoop).size > arrObject(iLoop).maxSize Then
                    arrObject(iLoop).size = arrObject(iLoop).maxSize
                    arrObject(iLoop).ds = -1
                ElseIf arrObject(iLoop).size < arrObject(iLoop).minSize Then
                    arrObject(iLoop).size = arrObject(iLoop).minSize
                    arrObject(iLoop).ds = 1
                End If
                'End If
                ' DRAW IT
                DrawCircle img, _
                    arrObject(iLoop).x, arrObject(iLoop).y, _
                    arrObject(iLoop).size, 1, _
                    ColorArray(arrObject(iLoop).color2), ColorArray(arrObject(iLoop).color1)
            Next iLoop
           
            ' SHOW FRAME # AND TIME
            _PrintString (350, 250), "Video Frame " + _ToStr$(videoIndex)
            _PrintString (350, 250 + (_FontHeight * 2)), "Time (seconds): " + _Trim$(Str$(sec%)) + sTotal
            _Dest 0
            '_PutImage (0, 0), img, img2, (0, frameHeight)-(frameWidth, 0) ' For VLC Player
            '_PutImage (0, 0), img, img2 ' For WMP, MPC-HC Player
           
           
            sm = _MemImage(img)
            frame = Space$(frameSize)
            _MemGet sm, sm.OFFSET, frame
            _MemFree sm
            Put #fileNum, , frame
            If (frameSize Mod 2) <> 0 Then Put #fileNum, , padByte

            idxCount = idxCount + 1
            idxArr(idxCount).chunkID = frameChunkID
            idxArr(idxCount).flags = &H10 ' Keyframe flag
            idxArr(idxCount).offset = chunkOffset
            idxArr(idxCount).size = frameSize
        Next i%

        ' Write audio chunk for the current second
        chunkOffsetAud = Seek(fileNum) - moviDataStart
        Put #fileNum, , audioChunkID
        audioChunkSize = sampleRate * 2 * 2 ' 1 second = sampleRate * (2 channels x 2 bytes)
        Put #fileNum, , audioChunkSize

        audioBuffer$ = String$(sampleRate * 4, 0)
        For l& = 0 To sampleRate - 1
            t! = l& / sampleRate: pan! = l& / totalSamplesPerChannel
            freq = freq + 5 - 10 * Int(Rnd * 2)
            SampleL = (32767 * 0.9 * (1 - pan!) * Sin(2 * 3.14159 * freq * t!)) And &HFFFF
            SampleR = (32767 * 0.9 * pan! * Sin(2 * 3.14159 * freq * t!)) And &HFFFF
            Asc(audioBuffer$, l& * 4 + 1) = SampleL And &HFF
            Asc(audioBuffer$, l& * 4 + 2) = (SampleL \ 256) And &HFF
            Asc(audioBuffer$, l& * 4 + 3) = SampleR And &HFF
            Asc(audioBuffer$, l& * 4 + 4) = (SampleR \ 256) And &HFF
        Next l&
        Put #fileNum, , audioBuffer$

        idxCount = idxCount + 1
        idxArr(idxCount).chunkID = audioChunkID
        idxArr(idxCount).flags = 0
        idxArr(idxCount).offset = chunkOffsetAud
        idxArr(idxCount).size = audioChunkSize
    Next sec%
    _Dest 0
    _FreeImage img

    currPos = Seek(fileNum)
    header.moviSize = currPos - moviSizePos - 4
    header.riffSize = Seek(fileNum) - 8

    Dim idxHeader As String: idxHeader = "idx1": Put #fileNum, , idxHeader
    Dim idxSize As Long: idxSize = idxCount * 16: Put #fileNum, , idxSize
    Dim n As Long
    For n = 1 To idxCount
        Put #fileNum, , idxArr(n).chunkID
        Put #fileNum, , idxArr(n).flags
        Put #fileNum, , idxArr(n).offset
        Put #fileNum, , idxArr(n).size
    Next n
    Put #fileNum, 1, header
    Close #fileNum
   
    Print
   
    ' Display file name/path in inputbox so user can copy to clipboard.
    Dim in$
    in$ = _INPUTBOX$(m_ProgramName$, _
        "File created." + chr$(13) + _
            "Open it with your media player program" + chr$(13) + _
            "(VLC player recommended).", _
        avi$) ' title, message, default
   
End Sub ' CreateAVI

' ################################################################################################################################################################
' BEGIN GENERAL PURPOSE ROUTINES #GEN
' ################################################################################################################################################################

' /////////////////////////////////////////////////////////////////////////////

Function AppendString$ (MyString As String, NewString As String, MyDelimiter As String)
    Dim sResult As String: sResult = MyString
    If Len(MyString) > 0 Then
        sResult = sResult + MyDelimiter
    End If
    sResult = sResult + NewString
    AppendString$ = sResult
End Function ' AppendString$

' /////////////////////////////////////////////////////////////////////////////

Sub AppendToStringArray (MyStringArray$(), MyString$)
    ReDim _Preserve MyStringArray$(LBound(MyStringArray$) To UBound(MyStringArray$) + 1)
    MyStringArray$(UBound(MyStringArray$)) = MyString$
End Sub ' AppendToStringArray

' /////////////////////////////////////////////////////////////////////////////
' See also StringTo2dArray

Function Array2dToString$ (MyArray() As String)
    Dim MyString As String
    Dim iY As Integer
    Dim iX As Integer
    Dim sLine As String
    MyString = ""
    For iY = LBound(MyArray, 1) To UBound(MyArray, 1)
        sLine = ""
        For iX = LBound(MyArray, 2) To UBound(MyArray, 2)
            sLine = sLine + MyArray(iY, iX)
        Next iX
        MyString = MyString + sLine + Chr$(13)
    Next iY
    Array2dToString$ = MyString
End Function ' Array2dToString$

' /////////////////////////////////////////////////////////////////////////////

$If  Then
    Function Array2dToStringTest$ (MyArray() As String)
    Dim MyString As String
    Dim iY As Integer
    Dim iX As Integer
    Dim sLine As String
    MyString = ""
    MyString = MyString + "           11111111112222222222333" + Chr$(13)
    MyString = MyString + "  12345678901234567890123456789012" + Chr$(13)
    For iY = LBound(MyArray, 1) To UBound(MyArray, 1)
    sLine = ""
    sLine = sLine + Right$("  " + cstr$(iY), 2)
    For iX = LBound(MyArray, 2) To UBound(MyArray, 2)
    sLine = sLine + MyArray(iY, iX)
    Next iX
    sLine = sLine + Right$("  " + cstr$(iY), 2)
    MyString = MyString + sLine + Chr$(13)
    Next iY
    MyString = MyString + "  12345678901234567890123456789012" + Chr$(13)
    MyString = MyString + "           11111111112222222222333" + Chr$(13)
    Array2dToStringTest$ = MyString
    End Function ' Array2dToStringTest$
$End If

' /////////////////////////////////////////////////////////////////////////////
' use angles in degrees units instead of radians (converted inside sub)
' Note this function uses whatever the default type is, better not be some Integer Type.

Function CosD (degrees)
    CosD = Cos(_D2R(degrees))
End Function ' CosD

' /////////////////////////////////////////////////////////////////////////////
' Integer to string

Function cstr$ (myValue)
    'cstr$ = LTRIM$(RTRIM$(STR$(myValue)))
    cstr$ = _Trim$(Str$(myValue))
End Function ' cstr$

' /////////////////////////////////////////////////////////////////////////////
' Long to string

Function cstrl$ (myValue As Long)
    cstrl$ = _Trim$(Str$(myValue))
End Function ' cstrl$

' /////////////////////////////////////////////////////////////////////////////
' Single to string

Function cstrs$ (myValue As Single)
    ''cstr$ = LTRIM$(RTRIM$(STR$(myValue)))
    cstrs$ = _Trim$(Str$(myValue))
End Function ' cstrs$

' /////////////////////////////////////////////////////////////////////////////
' Unsigned Long to string

Function cstrul$ (myValue As _Unsigned Long)
    cstrul$ = _Trim$(Str$(myValue))
End Function ' cstrul$

' /////////////////////////////////////////////////////////////////////////////
' Simple timestamp function
' In the format yyyy-mm-dd hh:mm:ss
' in 24-hour time.

Function CurrentDateTime$
    CurrentDateTime$ = Mid$(Date$, 7, 4) + "-" + _
        Mid$(Date$, 1, 5) + " " + _
        Time$
End Function ' CurrentDateTime$

' /////////////////////////////////////////////////////////////////////////////
' use angles in degrees units instead of radians (converted inside sub)
' Note this function uses whatever the default type is, better not be some Integer Type.

Function DAtan2 (x1, y1, x2, y2) ' The angle in degrees a 2nd point (x2, y2)  makes to a first point (x1, y1)
    ' Delta means change between 1 measure and another for example x2 - x1
    deltaX = x2 - x1
    deltaY = y2 - y1

    ' To find the angle point(x2, y2) makes to (x1, y1) in Degrees
    ' Take DegreeAngle = DAtan2(y2 - y1, x2 - x1)
    rtn = _R2D(_Atan2(deltaY, deltaX))
    If rtn < 0 Then
        DAtan2 = rtn + 360
    Else
        DAtan2 = rtn
    End If
End Function ' DAtan2

' /////////////////////////////////////////////////////////////////////////////
' Hack function, to be replaced with something better

Function DblToInt% (dblOld As Double)
    Dim dblNew As Double
    Dim sValue As String
    Dim iPos As Integer

    dblNew = RoundDouble#(dblOld, 0)
    'sValue = _Trim$(Str$(dblNew))

    sValue = DblToStr$(dblNew)

    'iPos = InStr(1, sValue, ".")
    'If iPos > 0 Then
    '    DblToInt% = Val(Left$(sValue, iPos - 1))
    'Else
    '    DblToInt% = Val(sValue)
    'End If

    DblToInt% = Val(sValue)
End Function ' DblToInt%

' /////////////////////////////////////////////////////////////////////////////
' Scientific notation - QB64 Wiki
' https://www.qb64.org/wiki/Scientific_notation

' Example: A string function that displays extremely small or large exponential decimal values.

Function DblToStr$ (n#)
    Dim result$: result$ = ""
    Dim value$
    Dim Xpos%
    Dim expo%
    Dim sign$
    Dim valu$
    Dim dot%
    Dim L%
    Dim add$
    Dim min$
    Dim DP$
    Dim n%

    value$ = UCase$(LTrim$(Str$(n#)))
    Xpos% = InStr(value$, "D") + InStr(value$, "E") ' only D or E can be present
    If Xpos% Then
        expo% = Val(Mid$(value$, Xpos% + 1))
        If Val(value$) < 0 Then
            sign$ = "-"
            valu$ = Mid$(value$, 2, Xpos% - 2)
        Else
            valu$ = Mid$(value$, 1, Xpos% - 1)
        End If
        dot% = InStr(valu$, ".")
        L% = Len(valu$)
        If expo% > 0 Then
            add$ = String$(expo% - (L% - dot%), "0")
        End If
        If expo% < 0 Then
            min$ = String$(Abs(expo%) - (dot% - 1), "0")
            DP$ = "."
        End If
        For n% = 1 To L%
            If Mid$(valu$, n%, 1) <> "." Then
                num$ = num$ + Mid$(valu$, n%, 1)
            End If
        Next n%
        result$ = _Trim$(sign$ + DP$ + min$ + num$ + add$)
    Else
        result$ = value$
    End If

    DblToStr$ = result$
End Function ' DblToStr$

' /////////////////////////////////////////////////////////////////////////////

Function DblRoundedToStr$ (dblValue As Double, intNumPlaces As Integer)
    Dim dblNew As Double
    dblNew = RoundDouble#(dblValue, intNumPlaces)
    DblRoundedToStr$ = DblToStr$(dblNew)
End Function ' DblRoundedToStr$

' /////////////////////////////////////////////////////////////////////////////
' Receives an {sDelim} delimited list {sInput}
' returns the list with all duplicate entries removed.

Function DedupeDelimList$ (sInput As String, sDelim As String)
    ReDim arrLines(-1) As String
    Dim sOutput As String
    Dim iLoop As Integer

    split sInput, sDelim, arrLines()
    sOutput = sDelim
    For iLoop = LBound(arrLines) To UBound(arrLines)
        If InStr(1, sOutput, sDelim + arrLines(iLoop) + sDelim) = 0 Then
            sOutput = sOutput + arrLines(iLoop) + sDelim
        End If
    Next iLoop

    DedupeDelimList$ = sOutput
End Function ' DedupeDelimList$

' /////////////////////////////////////////////////////////////////////////////

Function DoubleABS# (dblValue As Double)
    If Sgn(dblValue) = -1 Then
        DoubleABS# = 0 - dblValue
    Else
        DoubleABS# = dblValue
    End If
End Function ' DoubleABS#

' /////////////////////////////////////////////////////////////////////////////
' bplus: The Gold Standard is even better than THE QB64 CIRCLE sub in this respect!
' https://forum.qb64.org/index.php?topic=1044.135

' from Steve Gold standard
' Renamed fcirc to DrawCircleSolid

' Not as fast as DrawCircleTopLeft but pretty fast.

' Example:
' Screen _NewImage(800, 600, 32)
' _ScreenMove 250, 60
' For r = 250 To 0 Step -60
'     DrawCircleSolid 400, 300, r, _RGBA(255, 255, 255, 100)
' Next r

Sub DrawCircleSolid (CX As Integer, CY As Integer, R As Integer, C As _Unsigned Long)
    Dim Radius As Integer, RadiusError As Integer
    Dim X As Integer, Y As Integer

    Radius = Abs(R)
    RadiusError = -Radius
    X = Radius
    Y = 0

    If Radius = 0 Then PSet (CX, CY), C: Exit Sub

    ' Draw the middle span here so we don't draw it twice in the main loop,
    ' which would be a problem with blending turned on.
    Line (CX - X, CY)-(CX + X, CY), C, BF

    While X > Y
        RadiusError = RadiusError + Y * 2 + 1
        If RadiusError >= 0 Then
            If X <> Y + 1 Then
                Line (CX - Y, CY - X)-(CX + Y, CY - X), C, BF
                Line (CX - Y, CY + X)-(CX + Y, CY + X), C, BF
            End If
            X = X - 1
            RadiusError = RadiusError - X * 2
        End If
        Y = Y + 1
        Line (CX - X, CY - Y)-(CX + X, CY - Y), C, BF
        Line (CX - X, CY + Y)-(CX + X, CY + Y), C, BF
    Wend
End Sub ' DrawCircleSolid

' /////////////////////////////////////////////////////////////////////////////
' Draws scaled + rotated text to screen
' by BPlus

' Scale rotate font text strings by B+
' https://qb64phoenix.com/forum/showthread.php?tid=414&highlight=rotate+text

' INPUT:
' S$ is the string to display
' c is the color (will have a transparent background)
' midX and midY is the center of where you want to display the string
' xScale would multiply 8 pixel width of default font
' yScale would multiply the 16 pixel height of the default font
' Rotation is in Radian units, use _D2R to convert Degree units to Radian units

' DEPENDENCIES:
' drwString needs sub RotoZoom2, intended for graphics screens using the default font.

Sub drwString (S$, c As _Unsigned Long, midX, midY, xScale, yScale, Rotation As Single)
    I& = _NewImage(_PrintWidth(S$), _FontHeight, 32)
    _Dest I&
    Color c, _RGBA32(0, 0, 0, 0)
    _PrintString (0, 0), S$
    _Dest storeDest&
    RotoZoom2 midX, midY, I&, xScale, yScale, Rotation
    _FreeImage I&
End Sub ' drwString

' /////////////////////////////////////////////////////////////////////////////

Sub DumpScreenAndFontSize ()
    Dim iCols As Integer
    Dim iRows As Integer
    'Screen _NewImage(1024, 768, 32): _ScreenMove 0, 0
    iCols = _Width(0) \ _FontWidth
    iRows = _Height(0) \ _FontHeight
    Print "_Width(0)  =" + _Trim$(Str$(_Width(0)))
    Print "_Height(0) =" + _Trim$(Str$(_Height(0)))
    Print "_FontWidth =" + _Trim$(Str$(_FontWidth))
    Print "_FontHeight=" + _Trim$(Str$(_FontHeight))
    Print "iCols = _Width(0)  \ _FontWidth  = " + _Trim$(Str$(iCols))
    Print "iRows = _Height(0) \ _FontHeight = " + _Trim$(Str$(iRows))
End Sub ' DumpScreenAndFontSize

' /////////////////////////////////////////////////////////////////////////////
' SMcNeill
' More efficient version of ExtendedTimer.
' This stores our day values and only updates them when necessary.
' We really don't need to figure out how many seconds are in today over and over endlessly
' -- just count them once, and when the clock swaps back to 0:0:0, add 24*60*60 seconds to the count.
' Re: how to time something (ie do loop for n seconds)
' https://forum.qb64.org/index.php?topic=4682.0

Function ExtendedTimer##
    'modified extendedtimer to store the old day's count, and not have to recalculate it every time the routine is called.

    Static olds As _Float, old_day As _Float
    Dim m As Integer, d As Integer, y As Integer
    Dim s As _Float, day As String
    If olds = 0 Then 'calculate the day the first time the extended timer runs
        day = Date$
        m = Val(Left$(day, 2))
        d = Val(Mid$(day, 4, 2))
        y = Val(Right$(day, 4)) - 1970
        Select Case m 'Add the number of days for each previous month passed
            Case 2: d = d + 31
            Case 3: d = d + 59
            Case 4: d = d + 90
            Case 5: d = d + 120
            Case 6: d = d + 151
            Case 7: d = d + 181
            Case 8: d = d + 212
            Case 9: d = d + 243
            Case 10: d = d + 273
            Case 11: d = d + 304
            Case 12: d = d + 334
        End Select
        If (y Mod 4) = 2 And m > 2 Then d = d + 1 'add a day if this is leap year and we're past february
        d = (d - 1) + 365 * y 'current month days passed + 365 days per each standard year
        d = d + (y + 2) \ 4 'add in days for leap years passed
        s = d * 24 * 60 * 60 'Seconds are days * 24 hours * 60 minutes * 60 seconds
        old_day = s
    End If
    If Timer < oldt Then 'we went from 23:59:59 (a second before midnight) to 0:0:0 (midnight)
        old_day = s + 83400 'add another worth of seconds to our counter
    End If
    oldt = Timer
    olds = old_day + oldt
    ExtendedTimer## = olds
End Function ' ExtendedTimer##

' /////////////////////////////////////////////////////////////////////////////

Function FloatRoundedToStr$ (fValue As _Float, intNumPlaces As Integer)
    Dim fNew As _Float
    fNew = Round##(fValue, intNumPlaces)
    FloatRoundedToStr$ = FloatToStr$(fNew)
End Function ' DblRoundedToStr$

' /////////////////////////////////////////////////////////////////////////////
' Scientific notation - QB64 Wiki
' https://www.qb64.org/wiki/Scientific_notation

' Example: A string function that displays extremely small or large exponential decimal values.

Function FloatToStr$ (n##)
    value$ = UCase$(LTrim$(Str$(n##)))
    Xpos% = InStr(value$, "D") + InStr(value$, "E") 'only D or E can be present
    If Xpos% Then
        expo% = Val(Mid$(value$, Xpos% + 1))
        If Val(value$) < 0 Then
            sign$ = "-"
            valu$ = Mid$(value$, 2, Xpos% - 2)
        Else
            valu$ = Mid$(value$, 1, Xpos% - 1)
        End If
        dot% = InStr(valu$, ".")
        L% = Len(valu$)
        If expo% > 0 Then
            add$ = String$(expo% - (L% - dot%), "0")
        End If
        If expo% < 0 Then
            min$ = String$(Abs(expo%) - (dot% - 1), "0")
            DP$ = "."
        End If
        For n = 1 To L%
            If Mid$(valu$, n, 1) <> "." Then
                num$ = num$ + Mid$(valu$, n, 1)
            End If
        Next n
    Else
        FloatToStr$ = value$
        Exit Function
    End If
    FloatToStr$ = _Trim$(sign$ + DP$ + min$ + num$ + add$)
End Function ' FloatToStr$

' /////////////////////////////////////////////////////////////////////////////
' TODO: find the newer formatting function?

Function FormatNumber$ (myValue, iDigits As Integer)
    Dim strValue As String
    strValue = DblToStr$(myValue) + String$(iDigits, " ")
    If myValue < 1 Then
        If myValue < 0 Then
            strValue = Replace$(strValue, "-.", "-0.")
        ElseIf myValue > 0 Then
            strValue = "0" + strValue
        End If
    End If
    FormatNumber$ = Left$(strValue, iDigits)
End Function ' FormatNumber$

' /////////////////////////////////////////////////////////////////////////////
' From: Bitwise Manipulations By Steven Roman
' http://www.romanpress.com/Articles/Bitwise_R/Bitwise.htm

' Returns the 8-bit binary representation
' of an integer iInput where 0 <= iInput <= 255

Function GetBinary$ (iInput1 As Integer)
    Dim sResult As String
    Dim iLoop As Integer
    Dim iInput As Integer: iInput = iInput1

    sResult = ""

    If iInput >= 0 And iInput <= 255 Then
        For iLoop = 1 To 8
            sResult = LTrim$(RTrim$(Str$(iInput Mod 2))) + sResult
            iInput = iInput \ 2
            'If iLoop = 4 Then sResult = " " + sResult
        Next iLoop
    End If

    GetBinary$ = sResult
End Function ' GetBinary$

' /////////////////////////////////////////////////////////////////////////////
' wonderfully inefficient way to read if a bit is set
' ival = GetBit256%(int we are comparing, int containing the bits we want to read)

' See also: GetBit256%, SetBit256%

Function GetBit256% (iNum1 As Integer, iBit1 As Integer)
    Dim iResult As Integer
    Dim sNum As String
    Dim sBit As String
    Dim iLoop As Integer
    Dim bContinue As Integer
    'DIM iTemp AS INTEGER
    Dim iNum As Integer: iNum = iNum1
    Dim iBit As Integer: iBit = iBit1

    iResult = _FALSE
    bContinue = _TRUE

    If iNum < 256 And iBit <= 128 Then
        sNum = GetBinary$(iNum)
        sBit = GetBinary$(iBit)
        For iLoop = 1 To 8
            If Mid$(sBit, iLoop, 1) = "1" Then
                'if any of the bits in iBit are false, return _FALSE
                If Mid$(sNum, iLoop, 1) = "0" Then
                    iResult = _FALSE
                    bContinue = _FALSE
                    Exit For
                End If
            End If
        Next iLoop
        If bContinue = _TRUE Then
            iResult = _TRUE
        End If
    End If

    GetBit256% = iResult
End Function ' GetBit256%

' /////////////////////////////////////////////////////////////////////////////
' Returns the text character at positon x%, y%

' Does the same as:
'   Locate y%, x%
'   GetCharXY% = Screen(CsrLin, Pos(0))

' See also: GetColorXY&

Function GetCharXY% (x%, y%)
    GetCharXY% = Screen(y%, x%, 0) ' when 3rd parameter = 0 returns character code
End Function ' GetCharXY%

' /////////////////////////////////////////////////////////////////////////////
' Returns the text color at positon x%, y%

' See also: GetCharXY%

Function GetColorXY& (x%, y%)
    GetColorXY& = Screen(y%, x%, 1) ' when 3rd parameter = 1 returns character color
End Function ' GetColorXY

' /////////////////////////////////////////////////////////////////////////////
' Simple timestamp function
' Format: {YYYY}-{MM}-{DD} {hh}:[mm}:{ss}

' Uses:
'     TIME$
'         The TIME$ Function returns a STRING representation
'         of the current computer time in a 24 hour format.
'         https://qb64phoenix.com/qb64wiki/index.php/TIME$
'     DATE$
'         The DATE$ function returns the current computer date
'         as a string in the format "mm-dd-yyyy".
'         https://qb64phoenix.com/qb64wiki/index.php/DATE$
'
' TODO: support template where
'       {yyyy} = 4 digit year
'       {mm}   = 2 digit month
'       {dd}   = 2 digit day
'       {hh}   = 2 digit hour (12-hour)
'       {rr}   = 2 digit hour (24-hour)
'       {nn}   = 2 digit minute
'       {ss}   = 2 digit second
'       {ampm} = AM/PM

' We got the nn for minute from Microsoft > Office VBA Reference > DateDiff function
' https://docs.microsoft.com/en-us/office/vba/language/reference/user-interface-help/datediff-function

' PRINT "Current date time (simple format) = " + Chr$(34) + GetCurrentDateTime$("{yyyy}-{mm}-{dd} {rr}:{nn}:{ss}") + Chr$(34)
' PRINT "Current date time (US format)     = " + Chr$(34) + GetCurrentDateTime$("{mm}/{dd}/{yyyy} {hh}:{nn}:{ss} {ampm}") + Chr$(34)
' PRINT "Filename timestamp                = " + Chr$(34) + GetCurrentDateTime$("{yyyy}{mm}{dd}_{rr}{nn}{ss}") + Chr$(34)

Function GetCurrentDateTime$ (sTemplate$)
    Dim sDate$: sDate$ = Date$
    Dim sTime$: sTime$ = Time$
    Dim sYYYY$: sYYYY$ = Mid$(sDate$, 7, 4)
    Dim sMM$: sMM$ = Mid$(sDate$, 1, 2)
    Dim sDD$: sDD$ = Mid$(sDate$, 4, 2)
    Dim sHH24$: sHH24$ = Mid$(sTime$, 1, 2)
    Dim sHH$: sHH$ = ""
    Dim sMI$: sMI$ = Mid$(sTime$, 4, 2)
    Dim sSS$: sSS$ = Mid$(sTime$, 7, 2)
    Dim iHour%: iHour% = Val(sHH24$)
    Dim sAMPM$: sAMPM$ = ""
    Dim result$: result$ = ""

    ' FIGURE OUT AM/PM
    If InStr(sTemplate$, "{ampm}") > 0 Then
        If iHour% = 0 Then
            sAMPM$ = "AM"
            iHour% = 12
        ElseIf iHour% > 0 And iHour% < 12 Then
            sAMPM$ = "AM"
        ElseIf iHour% = 12 Then
            sAMPM$ = "PM"
        Else
            sAMPM$ = "PM"
            iHour% = iHour% - 12
        End If
        sHH$ = Right$("00" + _Trim$(Str$(iHour%)), 2)
    End If

    ' POPULATE TEMPLATE
    result$ = sTemplate$
    result$ = Replace$(result$, "{yyyy}", sYYYY$)
    result$ = Replace$(result$, "{mm}", sMM$)
    result$ = Replace$(result$, "{dd}", sDD$)
    result$ = Replace$(result$, "{hh}", sHH$)
    result$ = Replace$(result$, "{rr}", sHH24$)
    result$ = Replace$(result$, "{nn}", sMI$)
    result$ = Replace$(result$, "{ss}", sSS$)
    result$ = Replace$(result$, "{ampm}", sAMPM$)

    ' RETURN RESULT
    GetCurrentDateTime$ = result$
End Function ' GetCurrentDateTime$

' /////////////////////////////////////////////////////////////////////////////
' From: Bitwise Manipulations By Steven Roman
' http://www.romanpress.com/Articles/Bitwise_R/Bitwise.htm

' Returns the integer that corresponds to a binary string of length 8

Function GetIntegerFromBinary% (sBinary1 As String)
    Dim iResult As Integer
    Dim iLoop As Integer
    Dim strBinary As String
    Dim sBinary As String: sBinary = sBinary1

    iResult = 0
    strBinary = Replace$(sBinary, " ", "") ' Remove any spaces
    For iLoop = 0 To Len(strBinary) - 1
        iResult = iResult + 2 ^ iLoop * Val(Mid$(strBinary, Len(strBinary) - iLoop, 1))
    Next iLoop

    GetIntegerFromBinary% = iResult
End Function ' GetIntegerFromBinary%

' /////////////////////////////////////////////////////////////////////////////
' Receives a {sDelimeter} delimited list of numbers {MyString}
' and splits it up into an integer array arrInteger()
' beginning at index {iMinIndex}.

Sub GetIntegerArrayFromDelimList (MyString As String, sDelimiter As String, iMinIndex As Integer, arrInteger() As Integer)
    ReDim arrString(-1) As String
    Dim CleanString As String
    Dim iLoop As Integer
    Dim iCount As Integer: iCount = iMinIndex - 1

    ReDim arrInteger(-1) As Integer

    'DebugPrint "GetIntegerArrayFromDelimList " + _
    '    "MyString=" + chr$(34) + MyString + chr$(34) + ", " + _
    '    "sDelimiter=" + chr$(34) + sDelimiter + chr$(34) + ", " + _
    '    "iMinIndex=" + cstr$(iMinIndex) + ", " + _
    '    "arrInteger()"

    If Len(sDelimiter) > 0 Then
        CleanString = MyString
        If sDelimiter <> " " Then
            CleanString = Replace$(CleanString, " ", "")
        End If

        split CleanString, sDelimiter, arrString()
        iCount = iMinIndex - 1
        For iLoop = LBound(arrString) To UBound(arrString)
            If IsNum%(arrString(iLoop)) = _TRUE Then
                iCount = iCount + 1
                ReDim _Preserve arrInteger(iMinIndex To iCount) As Integer
                arrInteger(iCount) = Val(arrString(iLoop))
                'DebugPrint "5633 arrInteger(" + cstr$(iCount) + ") = VAL(arrString(" + cstr$(iLoop) + ")) = " + cstr$(arrInteger(iCount))

            End If
        Next iLoop
    Else
        If IsNum%(MyString) = _TRUE Then
            ReDim _Preserve arrInteger(iMinIndex To iMinIndex) As Integer
            arrInteger(iMinIndex) = Val(MyString)
        End If
    End If

    'CleanString=""
    'for iLoop=lbound(arrInteger) to ubound(arrInteger)
    'CleanString = CleanString + iifstr$(iLoop=lbound(arrInteger), "", ",") + cstr$(arrInteger(iLoop))
    'next iLoop
    'DebugPrint "arrInteger=(" + CleanString + ")"
End Sub ' GetIntegerArrayFromDelimList

' /////////////////////////////////////////////////////////////////////////////
' Gets the # of seconds so far today

Function GetTimeSeconds& ()
    Dim result&: result& = 0
    Dim sTime$: sTime$ = Time$
    Dim sHH24$: sHH24$ = Mid$(sTime$, 1, 2)
    Dim sMI$: sMI$ = Mid$(sTime$, 4, 2)
    Dim sSS$: sSS$ = Mid$(sTime$, 7, 2)
   
    result& = result& + Val(sSS$)
    result& = result& + (Val(sMI$) * 60)
    result& = result& + ((Val(sHH24$) * 60) * 60)
   
    ' RETURN RESULT
    GetTimeSeconds& = result&
End Function ' GetTimeSeconds&

' /////////////////////////////////////////////////////////////////////////////
' Get timestamp for filename
' In the format yyyymmdd_hhmmss
' in 24-hour time.

Function GetTimestampForFilename$
    Dim MyDate$
    Dim MyTime$
    MyDate$ = Date$
    MyTime$ = Time$
    MyDate$ = Right$(MyDate$, 4) + Left$(MyDate$, 2) + Mid$(MyDate$, 4, 2)
    MyTime$ = Replace$(MyTime$, ":", "")
    GetTimestampForFilename$ = MyDate$ + "_" + MyTime$
End Function ' GetTimestampForFilename$

' /////////////////////////////////////////////////////////////////////////////
' IIF function for QB for integers

Function IIF (Condition, IfTrue, IfFalse)
    If Condition Then IIF = IfTrue Else IIF = IfFalse
End Function

' /////////////////////////////////////////////////////////////////////////////
' IIF function for QB for strings

Function IIFSTR$ (Condition, IfTrue$, IfFalse$)
    If Condition Then IIFSTR$ = IfTrue$ Else IIFSTR$ = IfFalse$
End Function

' /////////////////////////////////////////////////////////////////////////////

Function IntPadLeft$ (iValue As Integer, iWidth As Integer)
    IntPadLeft$ = Right$(String$(iWidth, " ") + _Trim$(Str$(iValue)), iWidth)
End Function ' IntPadLeft$

' /////////////////////////////////////////////////////////////////////////////

Function IntPadRight$ (iValue As Integer, iWidth As Integer)
    IntPadRight$ = Left$(_Trim$(Str$(iValue)) + String$(iWidth, " "), iWidth)
End Function ' IntPadRight$

' /////////////////////////////////////////////////////////////////////////////
' Returns _TRUE if number n is even
' https://slaystudy.com/qbasic-program-to-check-if-number-is-even-or-odd/
' see also: IsOdd%

Function IsEven% (n)
    If n Mod 2 = 0 Then
        IsEven% = _TRUE
    Else
        IsEven% = _FALSE
    End If
End Function ' IsEven%

' /////////////////////////////////////////////////////////////////////////////
' Returns _TRUE if value text$ is numeric.

Function IsNum% (text$)
    IsNum% = IsNumber%(text$)
End Function ' IsNum%

'' NOTE: THIS FAILS FOR NUMBERS LIKE "002" AND "2.000":
'' from https://www.qb64.org/forum/index.php?topic=896.0
'Function IsNum% (text$)
'    Dim a$
'    Dim b$
'    a$ = _Trim$(text$)
'    b$ = _Trim$(Str$(Val(text$)))
'    If a$ = b$ Then
'        IsNum% = _TRUE
'    Else
'        IsNum% = _FALSE
'    End If
'End Function ' IsNum%

' /////////////////////////////////////////////////////////////////////////////
' Returns _TRUE if value OriginalString$ is numeric.

' Re: Does a Is Number function exist in QB64?
' https://www.qb64.org/forum/index.php?topic=896.15

' Version 2 by madscijr
' Returns _TRUE (-1) if string is an integer, _FALSE (0) if not

' Version 1 by MWheatley
' Reply #18 on: January 01, 2019, 11:24:30 AM
' returns 1 if string is an integer, 0 if not

Function IsNumber% (OriginalString$)
    Dim bResult%: bResult% = _FALSE
    Dim iLoop%
    Dim TestString$
    'Dim bNegative%
    Dim iDecimalCount%
    Dim sNextChar$

    'THEY SHOULD TRIM OUTSIDE THE FUNCTION!
    'TestString$ = _TRIM$(OriginalString$)

    If Len(OriginalString$) > 0 Then
        TestString$ = ""
        If Left$(OriginalString$, 1) = "+" Then
            TestString$ = Right$(OriginalString$, Len(OriginalString$) - 1)
            'bNegative% = _FALSE
        ElseIf Left$(OriginalString$, 1) = "-" Then
            TestString$ = Right$(OriginalString$, Len(OriginalString$) - 1)
            'bNegative% = _TRUE
        Else
            TestString$ = OriginalString$
            'bNegative% = _FALSE
        End If
        If Len(TestString$) > 0 Then
            bResult% = _TRUE
            iDecimalCount% = 0
            For iLoop% = 1 To Len(TestString$)
                sNextChar$ = Mid$(TestString$, iLoop%, 1)
                If sNextChar$ = "." Then
                    iDecimalCount% = iDecimalCount% + 1
                    If iDecimalCount% > 1 Then
                        ' TOO MANY DECIMAL POINTS, INVALID!
                        bResult% = _FALSE
                        Exit For
                    End If
                ElseIf Asc(sNextChar$) < 48 Or Asc(sNextChar$) > 57 Then
                    ' NOT A NUMERAL OR A DECIMAL, INVALID!
                    bResult% = _FALSE
                    Exit For
                End If
            Next iLoop%
        End If
    End If
    IsNumber% = bResult%
End Function ' IsNumber%

' /////////////////////////////////////////////////////////////////////////////

'Sub IsNumberTest
'    Dim in$
'    Cls
'    IsNumberTest1 "1"
'    IsNumberTest1 "01"
'    IsNumberTest1 "001"
'    IsNumberTest1 "-1"
'    IsNumberTest1 "-01"
'    IsNumberTest1 "-001"
'    IsNumberTest1 "+1"
'    IsNumberTest1 "+01"
'    IsNumberTest1 "+001"
'    IsNumberTest1 ".1"
'    IsNumberTest1 ".01"
'    IsNumberTest1 ".001"
'    IsNumberTest1 ".10"
'    IsNumberTest1 ".100"
'    IsNumberTest1 "..100"
'    IsNumberTest1 "100."
'    Input "PRESS ENTER TO CONTINUE TEST";in$
'    Cls
'    IsNumberTest1 "0.10"
'    IsNumberTest1 "00.100"
'    IsNumberTest1 "000.1000"
'    IsNumberTest1 "000..1000"
'    IsNumberTest1 "000.1000.00"
'    IsNumberTest1 "+1.00"
'    IsNumberTest1 "++1.00"
'    IsNumberTest1 "+-1.00"
'    IsNumberTest1 "-1.00"
'    IsNumberTest1 "-+1.00"
'    IsNumberTest1 " 1"
'    IsNumberTest1 "1 "
'    IsNumberTest1 "1. 01"
'    IsNumberTest1 "+1 "
'End Sub ' IsNumberTest
'Sub IsNumberTest1(MyString As String)
'    Const cWidth = 16
'    Dim sInput As String : sInput = left$(Chr$(34) + MyString + Chr$(34) + String$(cWidth, " "), cWidth)
'    Dim sResult As String : sResult = right$(String$(2, " ") + _Trim$(Str$(IsNumber%(MyString))), 2)
'    Print "IsNumber%(" + sInput + ") returns " + sResult
'End Sub ' IsNumberTest1

' /////////////////////////////////////////////////////////////////////////////
' Returns _TRUE if number n is odd
' https://slaystudy.com/qbasic-program-to-check-if-number-is-even-or-odd/
' see also: IsEven%

Function IsOdd% (n)
    If n Mod 2 = 1 Then
        IsOdd% = _TRUE
    Else
        IsOdd% = _FALSE
    End If
End Function ' IsOdd%

' /////////////////////////////////////////////////////////////////////////////
' Split and join strings
' https://www.qb64.org/forum/index.php?topic=1073.0

'Combine all elements of in$() into a single string with delimiter$ separating the elements.

Function join$ (in$(), delimiter$)
    Dim result$
    Dim i As Long
    result$ = in$(LBound(in$))
    For i = LBound(in$) + 1 To UBound(in$)
        result$ = result$ + delimiter$ + in$(i)
    Next i
    join$ = result$
End Function ' join$

' /////////////////////////////////////////////////////////////////////////////

Function LeftPadString$ (myString$, toWidth%, padChar$)
    LeftPadString$ = Right$(String$(toWidth%, padChar$) + myString$, toWidth%)
End Function ' LeftPadString$

' /////////////////////////////////////////////////////////////////////////////
' ABS was returning strange values with type LONG
' so I created this which does not.

Function LongABS& (lngValue As Long)
    If Sgn(lngValue) = -1 Then
        LongABS& = 0 - lngValue
    Else
        LongABS& = lngValue
    End If
End Function ' LongABS&

' /////////////////////////////////////////////////////////////////////////////
' remove scientific Notation to String (~40 LOC)
' SMcNeill Jan 7, 2020
' https://www.qb64.org/forum/index.php?topic=1555.msg112989#msg112989

' Last Function in code marked Best Answer (removed debug comments and
' blank lines added these 2 lines.)

Function N2S$ (EXP$)
    ReDim t$, sign$, l$, r$, r&&
    ReDim dp As Long, dm As Long, ep As Long, em As Long, check1 As Long, l As Long, i As Long
    t$ = LTrim$(RTrim$(EXP$))
    If Left$(t$, 1) = "-" Or Left$(t$, 1) = "N" Then sign$ = "-": t$ = Mid$(t$, 2)
    dp = InStr(t$, "D+"): dm = InStr(t$, "D-")
    ep = InStr(t$, "E+"): em = InStr(t$, "E-")
    check1 = Sgn(dp) + Sgn(dm) + Sgn(ep) + Sgn(em)
    If check1 < 1 Or check1 > 1 Then N2S = _Trim$(EXP$): Exit Function ' If no scientic notation is found, or if we find more than 1 type, it's not SN!
    Select Case l ' l now tells us where the SN starts at.
        Case Is < dp: l = dp
        Case Is < dm: l = dm
        Case Is < ep: l = ep
        Case Is < em: l = em
    End Select
    l$ = Left$(t$, l - 1) ' The left of the SN
    r$ = Mid$(t$, l + 1): r&& = Val(r$) ' The right of the SN, turned into a workable long
    If InStr(l$, ".") Then ' Location of the decimal, if any
        If r&& > 0 Then
            r&& = r&& - Len(l$) + 2
        Else
            r&& = r&& + 1
        End If
        l$ = Left$(l$, 1) + Mid$(l$, 3)
    End If
    Select Case r&&
        Case 0 ' what the heck? We solved it already?
            ' l$ = l$
        Case Is < 0
            For i = 1 To -r&&
                l$ = "0" + l$
            Next
            l$ = "." + l$
        Case Else
            For i = 1 To r&&
                l$ = l$ + "0"
            Next
            l$ = l$
    End Select
    N2S$ = sign$ + l$
End Function ' N2S$

' /////////////////////////////////////////////////////////////////////////////
' Return filename sFile without file extension (anything after last ".")

Function NoExt$ (sFile As String)
    Dim iPos As Integer
    iPos = _InStrRev(sFile, ".")
    If iPos > 0 Then
        If Len(sFile) > 1 Then
            If iPos > 1 Then
                NoExt$ = Left$(sFile, iPos - 1)
            Else
                ' dot is first character, removing it returns blank!
                ' our version will just return the name unchanged
                ' but you can return blank if you prefer
                NoExt$ = sFile
            End If
        Else
            ' file only has one character, the dot, removing it returns blank!
            ' our version will just return the name unchanged
            ' but you can return blank if you prefer
            NoExt$ = sFile
        End If
    Else
        ' no dot found
        ' return the name unchanged
        NoExt$ = sFile
    End If
End Function ' NoExt$

' /////////////////////////////////////////////////////////////////////////////
' Pauses for iDS deciseconds (iDS * 100 ms)

Sub PauseDecisecond (iDS As Integer)
    Dim iCount As Integer
    iCount = 0
    Do
        iCount = iCount + 1
        _Limit 10 ' run 10x every second
    Loop Until iCount = iDS
End Sub ' PauseDecisecond

' /////////////////////////////////////////////////////////////////////////////
' Returns _TRUE if point (x1%, y1%) is adjacent to point (x2%, y2%)

Function PointsAreAdjacent% (x1%, y1%, x2%, y2%)
    Dim bResult%: bResult% = _FALSE

    ' x or y can be the same, but not both
    If (x1% <> x2%) Or (y1% <> y2%) Then
        If (x1% = x2%) Or ((x1% = (x2% + 1)) Or (x2% = (x1% + 1))) Then
            If (y1% = y2%) Or ((y1% = (y2% + 1)) Or (y2% = (y1% + 1))) Then
                bResult% = _TRUE
            End If
        End If
    End If
    PointsAreAdjacent% = bResult%
End Function ' PointsAreAdjacent%

' /////////////////////////////////////////////////////////////////////////////
' Does a _PrintString at the specified row+column.
'
' iRow% and iCol% are 0-based in this version
'
' See also: PrintString, PrintString1, PutCharXY

Sub PrintAt (iRow%, iCol%, sText$)
    '_PrintString (iCol% * 8, iRow% * 16), sText$
    _PrintString (iCol% * 8, iRow% * 16), sText$
    '_PrintString (iCol%, iRow%), sText$
End Sub ' PrintAt

' /////////////////////////////////////////////////////////////////////////////
' Writes sText to file sFileName.
' If bAppend=_TRUE appends to file, else overwrites it.

' Returns blank if successful else returns error message.

' Example:
' ProgramPath$ = Left$(Command$(0), _InStrRev(Command$(0), "\"))
' ProgramName$: m_ProgramName$ = Mid$(Command$(0), _InStrRev(Command$(0), "\") + 1)
' sFileName = ProgramPath$ + ProgramName$ + ".OUT.txt"
' sText = "This is a test." + chr$(13) + "Here is line 2." + chr$(13) + "End."
' sError = PrintFile$(sFileName, sText, _FALSE)

Function PrintFile$ (sFileName As String, sText As String, bAppend As Integer)
    Dim sError As String: sError = ""

    If Len(sError) = 0 Then
        If (bAppend = _TRUE) Then
            If _FileExists(sFileName) Then
                Open sFileName For Append As #1 ' opens an existing file for appending
            Else
                sError = "Error in PrintFile$ : File not found. Cannot append."
            End If
        Else
            Open sFileName For Output As #1 ' opens and clears an existing file or creates new empty file
        End If
    End If
    If Len(sError) = 0 Then
        ' NOTE: WRITE places text in quotes in the file
        'WRITE #1, x, y, z$
        'WRITE #1, sText

        ' PRINT does not put text inside quotes
        Print #1, sText

        Close #1
    End If

    PrintFile$ = sError
End Function ' PrintFile$

' /////////////////////////////////////////////////////////////////////////////
' Does a _PrintString at the specified row+column.
' iRow and iCol are 0-based.
' See also: PrintString1

Sub PrintString (iRow As Integer, iCol As Integer, MyString As String)
    Dim iX As Integer
    Dim iY As Integer
    iX = _FontWidth * iCol
    iY = _FontHeight * iRow ' (iRow + 1)
    _PrintString (iX, iY), MyString
End Sub ' PrintString

' /////////////////////////////////////////////////////////////////////////////
' Does a _PrintString at the specified row+column.
' iRow and iCol are 1-based.
' See also: PrintString

Sub PrintString1 (iRow As Integer, iCol As Integer, MyString As String)
    Dim iX As Integer
    Dim iY As Integer
    iX = _FontWidth * (iCol - 1)
    iY = _FontHeight * (iRow - 1)
    _PrintString (iX, iY), MyString
End Sub ' PrintString1

' /////////////////////////////////////////////////////////////////////////////
' iNum% = PromptForIntegerInRange%("Please type a number between {min} and {max} (or blank to quit).", 1, 4, 0) ' prompt, min, max, default

Function PromptForIntegerInRange% (sPrompt$, iMin%, iMax%, iDefault%)
    Dim iValue%
    Dim bFinished%
    Dim sPrompt1$
    Dim in$

    If Len(sPrompt$) > 0 Then
        sPrompt1$ = sPrompt$
    Else
        sPrompt1$ = "Please type a number between {min} and {max} (or blank to quit)."
    End If

    sPrompt1$ = Replace$(sPrompt1$, "{min}", cstr$(iMin%))
    sPrompt1$ = Replace$(sPrompt1$, "{max}", cstr$(iMax%))

    bFinished% = _FALSE
    Do
        Print sPrompt1$

        Input in$
        in$ = _Trim$(in$)
        If Len(in$) > 0 Then
            If IsNumber(in$) Then
                iValue% = Val(in$)
                If iValue% >= iMin% And iValue% <= iMax% Then
                    'bFinished% = _TRUE
                    Exit Do
                Else
                    Print "Number out of range."
                    Print
                End If
            Else
                Print "Not a valid number."
                Print
            End If
        Else
            iValue% = iDefault%
            Exit Do
            'bFinished% = _TRUE
        End If
    Loop Until bFinished% = _TRUE

    PromptForIntegerInRange% = iValue%
End Function ' PromptForIntegerInRange%

' /////////////////////////////////////////////////////////////////////////////
' Prints text character char$ at positoin x%,y% in color myColor&.

Sub PutCharXY (x%, y%, char$, myColor&)
    Color myColor&
    Locate y%, x%
    Print char$;
End Sub ' PutCharXY

' /////////////////////////////////////////////////////////////////////////////
' Initializes random number generator seed

' NOTE:
' using # of seconds so far in the day
' may be slightly more random than Randomize Timer
' unless user runs program at exact same time every day

Sub InitializeRandom
    Dim iSeed As Integer
   
    'iSeed = GetTimeSeconds& MOD 32767
   
    t9# = (Timer * 1000000) Mod 32767
   
    Randomize iSeed
    'print "Randomize " + cstr$(iSeed)
    'Sleep
End Sub ' InitializeRandom

' /////////////////////////////////////////////////////////////////////////////
' Generate random value between Min and Max inclusive.

' Before using, random-number generator should be initialized with
' Randomize Timer

Function RandomNumber% (Min%, Max%)
    Dim NumSpread%
    NumSpread% = (Max% - Min%) + 1
    RandomNumber% = Int(Rnd * NumSpread%) + Min% ' GET RANDOM # BETWEEN Max% AND Min%
End Function ' RandomNumber%

' /////////////////////////////////////////////////////////////////////////////

$If  Then
    Sub RandomNumberTest
    Dim iCols As Integer: iCols = 10
    Dim iRows As Integer: iRows = 20
    Dim iLoop As Integer
    Dim iX As Integer
    Dim iY As Integer
    Dim sError As String
    Dim sFileName As String
    Dim sText As String
    Dim bAppend As Integer
    Dim iMin As Integer
    Dim iMax As Integer
    Dim iNum As Integer
    Dim iErrorCount As Integer
    Dim sInput$

    sFileName = "c:\temp\maze_test_1.txt"
    sText = "Count" + Chr$(9) + "Min" + Chr$(9) + "Max" + Chr$(9) + "Random"
    bAppend = _FALSE
    sError = PrintFile$(sFileName, sText, bAppend)
    If Len(sError) = 0 Then
    bAppend = _TRUE
    iErrorCount = 0

    iMin = 0
    iMax = iCols - 1
    For iLoop = 1 To 100
    iNum = RandomNumber%(iMin, iMax)
    sText = Str$(iLoop) + Chr$(9) + Str$(iMin) + Chr$(9) + Str$(iMax) + Chr$(9) + Str$(iNum)
    sError = PrintFile$(sFileName, sText, bAppend)
    If Len(sError) > 0 Then
    iErrorCount = iErrorCount + 1
    Print Str$(iLoop) + ". ERROR"
    Print "    " + "iMin=" + Str$(iMin)
    Print "    " + "iMax=" + Str$(iMax)
    Print "    " + "iNum=" + Str$(iNum)
    Print "    " + "Could not write to file " + Chr$(34) + sFileName + Chr$(34) + "."
    Print "    " + sError
    End If
    Next iLoop

    iMin = 0
    iMax = iRows - 1
    For iLoop = 1 To 100
    iNum = RandomNumber%(iMin, iMax)
    sText = Str$(iLoop) + Chr$(9) + Str$(iMin) + Chr$(9) + Str$(iMax) + Chr$(9) + Str$(iNum)
    sError = PrintFile$(sFileName, sText, bAppend)
    If Len(sError) > 0 Then
    iErrorCount = iErrorCount + 1
    Print Str$(iLoop) + ". ERROR"
    Print "    " + "iMin=" + Str$(iMin)
    Print "    " + "iMax=" + Str$(iMax)
    Print "    " + "iNum=" + Str$(iNum)
    Print "    " + "Could not write to file " + Chr$(34) + sFileName + Chr$(34) + "."
    Print "    " + sError
    End If
    Next iLoop

    Print "Finished generating numbers. Errors: " + Str$(iErrorCount)
    Else
    Print "Error creating file " + Chr$(34) + sFileName + Chr$(34) + "."
    Print sError
    End If

    Input "Press <ENTER> to continue", sInput$
    End Sub ' RandomNumberTest
$End If

' /////////////////////////////////////////////////////////////////////////////
' FROM: String Manipulation
' found at abandoned, outdated and now likely malicious qb64 dot net website
' http://www.qb64.[net]/forum/index_topic_5964-0/
'
'SUMMARY:
'   Purpose:  A library of custom functions that transform strings.
'   Author:   Dustinian Camburides (dustinian@gmail.com)
'   Platform: QB64 (www.qb64.org)
'   Revision: 1.6
'   Updated:  5/28/2012

'SUMMARY:
'[Replace$] replaces all instances of the [Find] sub-string with the [Add] sub-string within the [Text] string.
'INPUT:
'Text: The input string; the text that's being manipulated.
'Find: The specified sub-string; the string sought within the [Text] string.
'Add: The sub-string that's being added to the [Text] string.

Function Replace$ (Text1 As String, Find1 As String, Add1 As String)
    ' VARIABLES:
    Dim Text2 As String
    Dim Find2 As String
    Dim Add2 As String
    Dim lngLocation As Long ' The address of the [Find] substring within the [Text] string.
    Dim strBefore As String ' The characters before the string to be replaced.
    Dim strAfter As String ' The characters after the string to be replaced.

    ' INITIALIZE:
    ' MAKE COPIESSO THE ORIGINAL IS NOT MODIFIED (LIKE ByVal IN VBA)
    Text2 = Text1
    Find2 = Find1
    Add2 = Add1

    lngLocation = InStr(1, Text2, Find2)

    ' PROCESSING:
    ' While [Find2] appears in [Text2]...
    While lngLocation
        ' Extract all Text2 before the [Find2] substring:
        strBefore = Left$(Text2, lngLocation - 1)

        ' Extract all text after the [Find2] substring:
        strAfter = Right$(Text2, ((Len(Text2) - (lngLocation + Len(Find2) - 1))))

        ' Return the substring:
        Text2 = strBefore + Add2 + strAfter

        ' Locate the next instance of [Find2]:
        lngLocation = InStr(1, Text2, Find2)

        ' Next instance of [Find2]...
    Wend

    ' OUTPUT:
    Replace$ = Text2
End Function ' Replace$

' /////////////////////////////////////////////////////////////////////////////

$If  Then
    Sub ReplaceTest
    Dim in$

    Print "-------------------------------------------------------------------------------"
    Print "ReplaceTest"
    Print

    Print "Original value"
    in$ = "Thiz iz a teZt."
    Print "in$ = " + Chr$(34) + in$ + Chr$(34)
    Print

    Print "Replacing lowercase " + Chr$(34) + "z" + Chr$(34) + " with " + Chr$(34) + "s" + Chr$(34) + "..."
    in$ = Replace$(in$, "z", "s")
    Print "in$ = " + Chr$(34) + in$ + Chr$(34)
    Print

    Print "Replacing uppercase " + Chr$(34) + "Z" + Chr$(34) + " with " + Chr$(34) + "s" + Chr$(34) + "..."
    in$ = Replace$(in$, "Z", "s")
    Print "in$ = " + Chr$(34) + in$ + Chr$(34)
    Print

    Print "ReplaceTest finished."
    End Sub ' ReplaceTest
$End If

' /////////////////////////////////////////////////////////////////////////////

Function RightPadString$ (myString$, toWidth%, padChar$)
    RightPadString$ = Left$(myString$ + String$(toWidth%, padChar$), toWidth%)
End Function ' RightPadString$

' /////////////////////////////////////////////////////////////////////////////
' This sub gives really nice control over displaying an Image.
' by BPlus

' Scale rotate font text strings by B+
' https://qb64phoenix.com/forum/showthread.php?tid=414&highlight=rotate+text

' USED BY: drwString

Sub RotoZoom2 (centerX As Long, centerY As Long, Image As Long, xScale As Single, yScale As Single, Rotation As Single)
    Dim px(3) As Single: Dim py(3) As Single
    W& = _Width(Image&): H& = _Height(Image&)
    px(0) = -W& / 2: py(0) = -H& / 2: px(1) = -W& / 2: py(1) = H& / 2
    px(2) = W& / 2: py(2) = H& / 2: px(3) = W& / 2: py(3) = -H& / 2
    sinr! = Sin(-Rotation): cosr! = Cos(-Rotation)
    For i& = 0 To 3
        x2& = (px(i&) * cosr! + sinr! * py(i&)) * xScale + centerX: y2& = (py(i&) * cosr! - px(i&) * sinr!) * yScale + centerY
        px(i&) = x2&: py(i&) = y2&
    Next
    _MapTriangle (0, 0)-(0, H& - 1)-(W& - 1, H& - 1), Image& To(px(0), py(0))-(px(1), py(1))-(px(2), py(2))
    _MapTriangle (0, 0)-(W& - 1, 0)-(W& - 1, H& - 1), Image& To(px(0), py(0))-(px(3), py(3))-(px(2), py(2))
End Sub ' RotoZoom2

'' /////////////////////////////////////////////////////////////////////////////
'' https://qb64phoenix.com/forum/showthread.php?tid=644
'' From: bplus
'' Date: 07-18-2022, 03:16 PM
'' Here is a Round$ that acts the way you'd expect in under 100 LOC
'' b = b + ...
'
'Function Round$ (anyNumber, dp As Long)
'    ' 5 and up at decimal place dp+1 > +1 at decimal place   4 and down  > +0 at dp
'    ' 2 1 0.-1 -2 -3 -4 ...  pick dp like this for this Round$ Function
'    sn$ = N2S$(Str$(anyNumber + .5 * 10 ^ dp)) ' get rid of sci notation, steve trims it so next find dot
'    dot = InStr(sn$, ".")
'    If dot Then
'        predot = dot - 1
'        postdot = Len(sn$) - (dot + 1)
'    Else
'        predot = Len(sn$)
'        postdot = 0
'    End If
'    ' xxx.yyyyyy  dp = -2
'    '      ^ dp
'    If dp >= 0 Then
'        Rtn$ = Mid$(sn$, 1, predot - dp) + String$(dp, "0")
'    Else
'        Rtn$ = Mid$(sn$, 1, predot) + "." + Mid$(sn$, dot + 1, -dp)
'    End If
'    If Rtn$ = "" Then
'        Round$ = "0"
'    Else
'        Round$ = Rtn$
'    End If
'End Function ' Round$
'
'' /////////////////////////////////////////////////////////////////////////////
''
''Sub RoundTest
''   Print Round$(.15, 0) '  0
''   Print Round$(.15, -1) ' .2
''   Print Round$(.15, -2) ' .15
''   Print Round$(.15, -3) ' .150
''   Print
''   Print Round$(3555, 0) ' 3555
''   Print Round$(3555, 1) ' 3560
''   Print Round$(3555, 2) ' 3600 'good
''   Print Round$(3555, 3) ' 4000
''   Print
''   Print Round$(23.149999, -1) ' 23.1
''   Print Round$(23.149999, -2) ' 23.15
''   Print Round$(23.149999, -3) ' 23.150
''   Print Round$(23.149999, -4) ' 23.1500
''   Print
''   Print Round$(23.143335, -1) ' 23.1 OK?
''   Print Round$(23.143335, -2) ' 23.14
''   Print Round$(23.143335, -3) ' 23.143
''   Print Round$(23.143335, -4) ' 23.1433
''   Print Round$(23.143335, -5) ' 23.14334
''   Print
''   Dim float31 As _Float
''   float31 = .310000000000009
''   Print Round$(.31, -2) ' .31
''   Print Round$(.31##, -2)
''   Print Round$(float31, -2)
''End Sub ' RoundTest

' /////////////////////////////////////////////////////////////////////////////
' https://www.qb64.org/forum/index.php?topic=3605.0
' Quote from: SMcNeill on Today at 03:53:48 PM
'
' Sometimes, you guys make things entirely too  complicated.
' There ya go!  Three functions to either round naturally,
' always round down, or always round up, to whatever number of digits you desire.
' EDIT:  Modified to add another option to round scientific,
' since you had it's description included in your example.

' Receives + returns _FLOAT myVar## (-1.18E-4932 to +1.18E+4932)

' =============================================================================
' ROUNDING FUNCTIONS FOR TYPE _FLOAT

' old name: RoundNatural##
Function Round## (num##, digits%)
    Round## = Int(num## * 10 ^ digits% + .5) / 10 ^ digits%
End Function

Function RoundUp## (num##, digits%)
    RoundUp## = _Ceil(num## * 10 ^ digits%) / 10 ^ digits%
End Function

Function RoundDown## (num##, digits%)
    RoundDown## = Int(num## * 10 ^ digits%) / 10 ^ digits%
End Function

' old name: Round_Scientific##
Function RoundScientific## (num##, digits%)
    RoundScientific## = _Round(num## * 10 ^ digits%) / 10 ^ digits%
End Function

' =============================================================================
' ROUNDING FUNCTIONS FOR TYPE DOUBLE

Function RoundDouble# (num#, digits%)
    RoundDouble# = Int(num# * 10 ^ digits% + .5) / 10 ^ digits%
End Function

Function RoundUpDouble# (num#, digits%)
    RoundUpDouble# = _Ceil(num# * 10 ^ digits%) / 10 ^ digits%
End Function

Function RoundDownDouble# (num#, digits%)
    RoundDownDouble# = Int(num# * 10 ^ digits%) / 10 ^ digits%
End Function

Function RoundScientificDouble# (num#, digits%)
    RoundScientificDouble# = _Round(num# * 10 ^ digits%) / 10 ^ digits%
End Function

' =============================================================================
' ROUNDING FUNCTIONS FOR TYPE SINGLE

Function RoundSingle! (num!, digits%)
    RoundSingle! = Int(num! * 10 ^ digits% + .5) / 10 ^ digits%
End Function

' NOTE: not sure this one works: when digits%=3, it rounds .31 to .32
Function RoundUpSingle! (num!, digits%)
    RoundUpSingle! = _Ceil(num! * 10 ^ digits%) / 10 ^ digits%
End Function

Function RoundDownSingle! (num!, digits%)
    RoundDownSingle! = Int(num! * 10 ^ digits%) / 10 ^ digits%
End Function

Function RoundScientificSingle! (num!, digits%)
    RoundScientificSingle! = _Round(num! * 10 ^ digits%) / 10 ^ digits%
End Function

' /////////////////////////////////////////////////////////////////////////////
' fantastically inefficient way to set a bit

' example use: arrMaze(iX, iY) = SetBit256%(arrMaze(iX, iY), cS, _FALSE)

' See also: GetBit256%, SetBit256%

' newint=SetBit256%(oldint, int containing the bits we want to set, value to set them to)
Function SetBit256% (iNum1 As Integer, iBit1 As Integer, bVal1 As Integer)
    Dim sNum As String
    Dim sBit As String
    Dim sVal As String
    Dim iLoop As Integer
    Dim strResult As String
    Dim iResult As Integer
    Dim iNum As Integer: iNum = iNum1
    Dim iBit As Integer: iBit = iBit1
    Dim bVal As Integer: bVal = bVal1

    If iNum < 256 And iBit <= 128 Then
        sNum = GetBinary$(iNum)
        sBit = GetBinary$(iBit)
        If bVal = _TRUE Then
            sVal = "1"
        Else
            sVal = "0"
        End If
        strResult = ""
        For iLoop = 1 To 8
            If Mid$(sBit, iLoop, 1) = "1" Then
                strResult = strResult + sVal
            Else
                strResult = strResult + Mid$(sNum, iLoop, 1)
            End If
        Next iLoop
        iResult = GetIntegerFromBinary%(strResult)
    Else
        iResult = iNum
    End If

    SetBit256% = iResult
End Function ' SetBit256%

' /////////////////////////////////////////////////////////////////////////////

$If  Then
    Sub ShowDegreesAndRadians
    Dim iDegree As Integer
    Dim sngRadian As Single

    DebugPrint "Degree   Radian"
    DebugPrint "------   ------"
    For iDegree = 0 To 360
    sngRadian = _D2R(iDegree)

    'DebugPrint LeftPadString$(cstr$(iDegree), 3, " ") + "   " + LeftPadString$(cstr$(iRadian), 3, " ")

    DebugPrint LeftPadString$(cstr$(iDegree), 3, " ") + "   " + SngToStr$(sngRadian)

    'Print "SngToStr$(MyValue)           =" + SngToStr$(MyValue)
    'Print "SngRoundedToStr$(MyValue, 12)=" + SngRoundedToStr$(MyValue, 12)

    Next iDegree
    End Sub ' ShowDegreesAndRadians
$End If

' /////////////////////////////////////////////////////////////////////////////
' use angles in degrees units instead of radians (converted inside sub)
' Note this function uses whatever the default type is, better not be some Integer Type.

Function SinD (degrees)
    SinD = Sin(_D2R(degrees))
End Function ' SinD

' /////////////////////////////////////////////////////////////////////////////

Function SmallestOf3% (i1%, i2%, i3%)
    Dim iMin%
    iMin% = i1%
    If i2% < iMin% Then iMin% = i2%
    If i3% < iMin% Then iMin% = i3%
    SmallestOf3% = iMin%
End Function ' SmallestOf3

' /////////////////////////////////////////////////////////////////////////////

Function SngRoundedToStr$ (sngValue As Single, intNumPlaces As Integer)
    Dim sngNew As Single
    sngNew = RoundSingle!(sngValue, intNumPlaces)
    SngRoundedToStr$ = SngToStr$(sngNew)
End Function ' SngRoundedToStr$

' /////////////////////////////////////////////////////////////////////////////
' Hack function, to be replaced with something better

Function SngToInt% (sngOld As Single)
    Dim sngNew As Single
    Dim sValue As String
    Dim iPos As Integer

    sngNew = RoundSingle!(sngOld, 0)
    'sValue = _Trim$(Str$(sngNew))

    sValue = SngToStr$(sngNew)

    'iPos = InStr(1, sValue, ".")
    'If iPos > 0 Then
    '    SngToInt% = Val(Left$(sValue, iPos - 1))
    'Else
    '    SngToInt% = Val(sValue)
    'End If

    SngToInt% = Val(sValue)
End Function ' SngToInt%

' /////////////////////////////////////////////////////////////////////////////
' Scientific notation - QB64 Wiki
' https://www.qb64.org/wiki/Scientific_notation

' Example: A string function that displays extremely small or large exponential decimal values.

Function SngToStr$ (n!)
    Dim result$: result$ = ""
    Dim value$
    Dim Xpos%
    Dim expo%
    Dim sign$
    Dim valu$
    Dim dot%
    Dim L%
    Dim add$
    Dim min$
    Dim DP$
    Dim n%

    value$ = UCase$(LTrim$(Str$(n!)))
    Xpos% = InStr(value$, "D") + InStr(value$, "E") ' only D or E can be present
    If Xpos% Then
        expo% = Val(Mid$(value$, Xpos% + 1))
        If Val(value$) < 0 Then
            sign$ = "-"
            valu$ = Mid$(value$, 2, Xpos% - 2)
        Else
            valu$ = Mid$(value$, 1, Xpos% - 1)
        End If
        dot% = InStr(valu$, ".")
        L% = Len(valu$)
        If expo% > 0 Then
            add$ = String$(expo% - (L% - dot%), "0")
        End If
        If expo% < 0 Then
            min$ = String$(Abs(expo%) - (dot% - 1), "0")
            DP$ = "."
        End If
        For n% = 1 To L%
            If Mid$(valu$, n%, 1) <> "." Then
                num$ = num$ + Mid$(valu$, n%, 1)
            End If
        Next n%
        result$ = _Trim$(sign$ + DP$ + min$ + num$ + add$)
    Else
        result$ = value$
    End If

    SngToStr$ = result$
End Function ' SngToStr$

' /////////////////////////////////////////////////////////////////////////////
' Split and join strings
' https://www.qb64.org/forum/index.php?topic=1073.0
'
' FROM luke, QB64 Developer
' Date: February 15, 2019, 04:11:07 AM
'
' Given a string of words separated by spaces (or any other character),
' splits it into an array of the words. I've no doubt many people have
' written a version of this over the years and no doubt there's a million
' ways to do it, but I thought I'd put mine here so we have at least one
' version. There's also a join function that does the opposite
' array -> single string.
'
' Code is hopefully reasonably self explanatory with comments and a little demo.
' Note, this is akin to Python/JavaScript split/join, PHP explode/implode.

'Split in$ into pieces, chopping at every occurrence of delimiter$. Multiple consecutive occurrences
'of delimiter$ are treated as a single instance. The chopped pieces are stored in result$().
'
'delimiter$ must be one character long.
'result$() must have been REDIMmed previously.

' Modified to handle multi-character delimiters

Sub split (in$, delimiter$, result$())
    Dim start As Integer
    Dim finish As Integer
    Dim iDelimLen As Integer
    ReDim result$(-1)

    iDelimLen = Len(delimiter$)

    start = 1
    Do
        'While Mid$(in$, start, 1) = delimiter$
        While Mid$(in$, start, iDelimLen) = delimiter$
            'start = start + 1
            start = start + iDelimLen
            If start > Len(in$) Then
                Exit Sub
            End If
        Wend
        finish = InStr(start, in$, delimiter$)
        If finish = 0 Then
            finish = Len(in$) + 1
        End If

        ReDim _Preserve result$(0 To UBound(result$) + 1)

        result$(UBound(result$)) = Mid$(in$, start, finish - start)
        start = finish + 1
    Loop While start <= Len(in$)
End Sub ' split

' /////////////////////////////////////////////////////////////////////////////

$If  Then
    Sub SplitTest
    Dim in$
    Dim delim$
    ReDim arrTest$(0)
    Dim iLoop%

    delim$ = Chr$(10)
    in$ = "this" + delim$ + "is" + delim$ + "a" + delim$ + "test"
    Print "in$ = " + Chr$(34) + in$ + Chr$(34)
    Print "delim$ = " + Chr$(34) + delim$ + Chr$(34)
    split in$, delim$, arrTest$()

    For iLoop% = LBound(arrTest$) To UBound(arrTest$)
    Print "arrTest$(" + LTrim$(RTrim$(Str$(iLoop%))) + ") = " + Chr$(34) + arrTest$(iLoop%) + Chr$(34)
    Next iLoop%
    Print
    Print "Split test finished."
    End Sub ' SplitTest
$End If

' /////////////////////////////////////////////////////////////////////////////

$If  Then
    Sub SplitAndReplaceTest
    Dim in$
    Dim out$
    Dim iLoop%
    ReDim arrTest$(0)

    Print "-------------------------------------------------------------------------------"
    Print "SplitAndReplaceTest"
    Print

    Print "Original value"
    in$ = "This line 1 " + Chr$(13) + Chr$(10) + "and line 2" + Chr$(10) + "and line 3 " + Chr$(13) + "finally THE END."
    out$ = in$
    out$ = Replace$(out$, Chr$(13), "\r")
    out$ = Replace$(out$, Chr$(10), "\n")
    out$ = Replace$(out$, Chr$(9), "\t")
    Print "in$ = " + Chr$(34) + out$ + Chr$(34)
    Print

    Print "Fixing linebreaks..."
    in$ = Replace$(in$, Chr$(13) + Chr$(10), Chr$(13))
    in$ = Replace$(in$, Chr$(10), Chr$(13))
    out$ = in$
    out$ = Replace$(out$, Chr$(13), "\r")
    out$ = Replace$(out$, Chr$(10), "\n")
    out$ = Replace$(out$, Chr$(9), "\t")
    Print "in$ = " + Chr$(34) + out$ + Chr$(34)
    Print

    Print "Splitting up..."
    split in$, Chr$(13), arrTest$()

    For iLoop% = LBound(arrTest$) To UBound(arrTest$)
    out$ = arrTest$(iLoop%)
    out$ = Replace$(out$, Chr$(13), "\r")
    out$ = Replace$(out$, Chr$(10), "\n")
    out$ = Replace$(out$, Chr$(9), "\t")
    Print "arrTest$(" + cstr$(iLoop%) + ") = " + Chr$(34) + out$ + Chr$(34)
    Next iLoop%
    Print

    Print "SplitAndReplaceTest finished."
    End Sub ' SplitAndReplaceTest
$End If

' /////////////////////////////////////////////////////////////////////////////
' Converts a chr$(13) delimited string
' into a 2-dimensional array.

' Usage:
' Dim StringArray(1 To 48, 1 To 128) As String
' StringTo2dArray StringArray(), GetMap$

' Version 2 with indexed array(row, columm)
' Renamed StringToArray to StringTo2dArray.

' See also: Array2dToString$

Sub StringTo2dArray (MyArray() As String, MyString As String)
    Dim sDelim As String
    ReDim arrLines(0) As String
    Dim iRow As Integer
    Dim iCol As Integer
    Dim sChar As String
    Dim iDim1 As Integer
    Dim iDim2 As Integer
    Dim iIndex1 As Integer
    Dim iIndex2 As Integer

    iDim1 = LBound(MyArray, 1)
    iDim2 = LBound(MyArray, 2)
    sDelim = Chr$(13)
    split MyString, sDelim, arrLines()
    For iRow = LBound(arrLines) To UBound(arrLines)
        If iRow <= UBound(MyArray, 1) Then
            For iCol = 1 To Len(arrLines(iRow))
                If iCol <= UBound(MyArray, 2) Then
                    sChar = Mid$(arrLines(iRow), iCol, 1)

                    If Len(sChar) > 1 Then
                        sChar = Left$(sChar, 1)
                    Else
                        If Len(sChar) = 0 Then
                            sChar = "."
                        End If
                    End If

                    iIndex1 = iRow + iDim1
                    iIndex2 = (iCol - 1) + iDim2
                    MyArray(iIndex1, iIndex2) = sChar
                    'DebugPrint "MyArray(" + cstr$(iIndex1) + ", " + cstr$(iIndex2) + " = " + chr$(34) + sChar + chr$(34)
                Else
                    ' Exit if out of bounds
                    Exit For
                End If
            Next iCol
        Else
            ' Exit if out of bounds
            Exit For
        End If
    Next iRow
End Sub ' StringTo2dArray

' /////////////////////////////////////////////////////////////////////////////

Function StrPadLeft$ (sValue As String, iWidth As Integer)
    StrPadLeft$ = Right$(String$(iWidth, " ") + sValue, iWidth)
End Function ' StrPadLeft$

' /////////////////////////////////////////////////////////////////////////////

Function StrJustifyRight$ (sValue As String, iWidth As Integer)
    StrJustifyRight$ = Right$(String$(iWidth, " ") + sValue, iWidth)
End Function ' StrJustifyRight$

' /////////////////////////////////////////////////////////////////////////////

Function StrPadRight$ (sValue As String, iWidth As Integer)
    StrPadRight$ = Left$(sValue + String$(iWidth, " "), iWidth)
End Function ' StrPadRight$

' /////////////////////////////////////////////////////////////////////////////

Function StrJustifyLeft$ (sValue As String, iWidth As Integer)
    StrJustifyLeft$ = Left$(sValue + String$(iWidth, " "), iWidth)
End Function ' StrJustifyLeft$

' /////////////////////////////////////////////////////////////////////////////
' div: int1% = num1% \ den1%
' mod: rem1% = num1% MOD den1%

Function StrJustifyCenter$ (sValue As String, iWidth As Integer)
    Dim iLen0 As Integer
    Dim iLen1 As Integer
    Dim iLen2 As Integer
    Dim iExtra As Integer

    iLen0 = Len(sValue)
    If iWidth = iLen0 Then
        ' no extra space: return unchanged
        StrJustifyCenter$ = sValue
    ElseIf iWidth > iLen0 Then
        If IsOdd%(iWidth) Then
            iWidth = iWidth - 1
        End If

        ' center
        iExtra = iWidth - iLen0
        iLen1 = iExtra \ 2
        iLen2 = iLen1 + (iExtra Mod 2)
        StrJustifyCenter$ = String$(iLen1, " ") + sValue + String$(iLen2, " ")
    Else
        ' string is too long: truncate
        StrJustifyCenter$ = Left$(sValue, iWidth)
    End If
End Function ' StrJustifyCenter$

' /////////////////////////////////////////////////////////////////////////////
' Use to pretty print _TRUE and _FALSE values.

Function TrueFalse$ (myValue)
    If myValue = _TRUE Then
        TrueFalse$ = "_TRUE"
    Else
        TrueFalse$ = "_FALSE"
    End If
End Function ' TrueFalse$

' /////////////////////////////////////////////////////////////////////////////

' ################################################################################################################################################################
' END GENERAL PURPOSE ROUTINES @GEN
' ################################################################################################################################################################

' ################################################################################################################################################################
' BEGIN BOX DRAWING ROUTINES
' ################################################################################################################################################################

' /////////////////////////////////////////////////////////////////////////////
' DRAW A 2-D BOX (OUTLINE)
' https://www.qb64.org/wiki/LINE

Sub DrawBoxOutline (iX As Integer, iY As Integer, iSize As Integer, fgColor As _Unsigned Long)
    Line (iX, iY)-(iX + (iSize - 1), iY + (iSize - 1)), fgColor, B ' Draw box outline
End Sub ' DrawBoxOutline

' /////////////////////////////////////////////////////////////////////////////
' DRAW A 2-D BOX (SOLID)
' https://www.qb64.org/wiki/LINE

' Renamed DrawBox/DrawBoxLine to DrawSolidBox

Sub DrawBoxSolid (iX As Integer, iY As Integer, iSize As Integer, fgColor As _Unsigned Long)
    Line (iX, iY)-(iX + (iSize - 1), iY + (iSize - 1)), fgColor, BF ' Draw a solid box
End Sub ' DrawBoxSolid

' /////////////////////////////////////////////////////////////////////////////
' DRAW A 2-D RECTANGLE

'DrawRect 0, iX, iY, iSizeW, iSizeH, fgColor, bgColor

Sub DrawRect (img&, iX As Integer, iY As Integer, iSizeW As Integer, iSizeH As Integer, fgColor As _Unsigned Long, bgColor As _Unsigned Long)
    'If img& < -1 Then
    If img& <= 0 Then
        ' Select target image
        _Dest img& ': Cls , cEmpty
       
        ' Draw fill (bgColor)
        If bgColor <> cEmpty Then
            Line (iX, iY)-(iX + (iSizeW - 1), iY + (iSizeH - 1)), bgColor, BF ' Draw a solid rectangle
        End If
       
        ' Draw outline (fgColor)
        If fgColor <> cEmpty Then
            Line (iX, iY)-(iX + (iSizeW - 1), iY + (iSizeH - 1)), fgColor, B ' Draw rectangle outline
        End If
    End If
End Sub ' DrawRect

' /////////////////////////////////////////////////////////////////////////////
' bplus: The Gold Standard is even better than THE QB64 CIRCLE sub in this respect!
' https://forum.qb64.org/index.php?topic=1044.135

' from Steve Gold standard
' Renamed fcirc to DrawCircleSolid

' Not as fast as DrawCircleTopLeft but pretty fast.

' Example:
' Screen _NewImage(800, 600, 32)
' _ScreenMove 250, 60
' For r = 250 To 0 Step -60
'     DrawCircleSolid 400, 300, r, _RGBA(255, 255, 255, 100)
' Next r

'DrawCircle 0, iX, iY, iRadius, iThickness, fgColor, bgColor
Sub DrawCircle (img&, iX As Integer, iY As Integer, iRadius As Integer, iThickness As Integer, fgColor As _Unsigned Long, bgColor As _Unsigned Long)
    Dim iLoop As Integer
    Dim iNextRadius As Integer
    Dim iRadiusError As Integer
    Dim iNextX As Integer
    Dim iNextY As Integer
    If (img& <= 0) And (iThickness > 0) And (fgColor <> cEmpty) Then
        ' Select target image
        _Dest img& ': Cls , cEmpty
       
        ' Draw circle fill
        If bgColor <> cEmpty Then
            iNextRadius = Abs(iRadius)
            iRadiusError = -iNextRadius
            iNextX = iNextRadius
            iNextY = 0
            If iNextRadius = 0 Then
                PSet (iX, iY), bgColor
            Else
                ' Draw the middle span here so we don't draw it twice in the main loop,
                ' which would be a problem with blending turned on.
                Line (iX - iNextX, iY)-(iX + iNextX, iY), bgColor, BF
                While iNextX > iNextY
                    iRadiusError = iRadiusError + iNextY * 2 + 1
                    If iRadiusError >= 0 Then
                        If iNextX <> iNextY + 1 Then
                            Line (iX - iNextY, iY - iNextX)-(iX + iNextY, iY - iNextX), bgColor, BF
                            Line (iX - iNextY, iY + iNextX)-(iX + iNextY, iY + iNextX), bgColor, BF
                        End If
                        iNextX = iNextX - 1
                        iRadiusError = iRadiusError - iNextX * 2
                    End If
                    iNextY = iNextY + 1
                    Line (iX - iNextX, iY - iNextY)-(iX + iNextX, iY - iNextY), bgColor, BF
                    Line (iX - iNextX, iY + iNextY)-(iX + iNextX, iY + iNextY), bgColor, BF
                Wend
            End If
        End If
       
        ' Draw circle outline
        If fgColor <> cEmpty Then
            If iRadius = 0 Then
                PSet (iX, iY), fgColor
            Else
                iNextRadius = iRadius
                For iLoop = 1 To iThickness
                   
                    ' DRAW CIRCLE
                    ' CIRCLE (x, y), radius, color
                    'CIRCLE [[[STEP]]](column, row), radius%, [drawColor%][, startRadian!, stopRadian!] [, aspect!]
                    Circle (iX, iY), iNextRadius, fgColor
                   
                    'Line (iLoop * 8, iHeight * 16)-(iLoop * 8 + 8, _Height), cGray, BF
                    'Circle (iX, iY), 4, cRed
                   
                    iNextRadius = iNextRadius - 1
                    If iNextRadius = 0 Then
                        PSet (iX, iY), fgColor
                        Exit For
                    End If
                Next iLoop
            End If
        End If
       
    End If
End Sub ' DrawCircle

' /////////////////////////////////////////////////////////////////////////////

'DrawCircleOutline 0, iX, iY, iRadius, iThickness, fgColor
Sub DrawCircleOutline (img&, iX As Integer, iY As Integer, iRadius As Integer, iThickness As Integer, fgColor As _Unsigned Long)
    Dim iNextRadius As Integer
    Dim iLoop As Integer
    If (img& <= 0) And (iThickness > 0) And (fgColor <> cEmpty) Then
        ' Select target image
        _Dest img& ': Cls , cEmpty
       
        ' Initialize
        iNextRadius = iRadius
       
        ' Draw circle
        If Radius = 0 Then
            PSet (iX, iY), fgColor
        Else
            For iLoop = 1 To iThickness
               
                ' DRAW CIRCLE
                ' CIRCLE (x, y), radius, color
                'CIRCLE [[[STEP]]](column, row), radius%, [drawColor%][, startRadian!, stopRadian!] [, aspect!]
                Circle (iX, iY), iNextRadius, fgColor
               
                'Line (iLoop * 8, iHeight * 16)-(iLoop * 8 + 8, _Height), cGray, BF
                'Circle (iX, iY), 4, cRed
               
                iNextRadius = iNextRadius - 1
                If iNextRadius = 0 Then
                    PSet (iX, iY), fgColor
                    Exit For
                End If
            Next iLoop
        End If
    End If
End Sub ' DrawCircleOutline

' /////////////////////////////////////////////////////////////////////////////
' DRAW A 2-D RECTANGLE (OUTLINE)

'DrawRectOutline iX, iY, iSizeW, iSizeH, fgColor

Sub DrawRectOutline (iX As Integer, iY As Integer, iSizeW As Integer, iSizeH As Integer, fgColor As _Unsigned Long)
    Line (iX, iY)-(iX + (iSizeW - 1), iY + (iSizeH - 1)), fgColor, B ' Draw rectangle outline
End Sub ' DrawRectOutline

' /////////////////////////////////////////////////////////////////////////////
' DRAW A 2-D RECTANGLE (SOLID)

'DrawRectSolid iX, iY, iSizeW, iSizeH, fgColor

Sub DrawRectSolid (iX As Integer, iY As Integer, iSizeW As Integer, iSizeH As Integer, fgColor As _Unsigned Long)
    Line (iX, iY)-(iX + (iSizeW - 1), iY + (iSizeH - 1)), fgColor, BF ' Draw a solid rectangle
End Sub ' DrawRectSolid

' /////////////////////////////////////////////////////////////////////////////
' DRAW A 2-D BOX (OUTLINE)
' https://www.qb64.org/wiki/LINE

' The style parameter 0-255 doesn't seem to have a solid line?
' For that, use DrawOutlineBox.

' LINE [STEP] [(column1, row1)]-[STEP] (column2, row2), color[, [{B|BF}], style%]
' B creates a box outline with each side parallel to the program screen sides. BF creates a filled box.
' The style% signed INTEGER value sets a dotted pattern to draw the line or rectangle outline.

Sub DrawStyledOutlineBox (iX%, iY%, iSize%, iColor~&, iStyle%)
    Line (iX%, iY%)-(iX% + (iSize% - 1), iY% + (iSize% - 1)), iColor~&, B , iStyle%
End Sub ' DrawStyledOutlineBox

' /////////////////////////////////////////////////////////////////////////////
' DRAW A 2-D BOX (OUTLINE) WITH A SOLID LINE

Sub DrawOutlineBox (iX%, iY%, iSize2%, iColor~&, iWeight2%)
    Dim iFromX%
    Dim iFromY%
    Dim iToX%
    Dim iToY%
    iSize% = iSize2% - 1
    iWeight% = iWeight2% - 1
    If iWeight% = 0 Then
        ' TOP LINE
        iFromX% = iX%
        iFromY% = iY%
        iToX% = iX% + iSize%
        iToY% = iY%
        Line (iFromX%, iFromY%)-(iToX%, iToY%), iColor~&, BF

        ' BOTTOM LINE
        iFromX% = iX%
        iFromY% = iY% + iSize%
        iToX% = iX% + iSize%
        iToY% = iY% + iSize%
        Line (iFromX%, iFromY%)-(iToX%, iToY%), iColor~&, BF

        ' LEFT LINE
        iFromX% = iX%
        iFromY% = iY%
        iToX% = iX%
        iToY% = iY% + iSize%
        Line (iFromX%, iFromY%)-(iToX%, iToY%), iColor~&, BF

        ' RIGHT LINE
        iFromX% = iX% + iSize%
        iFromY% = iY%
        iToX% = iX% + iSize%
        iToY% = iY% + iSize%
        Line (iFromX%, iFromY%)-(iToX%, iToY%), iColor~&, BF
    ElseIf iWeight% > 0 Then
        ' TOP LINE
        For iFromY% = iY% To (iY% + iWeight%)
            iFromX% = iX%
            'iFromY% = iY%
            iToX% = iX% + iSize%
            iToY% = iFromY%
            Line (iFromX%, iFromY%)-(iToX%, iToY%), iColor~&, BF
        Next iFromY%

        ' BOTTOM LINE
        For iFromY% = ((iY% + iSize%) - iWeight%) To (iY% + iSize%)
            iFromX% = iX%
            'iFromY% = iY% + iSize%
            iToX% = iX% + iSize%
            iToY% = iFromY%
            Line (iFromX%, iFromY%)-(iToX%, iToY%), iColor~&, BF
        Next iFromY%

        ' LEFT LINE
        For iFromX% = iX% To (iX% + iWeight%)
            'iFromX% = iX%
            iFromY% = iY%
            iToX% = iFromX%
            iToY% = iY% + iSize%
            Line (iFromX%, iFromY%)-(iToX%, iToY%), iColor~&, BF
        Next iFromX%

        ' RIGHT LINE
        For iFromX% = ((iX% + iSize%) - iWeight%) To (iX% + iSize%)
            'iFromX% = iX% + iSize%
            iFromY% = iY%
            iToX% = iFromX%
            iToY% = iY% + iSize%
            Line (iFromX%, iFromY%)-(iToX%, iToY%), iColor~&, BF
        Next iFromX%
    End If
End Sub ' DrawOutlineBox

' /////////////////////////////////////////////////////////////////////////////
'DrawSquare 0, x1, y1, size, fgcolor, bgcolor

Sub DrawSquare (img&, x1%, y1%, size%, fgcolor~&, bgcolor~&)
    Dim x2%, y2%
    If img& < -1 Then
        _Dest img& ': Cls , cEmpty

        x2% = (x1% + size%) - 1
        y2% = (y1% + size%) - 1

        Line (x1%, y1%)-(x2%, y1%), fgcolor~&, , 65535
        Line (x2%, y1%)-(x2%, y2%), fgcolor~&, , 65535
        Line (x2%, y2%)-(x1%, y2%), fgcolor~&, , 65535
        Line (x1%, y2%)-(x1%, y1%), fgcolor~&, , 65535

        If bgcolor~& <> cEmpty Then
            'PAINT [STEP] (column%, row%), fillColor[, borderColor%]
            Paint (x1% + 1, y1% + 1), bgcolor~&, fgcolor~&
        End If
    End If
End Sub ' Draw Square

' ################################################################################################################################################################
' END BOX DRAWING ROUTINES
' ################################################################################################################################################################

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' BEGIN RGB COLOR FUNCTIONS #RGB
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Function cRed~& ()
    cRed = _RGB32(255, 0, 0)
End Function

Function cOrangeRed~& ()
    cOrangeRed = _RGB32(255, 69, 0)
End Function ' cOrangeRed~&

Function cDarkOrange~& ()
    cDarkOrange = _RGB32(255, 140, 0)
End Function ' cDarkOrange~&

Function cOrange~& ()
    cOrange = _RGB32(255, 165, 0)
End Function ' cOrange~&

Function cGold~& ()
    cGold = _RGB32(255, 215, 0)
End Function ' cGold~&

Function cYellow~& ()
    cYellow = _RGB32(255, 255, 0)
End Function ' cYellow~&

' LONG-HAIRED FRIENDS OF JESUS OR NOT,
' THIS IS NOT YELLOW ENOUGH (TOO CLOSE TO LIME)
' TO USE FOR OUR COMPLEX RAINBOW SEQUENCE:
Function cChartreuse~& ()
    cChartreuse = _RGB32(127, 255, 0)
End Function ' cChartreuse~&

' WE SUBSTITUTE THIS CSS3 COLOR FOR INBETWEEN LIME AND YELLOW:
Function cOliveDrab1~& ()
    cOliveDrab1 = _RGB32(192, 255, 62)
End Function ' cOliveDrab1~&

Function cLime~& ()
    cLime = _RGB32(0, 255, 0)
End Function ' cLime~&

Function cMediumSpringGreen~& ()
    cMediumSpringGreen = _RGB32(0, 250, 154)
End Function ' cMediumSpringGreen~&

' ADDED THIS FOR THE GAUGE COLOR:
Function cSpringGreen~& ()
    cSpringGreen = _RGB32(0, 255, 160)
End Function ' cSpringGreen~&

Function cCyan~& ()
    cCyan = _RGB32(0, 255, 255)
End Function ' cCyan~&

Function cDeepSkyBlue~& ()
    cDeepSkyBlue = _RGB32(0, 191, 255)
End Function ' cDeepSkyBlue~&

Function cDodgerBlue~& ()
    cDodgerBlue = _RGB32(30, 144, 255)
End Function ' cDodgerBlue~&

Function cSeaBlue~& ()
    cSeaBlue = _RGB32(0, 64, 255)
End Function ' cSeaBlue~&

Function cBlue~& ()
    cBlue = _RGB32(0, 0, 255)
End Function ' cBlue~&

Function cBluePurple~& ()
    cBluePurple = _RGB32(64, 0, 255)
End Function ' cBluePurple~&

Function cDeepPurple~& ()
    cDeepPurple = _RGB32(96, 0, 255)
End Function ' cDeepPurple~&

Function cPurple~& ()
    cPurple = _RGB32(128, 0, 255)
End Function ' cPurple~&

Function cPurpleRed~& ()
    cPurpleRed = _RGB32(128, 0, 192)
End Function ' cPurpleRed~&

Function cDarkRed~& ()
    cDarkRed = _RGB32(160, 0, 64)
End Function ' cDarkRed~&

Function cBrickRed~& ()
    cBrickRed = _RGB32(192, 0, 32)
End Function ' cBrickRed~&

Function cDarkGreen~& ()
    cDarkGreen = _RGB32(0, 100, 0)
End Function ' cDarkGreen~&

Function cGreen~& ()
    cGreen = _RGB32(0, 128, 0)
End Function ' cGreen~&

Function cOliveDrab~& ()
    cOliveDrab = _RGB32(107, 142, 35)
End Function ' cOliveDrab~&

Function cLightPink~& ()
    cLightPink = _RGB32(255, 182, 193)
End Function ' cLightPink~&

Function cHotPink~& ()
    cHotPink = _RGB32(255, 105, 180)
End Function ' cHotPink~&

Function cDeepPink~& ()
    cDeepPink = _RGB32(255, 20, 147)
End Function ' cDeepPink~&

Function cMagenta~& ()
    cMagenta = _RGB32(255, 0, 255)
End Function ' cMagenta~&

Function cBlack~& ()
    cBlack = _RGB32(0, 0, 0)
End Function ' cBlack~&

Function cDimGray~& ()
    cDimGray = _RGB32(105, 105, 105)
End Function ' cDimGray~&

Function cGray~& ()
    cGray = _RGB32(128, 128, 128)
End Function ' cGray~&

Function cDarkGray~& ()
    cDarkGray = _RGB32(169, 169, 169)
End Function ' cDarkGray~&

Function cSilver~& ()
    cSilver = _RGB32(192, 192, 192)
End Function ' cSilver~&

Function cLightGray~& ()
    cLightGray = _RGB32(211, 211, 211)
End Function ' cLightGray~&

Function cGainsboro~& ()
    cGainsboro = _RGB32(220, 220, 220)
End Function ' cGainsboro~&

Function cWhiteSmoke~& ()
    cWhiteSmoke = _RGB32(245, 245, 245)
End Function ' cWhiteSmoke~&

Function cWhite~& ()
    cWhite = _RGB32(255, 255, 255)
    'cWhite = _RGB32(254, 254, 254)
End Function ' cWhite~&

Function cDarkBrown~& ()
    cDarkBrown = _RGB32(128, 64, 0)
End Function ' cDarkBrown~&

Function cLightBrown~& ()
    cLightBrown = _RGB32(196, 96, 0)
End Function ' cLightBrown~&

Function cKhaki~& ()
    cKhaki = _RGB32(240, 230, 140)
End Function ' cKhaki~&

Function cEmpty~& ()
    'cEmpty~& = -1
    cEmpty = _RGB32(0, 0, 0, 0)
End Function ' cEmpty~&

' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
' END RGB COLOR FUNCTIONS @RGB
' ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

' ################################################################################################################################################################
' BEGIN RGB COLOR ARRAY FUNCTIONS #COLR
' ################################################################################################################################################################

' /////////////////////////////////////////////////////////////////////////////

Sub AddColor (ColorValue As _Unsigned Long, arrColor() As _Unsigned Long)
    ReDim _Preserve arrColor(0 To UBound(arrColor) + 1) As _Unsigned Long
    arrColor(UBound(arrColor)) = ColorValue
End Sub ' AddColor

' /////////////////////////////////////////////////////////////////////////////

Sub AddColors (ColorValue As _Unsigned Long, arrColor() As _Unsigned Long, HowMany As Long)
    Dim iLoop As Integer
    For iLoop = 1 To HowMany
        AddColor ColorValue, arrColor()
    Next iLoop
End Sub ' AddColors

' /////////////////////////////////////////////////////////////////////////////

Sub AddSpectrumColors (arrColor() As _Unsigned Long)
    Dim iNum As Integer
    iNum = 1
    AddColors cRed, arrColor(), iNum
    AddColors cOrangeRed, arrColor(), iNum
    AddColors cDarkOrange, arrColor(), iNum
    AddColors cOrange, arrColor(), iNum
    AddColors cGold, arrColor(), iNum
    AddColors cYellow, arrColor(), iNum
    AddColors cChartreuse, arrColor(), iNum
    AddColors cOliveDrab1, arrColor(), iNum
    AddColors cLime, arrColor(), iNum
    AddColors cMediumSpringGreen, arrColor(), iNum
    AddColors cSpringGreen, arrColor(), iNum
    AddColors cCyan, arrColor(), iNum
    AddColors cDeepSkyBlue, arrColor(), iNum
    AddColors cDodgerBlue, arrColor(), iNum
    AddColors cSeaBlue, arrColor(), iNum
    AddColors cBlue, arrColor(), iNum
    AddColors cBluePurple, arrColor(), iNum
    AddColors cDeepPurple, arrColor(), iNum
    AddColors cPurple, arrColor(), iNum
    AddColors cPurpleRed, arrColor(), iNum
End Sub ' AddSpectrumColors

' /////////////////////////////////////////////////////////////////////////////

Sub AddGrayscaleColors (arrColor() As _Unsigned Long)
    Dim iNum As Integer
    iNum = 1
    AddColors cDimGray, arrColor(), iNum
    AddColors cGray, arrColor(), iNum
    AddColors cDarkGray, arrColor(), iNum
    AddColors cSilver, arrColor(), iNum
    AddColors cLightGray, arrColor(), iNum
    AddColors cGainsboro, arrColor(), iNum
    AddColors cWhiteSmoke, arrColor(), iNum
    AddColors cWhite, arrColor(), iNum '* 2
    AddColors cWhiteSmoke, arrColor(), iNum
    AddColors cGainsboro, arrColor(), iNum
    AddColors cLightGray, arrColor(), iNum
    AddColors cSilver, arrColor(), iNum
    AddColors cDarkGray, arrColor(), iNum
    AddColors cGray, arrColor(), iNum
End Sub ' AddGrayscaleColors

' ################################################################################################################################################################
' END COLOR ARRAY FUNCTIONS @COLR
' ################################################################################################################################################################
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  APNG File Format Petr 5 559 11-20-2025, 02:32 PM
Last Post: ahenry3068
  GIF89a File Format Petr 6 1,342 03-04-2025, 01:20 AM
Last Post: a740g
  PCX file format Petr 13 3,414 03-01-2025, 10:52 PM
Last Post: Petr
  BMP File format Petr 8 1,762 02-23-2025, 07:54 PM
Last Post: Petr

Forum Jump:


Users browsing this thread: