Thread Rating:
  • 1 Vote(s) - 5 Average
  • 1
  • 2
  • 3
  • 4
  • 5
QB64-PE v4's new 4-voice sound generator
#1
As many of you have noticed, QB64-PE v4.0.0 brings exciting new retro audio capabilities! Here's a quick look at the updated commands:

Code: (Select All)
SOUND frequency!, duration![, volume!][, panPosition!][, waveform&][, waveformParameters!][, voice&]
SOUND WAIT
SOUND RESUME
PLAY mmlString1$[, mmlString2$][, mmlString3$][, mmlString4$]
remaining# = PLAY(voice&)
_WAVE voice&, waveDefinition%%([index&])[, frameCount&]

With these features, we can now play retro music using 4 simultaneous channels (a.k.a. voices). Each voice can have its own waveform, volume, and pan position, allowing for rich, layered audio playback.

The notes can be programmed in two main ways: 
  1. Using the SOUND command, combined with SOUND WAIT and SOUND RESUME for synchronization between voices.
  2. Using the PLAY command, where each voice can be controlled with its own MML string.

In v4, the QB64-PE MML syntax has been extended to support all the new features. Much of the design draws inspiration from the SOUND and PLAY capabilities of Amiga Basic and Advanced BASIC (for the Tandy 1000 and IBM PCjr systems). This means many classic BASIC programs using SOUND and PLAY can run in QB64-PE with minimal or no modifications.

The purpose of this post is to showcase what’s possible with these new features. 

I’m no musician by any stretch of the imagination. But I do understand the math (well, mostly!), and I’ve been able to port a few forgotten and abandoned gems from Amiga Basic and BASICA into QB64-PE. Below are a few examples of these ports. I’ll continue to share more as I complete them.

If you’ve created something cool using these new audio features, feel free to post and share it here!

[Image: Screenshot-2024-12-18-044813.png]

Code: (Select All)
' Music - AmigaBasic Music/Graphic-Demo --- 20. July 1985
DEFLNG A-Z

CONST VOLUME! = 0.25!

DIM F#(88), CF(19), CT#(19)

GOSUB InitSound
GOSUB InitGraphics

DO
    SOUND RESUME
    RESTORE Song
    GOSUB PlaySong

    ' This ensures all voices have played completely before playing the song again
    WHILE PLAY(0) > 0 _ORELSE PLAY(1) > 0 _ORELSE PLAY(2) > 0 _ORELSE PLAY(3) > 0
        IF _KEYHIT = 27 THEN END
        _LIMIT 60
    WEND
LOOP

InitGraphics:
SCREEN 12
_TITLE "AmigaBasic Music/Graphic Demo (QB64-PE port by a740g)"
iDraw = 30
iErase = 0
ON TIMER(1) GOSUB TimeSlice
TIMER ON
RETURN

TimeSlice:
FOR linestep = 1 TO 15
    DrawLine iDraw, 1
    DrawLine iErase, 0
    _LIMIT 60
NEXT linestep
RETURN

PlaySong:
' Array VO contains the base octave for a voice.
FOR v = 0 TO 3
    READ VO(v)
    VO(v) = 12 * VO(v) + 3
NEXT v

DO
    SOUND WAIT
    FOR v = 0 TO 3
        t# = VT#(v)
        Fi = -1
        READ p$
        IF p$ = "x" THEN RETURN
        FOR I = 1 TO LEN(p$)
            Ci = INSTR(C$, MID$(p$, I, 1))
            IF Ci <= 8 THEN
                IF Fi >= 0 THEN SOUND F#(Fi), t#, VOLUME, , , , v: t# = VT#(v)
                IF Ci = 8 THEN Fi = 0 ELSE Fi = CF(Ci) + VO(v)
            ELSEIF Ci < 11 THEN '# or -
                Fi = Fi + CF(Ci)
            ELSEIF Ci < 17 THEN '1 through 8
                t# = CT#(Ci)
            ELSEIF Ci < 19 THEN '< or >
                VO(v) = VO(v) + CF(Ci)
            ELSE 'ln
                I = I + 1
                Ci = INSTR(C$, MID$(p$, I, 1))
                VT#(v) = CT#(Ci)
                IF Fi < 0 THEN t# = VT#(v)
            END IF
        NEXT I
        IF Fi >= 0 THEN SOUND F#(Fi), t#, VOLUME, , , , v
    NEXT v
    SOUND RESUME
    IF _KEYHIT = 27 THEN END
    _LIMIT 60
