Yesterday, 06:52 AM
(This post was last modified: 9 hours ago by a740g.
Edit Reason: Add images
)
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:
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:
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!
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:
- Using the SOUND command, combined with SOUND WAIT and SOUND RESUME for synchronization between voices.
- 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!
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
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