Chapter 6 - More Dimensions
6.1 3D
There is enormous interest in three dimensional representation using computers. With the arrival of cheap and powerful domestic machines it was only to be expected that this would result in many 3D games. There are now reams of mathematical papers and discussion documents on 3D projection and rotation techniques so I'll give you just enough to get started, with the minimum of mathematics. If you want to go further then I suggest you read up on the subject.
6.1.1 Cartoon Styles
One of the benefits of having scalable sprites is that you can construct pseudo 3D effects in the same way as used in cartoon animation. Although not true 3D the result is quite acceptable and widely used in games. The principle used is based on the fact that the further away an object is the smaller it seems. Typically in a cartoon and most games there will only be four or five distinct layers, referred to as parallax layers. These are usually set out as horizontal strips, the layer furthest away enclosing the horizon line. In figure 6.1 you can see how this fits together.
For simplicity I've shown only three layers and offset them for clarity. I have also outlined the area enclosing each with dotted lines. If you define sprites for all these sections then they can be plotted quickly with the objects in your game.
To get objects to appear on the right layers you will need to plot them in the right order. The background scene should be plotted first, then any objects immediately in front of that layer. Next you plot the middle scene, followed by the sprites on these layers. Finally you plot the foreground scene and any very prominent objects. As you can see you don't need to plot the parts of the scenes that are covered by layers that are to be nearer the front so the sprites need only be just big enough to ensure that no gaps appear between levels. Also, you don't need to plot any object sprites wholly behind one of the intermediate layers, thus saving processor time. Even so you may find that you either have to keep all other game activities to the absolute minimum or use an ARM code sprite routine.
6.1.2 Scaling
The apparent distance from the viewer is determined by a scaling factor derived from the distance of the viewer from the screen and the size of the nearest objects. As you can't control viewing distance you must make sure the foreground objects are of a size and type to give the illusion of placing the foreground where you want it. For this to work you must give the viewer some idea of the size of at least one object. For example in car racing games a pair of hands on a steering wheel may be used as a foreground object. Everyone knows how big hands are so they make a subconscious adjustment to bring the observed view into scale.
Also, it's not a good idea to use an extreme theoretical viewing distance. This will either give a pinhole effect or the effect of a fisheye lens. Figure 6.2 shows the results of changing the viewing distance on apparent sizes and distances. You will see that I have kept the screen image size and overall distance the same for both drawings so you can see how much difference just altering the viewing distance makes. You can of course, intentionally make use of this for special effects.
The values S and T are the viewer to screen distance and the total distance. To correctly scale any object at any distance, you only need a simple piece of maths.
apparent size = real size * S / T
For various reasons it is often more practical to use the object to screen distance, O, rather than the total distance. One reason for this is that any object that has moved beyond the screen towards the viewer develops a negative value, and for plotting purposes can therefore be ignored. Our formula then becomes:
apparent size = real size * S / (S + O)
6.1.3 Perspective
In reality it's more complicated than just scaling the sprite to the distance from the screen to the horizon line. If the sprite is to one side of the centre line of the display steadily reducing its size will make it seem to veer further to the side as it moves backwards. This is where perspective comes in. All objects moving away from you seem to converge on a single point, called the vanishing point , directly at the centre of your line of vision. So to get realistic movement backwards and forwards you need to alter not only the size of the sprite but also its position relative to the centre line. From the distance formula above we can derive a perspective factor for calculations on any given object.
P = S / (S + O)
If we assume the centre line, or Z axis, has a value of zero at the screen surface with positive values towards the vanishing point, and that all X and Y coordinates are relative to the centre line, then we can develop the following scaling formula:
X plot = X position * P
Y plot = Y position * P
sprite scale = object size * P
Listing 6.1 shows these basic principles and the practical application of the maths. This gives you typical flat object cartoon 3D movement, but with rather more freedom of depth as movement is not restricted to only four or five horizontal planes. As sprites are plotted from their bottom left-hand corner it is necessary to add a half size offset to both the X and Y coordinates for correct positioning.
REM > Cartoon
:
ON ERROR PROCerror:END
PROCinitialise
PROCsprite(rockx%,rocky%)
PROCstart
REPEAT
MOUSE nx%,ny%,b%
UNTIL b%=0
REPEAT
MOUSE nx%,ny%,b%
dx%=SGN((nx%-x%)>>4)
dy%=SGN((ny%-y%)>>4)
x%+=dx%<<4
y%+=dy%<<4
IF b%=4 z%-=16 ELSE IF b%=1 z%+=16
IF z%<64 z%=64
WAIT
SYS byte%,113,sc%
sc%=sc% EOR 3
SYS byte%,112,sc%
PROCback
IF z%<256 PROCmiddle
IF z%<100 PROCfore
PROCdisplay(rock%,(x%-rockx%*2)*s%/(s%+z%),(y%-rocky%*2)*s%/(s%+z%),s%<<2,s%<<2,s%+z%,s%+z%)
IF z%>255 PROCmiddle
IF z%>99 PROCfore
PROCprint
UNTIL b%=2
SYS byte%,113,sc%
END
:
DEFPROCerror
MODE 12
IF ERR<>17 PRINT REPORT$ " @ ";ERL
ENDPROC
:
DEFPROCinitialise
*Pointer 1
MODE 15
MODE 13
PRINT TAB(10,10)"Please Wait"
SYS "OS_SWINumberFromString",,"OS_SpriteOp" TO sprite%
SYS "OS_SWINumberFromString",,"OS_Byte" TO byte%
DIM block% 19
block%!0=4
block%!4=5
block%!8=-1
SYS "OS_ReadVduVariables",block%,block%+12
xeig%=block%!12
yeig%=block%!16
size%=&2000
DIM area% size%
area%!0=size%
area%!4=0
area%!8=16
DIM scale% 15
scale%!0=1
scale%!4=1
scale%!8=1
scale%!12=1
init%=256+9
def%=256+15
select%=256+24
mask%=512+29
getpix%=512+41
putpix%=512+44
plot%=512+52
writeto%=256+60
style%=8
SYS sprite%,init%,area%
rockx%=202
rocky%=112
ENDPROC
:
DEFPROCsprite(x%,y%)
rock%=FNdefsprite("rock",x%,y%)
PROCrock(x%,y%)
PROCmasksprite(rock%,x%,y%)
SYS sprite%,writeto%,area%,0
ENDPROC
:
DEFFNdefsprite(a$,x%,y%)
LOCAL add%
x%=x%>>xeig%
y%=y%>>yeig%
SYS sprite%,def%,area%,a$,0,x%,y%,MODE
SYS sprite%,writeto%,area%,a$
SYS sprite%,select%,area%,a$ TO ,,add%
=add%
:
DEFPROCrock(x%,y%)
GCOL %101010 TINT &C0
x%=x% DIV 11
y%=y% DIV 5
MOVE x%*2,y%*4
MOVE BY x%*4,-y%*2
PLOT&71,x%*3,y%
GCOL %101010 TINT &40
PLOT&51,x%*2,-y%*2
GCOL %010101 TINT &C0
MOVE BY -x%*7,-y%
PLOT&51,x%*2,y%*2
GCOL %10101 TINT &40
PLOT&71,-x%*4,y%*2
ENDPROC
:
DEFPROCmasksprite(add%,x%,y%)
LOCAL I%,J%,c%
x%=x%>>xeig%
y%=y%>>yeig%
SYS sprite%,mask%,area%,add%
FOR J%=0 TO y%-1
FOR I%=0 TO x%-1
SYS sprite%,getpix%,area%,add%,I%,J% TO,,,,,c%
IF c%=0 SYS sprite%,putpix%,area%,add%,I%,J%
NEXT
NEXT
ENDPROC
:
DEFPROCstart
VDU 5
x%=640
y%=512
s%=128
z%=256
sc%=1
ORIGIN 640,512
MOUSE ON
ENDPROC
:
DEFPROCdisplay(add%,x%,y%,scale%!0,scale%!4,scale%!8,scale%!12)
SYS sprite%,plot%,area%,add%,x%,y%,style%,scale%
ENDPROC
:
DEFPROCback
GCOL %111010
RECTANGLE FILL -640,0,1280,124
GCOL %101111
RECTANGLE FILL -640,-128,1280,124
ENDPROC
:
DEFPROCmiddle
GCOL %100101
RECTANGLE FILL -640,128,1280,156
GCOL %1110
RECTANGLE FILL -640,-288,1280,156
ENDPROC
:
DEFPROCfore
GCOL %010000
RECTANGLE FILL -640,288,1280,220
GCOL %1001
RECTANGLE FILL -640,-512,1280,220
ENDPROC
:
DEFPROCprint
GCOL %111111
MOVE -400,440
PRINT "The mouse moves the rock"
MOVE -480,360
PRINT "Use select to bring it nearer"
MOVE -440,280
PRINT "Use adjust to move it away"
MOVE -300,200
PRINT "Use menu to stop"
ENDPROC
In the demonstration you can see the effect of background layering as the rock drifts downwards, apparently from behind the middle level. Moving the rock backwards and forwards while keeping it either high or low on the screen will make it jump from level to level.
For the sake of simplicity I've used filled rectangles instead of sprites to represent background levels and a simple distance check to set plotting order. However you should be able to see the possibilities. With sprite plotting I suggest you maintain an array with the sprite pointers, and as objects move forward and back you just swap adjacent pointers to ensure they are always plotted in the right order.
There are occasions where you can have more than one vanishing point. These will be at the edges of the screen, or could even be off screen. For our purposes we'll ignore this situation as it gets rather complicated. Finally, we've only considered the horizon placed half way up the screen. Depending on the type of game it may be better to have it below half way. Setting the sky to ground ratio at about 1.6 to 1 usually looks good. As a general rule the higher the horizon the more you seem to look down at the scene, whereas a low horizon gives the impression of being close to the ground looking up. Changing the height of the horizon is often used in race games and is essential for flight simulators.
6.1.4 Wire Frame Drawings
This is the most familiar form of 3D projection and very effective if used carefully. Perspective affects any face of an object that isn't exactly at right angles to the viewer. This is illustrated by the buildings either side of the road in Figure 6.3. If you think of the drawing in terms of a collection of points connected by straight lines you will see that the perspective rules can be applied to individual objects as well as the overall scene. The lines representing the depth of an object are scaled to the perspective factor in exactly the same way as the overall distance.
It is usual to consider the corner points of an object from a central reference point rather than one of the corners. This makes scaling and positioning much simpler as well as assisting collision detection. You can maintain an array of the positions of the corner points relative to this centre then another array that lists these points to give the actual lines that are to be drawn. It sometimes takes a bit of thought to get a clear picture of this. What we have is one table looking into another. So you could have an entry in the line list table of 2,3. These are not the actual coordinates but the index numbers for the points table where the two sets of actual X,Y,Z coordinates can be found. This is further complicated by the fact that you need to maintain another array giving the absolute position of each object in a given scene. Combining the relative corner positions with the absolute object position and adding the perspective factor will then give the actual screen plotting points for every line.
Listing 6.2 shows this in action. A wire frame cube can be moved and rotated in all planes. However, as it stands the system works only for a fixed viewpoint relative to the absolute centre of the game scene, this being a point apparently at the surface of the screen. The program has been kept as simple as possible so movement is a little jerky, but as you will see later how this can be improved.
REM > WireFrame
:
ON ERROR PROCerror
PROCinitialise
REPEAT
PROCmove
WAIT
SYS byte%,113,sc%
sc%=sc% EOR 3
SYS byte%,112,sc%
CLS
PROCprint
PROCdraw
UNTIL FALSE
END
:
DEFPROCerror
MODE 12
*FX 21
IF ERR<> 17 PRINT REPORT$ " @ ";ERL
END
:
DEFPROCinitialise
MODE 12
MODE 9
OFF
SYS "OS_SWINumberFromString",,"OS_Byte" TO byte%
sc%=1
ORIGIN 640,512
scale%=800: REM overall scaling
E%=FALSE: REM zero/negative Z axis flag
spos=SIN.1
sneg=SIN-.1
cpos=COS.1
cneg=COS-.1
RESTORE+29
READ numpoints%
READ numfaces%
READ maxlines%
:
DIM px%(numpoints%): REM \ final
DIM py%(numpoints%): REM / plotting co-ordinates
DIM vx(numpoints%): REM \ object
DIM vy(numpoints%): REM > face
DIM vz(numpoints%): REM / vertices
DIM points%(numfaces%,maxlines%)
DIM lines%(numfaces%)
:
FOR I%=0 TO numpoints%
READ vx(I%)
READ vy(I%)
READ vz(I%)
NEXT
READ X%
READ Y%
READ Z%
READ S%
FOR J%=0 TO numfaces%
READ lines%(J%)
FOR I%=0 TO lines%(J%)
READ points%(J%,I%)
NEXT
NEXT
ENDPROC
DATA 7: REM number of points
DATA 5: REM number of faces
DATA 3: REM maximum number of lines
:
DATA -1,-1,-1: REM relative points
DATA 1,-1,-1
DATA 1,1,-1
DATA -1,1,-1
DATA -1,-1,1
DATA 1,-1,1
DATA 1,1,1
DATA -1,1,1
:
DATA 256,-128,0: REM game co-ordinates
DATA 128: REM size
DATA 3,0,1,2,3: REM lines this face / point numbers
DATA 3,1,5,6,2
DATA 3,5,4,7,6
DATA 3,4,0,3,7
DATA 3,3,2,6,7
DATA 3,4,5,1,0
:
DEFPROCmove
IF INKEY-98 X%-=8 ELSE IF INKEY-67 X%+=8
IF INKEY-80 Y%+=8 ELSE IF INKEY-105 Y%-=8
IF INKEY-99 Z%+=8:E%=1 ELSE IF INKEY-74 AND E%>0 Z%-=8
IF INKEY-58 PROCrot(vy(),vz(),spos,cpos)
IF INKEY-42 PROCrot(vy(),vz(),sneg,cneg)
IF INKEY-26 PROCrot(vx(),vz(),sneg,cneg)
IF INKEY-122 PROCrot(vx(),vz(),spos,cpos)
IF INKEY-57 PROCrot(vx(),vy(),spos,cpos)
IF INKEY-89 PROCrot(vx(),vy(),sneg,cneg)
ENDPROC
:
DEFPROCrot(RETURN a(),RETURN b(),s,c)
FOR I%=0 TO numpoints%
u=a(I%)
v=b(I%)
a(I%)=u*c-v*s
b(I%)=v*c+u*s
NEXT
ENDPROC
:
DEFPROCprint
PRINT TAB(14,1) CHR$138 " " CHR$139 " - Rotate X"
PRINT TAB(14,3) CHR$136 " " CHR$137 " - Rotate Y"
PRINT TAB(14,5) " - Rotate Z"
PRINT TAB(10,7) "Z - left" SPC8 "X - right"
PRINT TAB(10,9) "' - up" SPC9 "/ - down"
PRINT TAB(3,11) "Spacebar - back Return - forward"
PRINT TAB(5,13) "Escape - stop"
ENDPROC
:
DEFPROCdraw
FOR J%=0 TO numfaces%
IF FNcansee PROCface(lines%(J%))
NEXT
ENDPROC
:
DEFFNcansee
IF FNset(0,2):=FALSE
=((px%(0)-px%(1))*(py%(2)-py%(1))-(py%(0)-py%(1))*(px%(2)-px%(1)))<0
:
DEFFNset(a%,b%)
FOR I%=a% TO b%
E%=(vz(points%(J%,I%))*S%+Z%+scale%)
IF E%>0 pers=scale%/E% ELSE I%=b%
px%(I%)=(vx(points%(J%,I%))*S%+X%)*pers
py%(I%)=(vy(points%(J%,I%))*S%+Y%)*pers
NEXT
=E%<1
:
DEFPROCface(n%)
IF FNset(3,n%) ENDPROC
GCOL J%+1
MOVE px%(n%),py%(n%)
FOR I%=0 TO n%
DRAW px%(I%),py%(I%)
NEXT
ENDPROC
PROCinitialise does quite a lot of work. As well as some important constants, such as sine and cosine values, there are two others of particular interest. The scaling factor, scale%, is more or less at it's optimum value, but can be altered to see the effect. As it is the drawn cube looks about right. A smaller scaling factor would make it look stretched, and a larger factor would give it a stubby appearance.
The ORIGIN command lets you move the vanishing point horizontally or vertically to any part of the screen for special effects. As shown it is central for simplicity.
Also in the initialising there is quite a lot of data. There are figures defined for the number of corners, or points that makes up the cube, and similar figures for faces and lines. The rest of the data is then placed in the appropriate arrays.
First there is a set of x,y,z offsets from the centre of the cube. These values are placed in arrays vx(), vy(), vz(). Next there are the x,y and z absolute coordinates of the whole object in the game world followed by it's size scaling factor.
The figures following the number of faces are put in the faces%() array. These are not actual point coordinates but are lists giving the order in which points are visited. The real coordinates are those found in the first list. This, as I pointed out earlier, takes some thinking about to grasp properly. It is easy to get this confused so when looking at the program I suggest you keep referring back to this.
PROCmove is a straightforward negative INKEY system to give movement for the demonstration.
I haven't limited the range of movement and have allowed the absolute object Z coordinate (not it's final drawing value) to become negative. If you use the Return key to bring the cube right forward you will see that sideways movement then gives the impression that the object is floating just in front of the screen.
6.1.5 Hidden Lines
If you want to make your drawing look more realistic, or if you want to draw solid faces, then the first problem that becomes apparent is removing the parts that are hidden by the bulk of the object. The solution is to consider the face that each group of lines bounds. If the face is pointing away from you, towards the vanishing point, it is invisible and therefore doesn't need to be drawn.
To determine which way the face is pointing you need to use a little vector mathematics. In the first place you must ensure that when you construct each face the corner points would always visited in the same direction, assuming you were looking directly at the face. This is conventionally anti-clockwise. Once you have performed all the adjustments to these coordinates to represent the face in it's correct orientation then the direction of the final plotting points will have become clockwise if the face is pointing away from you.
For each face you only need use the expression below with the first three pairs of coordinates. X1,Y1 X2,Y2 X3,Y3 are of course the relevant coordinate pairs.
visible = ((X1-X2) * (Y3-Y2) - (Y1-Y2) * (X3-X2))<0
Logically you would consider that for wire frame drawings none of the bounding lines need be drawn round an invisible face, but the situation is complicated by the fact that some of these lines are shared with other, visible, faces. Therefore you either have to find some method of discovering which edges are visible or accept the time loss of drawing some lines twice, as we have done in the example.
In the example program FNcansee returns the visibility result for each face, calling PROCface if all is well. The call to FNset performs two operations. The call from FNcansee calculates only the first three coordinate pairs, the second call from PROCface completes calculation of all other coordinate pairs of the face for the drawing routine. This avoids wasting time calculating points on an invisible face.
FNset performs a second function. If E% is zero a division by zero error could result, and if it was negative then that object face is the wrong side of the viewer and therefore can't be drawn. It is always preferable to trap possible errors in this way before they actually occur rather than rely on error correction later.
To be more rigorous with line drawing it is probably best to arrange a system of flags so that once a line is drawn or has been declared invisible it is flagged as not being needed again. If the calculation is separated from the drawing, lines can be drawn easily and quickly irrespective of which face they are connected with.
6.1.6 Rotation
If you want your object to rotate then you have to delve into yet more maths. It looks horrible but is in fact quite easy to implement. Assuming you want to rotate around the Z axis, then only the X and Y coordinates need to be altered.
newX = oldX * COS(angle) - oldY * SIN(angle)
newY = oldY * COS(angle) + oldX * SIN(angle)
If it is the Y axis you want to rotate around then you simply exchange all the Y terms for Z terms in the two expressions. Similarly you can swap in Z for X if you want rotation on the X axis.
One of the features of Basic V is that you can pass whole arrays as parameters in procedures. This is particularly useful here because you can produce a generalised rotation procedure for all objects and all rotation directions. You can see this in PROCrot in the listing 6.2 example.
You must also consider whether accuracy or speed is most important. By defining a rotation increment and repeatedly adding it the the object coordinates I've chosen speed. For accuracy define the total rotation angle for each step then use this to produce a rotated copy of the master array to eliminate cumulative errors.
6.1.7 Matrices
Basic 5, as well as supporting whole array arithmetic, also provides true matrix multiplication. As with all the array operations this is considerably faster than using nested FOR-NEXT loops and picking out individual values. This is particularly useful when you want to perform a rotation of an object with a large number of points or a group of objects round a common centre.
In the fragment below, I've shown the significant points of the rotation method. I won't go into the details of matrix manipulation. You should have no difficulty using the transformation routines without having to understand them.
DIM obs(points-1,2) :REM object points/x,y,z
DIM rotate(2,2) :REM 3d rotation matrix
DIM r(2,2) :REM construction matrix
X = 0.1
Y = 0.02 :REM specimen values
Z = 0.03
PROCtemplate
REM main loop
obs()=obs().rotate() :REM rotates the whole array
PROCdraw
REM end of loop and end of everything
DEFPROCtemplate
sz=SIN(Z):cz=COS(Z)
sy=SIN(Y):cy=COS(Y)
sx=SIN(X):cx=COS(X)
r(0,0)= cz:r(1,0)= sz:r(2,0)= 0
r(0,1)=-sz:r(1,1)= cz:r(2,1)= 0
r(0,2)= 0:r(1,2)= 0:r(2,2)= 1
rotate()=r()
r(0,0)= cy:r(1,0)= 0:r(2,0)= sy
r(0,1)= 0:r(1,1)= 1:r(2,1)= 0
r(0,2)=-sy:r(1,2)= 0:r(2,2)= cy
rotate()=rotate().r()
r(0,0)= 1:r(1,0)= 0:r(2,0)= 0
r(0,1)= 0:r(1,1)= cx:r(2,1)= sx
r(0,2)= 0:r(1,2)=-sx:r(2,2)= cx
rotate()=rotate().r()
ENDPROC
The array obs contains the X,Y,Z coordinates of the whole scene to be rotated. This could be a complex, many cornered object, or a group of simpler objects. In the earlier example we gained speed by keeping the X,Y,Z object coordinates in separate arrays. You will see that we now use a common two dimensional array for matrix manipulation. The payoff is that the actual rotation is performed by a single statement that executes remarkably fast.
The rotation matrix itself is built up in PROCtemplate from three matrices, one for each rotation axis. While it would have been possible to perform the necessary mathematics to combine these matrices by hand it seems pointless to do so when the computer can do it for you. It is ironic that the rotation matrix can be built up with exactly the same command that is used to perform the actual rotation. If you need to alter the rotation matrix within the loop then it might pay to use a single hand calculated combination as it will execute faster.
The same comment about speed and accuracy applies to matrix rotation as with the form in the earlier example.
6.1.8 Universal Movement and Rotation
So far we have regarded the viewer as the centre of our game world. With a game of any size, a space game for example, this isn't a good idea. What you should do is maintain a map - a three dimensional array - of all objects with their coordinates relative to some fixed central point. By doing this, instead of moving objects relative to the viewer, you can move them in this absolute map, and more importantly, you can move the viewer as well in, say, a space ship.
Rotating the direction of view also becomes a practical possibility. I haven't given an example program for this but if you want to pursue this I suggest that you start by regarding the viewer as another object. A good choice for this would be object 0. This object's coordinates can be moved and rotated in the same way as any other. This has the added benefit of giving you the capability to swap object coordinates, and therefore hop from one object to another. A further benefit is that the viewer's object can easily be incorporated into your main collision system.
The only problem then becomes that of translating the view of the whole game world to that of the viewing object. First you need to find out which objects are actually visible.
As well as being in the viewers line of sight objects need to be close enough to be seen. With the scale factor used in the example you can consider an object whose distance from the viewer is around 9000 to 10,000 as too far away to be seen.
To avoid wasting time drawing objects that are off the screen sideways you can make a rough check in the face visibility procedure. If the first pair of coordinates is well outside the normal graphic coordinate range and the object isn't enormous then the routine can safely mark it as invisible.
6.2 Sound
With a little imagination you can easily regard sound as another dimension to your game, and probably a fairly essential one these days. It is certain that your game will have a distinctly 'flat' feel to it without any sound at all.
6.2.1 Music
Many games have background music. As mentioned earlier, if you do this you must have the option of turning it off. A number of sound tracker utilities are available to make this easy. Having created a music file this is run within the playing module transparently to your game. Usually all you need do is make simple SYS calls to start and stop the music. While I strongly recommend that you use a sound tracker utility you can produce quite acceptable results from a home grown Basic routine interleaved with your main game.
The Archimedes sound system trades amplitude against number of channels, so you don't really want to enable any more channels than you actually require. Having said that, it often pays to use different channels for your music and your sound effects. That way you won't have them fighting each other for control and producing some rather strange sounds in the process.
The Acorn method of timing the beats of music is quite sophisticated and although fine for most purposes, especially in music editing programs, it is not so easy to use inside a game loop. It is simpler to fall back on the older method used in the eight bit machines. This is shown in listing 6.3.
REM > Music
:
ON ERROR PROCerror:END
PROCinitialise
PROCsetchans
PROCtitle
mark%=TIME
count%=mark%
REPEAT
IF INKEY-17 count%=TIME+&FFFFF
IF INKEY-102 mark%=TIME:count%=mark%
IF INKEY-38 SOUND 4,-15,0,20
IF TIME-count%>0 PROCsound
WAIT
SYS byte%,113,sc%
sc%=sc% EOR 3
SYS byte%,112,sc%
CLS
PRINT text$;
PROCdraw
UNTIL FALSE
END
:
DEFPROCerror
MODE 12
IF ERR<>17 PRINT REPORT$ " @ ";ERL
VOICES 1
*ChannelVoice 1 WaveSynth-Beep
ENDPROC
:
DEFPROCinitialise
MODE 12
MODE 9
OFF
COLOUR 0,0,0,128
GCOL 2
SYS "OS_SWINumberFromString",,"OS_Byte" TO byte%
sc%=1
blobs%=3
DIM x%(blobs%)
DIM y%(blobs%)
DIM dx%(blobs%)
DIM dy%(blobs%)
FOR I%=0 TO blobs%
x%(I%)=4+RND(1277)
y%(I%)=4+RND(1019)
dx%(I%)=RND(9)-5
dy%(I%)=RND(9)-5
size%=24
xmin%=5+size%
ymin%=5+size%
xmax%=1274-size%
ymax%=1018-size%
NEXT
index%=0
RESTORE+10
READ notes%
DIM chan%(notes%)
DIM vol%(notes%)
DIM pitch%(notes%)
DIM time%(notes%)
FOR I%=0 TO notes%
READ chan%(I%),vol%(I%),pitch%(I%),time%(I%)
NEXT
ENDPROC
DATA 78
DATA 3,-15,69,60
DATA 3,-13,61,60
DATA 3,-13,53,120
DATA 2,-8,5,0
DATA 3,-15,69,60
DATA 2,-8,33,0
DATA 3,-13,61,60
DATA 2,-8,5,0
DATA 3,-13,53,120
:
DATA 2,-8,21,0
DATA 1,-10,0,0
DATA 3,-15,81,60
DATA 2,-8,25,0
DATA 3,-15,73,40
DATA 3,-12,73,20
DATA 2,-8,33,0
DATA 1,-10,0,0
DATA 3,-12,69,120
DATA 2,-8,21,0
DATA 1,-10,0,0
DATA 3,-15,81,60
DATA 2,-8,25,0
DATA 3,-15,73,40
DATA 3,-12,73,20
DATA 2,-8,33,0
DATA 1,-10,0,0
DATA 3,-12,69,100
:
DATA 3,-12,81,20
:
DATA 2,-8,33,0
DATA 1,-10,0,0
DATA 3,-15,101,40
DATA 3,-12,101,20
DATA 2,-8,25,0
DATA 3,-15,97,20
DATA 3,-12,89,20
DATA 3,-12,97,20
DATA 2,-8,21,0
DATA 1,-10,0,0
DATA 3,-15,101,40
DATA 3,-12,81,20
DATA 3,-12,81,40
:
DATA 3,-12,81,20
:
DATA 2,-8,33,0
DATA 1,-10,0,0
DATA 3,-15,101,40
DATA 3,-12,101,20
DATA 2,-8,25,0
DATA 3,-15,97,20
DATA 3,-12,89,20
DATA 3,-12,97,20
DATA 2,-8,21,0
DATA 1,-10,0,0
DATA 3,-15,101,40
DATA 3,-12,81,20
DATA 3,-12,81,40
:
DATA 3,-12,81,20
:
DATA 2,-8,33,0
DATA 1,-10,0,0
DATA 3,-15,101,20
DATA 3,-12,101,20
DATA 3,-12,101,20
DATA 2,-8,25,0
DATA 3,-15,97,20
DATA 3,-12,89,20
DATA 3,-12,97,20
DATA 2,-8,21,0
DATA 1,-10,0,0
DATA 3,-15,101,20
DATA 3,-12,81,20
DATA 3,-12,81,20
DATA 3,-12,81,40
:
DATA 3,-12,73,20
:
DATA 2,-8,5,0
DATA 1,-10,0,0
DATA 3,-15,69,60
DATA 2,-8,33,0
DATA 3,-13,61,60
DATA 2,-8,5,0
DATA 3,-13,53,120
:
DEFPROCsetchans
VOICES 4
*ChannelVoice 1 Percussion-Soft
*ChannelVoice 2 StringLib-Soft
*ChannelVoice 3 WaveSynth-Beep
*ChannelVoice 4 Percussion-Snare
ENDPROC
:
DEFPROCtitle
RESTORE+0
READ num%
FOR I%=0 TO num%
READ x%,y%,t$
text$=text$+CHR$31+CHR$x%+CHR$y%+t$
NEXT
ENDPROC
DATA 4
DATA 5,12,Interleaved sound and graphics
DATA 8,14,Q - Quiet
DATA 8,16,M - Music
DATA 8,18,I - Immediate
DATA 5,20,Escape to stop
:
DEFPROCsound
SOUND chan%(index%),vol%(index%),pitch%(index%),10
count%=time%(index%)+mark%
index%+=1
IF index%>notes% index%=0
IF count%>mark% mark%=count% ELSE PROCsound
ENDPROC
:
DEFPROCdraw
x%()=x%()+dx%()
y%()=y%()+dy%()
FOR I%=0 TO blobs%
CIRCLE FILL x%(I%),y%(I%),size%
IF RND(50)=1 dx%(I%)=RND(9)-5:dy%(I%)=RND(9)-5
IF x%(I%)<xmin% dx%(I%)=1 ELSE IF x%(I%)>xmax% dx%(I%)=-1
IF y%(I%)<ymin% dy%(I%)=1 ELSE IF y%(I%)>ymax% dy%(I%)=-1
NEXT
ENDPROC
In this example four of the possible eight channels are used. Three are dedicated to the music and one is used for a gunfire type sound effect. Tune data is stored as four items per note, in the form; channel, volume, pitch, time to next note. The whole tune is stored in an array for easy, fast access, the pointer to the next note, index%, being incremented after each call to PROCsound. When the whole tune has been played the pointer is zeroed so that the tune continuously repeats.
Having a choice of channels not only gives you the option of different instrument sounds as I have done here but can also be used for producing chords.
The pitch is in quarter tones with middle C having a value of 53, provided you haven't altered the tuning.
Time is in centi-seconds. You will notice that I use time to next note rather than note length. This is fixed at 10 in the example but if you use your own sound module and a fast tune the final note length may be long enough for the sound to appear continuous. Also, you will see that in PROCsound I use a recursive call where a note length is zero. This gives almost perfect synchronisation. Only when all eight channels are is use is there any significant ripple.
The variables count% and mark% are used to give a queueing system that is immune to quite large time fluctuations in the game loop. To prove the point try changing the constant blobs% in PROCinitialise to around 20. The animation will slow dramatically but the tune will hardly be affected. Actual time accuracy of each individual note is the game loop time. In a game that runs at 50 frames per second this accuracy is 2 centi-seconds. However, as the time to next note is added on to mark% and not TIME itself these errors are not cumulative, and will sound like natural note variations in normal playing.
Silencing the music is a bit of a cheat. What I've done is to force count% to such a high value that TIME-count% will never fall below zero while the game is running. To restore the music count% and mark% are brought back into range.
6.2.2 More Voices
The range of voices available by default is distinctly limited, and it is almost certain that you will need to use extra voice modules to get useful results. At the end of this chapter is a utility that enables you to create your own voices for music tracks, as well as giving you an interesting keyboard player.
Once you have your voice module, whether it comes from a source like MusicMaker or a sampler or other commercial source, you must integrate it into the sound system. To do this you need a procedure like the one below. There are two steps to be taken. First, the module is loaded and the voices initialised. Then each channel is assigned a voice but in a slightly different way to the previous example.
DEFPROCattach
*RMLoad SoundMod
RESTORE+12
READ voices%
SYS "Sound_Configure",voices%,208,48,0,0 TO oldvc%,oldsm%,oldhd%,oldsh%
SYS "Sound_Volume",100 TO oldvl%
SYS "Sound_Tuning",&7000 TO oldtn%
DIM oldch%(voices%)
FOR I%=1 TO voices%
READ voice$
SYS "Sound_AttachVoice",I%,0 TO ,oldch%(I%)
SYS "Sound_AttachNamedVoice",I%,voice$
NEXT
ENDPROC
DATA 3
DATA UserLib-Bell,UserLib-Saw,UserLib-BlockSynth
This procedure, as well as attaching the new voices, stores a list of them and a lot of other information about the sound system. This is very important. You must detach voices when the game exits, then kill the module to release RMA to RISC OS. If you fail to do this then the machine is liable to crash as your, now unused, module area could move or be overwritten, leaving the sound controller with pointers aimed at gibberish. The procedure below takes care of this problem.
DEFPROCdetach
FOR I%=1 TO voices%
SYS "Sound_AttachVoice",I%,oldch%(I%)
NEXT
SYS "Sound_Tuning",oldtn%
SYS "Sound_Volume",oldvl%
SYS "Sound_Configure",oldvc%,oldsm%,oldhd%,oldsh%
*RMKill UserVoiceLib
ENDPROC
Finally, you will see that all of the most significant sound configuration information is also read and restored afterwards. If you use these two procedures together you can be assured that you will always receive the sound system in a comprehensible form, and will always return it as you found it.
6.2.3 Voice Generator Utility
Operation of MusicMaker is fairly straightforward. The voices it creates fully implement extended pitch control, but only partially implement extended amplitude. Duration is implemented up to the envelope length only, infinite sound is not possible.
REM > MusicMaker
:
ON ERROR PROCerror
PROCinit
PROCassemble
PROCchannels
MODE 12
PROChour(FALSE)
ON ERROR PROCerror
PROCdisplay
PROCmenu
PROCtidy
END
:
DEFPROCerror
VDU 4
PRINT REPORT$ " - Press a key"
IF GET
IF NOT INKEY TRUE ENDPROC
ON ERROR OFF
*RMREINIT SoundDMA
*RMREINIT SoundChannels
*RMREINIT SoundScheduler
*RMREINIT WaveSynth
*RMREINIT StringLib
*RMREINIT Percussion
*FX 4
*FX 12
*FX 229
END
:
DEFPROCinit
I%=PAGE+4
REPEAT
I%+=1
UNTIL I%?-1=ASC">"
PRINT $I%
VoiceSize%=&500
VoiceMax%=12
DIM Module% &200,Code% VoiceSize%*VoiceMax%,Work% &40
DIM VoiceName%(VoiceMax%),VoiceEnable%(VoiceMax%)
DIM AmpEnv%(VoiceMax%),FreEnv%(VoiceMax%),WaveTable%(VoiceMax%)
DIM OldVoice%(VoiceMax%),OldChannel%(8)
DIM w%(VoiceMax%,255),par%(VoiceMax%,25),Sine%(255,9)
*Pointer 1
MOUSE OFF
PROChour(TRUE)
FOR I%=0 TO 255
PROChour(I% DIV 25+1)
Sine%(I%,0)=(RND(1)-.5)*16777216
FOR J%=1 TO 9
Sine%(I%,J%)=SIN(PI*I%*J%/128)*16777216
NEXT
NEXT
*KEY 0 |!~
*FX 4 1
*FX 229 1
*KEY 13 |!!
ENDPROC
:
DEFPROCassemble
FOR v%=1 TO VoiceMax%
PROChour(10+v%)
VoiceEnable%(v%)=TRUE
FOR I%=0 TO 2 STEP 2
P%=Code%+VoiceSize%*v%-VoiceSize%
[OPT I%
.VoiceBase
B Fill
B Fill
B GateOn
B GateOff
B Instance
LDMFD R13!,
LDMFD R13!,
EQUD VoiceName%(v%)-VoiceBase
;
.VoiceName%(v%)
=FNbytes(19)
;
.LogAmpPtr
EQUD 0
.WaveBase
EQUD 0
.AmpEnvPtr
EQUD 0
.FreEnvPtr
EQUD 0
.AmpEnv%(v%)
=FNbytes(255)
.FreEnv%(v%)
=FNbytes(255)
.WaveTable%(v%)
=FNbytes(255)
;
.Instance
STMFD R13!,
ADR R1,VoiceBase
MOV R0,#AmpEnv%(v%)-VoiceBase
ADD R0,R0,R1
STR R0,AmpEnvPtr
MOV R0,#FreEnv%(v%)-VoiceBase
ADD R0,R0,R1
STR R0,FreEnvPtr
MOV R0,#WaveTable%(v%)-VoiceBase
ADD R0,R0,R1
STR R0,WaveBase
MOV R0,#0
MOV R1,#0
MOV R2,#0
MOV R3,#0
MOV R4,#0
SWI "Sound_Configure"
LDR R0,[R3,#12]
STR R0,LogAmpPtr
LDMFD R13!,
;
.GateOn
LDMIA R9,
LDR R3,AmpEnvPtr
LDR R5,WaveBase
LDR R6,LogAmpPtr
LDR R7,FreEnvPtr
B FillGate
;
.Fill
LDMIA R9,
.FillGate
LDRB R0,[R7],#1
MOV R0,R0,ASL #24
ADD R2,R2,R0,ASR #24
AND R1,R1,#127
LDRB R0,[R3],#1
CMP R0,#255
MOVEQ R4,#0
CMPNE R4,#0
MOVEQ R0,#2
LDMEQFD R13!,
;
SUBS R1,R1,R0
MOVMI R1,#0
LDRB R1,[R6,R1,LSL #1]
MOV R1,R1,LSR #1
RSB R1,R1,#127
.FillLoop
ADD R2,R2,R2,LSL #16
LDRB R0,[R5,R2,LSR #24]
SUBS R0,R0,R1,LSL #1
MOVMI R0,#0
STRB R0,[R12],R11
ADD R2,R2,R2,LSL #16
LDRB R0,[R5,R2,LSR #24]
SUBS R0,R0,R1,LSL #1
MOVMI R0,#0
STRB R0,[R12],R11
ADD R2,R2,R2,LSL #16
LDRB R0,[R5,R2,LSR #24]
SUBS R0,R0,R1,LSL #1
MOVMI R0,#0
STRB R0,[R12],R11
ADD R2,R2,R2,LSL #16
LDRB R0,[R5,R2,LSR #24]
SUBS R0,R0,R1,LSL #1
MOVMI R0,#0
STRB R0,[R12],R11
CMP R12,R10
BLT FillLoop
.FillExit
SUB R4,R4,#1
STMIB R9,
MOV R0,#8
LDMFD R13!,
;
.GateOff
MOV R0,#0
.FlushLoop
STRB R0,[R12],R11
STRB R0,[R12],R11
STRB R0,[R12],R11
STRB R0,[R12],R11
CMP R12,R10
BLT FlushLoop
MOV R0,#1
LDMFD R13!,
]
NEXT
NEXT
:
FOR I%=0 TO 2 STEP 2
P%=Work%
[ OPT I%
.Log
SWI "Sound_SoundLog"
SWI "Sound_LogScale"
STRB R0,[R2,R1]
MOV PC,R14
.Type
=FNbytes(7)
]
NEXT
ENDPROC
:
DEFFNbytes(n%)
P%+=n%
=I%
:
DEFPROCchannels
RESTORE +0
FOR v%=1 TO VoiceMax%
PROChour(22+v%*6)
OSCLI"KEY "+STR$ v%+CHR$(v%+199)+"|M"
READ a$,b$
$VoiceName%(v%)="UserLib-"+LEFT$(a$+STRING$(11,CHR$0),11)
FOR I%=0 TO 25
par%(v%,I%)=EVAL("&"+LEFT$(b$,2))
b$=MID$(b$,3)
NEXT
PROCfillwave(v%)
PROChour(25+v%*6)
PROCenvelope(v%)
SYS "Sound_InstallVoice",Code%+v%*VoiceSize%-VoiceSize%,0 TO a%,OldVoice%(v%)
NEXT
Voice%=1
FOR v%=1 TO 8
SYS "Sound_AttachVoice",v%,0 TO z%,OldChannel%(v%)
VOICE v%,$VoiceName%(Voice%)
NEXT
VOICES 8
ENDPROC
DATA Bell,003A0000230028002D00007F01700804FB000500000800080835
DATA HammondOrg,002F120D1D001E0023000F77047F1015FE070500000C00110F05
DATA Ethereal,0161121106080806090A0F6F077F1926FF000203000200320835
DATA Saw,00512E1C00000001000012690B7F2808FF000500000200140865
DATA Vibraphone,004701000F00010027000C7E016F041AF0170602020202000435
DATA ChurchOrg,0771071B0000000006000F6F077F1932F8000203020200000805
DATA Harpsicord,0139100E150E12100F11007C0170080AC2000203000200000805
DATA Flute,13561529030001000000106E077F160A9E050400020200000835
DATA Horn,062E2719151308080200007C036E1E103B000202000200000705
DATA PipeOrg,083C0014012C011A0100126003683F37FA050300020200000705
DATA BlockSynth,00170017001600280129087A0170092AFF230300023602110B05
DATA Fantasy,003300141800080016230A7C0454332CF5220500401340110530
:
DEFPROCmenu
Sel%=14
REPEAT
IF Sel%>12 THEN
OFF
PROCmenu_bars
MOUSE TO 560,48
ENDIF
MOUSE ON
*FX 21 9
REPEAT
MOUSE x%,y%,b%
IF y%>288 PROCedit
x%=x% DIV 208
IF x%>5 x%=5
y%=3-(y%+16) DIV 64
Sel%=x%+y%*6+1
UNTIL b%=1 OR b%=4
REPEAT
MOUSE x%,x%,z%
UNTIL z%=0
*FX 21
MOUSE OFF
VDU 28,0,31,79,24
IF Sel%>12 CLS
CASE Sel% OF
WHEN 1,2,3,4,5,6,7,8,9,10,11,12:IF b%=1 THEN
IF Sel%<>Voice% VoiceEnable%(Sel%)=VoiceEnable%(Sel%)EOR TRUE
PROCmenu_bars
ELSE
PROCvoice(Sel%)
ENDIF
WHEN 13:PROCplay
WHEN 14:PROCload
WHEN 15:PROCsave
WHEN 16:PROCrename
WHEN 17:PROCmodule
WHEN 18:PROCstar
ENDCASE
UNTIL Sel%=19
ENDPROC
:
DEFPROCtidy
FOR v%=1 TO VoiceMax%
SYS "Sound_RemoveVoice",0,OldVoice%(v%)
NEXT
FOR v%=1 TO 8
SYS "Sound_AttachVoice",v%,OldChannel%(v%)
NEXT
VOICES 1
*FX 4
*FX 12
*FX 229
MOUSE OFF
ON
PRINT
ENDPROC
:
DEFPROCmenu_bars
VDU 28,0,31,79,24,12
RESTORE+0
FOR I%=0 TO VoiceMax%-1
IF VoiceEnable%(I%+1) THEN
COLOUR 0
COLOUR 134
ELSE
COLOUR 7
COLOUR 132
ENDIF
PRINT TAB(I% MOD 6*13,I% DIV 6*2+1) SPC 12 STRING$(11,CHR$ 8) MID$($VoiceName%(I%+1),9)
NEXT
COLOUR 0
COLOUR 134
FOR I%=0 TO 6
READ a$
PRINT TAB(I% MOD 6*13,I% DIV 6*2+5) SPC 12 STRING$(11,CHR$ 8) a$;
NEXT
COLOUR 7
COLOUR 128
ENDPROC
DATA Play,Load,Save,Rename,Module,MOS (*),Quit
:
DEFPROCplay
V%=1
OSCLI"FX 11 "+STR$ par%(Voice%,10)
IF par%(Voice%,10) OSCLI"FX 12 "+STR$ par%(Voice%,10)
VDU 26
RESTORE+0
PROCdata_print(7)
PROCprompt
REPEAT
*FX 21
G%=GET
IF INKEY-98:PROCkey(0)
IF INKEY-82:PROCkey(4)
IF INKEY-67:PROCkey(8)
IF INKEY-51:PROCkey(12)
IF INKEY-83:PROCkey(16)
IF INKEY-100:PROCkey(20)
IF INKEY-84:PROCkey(24)
IF INKEY-101:PROCkey(28)
IF INKEY-85:PROCkey(32)
IF INKEY-86:PROCkey(36)
IF INKEY-70:PROCkey(40)
IF INKEY-102:PROCkey(44)
IF INKEY-103:PROCkey(48)
IF INKEY-87:PROCkey(52)
IF INKEY-104:PROCkey(56)
IF INKEY-88:PROCkey(60)
IF INKEY-105:PROCkey(64)
IF INKEY-97:PROCkey(20)
IF INKEY-49:PROCkey(24)
IF INKEY-17:PROCkey(28)
IF INKEY-50:PROCkey(32)
IF INKEY-34:PROCkey(36)
IF INKEY-18:PROCkey(40)
IF INKEY-35:PROCkey(44)
IF INKEY-52:PROCkey(48)
IF INKEY-20:PROCkey(52)
IF INKEY-36:PROCkey(56)
IF INKEY-53:PROCkey(60)
IF INKEY-69:PROCkey(64)
IF INKEY-54:PROCkey(68)
IF INKEY-22:PROCkey(72)
IF INKEY-38:PROCkey(76)
IF INKEY-39:PROCkey(80)
IF INKEY-55:PROCkey(84)
IF INKEY-40:PROCkey(88)
IF INKEY-56:PROCkey(92)
IF INKEY-57:PROCkey(96)
IF INKEY-94:PROCkey(100)
IF INKEY-89:PROCkey(104)
IF INKEY-47:PROCkey(108)
IF INKEY-121:PROCkey(112)
IF INKEY-42:PROCpitch(-1)
IF INKEY-58:PROCpitch(1)
IF G%>199 AND G%<213 PROCvoice(G%-199):PROCprompt
UNTIL G%=27
PRINT TAB(44,0) SPC20 TAB(45,1) SPC18
ENDPROC
DATA 3,1
DATA 64,26,Upper Keyboard,64,29,Lower Keyboard
DATA 1,1
DATA 4,25,1 2 3 5 6 8 9 0 = FALSE
DATA 12,29,S D G H J L ;
DATA 7,1
DATA 1,26,tab Q W E R T Y U I O P [ ] \
DATA 10,30,"Z X C V B N M , . /"
:
DEFPROCedit
LOCAL Env%,Wave%,Flag%
VDU 26
W%=TIME
REPEAT
MOUSE X%,Y%,B%
IF B%=4 THEN
IF Flag%=0 PROCflag
CASE Flag% OF
WHEN 10:Wave%=FNsetwave
WHEN 9:PROCsetrep
WHEN 1,2,3:PROCsetamp
WHEN 7:Env%=FNsettrem
WHEN 4,5,6:PROCsetfre
WHEN 8:Env%=FNsetvib
ENDCASE
ELSE
Flag%=0
ENDIF
IF Wave% PROCfillwave(Voice%)
IF Env% PROCenvelope(Voice%)
IF Env% OR Wave% PROCdisplay:Env%=FALSE:Wave%=FALSE
REPEAT UNTIL TIME-W%>10
W%=TIME
UNTIL Y%<256
ENDPROC
:
DEFPROCload
LOCAL File%,File$
File$=MID$($VoiceName%(Voice%),9)
PROCname("voice to load",File$)
File%=OPENIN File$
IF File%=0 THEN
PRINT "No such file - Press a key"
IF GET
ELSE
FOR I%=0 TO 5
Type?I%=BGET#File%
NEXT
Type?5=13
IF $Type<>"Synth" THEN
PRINT "Invalid file type - Press a key"
IF GET
ELSE
FOR I%=0 TO 25
INPUT#File%,par%(Voice%,I%)
NEXT
PROCfillwave(Voice%)
PROCenvelope(Voice%)
$VoiceName%(Voice%)="UserLib-"+LEFT$(File$+STRING$(11,CHR$0),11)
PROCdisplay
ENDIF
CLOSE#File%
ENDIF
ENDPROC
:
DEFPROCsave
LOCAL File%,File$
File$=MID$($VoiceName%(Voice%),9)
PROCname("voice to save",File$)
File%=OPENOUT File$
IF File% THEN
BPUT#File%,"Synth"
FOR I%=0 TO 25
PRINT#File%,par%(Voice%,I%)
NEXT
CLOSE#File%
$VoiceName%(Voice%)="UserLib-"+LEFT$(File$+STRING$(11,CHR$0),11)
PROCdisplay
ENDIF
ENDPROC
:
DEFPROCrename
LOCAL Name$
Name$=MID$($VoiceName%(Voice%),9)
PROCname("new voice",Name$)
$VoiceName%(Voice%)="UserLib-"+LEFT$(Name$+STRING$(11,CHR$0),11)
PROCdisplay
ENDPROC
:
DEFPROCmodule
LOCAL VoiceTotal%,File%,File$
File$="SMOD"
PROCname("module to save",File$)
FOR v%=1 TO VoiceMax%
IF VoiceEnable%(v%) VoiceTotal%+=1
NEXT
FOR I%=0 TO 2 STEP 2
P%=Module%
[ OPT I%
EQUD 0
EQUD Initialise-Module%
EQUD Finalise-Module%
EQUD 0
EQUD Title-Module%
EQUD Help-Module%
EQUD 0
;
.Title
EQUS File$+"-UserVoiceLib"
EQUB 0
ALIGN
;
.Help
EQUS "User Defined Sound Voices"+MID$(TIME$,7,9)
EQUB 0
ALIGN
;
.VoiceNumbers
=FNbytes(VoiceTotal%-1)
ALIGN
;
.Initialise
STMFD R13!,
ADR R2,VoiceCode
ADR R3,VoiceNumbers
]
FOR v%=1 TO VoiceMax%
IF VoiceEnable%(v%) THEN
[OPT I%
MOV R0,R2
MOV R1,#0
SWI "Sound_InstallVoice"
STRB R1,[R3],#1
ADD R2,R2,#VoiceSize%
]
ENDIF
NEXT
[OPT I%
LDMFD R13!,
;
.Finalise
STMFD R13!,
ADR R2,VoiceNumbers
ADD R3,R2,#VoiceTotal%
.FinLoop
LDRB R1,[R2],#1
SWI "Sound_RemoveVoice"
CMP R3,R2
BNE FinLoop
LDMFD R13!,
.VoiceCode
]
NEXT
File%=OPENOUT File$
FOR I%=Module% TO VoiceCode-1
BPUT# File%,?I%
NEXT
FOR v%=1 TO VoiceMax%
IF VoiceEnable%(v%) THEN
FOR I%=0 TO VoiceSize%-1
BPUT# File%,Code%?(I%+v%*VoiceSize%-VoiceSize%)
NEXT
ENDIF
NEXT
CLOSE# File%
OSCLI "SETTYPE "+File$+" Module"
ENDPROC
:
DEFPROCstar
LOCAL Input$
ON
REPEAT
INPUT'"*" Input$
OSCLI Input$
PRINT "Press a key";
Input$=GET$
UNTIL Input$<>"*"
ENDPROC
:
DEFPROCdisplay
VDU 28,0,23,79,0,12,5
GCOL0,2
RECTANGLE 0,680,512,256
RECTANGLE 0,488,1024,128
RECTANGLE 0,298,1024,128
MOVE 0,808:PLOT 49,512,0
MOVE 0,362:PLOT 49,1024,0
FOR I%=0 TO 9
GCOL0,3:MOVE I%*64+560,796:PRINT"f";I%
GCOL0,6:MOVE I%*64+568,764:PRINT"+";
PRINT CHR$8 CHR$10"-"
NEXT
PROCharmonic(7)
MOVE 0,808
FOR I%=0 TO 255
DRAW I%*2,w%(Voice%,I%)+808
NEXT
PROCamplitude(7)
PROCfrequency(7)
VDU 4:OFF
RESTORE+0
PROCdata_print(6)
COLOUR 7
PROCpitch(0)
PRINT TAB(8,0)"f";Voice% " - " MID$($VoiceName%(Voice%),9)
TAB(8,1)CHR$139" " CHR$138 TAB(57,11);par%(Voice%,10)
OSCLI"FX 11 "+STR$ par%(Voice%,10)
IF par%(Voice%,10) OSCLI"FX 12 "+STR$ par%(Voice%,10)
ENDPROC
DATA 3,9
DATA 0,0,Playing
DATA 0,1,Octave,10,11,Waveshape,50,11,Repeat,68,13,Tremelo
DATA 66,14,Depth Speed,10,17,Amplitude Envelope,68,19,Vibrato
DATA 66,20,Depth Speed,10,23,Pitch Envelope
DATA 6,5
DATA 62,11,+,68,15,+ +,68,21,+ +
DATA 60,11,-,68,16,- -,68,22,- -
:
DEFPROCkey(T%)
SOUND V%,-15,T%+par%(Voice%,25),40:V%=V%MOD8+1
ENDPROC
:
DEFPROCvoice(n%)
IF VoiceEnable%(n%) THEN
IF Voice%<>n%
Voice%=n%
FOR v%=1 TO 8
VOICE v%,$VoiceName%(Voice%)
NEXT
PROCdisplay
ENDIF
ENDIF
ENDPROC
:
DEFPROCpitch(n%)
IF n%>0 THEN
IF par%(Voice%,25)<101 par%(Voice%,25)+=48
ELSE
IF n%<0 THEN
IF par%(Voice%,25)>5 par%(Voice%,25)-=48
ENDIF
ENDIF
PRINT TAB(12,1);par%(Voice%,25) DIV6-8" "
REPEAT
UNTIL NOT INKEY-42 AND NOT INKEY-58
ENDPROC
:
DEFPROCflag
LOCAL a%
a%=X% DIV4
IF Y%>636 THEN
IF X%>512 AND Y%>696 AND Y%<768 Flag%=10 ELSE IF X%>928 AND Y%<672 Flag%=9
ELSE
IF X%<1024 THEN
IF Y%>488 AND Y%<620 THEN
IF a%-par%(Voice%,12)<par%(Voice%,14)-a% Flag%=1
IF a%-par%(Voice%,14)>par%(Voice%,16)-a% Flag%=3 ELSE Flag%=2
ELSE
IF Y%>296 AND Y%<424 THEN
IF a%<par%(Voice%,21)-a% Flag%=4
IF a%-par%(Voice%,21)>par%(Voice%,16)-a% Flag%=6 ELSE Flag%=5
ENDIF
ENDIF
ELSE
IF X%>1036 THEN
IF Y%>482 AND Y%<544 Flag%=7 ELSE IF Y%>290 AND Y%<352 Flag%=8
ENDIF
ENDIF
ENDIF
ENDPROC
:
DEFFNsetwave
PROCharmonic(0)
N%=(X%-544)DIV64
IF N%>9 N%=9
IF Y%<732 AND par%(Voice%,N%)>0 par%(Voice%,N%)=par%(Voice%,N%)-1
IF par%(Voice%,N%)<127 par%(Voice%,N%)=par%(Voice%,N%)+1
*FX 21
PROCharmonic(7)
=TRUE
:
DEFPROCsetrep
COLOUR 7
IF X%<992 THEN
IF par%(Voice%,10)>6 par%(Voice%,10)-=1 ELSE par%(Voice%,10)=0
ELSE
IF par%(Voice%,10)<98 AND par%(Voice%,10) par%(Voice%,10)+=1 ELSE par%(Voice%,10)=6
ENDIF
PRINT TAB(57,11);par%(Voice%,10)" "
ENDPROC
:
DEFPROCsetamp
PROCamplitude(0)
PROCfrequency(0)
IF Flag%=1 PROCsetbars(488,par%(Voice%,12),par%(Voice%,11))
IF Flag%=2 PROCsetbars(488,par%(Voice%,14),par%(Voice%,13))
ELSE PROCsetbars(488,par%(Voice%,16),par%(Voice%,15))
IF par%(Voice%,16)<12 par%(Voice%,16)=12
IF par%(Voice%,21)>par%(Voice%,16) par%(Voice%,21)=par%(Voice%,16)-1
IF par%(Voice%,12)=0 par%(Voice%,12)=1
IF par%(Voice%,14)<=par%(Voice%,12) par%(Voice%,14)=par%(Voice%,12)+1
ELSE IF par%(Voice%,14)>=par%(Voice%,16) par%(Voice%,14)=par%(Voice%,16)-1
PROCfrequency(7)
PROCamplitude(7)
ENDPROC
:
DEFFNsettrem
PROCamplitude(0)
IF X%<1152 THEN
IF Y%<508 THEN
IF par%(Voice%,17)>0 par%(Voice%,17)-=1
ELSE
IF par%(Voice%,17)<100 par%(Voice%,17)+=1
ENDIF
ELSE
IF Y%<508 THEN
IF par%(Voice%,18)<40 par%(Voice%,18)+=1
ELSE
IF par%(Voice%,18)>2 par%(Voice%,18)-=1
ENDIF
ENDIF
PROCamplitude(7)
=TRUE
:
DEFPROCsetfre
LOCAL Null%
IF X%<12 Null%=par%(Voice%,16)
PROCfrequency(0)
IF Flag%=4 PROCsetbars(360,Null%,par%(Voice%,19))
IF Flag%=5 PROCsetbars(360,par%(Voice%,21),par%(Voice%,20))
ELSE PROCsetbars(360,Null%,par%(Voice%,22))
IF par%(Voice%,21)<2 par%(Voice%,21)=2
IF par%(Voice%,21)>=par%(Voice%,16) par%(Voice%,21)=par%(Voice%,16)-1
PROCfrequency(7)
ENDPROC
:
DEFFNsetvib
PROCfrequency(0)
IF X%<1152 THEN
IF Y%<316 THEN
IF par%(Voice%,23)>0 par%(Voice%,23)-=1
ELSE
IF par%(Voice%,23)<100 par%(Voice%,23)+=1
ENDIF
ELSE
IF Y%<316 THEN
IF par%(Voice%,24)<36 par%(Voice%,24)+=1
ELSE
IF par%(Voice%,24)>2 par%(Voice%,24)-=1
ENDIF
ENDIF
PROCfrequency(7)
=TRUE
:
DEFPROCsetbars(a%,RETURN H%,RETURN V%)
Env%=TRUE
H%=X% DIV 4
V%=Y%-a%
IF a%=488 THEN
IF V%>127 V%=127 ELSE IF V%<0 V%=0
ELSE
IF V%>64 V%=64 ELSE IF V%<-64 V%=-64
ENDIF
IF H%>255 H%=255
ENDPROC
:
DEFPROCfillwave(Voice%)
FOR B%=0 TO 255
A%=0
FOR J%=0 TO 9
IF par%(Voice%,J%) A%+=par%(Voice%,J%)*Sine%(B%,J%)
NEXT
w%(Voice%,B%)=A%>>24
C%=WaveTable%(Voice%)
CALL Log
NEXT
ENDPROC
:
DEFPROCenvelope(Voice%)
M%=AmpEnv%(Voice%)
level=0
PROCamp_env(par%(Voice%,11)/par%(Voice%,12),par%(Voice%,12))
PROCamp_env((par%(Voice%,13)-par%(Voice%,11))/(par%(Voice%,14)
-par%(Voice%,12)),par%(Voice%,14)-par%(Voice%,12))
S%=par%(Voice%,14)+par%(Voice%,18)
sus=(par%(Voice%,13)-par%(Voice%,15))/(par%(Voice%,16)-par%(Voice%,14))*4
WHILE S%<par%(Voice%,16)
PROCamp_env(-sus-par%(Voice%,17)/par%(Voice%,18),par%(Voice%,18))
S%+=par%(Voice%,18)
IF S%<par%(Voice%,16) PROCamp_env(0,par%(Voice%,18)):S%+=par%(Voice%,18)
IF S%<par%(Voice%,16) PROCamp_env(par%(Voice%,17)/par%(Voice%,18),
par%(Voice%,18)):S%+=par%(Voice%,18)
IF S%<par%(Voice%,16) PROCamp_env(0,par%(Voice%,18)):S%+=par%(Voice%,18)
ENDWHILE
IF M%-AmpEnv%(Voice%)<256 ?M%=255 ELSE AmpEnv%(Voice%)?255=255
M%=FreEnv%(Voice%)
PROCfre_env(par%(Voice%,19),1)
PROCfre_env((par%(Voice%,20)-par%(Voice%,19))/(par%(Voice%,21)-1),par%(Voice%,21))
S%=par%(Voice%,21)+par%(Voice%,24)
sus=(par%(Voice%,20)-par%(Voice%,22))/(par%(Voice%,16)-par%(Voice%,21))*2
WHILE S%<par%(Voice%,16)
PROCfre_env(-sus-par%(Voice%,23)/par%(Voice%,24),par%(Voice%,24))
S%+=par%(Voice%,24)
IF S%<par%(Voice%,16) PROCfre_env(par%(Voice%,23)/par%(Voice%,24),
par%(Voice%,24)):S%+=par%(Voice%,24)
ENDWHILE
IF M%-FreEnv%(Voice%)>255 THEN
M%?255=0
ELSE
WHILE M%-FreEnv%(Voice%)<256
?M%=0
M%+=1
ENDWHILE
ENDIF
ENDPROC
:
DEFPROCamp_env(s,n%)
FOR I%=1 TO n%
level+=s
IF level>127 ?M%=0 ELSE IF level<0 ?M%=127 ELSE ?M%=127-level
M%+=1
NEXT
ENDPROC
:
DEFPROCfre_env(s%,n%)
FOR I%=1 TO n%
?M%=s%
M%+=1
NEXT
ENDPROC
:
DEFPROCharmonic(Col%)
GCOL 0,Col%
FOR I%=0 TO 9
MOVE I%*64+576,822:PLOT 1,0,par%(Voice%,I%)
NEXT
ENDPROC
:
DEFPROCamplitude(Col%)
GCOL0,Col%:MOVE 0,488:PLOT 1,par%(Voice%,12)*4,par%(Voice%,11)
PROCpeg
PLOT 1,(par%(Voice%,14)-par%(Voice%,12))*4,(par%(Voice%,13)-par%(Voice%,11))
PROCpeg
S%=par%(Voice%,14)+par%(Voice%,18)
sus=(par%(Voice%,13)-par%(Voice%,15))/(par%(Voice%,16)-par%(Voice%,14))*4
WHILE S%<par%(Voice%,16)
PLOT 1,par%(Voice%,18)*4,-sus*par%(Voice%,18)-par%(Voice%,17)
S%+=par%(Voice%,18)
IF S%<par%(Voice%,16) PLOT 1,par%(Voice%,18)*4,0:S%+=par%(Voice%,18)
IF S%<par%(Voice%,16) PLOT 1,par%(Voice%,18)*4,par%(Voice%,17):S%+=par%(Voice%,18)
IF S%<par%(Voice%,16) PLOT 1,par%(Voice%,18)*4,0:S%+=par%(Voice%,18)
ENDWHILE
PLOT 1,(par%(Voice%,16)-S%+par%(Voice%,18))*4,0
PROCpeg
ENDPROC
:
DEFPROCfrequency(Col%)
GCOL0,Col%:MOVE 0,360+par%(Voice%,19)
PROCpeg
PLOT 1,par%(Voice%,21)*4,par%(Voice%,20)-par%(Voice%,19)
PROCpeg
S%=par%(Voice%,21)+par%(Voice%,24)
sus=(par%(Voice%,20)-par%(Voice%,22))/(par%(Voice%,16)-par%(Voice%,21))*2
WHILE S%<par%(Voice%,16)
PLOT 1,par%(Voice%,24)*4,-sus*par%(Voice%,24)-par%(Voice%,23)
S%+=par%(Voice%,24)
IF S%<par%(Voice%,16) PLOT 1,par%(Voice%,24)*4,par%(Voice%,23):S%+=par%(Voice%,24)
ENDWHILE
PLOT 1,(par%(Voice%,16)-S%+par%(Voice%,24))*4,0
PROCpeg
ENDPROC
:
DEFPROCpeg
IF Col% GCOL0,6 ELSE GCOL0,0
PLOT 0,0,16:PLOT 1,0,-32:PLOT 1,2,0:PLOT 1,0,32:PLOT 1,-2,0:PLOT 0,0,-16
GCOL0,Col%
ENDPROC
:
DEFPROCprompt
RESTORE+0
PROCdata_print(7)
ENDPROC
DATA 7,1
DATA 44,0,F1 - F12 For Voices,45,1,Escape For Options
:
DEFPROCname(Text$,RETURN Old$)
LOCAL x%,Input$
x%=17+LEN Text$
ON
REPEAT
PRINT'"Enter name of " Text$ " > " Old$;
INPUT TAB(x%,VPOS) "" Input$
UNTIL LEN Input$<11
IF Input$>"" Old$=Input$
ENDPROC
:
DEFPROCdata_print(Col%)
REPEAT
READ C%,J%
COLOUR C%
FOR I%=0 TO J%
READ U%,V%,a$
PRINT TAB(U%,V%)a$;
NEXT
UNTIL C%=Col%
ENDPROC
:
DEFPROChour(p%)
IF p%=FALSE SYS "Hourglass_Smash"
IF p%=TRUE SYS "Hourglass_On" ELSE SYS "Hourglass_Percentage",p%
ENDPROC
Clicking on PLAY will clear the other options and display your synthesizer keyboard. Two strips of letter keys are used, laid out piano style. On this keyboard white is for white notes and red for black notes.
The line ZXCVB has the letter Z as the note C and the line QWERTY with letter R as the note C one octave above Z. Up to eight notes will sound at any time. If a ninth note is pressed then the oldest one is lost. Pressing several keys at the same time won't always realise this number though as the negative inkey system can get confused.
You can step up and down octaves with the up and down cursor keys. Octave settings are -8, 0, 8. The change is instantaneous so you don't need to interrupt your playing when changing octaves. With -8 and no frequency envelope in use the Z key is middle C.
Function keys 1 to 12 will change voices reasonably quickly, usually fast enough for you to do this between notes whilst playing.
Pressing Escape returns to the edit mode. All cyan parts of the display are responsive to the mouse by dragging or clicking with Select . Generally yellow is used for information and green for outline limits, white indicates values that can be altered.
Clicking Select over any of the twelve listed voice names duplicates the function key action. This not only selects voices for playing but also for loading, saving and renaming.
Clicking Adjust on the voices will toggle them as enabled or disabled. Disabled voices are not selectable in play and will not be saved in the sound module. You can't disable the currently selected voice.
You can save the current voice by clicking on SAVE, or load a previously saved voice using LOAD. Prompts are given and filename is the displayed voice name. Saving and loading is to the currently selected function key number.
Saving a voice stores all its parameters, including the current octave. When loading a voice a check is made for file validity. Simply pressing Return when prompted for a filename will save or load to the currently displayed name.
Clicking on Rename changes the name of the current voice without altering any of its parameters.
Clicking on MODULE prompts for a filename in the same way as save and load, and then creates a re-locatable module of all the currently enabled voices. Therefore a library of from 1 to 12 different voices can be assembled.
Octave and Repeat are not stored in the module as these are a feature of the keyboard not the voices.
Editing the current voice is done simply by moving the mouse to the appropriate part of the display and clicking or dragging on the cyan parts.
When dragging envelope shape markers some of the white envelope outlines may go outside their frames. This does no harm but may give unpredictable results. Similarly the harmonic content can be set too high. This is easily identified by sharply re-entrant spikes in the waveshape and a sudden harshening of the sound.
The timbre is altered by summing sinewaves at the fundamental frequency and its main harmonics f1-f9. Random noise is added with f0 to give wind type sounds.
The amplitude envelope has three variable nodes, all of which can be changed vertically in amplitude and horizontally in time. These are attack, decay and sustain. Tremolo depth and speed modify the sustain phase only. It isn't usually possible to get zero tremolo amplitude, due to the way the sustain is created in chunks, however setting a very fast tremolo time will tend to smooth out the steps.
Careful selection of repeat time in conjunction with attack amplitude can be used to get almost continuous smooth sound.
The frequency envelope has a further three nodes. The attack and sustain nodes can only be altered vertically by frequency deviation, whereas decay can be altered horizontally in time as well. Vibrato depth and speed only modify the sustain phase.
|