LOOP

InitSound:
' F#() contains frequencies of the chromatic scale.
' Note A in octave 0 = F#(12) = 55 Hz.
Log2of27.5# = LOG(27.5#) / LOG(2#)
FOR x = 1 TO 88
    F#(x) = 2 ^ (Log2of27.5# + x / 12#)
NEXT x

' Create the waveform of tones,
' determines timbre.
DIM Timbre(255) AS _BYTE
FOR I = 0 TO 255
    READ Timbre(I)
NEXT I

' The following DATA rows were created using the following formula.
' Reading from these DATAs is faster than calculating the sine 1024 times.
'  K# = 2 * 3.14159265/256
'  FOR I = 0 TO 255
'    Timbre(I) = 31 * (SIN(I * K#) + SIN(2 * I * K#) + SIN(3 * I * K#) + SIN( 4 * I * K#))
'  NEXT I
DATA 0,8,15,23,30,37,44,51,57,63,69,74,79,83,87,91
DATA 93,96,98,99,100,100,100,99,98,97,95,92,89,86,83,79
DATA 75,71,66,62,57,52,48,43,39,34,30,25,21,18,14,11
DATA 8,5,3,0,-1,-3,-4,-5,-5,-6,-6,-5,-5,-4,-3,-1
DATA 0,2,3,5,7,9,11,13,15,17,18,20,21,23,24,25
DATA 26,26,27,27,27,27,27,26,25,24,23,22,20,18,17,15
DATA 13,11,9,7,5,3,1,-1,-3,-5,-6,-8,-9,-10,-11,-12
DATA -12,-13,-13,-13,-13,-13,-12,-11,-11,-10,-8,-7,-6,-4,-3,-2
DATA 0,2,3,4,6,7,8,10,11,11,12,13,13,13,13,13
DATA 12,12,11,10,9,8,6,5,3,1,-1,-3,-5,-7,-9,-11
DATA -13,-15,-17,-18,-20,-22,-23,-24,-25,-26,-27,-27,-27,-27,-27,-26
DATA -26,-25,-24,-23,-21,-20,-18,-17,-15,-13,-11,-9,-7,-5,-3,-2
DATA 0,1,3,4,5,5,6,6,5,5,4,3,1,0,-3,-5
DATA -8,-11,-14,-18,-21,-25,-30,-34,-39,-43,-48,-52,-57,-62,-66,-71
DATA -75,-79,-83,-86,-89,-92,-95,-97,-98,-99,-100,-100,-100,-99,-98,-96
DATA -93,-91,-87,-83,-79,-74,-69,-63,-57,-51,-44,-37,-30,-23,-15,-8

' Set AMIGA PAULA like panning (well mostly)
SOUND 0, 0, , -0.75!, 10, , 0 ' pan left
SOUND 0, 0, , 0.75!, 10, , 1 ' pan right
SOUND 0, 0, , 0.75!, 10, , 2 ' pan right
SOUND 0, 0, , -0.75!, 10, , 3 ' pan left

_WAVE 0, Timbre()
_WAVE 1, Timbre()
_WAVE 2, Timbre()
_WAVE 3, Timbre()

' Array CF maps MML commands to frequency indices.
C$ = "cdefgabp#-123468<>l"
FOR I = 1 TO 19
    READ CF(I)
NEXT I
DATA 0,2,4,5,7,9,11,0,1,-1,0,0,0,0,0,0,-12,12,0

' Array CT# assigns note lengths to MML commands.
FOR I = 1 TO 18
    READ CT#(I)
NEXT I
' MML commands p1,p2,p3,p4,p6,p8 correspond to pause times 36.4 ... 4.55 units
DATA 0,0,0,0,0,0,0,0,0,0,36.4,18.2,12.133333,9.1,6.0666667,4.55,0,0,0
RETURN


' The music is written in special commands (MML), but as per the Wiki page
' below MML not fully implemented here, missing o, v and t commands:
' https://en.wikipedia.org/wiki/Music_Macr...Modern_MML

' The first 4 numbers are the base octaves (0-7) for each voice.
' ln    - sets note length for the following notes of this voice:
'          l1 = whole note, l2 = half note, l4 = quarter note, etc.
' >      - selects the next higher octave for this voice.
' <      - selects the next lower octave for this voice.
' a to g - play the respective note,
'          # (sharp) or - (flat) may follow directly.
'          It may also follow a number to determine the duration of this note.
' pn    - make a rest/pause length as for note length (ln) above.

Song:
DATA 1,3,3,3
DATA l2g>ge,l2p2de,l2p2l6g3f#g3a,l6p6gab>dcced
DATA <b>e<e,ge<b,b3ab3ge3d,dgf#gd<bgab
DATA ab>c,a>dc,e3f#g3de3<b,>cdedc<babg
DATA df#d,c<a>f#,a3>da3ga3f#,f#gadf#a>c<ba
DATA gec,g<g>e,d3f#g3f#g3a,bgab>dcced
DATA <b>ed,ge<b,b3ab3ge3g,dgf#gd<bgab
DATA cc#d,>ced,a3f#g3e<a3>c,e>dc<bagdgf#
DATA <gp3>g6d3<b6,dp2b3g6,<b3>gb3>dg3d,gb>dgd<bgb>d
DATA g>f#e,d<gg,l2<g1g,l2<b1>c
DATA f#ed,agf#,a1b,d1d
DATA ef#g,gag,bag,c1<b
DATA dp3d6d3d6,f#a3a6>d3d6,al6d3ef#3g,l6adef#aga>c<b
DATA <d>p3d6d3d6,f#3a6f#3d6<a3>d6,a3>c<a3f#d3f#,>c<af#df#a>c<ba
DATA gf#e,dde,g3dg3f#g3a,bgab>dcced
DATA b<b>e,gd<b,b3ag3f#e3g,dgf#gd<bgab
DATA cd<d,l4>c<a>d<b>c<al2,a3gf#3ga3c,e>dc<bagdgf#
DATA g>ge,b>de,<b3>dg3f#g3a,gbab>dcced
DATA <b>e<e,ge<b,b3ab3ge3d,dgf#gd<bgab
DATA ab>c,a>dc,e3f#g3de3<b,>cdedc<babg
DATA df#d,c<a>f#,a3>f#a3ga3f#,f#gadf#a>c<ba
DATA gec,g<g>e,d3f#g3f#g3a,bgab>dcced
DATA <b>ed,ge<b,b3ab3ge3g,dgf#gd<bgab
DATA cc#d,>ced,a3f#g3e<a3>c,e>dc<bagdgf#
DATA <g>f#e,d<gg,l2b1>c,l2g1g
DATA f#ed,agf#,d1d,a1b
DATA ef#g,gag,c1<b,bag
DATA dp3d6d3d6,f#l6a3a>d3d,al6d3ef#3g,l6ddef#aga>c<b
DATA <dp3>d6d3d6,f#3af#3d<a3>d,a3>c<a3f#d3f#,>c<af#df#a>c<ba
DATA gf#e,l2dde,l2b1>c,bgab>dcced
DATA b<b>e,gd<b,d1<b,dgf#gd<bgab
DATA cd<d,l4>c<a>d<b>c<a,a4b8>c8<ba,e>dc<bagdgf#
DATA g>ge,l2b>de,l6g3dg3f#g3a,gbab>dcced
DATA <b>e<e,ge<b,b3ab3ge3d,dgf#gd<bgab
DATA ab>c,a>dc,e3f#g3de3<b,>cdedc<babg
DATA df#d,c<a>f#,a3>da3ga3f#,f#gadf#a>c<ba
DATA gec,g<g>e,d3f#g3f#g3a,bgab>dcced
DATA <b>ed,ge<b,b3ab3ge3g,dgf#gd<bgab
DATA cc#d,>ced,a3f#g3e<a3>c,e>dc<bagdgf#
DATA <gp3>g6f#3e6,dp3g6d3e6,<b3>gb3>dg3<g,gb>dgd<bdb>c#
DATA dc<b,f#dd,l2a1b,d<def#ag#g#ba
DATA a>a4g4f4e4,e<a>a,>c1c,a>c<b>c<aecde
DATA d<b>e,aag#,<bb4>c8d8<b,f>dcd<bg#ef#g#
DATA a>fd,e<a>f#,al6a3g#a3b,a>c<b>ceddfe
DATA cfe,afc,>c3<b>c3<af3a,eag#aec<ab>c
DATA dd#e,df#e,a3g#a3f#<b3>d,fedc<baeag#
DATA <a>ab,c<ag,>l2c1d,a>ceap3l2d
DATA >c<ae,>cag,e1e,l6ecdegfgb-a
DATA fdg,df#g,dd4e8f8d,a>c<b>c<afdef
DATA cec,geg,l6c3<g>c3<ge3d,egfgec<gab-
DATA fdg,fag,c3ef3ab3>d,a>c<b>c<afdef
DATA cp3c6<b3>d6,gp3d6d3d6,c3<g>c3<a>d3<f#,ecdegf#gba
DATA <g>ge,dde,l2b1>c,bgab>dcced
DATA <b>e<e,ge<b,d1d,dgf#gd<bgab
DATA ab>c,a>dc,c<b1,>cdedc<babg
DATA dp3d6d3d6,cl6<a3a>d3d,l6a3c#d3ef#3g,f#def#aga>c<b
DATA <dp3>d6d3d6,f#3af#3d<a3>d,a3>c<a3f#d3f#,>c<af#df#a>c<ba
DATA gf#e,l2dde,l2b1>c,bgab>dcced
DATA b<b>e,gd<b,d1<b,dgf#gd<bgab
DATA cd<d,l4>c<a>d<b>c<a,a4b8>c8<ba,e>dc<bagdgf#
DATA g1g2,l2gp3>g6d3g6,gl6<b3>dg3d,gb>dgd<bgb>a
DATA g1g2,dp3g6e3c6,<b3g>d3b>c2,fd<bgb>ded<a
DATA g1g2,<ap3>d6<b3>e6,c3<ab2b3g,f#a>cd<bgegb
DATA g1g2,<e3a6f#3>a6f#3d6,a2a3f#d3f#,>c<af#df#a>c<ba
DATA g>ge,dde,g3dg3f#g3a,bgab>dcced
DATA <b>e<e,ge<b,b3ab3ge3d,dgf#gd<bgab
DATA ab>c,a>d<c,e3f#g3de3<b,>cdedc<babg
DATA df#d,c<a>f#,a3>da3ga3f#,f#gadf#a>c<ba
DATA gec,g<g>e,d3f#g3f#g3a,bgab>dcced
DATA <b>ed,ge<d,b3ab3ge3g,dgf#gd<bgab
DATA cc#d,d1d2,a3f#g3e<a3>c,e>dc<bagdgf#
DATA <g1g2,p2,<b1b2,g1g2
DATA p1,p1,p1,p1
DATA x

SUB DrawLine (iStep, hue) STATIC
    winWidth = _WIDTH
    winHeight = _HEIGHT
    iStep = (iStep + 1) MOD 60
    side = iStep \ 15
    I! = (iStep MOD 15) / 15!
    i1! = 1! - I!

    ON side + 1 GOSUB dl_top, dl_left, dl_bottom, dl_right

    EXIT SUB

    dl_top:
    LINE (winWidth * I!, 0)-(winWidth, winHeight * I!), hue
    RETURN

    dl_left:
    LINE (winWidth, winHeight * I!)-(winWidth * i1!, winHeight), hue
    RETURN

    dl_bottom:
    LINE (winWidth * i1!, winHeight)-(0, winHeight * i1!), hue
    RETURN

    dl_right:
    LINE (0, winHeight * i1!)-(winWidth * I!, 0), hue
    RETURN
END SUB

[Image: Screenshot-2024-12-18-044902.png]

Code: (Select All)
'-----------------------------------------------------------------------------------------------------------------------
' QB64-PE v4.0.0 Multi-voice PLAY Demo by a740g
'-----------------------------------------------------------------------------------------------------------------------

$IF VERSION < 4.0.0 THEN
    $ERROR This requires the latest version of QB64-PE from https://github.com/QB64-Phoenix-Edition/...ses/latest
$END IF

_DEFINE A-Z AS LONG
OPTION _EXPLICIT

CONST APP_NAME = "Multi-voice PLAY Demo"
CONST LOOPS = 3

_TITLE APP_NAME

DIM AS STRING CH0Verse_1, CH0Verse_2, CH1Verse_1, CH1Verse_2, CH2Verse_1, CH2Verse_2, CH2Verse_3, CH3Verse_1
DIM AS STRING Channel_0, Channel_1, Channel_2, Channel_3, Caption
DIM c AS LONG

DO
    DO
        CLS
        PRINT
        PRINT "Enter number for a tune to play."
        PRINT
        PRINT "1. Demo 1 by J. Baker"
        PRINT "2. Demo 2 by Wilbert Brants"
        PRINT "3. Demo 3 by Wilbert Brants"
        PRINT "4. Demo 4 by J. Baker"
        PRINT "5. Demo 5 by Wilbert Brants"
        PRINT
        INPUT "Your choice (0 exits)"; c
    LOOP WHILE c < 0 OR c > 7

    SELECT CASE c
        CASE 1
            Caption = "Demo 1 by J. Baker"

            144
            CH0Verse_1 = "t144 l4 q0 w1 o0 ^75_100 v32 w1 ^75_100 o0 dd2 w8 ^100_80 o4 c"
            CH0Verse_2 = "w4 ^75_100 o0 d2 w8 ^100_80 o4 cd"

            CH1Verse_1 = "t144 q0 w1 o2 /1^100_99 v31 l1 gba /1\20^25_79 l4 gab2"
            CH1Verse_2 = "v40 /9^100\1 l1 gba \2 l4 gab2"

            CH2Verse_1 = "t144 l4 w2 o3 q20 v33 r1r1"
            CH2Verse_2 = "cd>d<e"
            CH2Verse_3 = "cd>d2<"

            CH3Verse_1 = "t144 q0 w9 y15 o3 _100 v29 l8 d"

            Channel_0 = RepeatVerse(CH0Verse_1 + CH0Verse_1 + CH0Verse_1 + CH0Verse_1 + CH0Verse_1 + CH0Verse_1 + CH0Verse_1 + CH0Verse_1 + CH0Verse_2 + CH0Verse_2 + CH0Verse_2 + CH0Verse_2, LOOPS)
            Channel_1 = RepeatVerse(CH1Verse_1 + CH1Verse_1 + CH1Verse_2, LOOPS)
            Channel_2 = RepeatVerse(CH2Verse_1 + CH2Verse_1 + CH2Verse_1 + CH2Verse_1 + CH2Verse_2 + CH2Verse_2 + CH2Verse_2 + CH2Verse_3, LOOPS)
            Channel_3 = RepeatVerse(CH3Verse_1, 96 * LOOPS)

        CASE 2
            Caption = "Demo 2 by Wilbert Brants"

            CH0Verse_1 = "t103 w2 o0 q8 v38 L8 G4B-B-G4B-B-  G4>E-E-<G4>E-E-<  A>CF4<A>CF4<  F4AAGB->D4<"
            CH1Verse_1 = "t103 w1 o1 q2 v33 L8 GB->D4<GB->D4<  GB->E-<B-GB->E-<B-  A>CF4<A>CF4<  FA>C<AGB->D4<"
            CH2Verse_1 = "t103 w1 o3 q1 v35 L4 GG2G8F8 E-E-2E-8D8 CCCD E-2D2"
            CH2Verse_2 = "B-B-2B-8A8 GG2G8F8 CCCD E-2D2"
            CH2Verse_3 = "B-B-2B-8A8 GG2G8F8 ACFA G2D2"

            Channel_0 = CH0Verse_1 + CH0Verse_1 + CH0Verse_1 + CH0Verse_1
            Channel_1 = CH1Verse_1 + CH1Verse_1 + CH1Verse_1 + CH1Verse_1
            Channel_2 = CH2Verse_1 + CH2Verse_2 + CH2Verse_1 + CH2Verse_3
            Channel_3 = _STR_EMPTY

        CASE 3
            Caption = "Demo 3 by Wilbert Brants"

            CH0Verse_1 = "t144 w2 o0 q16 v22 l8 v+cv-ceeg4 v+ f v- faa>cc< v+ c v- ceeg4 v+ f v- faa>cc< ggbb>d4< q8 g4c2"
            CH1Verse_1 = "t144 w1 o1 q2 v20 l4 gec f2a8c8 gec f2a8c8 gbd ec2"
            CH2Verse_1 = "t144 w3 o3 q2 v22 l8 cegceg l4 caf l8 cegceg l4 caf l8 gbdgbd l4 cgg"

            Channel_0 = RepeatVerse(CH0Verse_1, LOOPS)
            Channel_1 = RepeatVerse(CH1Verse_1, LOOPS)
            Channel_2 = RepeatVerse(CH2Verse_1, LOOPS)
            Channel_3 = _STR_EMPTY

        CASE 4
            Caption = "Demo 4 by J. Baker"

            Channel_0 = "t120 w1 o0 q2 v20 l8 <dced> dcge4 l4 q1 <d1> r8 l8 q2 dg l4 q1 <d1> l8 q2 dge q1 <d1> q2 dcdrr v+ <<dd>> v- r"
            Channel_1 = "t120 w1 o1 q1 v21 l8 >dcde< dcde#4 l4 >d1< l8 de l4 >f1 l8 ddfe1< dedfe dd r4"
            Channel_2 = _STR_EMPTY
            Channel_3 = _STR_EMPTY

        CASE 5
            Caption = "Demo 5 by Wilbert Brants"

            CH0Verse_1 = "T103 q16 O0 V22 W2 L4 V+CV-E8E8 <FA> <V+GV-B8B8> CE"
            CH1Verse_1 = "T103 q8 O2 V22 W1 L8 CEG>C< <FA>CF <GB>DG CEG>C<"
            CH2Verse_1 = "T103 q1 O3 v60 W4 L4 CEC<G> CEC2 CEC<G> CE16R16E16R16C2 CEC<G> CEC2 CEC<G> C<G16>R16E16R16C2"

            Channel_0 = RepeatVerse(CH0Verse_1, 4 * LOOPS)
            Channel_1 = RepeatVerse(CH1Verse_1, 4 * LOOPS)
            Channel_2 = RepeatVerse(CH2Verse_1, LOOPS)
            Channel_3 = _STR_EMPTY

        CASE ELSE
            EXIT DO ' Exit program
    END SELECT

    PlayMML Channel_0, Channel_1, Channel_2, Channel_3, Caption
LOOP

SYSTEM

SUB PlayMML (chan0 AS STRING, chan1 AS STRING, chan2 AS STRING, chan3 AS STRING, caption AS STRING)
    PLAY chan0, chan1, chan2, chan3

    PRINT
    PRINT "Playing "; caption; "..."

    DIM curLine AS LONG: curLine = CSRLIN

    DO
        _LIMIT 15

        LOCATE curLine, 1
    LOOP WHILE DisplayVoiceStats

    SLEEP 1
    _KEYCLEAR
END SUB

FUNCTION RepeatVerse$ (verse AS STRING, count AS _UNSIGNED LONG)
    DIM buffer AS STRING

    DIM i AS _UNSIGNED LONG

    WHILE i < count
        buffer = buffer + verse
        i = i + 1
    WEND

    RepeatVerse = buffer
END FUNCTION

FUNCTION DisplayVoiceStats%%
    STATIC voiceTotalTime(0 TO 3) AS DOUBLE

    DIM voiceElapsedTime(0 TO 3) AS DOUBLE
    DIM i AS LONG

    FOR i = 0 TO 3
        voiceElapsedTime(i) = PLAY(i)

        IF voiceElapsedTime(i) > voiceTotalTime(i) THEN
            voiceTotalTime(i) = voiceElapsedTime(i)
        END IF

        PRINT USING "Voice #: ### of ### seconds left"; i; voiceElapsedTime(i); voiceTotalTime(i)
    NEXT i

    DIM playing AS _BYTE: playing = voiceElapsedTime(0) > 0 _ORELSE voiceElapsedTime(1) > 0 _ORELSE voiceElapsedTime(2) > 0 _ORELSE voiceElapsedTime(3) > 0

    IF NOT playing THEN
        FOR i = 0 TO 3
            voiceTotalTime(i) = 0
        NEXT i
    END IF

    DisplayVoiceStats = playing
END FUNCTION
Reply


Messages In This Thread
QB64-PE v4's new 4-voice sound generator - by a740g - Yesterday, 06:52 AM



Users browsing this thread: 3 Guest(s)