TweetFollow Us on Twitter

Sound Made Simple
Volume Number:2
Issue Number:2
Column Tag:Pascal Procedures

Mac Sound Made Simple

By Alan Wootton, President, Top-Notch Productions, MacTutor Contributing Editor

This month we will attempt to coax some sounds from the Mac. We will try all three of the sound producing modes on the Mac. And, since there is a problem, I will present what I think is a better Pascal interface to the Sound Driver. Finally, I will introduce a method of saving sound effects in a standard form for later use.

The Sound Driver

The first source of information should be the Sound Driver chapter of Inside Macintosh. The first apparent fact about the Sound Driver is that it supports three different forms of synthesis. All three modes are reached by calling PROCEDURE StartSound( synthRec : Ptr ; NumBytes : LONGINT ; completionRtn : ProcPtr ) with synthRec set to the proper values. Since StartSound is defined in all three Pascals we are likely to use (Lisa Pascal, TML Pascal, and MacPascal; On-Stage Pascal was not available at the time of this writing), we will work in MacPascal and assume that our work is easily transportable to the others. As a driver, the Sound Driver will accept standard Device Manager control calls. There are predefined routines that make the driver calls for us in most cases. The Sound Driver is in the ROM and never needs an Open call (or a Close). As drivers go, it is fairly simple to deal with, so we won't go into extreme detail. Advanced users will want to use completion routines and other driver features.

Before we start making our first sound, it will be worthwhile to acknowledge how it is that the Mac can make sound at all. There is a special area in RAM where sound information is stored (much as there is a designated area of RAM that holds the screen information). Every time the video circuitry has completed painting one line on the screen, a byte is fetched from the sound RAM, and sent to a digital-to-analog converter. From there, the analog signal is sent to the speaker. A sound byte is processed 370 times for each 1/60th of a second. There are 60 'ticks', and also 60 frames of Mac video every second. This means that every second, 60*370=22,200 bytes, are processed by the sound circuitry. The Sound Bytes are meant to represent an output voltage at the speaker, with 255 being greatest and 0 being smallest (we will use 128 for a neutral point). So you see, the Mac is not much of a synthesizer at all. All it really can do is play back data. Any other functions desired will have to be provided for in software. Every tick the same 370 bytes in memory are used, so unless we want to hear the same thing over and over, 60 times per second, we will have to replace all 370 bytes repeatedly. If we had to do this ourselves we'd be talking assembly language now. Fortunately, the Sound Driver is in assembly language and it will do this for us. And it will do it three different ways.

Our main goal is to simply get program shells of the three sound modes in action, and to defer until later those data structures and routines one may use in a complex application.

The Square Wave mode

The square wave mode of synthesis appears to be the simplest. All the Sound Driver has to do, 60 times per second, is fill the sound buffer (370 bytes) with alternating values. If every other byte were 0 and 255, then we would get the loudest sound and the highest note (370/2 alternations per tick* 60 ticks per sec = 11,100 Hz). If one tick the 370 bytes were all 128, and the next tick they were all 129, we would get a very quiet note at 30 Hz. (You probably couldn't hear it from the Mac speaker). I don't think this is actually how the Sound Driver does it, but you get the idea.

Here is a MacPascal program that should get some different tones using the Square Wave Mode.

program Square_Wave_Scale;
   const
      notecount = 31;
   var
 i : integer;
 MySWSynthRec : record
 mode : integer;
 tones : array[1..notecount] of record
        count, amplitude, duration : integer;
         end;{ of tones }
   end;{ of mySWSynthRec }

begin
   with MySWSynthRec do
 begin
    mode := SWMode;
    for i := 1 to notecount do
    with tones[i] do
 begin
    count := i * 64 + 256;
    amplitude := 255 - i * 8;
    duration := 30;{ 1/2 sec. }
 end;
    StartSound(@MySWSynthRec,  sizeof(MySWSynthRec), nil);
    ShowText;
    Writeln('press mouse to stop');
    repeat
    until button;
 end;
   StopSound;
end.

The problem is that Square_Wave_scale does not work. The SWSynthRec is filled out correctly; the mode is set, and the tones are filled in. All is well, but there seems to be something wrong with StartSound. The likely culprit is the CompletionPtr parameter. According to the MacPascal manual, you pass the address of a procedure if you want that procedure to be executed when the sound is done. MacPascal will (currently) not allow you to get the address of any of its procedures. If you want the sound to start, and then finish before the program resumes, you are to use pointer(-1). When I did that the Mac locked up, and I had to reboot. If you want the sound to start, and then for the program to resume before the sound is done (seems like a good idea), use nil. When I did that nothing happened. Perhaps we should junk StartSound.

This brings up an interesting point. Clearly the point of this completion routine business is to be able to splice two sounds front to back with no seam. If our program has to wait for the first sound to finish, then there will be a delay (especially in MacPascal) before the next sound can be started. StartSound is going to have to make a Device Manager call (in this case PBWrite) to start the sound. To do that it will need to use a Parameter Block Record. What happens to that record when a second sound is started if the first is not done?

What must be done is to create our own version of StartSound. We will use two parameter block records. That way one can be filled out and then passed to the device managers I/O queue while the other is busy. Our sounds will follow one another naturally without much fuss or muss. The predefined procedure 'Generic' will be used to make a Device Manager call. Let's cover this strategy in more detail.

First, declare the procedure. We will call it myStartSound, and use the same parameter list, even though completionRtn will not be used.

Second, the Parameter Block Record. Instead of using the entire record declaration, found in the Device Manager chapter of Inside Macintosh, we will use an array of integers and use BlockMove to move data into the record. Note that the numbers used alongside the Device Manager routine descriptions must be divided by two to get an index into the array of words. Only ioRefNum, ioBuffer, and ioReqCount are used, so this is reasonable. You must declare the parameter blocks at the outermost level possible, ie. they must not go away because the procedure they are defined in is done. Also, note that MacPascal erases them to zero for us. In another Pascal you should do that during initialization.

Third, find a method of choosing which parameter block to use. The boolean variable AUsed is used to indicate which Parameter block was used last. It is toggled with every call. Once we know which block we are using, its ioResult field is checked to make sure it is done before modifying it.

Finally, we need a method of making a Device Manager Write call. The Sound Manager chapter tells us to set ioRefNum to -4, to set ioBuffer to point to the SynthRec, and to set ioReqCount to NumBytes. To make the call we declare an array of 13 longints which Generic will use to set the 68000 registers (A0 to A4 then D0 to D7. See Advanced Mac'ing, MacTutor, Vol.1 No. 5 for the introduction of Generic) for the toolbox call. The first longint in the array is A0 and that is set to the address of the Parameter Block. The number of the PBWrite toolbox routine is $A003, and we add $400 to it which causes asyncronous operation.

If you add the following to Square_Wave_Scale then it will work properly.

{ These two types are needed by MyStartSound }
   type
      Ptr = ^integer;
      ParamBlockFake = array[0..30] of integer;

{ put these variables in with the rest of the global var's }
{ var }
 blockA, blockB : ParamBlockFake;{ for myStartSound }
 AUsed : boolean;{ for MyStartSound }

{ use this procedure everywhere you might }
{ otherwise use MySound }
procedure MyStartSound (SynthRec : ptr;
               numbytes : longint;
     CompletionRtn : Ptr);
 { CompletionRtn ignored }
   var
 regs : array[0..12] of longint; { for generic }
 BlockPtr : ^ParamBlockFake;
   begin
 if Aused then
    BlockPtr := @BlockA
 else
    BlockPtr := @BlockB;
 Aused := not Aused;

 while BlockPtr^[8] <> 0 do { wait for ioResult to clear}
 ;
 BlockPtr^[12] := -4;{ set ioRefNum }
 BlockMove(@SynthRec, @BlockPtr^[16], 4);{ ioBuffer }
 BlockMove(@numbytes, @BlockPtr^[18], 4);{ioReqCount}

 { The following two lines do PBWrite(BlockPtr,true) }
 regs[0] := ord(BlockPtr);{ set A0 for Generic }
 Generic($A403, regs);{ Write,async }
   end;

Please note that all the previous technical talk you just encountered is for the sole purpose of creating a new version of StartSound. If you didn't understand it, don't worry. Just use myStartSound everywhere you would use StartSound, and set completionRtn to nil. What we have actually done is to simplify the situation, with the small cost of having to paste myStartSound into your sound programs.

The Four Tone mode

The Four Tone mode is a little more complicated. It provides a more complex synthesis function as well. The idea is that you provide a waveform table of bytes for each of four sounds. The Sound Driver will move through the tables, at a rate you specify, sum the four values for each byte, and place the result in the 370 byte sound buffer. Since it must sum 370*4 bytes 60 times per second, in addition to calculating which byte from which table to use, this takes about 1/120th of a second. When the Mac is making four tone sounds it appears to run at half speed.

The waveform tables are 256 bytes long. You may put into them any numbers you wish, thereby producing different sounds. We will talk more about waveforms later. The Sound Driver does not simply put the first byte in a wave table into the first byte of the sound buffer, and the second in the second, etc. What it seems to do is add the fixed (fixed numbers have an integer in the upper half, and the lower half is a fraction) quantity Rate to the (fixed?) quantity Phase, and then to take the result mod 256 as the index into the wave table. This means that a Rate of 2 will use every other byte in the wave table and a Rate of 1/2 will use every byte twice.

From a programming point of view all you have to do is fill out the data structures and call StartSound. The application point of view is left up to you. Let me now present a program that will make a single, pure tone using the Four Tone mode.

Program  Four_Tone_Test;
   uses
 Sane;
   type{ for MyStartSound }
 Ptr = ^integer;
 ParamBlockFake = array[0..30] of integer;
   var
 rate1 : integer;
 MyFTSynth : FTSynthRec;
 myFTSound : FTSoundRec;
 SinWave : packed array[0..255] of char;

 blockA, blockB : ParamBlockFake;{ for myStartSound }
 AUsed : boolean;{ for MyStartSound }

{ Paste myStartSound here }

{ Fill the array SinWave with bytes (chars) }
{ representing one cycle of a sine wave }
{ note that numbers are from 0 to 255 so that }
{ 128 is 'zero' }
   procedure FillSinWave;
 var
     i : integer;
    f, pi : extended;
   begin
 pi := arcTan(1) * 4;
 f := 2 * pi / 256;
 for i := 0 to 255 do
    SinWave[i] := chr(Num2Integer(sin(i * f) * 120 + 128));
   end;

begin { of main }
   FillSinWave;
   ShowText;

   MyFTSynth.mode := FTMode;
   MyFTSynth.SndRec := @MyFTSound;
   { Note that all MacPascal Records are initialised }
   { by the system to zero.  In another Pascal you may }
   { have to remember to initialize everything }
   { ie. when we start all rates are 0 }

   with MyFTSound do
 begin
    Duration := 1000;
    Sound1Wave := @SinWave[0];
    MyStartSound(@MyFTSynth, Sizeof(MyFTSynth), nil);
    rate1 := 1024;
 repeat
    Duration := 1000;
    Sound1Rate := FixRatio(rate1, 256);
    rate1 := rate1 + rate1 div 16;
    writeln('The rate is', rate1, ' div 256, = ',
                                             FixRatio(rate1, 256));
    if rate1 >= 2048 then
 rate1 := 1024;
 until Button;
    end;
 StopSound;
end.   

The Sane function Sin which you might normally think of as being used to find the vertical component of an angle (the angle is in radians, ie. 2*pi=360°, where pi is the ratio of the diameter to the circumference of a circle) is also used to produce the numbers required for a pure musical tone. Certain California Types might wax philosophical about this duality of function in nature, but we won't do that here (do California Types eat quiche?, do Real Programmers wax philosophical?). If you divide 2*pi by 256 and then multiply by a loop index that goes from 0 to 255, then you will get numbers corresponding to one complete waveform. This is what it looks like graphically (see fig. 1 top of next page).

Note that when you are using Four Tone mode, you don't have to call StartSound over and over if you don't want. If you keep the duration large then you can simply change the values for each of the four tones 'on the fly'. Remember, that while the sound is still going, the data structures belong to the Sound Driver. You may modify them, but do not delete them (like if they were declared inside a procedure that ended, or if you allocated them on the heap and then later disposed of them). When you are done, call StopSound, and all records being used are released.

Now that we have the Four Tone synthesizer working, try this program for a more interesting result. Sorry, but unlike graphic programs, there is no easy way to publish what the output of these programs is like. You have to type them in and see (I mean hear) for yourself.

program Four_part_sound;
   uses
 sane;
   const
 BaseNote = 1024;
   type
 Ptr = ^integer;
 ParamBlockFake = array[0..30] of integer;
   var
 rate1, rate2, rate3, rate4 : integer;
 MyFTSynth : FTSynthRec;
 myFTSound : FTSoundRec;
 SinWave : packed array[0..255] of char;

 blockA, blockB : ParamBlockFake;{ for myStartSound }
 AUsed : boolean;{ for MyStartSound }

{ Paste myStartSound here }

{ Paste FillSinWave here }

begin
   FillSinWave;
   ShowText;

   MyFTSynth.mode := FTMode;
   MyFTSynth.SndRec := @MyFTSound;

   with MyFTSound do
 begin
    Duration := 1000;{ will be made infinite later }

    Sound1Wave := @SinWave[0];
    Sound2Wave := @SinWave[0];
    Sound3Wave := @SinWave[0];
    Sound4Wave := @SinWave[0];

    MyStartSound(@MyFTSynth, Sizeof(MyFTSynth), nil);

    Rate1 := BaseNote;
    Rate2 := BaseNote;
    Rate3 := BaseNote;
    Rate4 := BaseNote;
    repeat
 begin
    Duration := 1000;{ keep going forever }
    Rate1 := Rate1 + Rate1 div 8;
    Sound1Rate := FixRatio(Rate1, 256);
    if Rate1 > 2 * BaseNote then
 begin
    Rate1 := BaseNote;
    Rate2 := Rate2 + Rate2 div 8;
    Sound2Rate := FixRatio(Rate2, 256);
    if Rate2 > 2 * BaseNote then
 begin
    Rate2 := BaseNote;
    Rate3 := Rate3 + Rate3 div 8;
    Sound3Rate := FixRatio(Rate3, 256);
    if Rate3 > 2 * BaseNote then
 begin
    Rate3 := BaseNote;
    Rate4 := Rate4 + Rate4 div 8;
    Sound4Rate := FixRatio(Rate4, 256);
     if Rate4 > 2 * BaseNote then
 Rate4 := BaseNote;
 end;{ rate3 }
 end;{ rate2 }
 end;{ rate 1 }
    end
 until button;
   end;{ with MyFTSound }
   StopSound;
end.

You may find it interesting to experment with different waveforms in this program. Those musical types among us will probably be able to modify the numbers and get a proper musical scale.

The Free Form mode

With the Free Form synthesizer you are able to play a digitized sound of almost any length and composition. The data used is simply an array with the sound bytes in it. Astonishingly realistic sounds can be produced. Some popular games for the Mac use this mode to create sound effects that are derived from recordings of real sounds.

In the examples I present we will calculate the data to produce and entire second of sound, ie. 22,200 bytes. Since it can take some time to calculate so many bytes, we will break the task into two parts. One program will calculate the bytes, and another will be used to play them. Once you have spent a long time producing a complicated sound effect it would be nice to have a place to put it until you want it again. The natural place would be the Scrapbook. In order to get to the Scrapbook it is necessary to first make it to the clipboard. In the Oct. 85 (Vol.1, No.11) issue of MacTutor I presented a routine to save Quickdraw pictures of type 'PICT' on the clipboard. The name of this was 'Pict_to_Clip'. Allow me to present Wave_to_Clip. You will find it embedded in the following program that calculates one second of a pure tone.

program FFSine_Wave_to_Clip;
   uses
 Sane;
   const
 WTSize = 22200;{ 370*60=one second of sound }
   type
 ptr = ^integer;
 Handle = ^ptr;

 MySynthH = ^MySynthP;
 MySynthP = ^MySynthRec;

 mySynthRec = record
    mode : integer;
    rate : fixed;
    WaveBytes : packed array[0..22199] of char;
 end;

   var
 waveH : MySynthH;
 i, n, ticks, samples : integer;
 pi, Freq : extended;

   procedure Wave_to_Clip (waveH : MySynthH;
          name : str255);
 var
    TheType, llength, lll : longint;
    str : str255;
 { The Hlock that is predefined does not work!!! }
 procedure Hlock (H : Handle);
    var
    regs : array[0..12] of longint;
 begin
    regs[0] := ord(h);{ set A0 }
    Generic($A029, regs);
 end;{ of Hlock }
   begin{ of Wave_to_clip }
 lll := LinlineF($A9FC);{ ZeroScrap }
 str := 'WAVE';
 BlockMove(@str[1], @TheType, 4);{ TheType:='WAVE' }
 llength := GetHandleSize(waveH);
 Hlock(pointer(ord(waveH)));
 lll := LinlineF($A9FE, llength, theType, waveH^);
 { PutScrap}
 Hunlock(pointer(ord(waveH)));

 llength := Length(name);
 str := 'TEXT';
 BlockMove(@str[1], @TheType, 4);{ TheType:='TEXT' }
 lll := LinlineF($A9FE, llength, theType, @name[1]);
 { PutScrap}
   end;

begin
   waveH := NewHandle(WTSize + 6);
   waveH^^.mode := FFMode;
   waveH^^.rate := FixRatio(1, 1);

   pi := arcTan(1) * 4;
   Freq := 2 * pi / 22200 * 1024;{1024 Hz }

   ShowDrawing;
   textmode(srcCopy);
   i := 0;
   for ticks := 1 to 60 do
 begin
    moveto(1, 256 - n);
    for samples := 1 to 370 do
 begin
    n := num2integer(sin(i * Freq) * 64 + 128);
    waveH^^.WaveBytes[i] := chr(n);
    lineto(samples, 256 - n);
    i := i + 1;
 end;
    DrawString(Stringof(ticks));
 end;
   Wave_to_Clip(waveH, 'Simple tone');
   DisposeHandle(waveH);
end.

The program FFSine_Wave_to_Clip first allocates, from the heap, a handle to some data of the correct size. The mode and rate fields (rate works the same as in Four Tone mode) are set and then a sine wave for 22,200 bytes is calculated. The sine wave calculation is much the same as when we calculated a wave table for Four Tone mode. The index, i, changes from 0 to 22199 and is multiplied by a frequency to obtain exactly 1024 full cycles. The purpose of using two nested loops, one for the samples, and for the ticks, is so that we can present a graphical depiction of what is happening (one ticks worth at a time). When the WaveBytes array is filled we pass it to Wave_to_Clip.

The first thing Wave_to_Clip does is to empty the clipboard of any prevoius data. This is the purpose of the ZeroScrap call (refer to the Scrap Manager chapter of Inside Mac). Then a BlockMove is done to move the ASCII bytes for 'WAVE' into a 4 byte data structure (ResType=packed array [0..3] of char is not assignable in MacPascal). In another Pascal you could just say TheType:='WAVE'. Then the length of the data is found and the data is put into the scrap, using PutScrap. When I first started doing this I ended up with lots of Scrapbook items of type 'WAVE', and the Scrapbook window would say "no data of type TEXT or PICT is available". There was no way of knowing what each Scrapbook item was. Then I found that if you put some TEXT on the scrap, then it would go along for the ride. This way the WAVEs in your scrapbook can have names. Therefore, when you call Wave_To_Clip you pass a string that will used to identify that wave.

Now that we have a sound in our Scrapbook let's look at a program to play it.

program Play_Clip_Wave;
   type
 Ptr = ^integer;
 ParamBlockFake = array[0..30] of integer;

 MySynthH = ^MySynthP;
 MySynthP = ^FFSynthRec;

   var
 waveH : MySynthH;

 TheType, offset, Size : Longint;
 str : str255;

 blockA, blockB : ParamBlockFake;
 AUsed : boolean;

{ Paste MyStartSound here }

begin
   ShowText;
   waveH := NewHandle(12);
   str := 'WAVE';
   BlockMove(@str[1], @TheType, 4);{ TheType:='WAVE' }
   size := LinlineF($A9FD, waveH, TheType, @offset);
 { GetScrap }

   if size > 0 then
 begin
    repeat
 myStartSound(waveH^, size - 6, nil);
    until button;
    StopSound;
          DisposeHandle(waveH);
 end
   else
 writeln('no scrap of type ''WAVE'' is available');
end. 

As you can see there is hardly anything to it. Once TheType is set up we call GetScrap which fills the handle with the data and returns the size of the data. If no data was returned then size is zero. This happens when you forget to copy the sound from the Scrapbook before you start.

I found two important things in the development of this routine. First of all, unless the length of the sound is an exact number of ticks, there will be a nasty click between each StartSound. This is because if the Sound Driver runs out of data, part way through the 370 bytes it is filling, it does not get the next sound right away. It waits until the next tick to pull the next sound command from the Device Managers I/O queue. Secondly, the examples in the Sound Driver chapter always pass sizeof(waveH^^) to StartSound. This is very wrong. You should pass the length of the actual sound data and not include the mode and rate field in the length. This is why we pass size-6 to myStartSound. Between the two of these I underwent a lot of grief (and missed my deadline) just to get the smooth, seamless, tone that FFSin_Wave_to_Clip and Play_Clip_Wave will make together.

Want to make a different sound? Just change the calculation. If you put this (below) in the nested loops you get a loud hiss, like the inside of a jet, or an FM radio tuned to nothing.

 n := random mod 256;
 waveH^^.WaveBytes[i] := chr(n);

I tried a different version of this that is much less wild. Instead of purely random numbers this (below) produces a wave form like a 'drunks walk'.

 repeat
    n := n - (random div 8192);
 until (n >= 0) and (n <= 255);
 waveH^^.WaveBytes[i] := chr(n);

Surprisingly, it sounded almost the same as the first, only softer. Also, since no provisions were made for the wave to end at the same value at which it started (it wanders all around), there is a faint click every second. Various combinations of random noise and pure tones might produce interesting effects. Also, you could use these methods to test methods of digital filtering (although not exactly in real time), or novel forms of music synthesis. Novel waveforms could also be used, and tested, in the Four Tone mode.

Fig. 1 Sound Representation

As a final exercise I have attempted to produce the "endless scale" effect. The effect is supposed to be one of a sound continuously increasing in pitch without ever going off the scale. A sort of aural illusion. The way you produce it is to combine many tones, some high, and some low. When they all are mixed (added) together they sound like one single noise. Then, as they all smoothly move together upward in pitch, the higher notes are faded out as new low notes are faded in. In my attempt I use one pure tone for each of 5 octaves (musically, two tones are an octave apart if the frequency of one is twice the frequency of the other). I had to work quite a while on the math to get it so that the last few cycles of the wave exactly match those of the first few. I did it, but the result is a less compelling illusion than I had hoped. Even though there is no definable place where the sound starts again, or drops lower. If you want to see for yourself then try the following program. Be warned, it takes 105 minutes to do the entire calculation. It would be much faster if I had used tables for the sine calculations.

I hoped you enjoyed this months expedition into the world of Macintosh sound, and I'll see you next month.

program FF_Endless_Scale_to_Clip;
   uses
 sane;
   const
   WTSize = 22200;{ 370*60=one second of sound }
   type
 ptr = ^integer;
 Handle = ^ptr;
 MySynthH = ^MySynthP;
 MySynthP = ^MySynthRec;

 mySynthRec = record
    mode : integer;
      rate : fixed;
      WaveBytes : packed array[0..22199] of char;
 { WTSize bytes }
 end;
   var
 waveH : MySynthH;
 j, n, ticks, retraces : integer;
 i, pi, pi_div2, pi_div2_5, T : extended;
 angle, inc, Octave, period, Period_div2 : extended;

 { Paste Wave_to_Clip here }

begin
   waveH := NewHandle(WTSize + 6);{ size of mySynthRec }
   waveH^^.mode := FFMode;
   waveH^^.rate := FixRatio(1, 1);

   pi := arcTan(1) * 4;
   pi_div2_5 := pi / 2.5;
   pi_div2 := pi / 2;
   Period := 1 / 22200;
   Period_div2 := period / 2;
   inc := 2 * pi / 22200 * 256;{ 256 cycles/sec }
   angle := 0;

   ShowDrawing;
   textmode(srcCopy);
   n := 128;
   for ticks := 0 to 59 do{ 60 ticks }
 begin
    moveto(0, 256 - n);
    for retraces := 0 to 369 do{ 370 retraces }
 begin
    i := retraces + ticks * 370;
    angle := inc * (i + (i * i + i) * period_div2);
    T := 0;
    Octave := Log2(1 + i * period) * pi_div2_5 - pi_div2;

    for j := 0 to 4 do
 begin
    T := T + Sin(angle) * (1 + Sin(Octave));
    angle := angle + angle;
    Octave := Octave + pi_div2_5;
 end;

 n := num2integer(T * 32 + 128);
 waveH^^.WaveBytes[num2integer(i)] := chr(n);
 lineto(retraces, 256 - n);
    end;{ of retraces loop }
 DrawString(Stringof(ticks));
   end;{ of ticks loop }
   Wave_to_Clip(waveH, 'Endless Scale Waveform');
   DisposeHandle(waveH);
end.
 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Latest Forum Discussions

See All

Top Mobile Game Discounts
Every day, we pick out a curated list of the best mobile discounts on the App Store and post them here. This list won't be comprehensive, but it every game on it is recommended. Feel free to check out the coverage we did on them in the links... | Read more »
Price of Glory unleashes its 1.4 Alpha u...
As much as we all probably dislike Maths as a subject, we do have to hand it to geometry for giving us the good old Hexgrid, home of some of the best strategy games. One such example, Price of Glory, has dropped its 1.4 Alpha update, stocked full... | Read more »
The SLC 2025 kicks off this month to cro...
Ever since the Solo Leveling: Arise Championship 2025 was announced, I have been looking forward to it. The promotional clip they released a month or two back showed crowds going absolutely nuts for the previous competitions, so imagine the... | Read more »
Dive into some early Magicpunk fun as Cr...
Excellent news for fans of steampunk and magic; the Precursor Test for Magicpunk MMORPG Crystal of Atlan opens today. This rather fancy way of saying beta test will remain open until March 5th and is available for PC - boo - and Android devices -... | Read more »
Prepare to get your mind melted as Evang...
If you are a fan of sci-fi shooters and incredibly weird, mind-bending anime series, then you are in for a treat, as Goddess of Victory: Nikke is gearing up for its second collaboration with Evangelion. We were also treated to an upcoming... | Read more »
Square Enix gives with one hand and slap...
We have something of a mixed bag coming over from Square Enix HQ today. Two of their mobile games are revelling in life with new events keeping them alive, whilst another has been thrown onto the ever-growing discard pile Square is building. I... | Read more »
Let the world burn as you have some fest...
It is time to leave the world burning once again as you take a much-needed break from that whole “hero” lark and enjoy some celebrations in Genshin Impact. Version 5.4, Moonlight Amidst Dreams, will see you in Inazuma to attend the Mikawa Flower... | Read more »
Full Moon Over the Abyssal Sea lands on...
Aether Gazer has announced its latest major update, and it is one of the loveliest event names I have ever heard. Full Moon Over the Abyssal Sea is an amazing name, and it comes loaded with two side stories, a new S-grade Modifier, and some fancy... | Read more »
Open your own eatery for all the forest...
Very important question; when you read the title Zoo Restaurant, do you also immediately think of running a restaurant in which you cook Zoo animals as the course? I will just assume yes. Anyway, come June 23rd we will all be able to start up our... | Read more »
Crystal of Atlan opens registration for...
Nuverse was prominently featured in the last month for all the wrong reasons with the USA TikTok debacle, but now it is putting all that behind it and preparing for the Crystal of Atlan beta test. Taking place between February 18th and March 5th,... | Read more »

Price Scanner via MacPrices.net

AT&T is offering a 65% discount on the ne...
AT&T is offering the new iPhone 16e for up to 65% off their monthly finance fee with 36-months of service. No trade-in is required. Discount is applied via monthly bill credits over the 36 month... Read more
Use this code to get a free iPhone 13 at Visi...
For a limited time, use code SWEETDEAL to get a free 128GB iPhone 13 Visible, Verizon’s low-cost wireless cell service, Visible. Deal is valid when you purchase the Visible+ annual plan. Free... Read more
M4 Mac minis on sale for $50-$80 off MSRP at...
B&H Photo has M4 Mac minis in stock and on sale right now for $50 to $80 off Apple’s MSRP, each including free 1-2 day shipping to most US addresses: – M4 Mac mini (16GB/256GB): $549, $50 off... Read more
Buy an iPhone 16 at Boost Mobile and get one...
Boost Mobile, an MVNO using AT&T and T-Mobile’s networks, is offering one year of free Unlimited service with the purchase of any iPhone 16. Purchase the iPhone at standard MSRP, and then choose... Read more
Get an iPhone 15 for only $299 at Boost Mobil...
Boost Mobile, an MVNO using AT&T and T-Mobile’s networks, is offering the 128GB iPhone 15 for $299.99 including service with their Unlimited Premium plan (50GB of premium data, $60/month), or $20... Read more
Unreal Mobile is offering $100 off any new iP...
Unreal Mobile, an MVNO using AT&T and T-Mobile’s networks, is offering a $100 discount on any new iPhone with service. This includes new iPhone 16 models as well as iPhone 15, 14, 13, and SE... Read more
Apple drops prices on clearance iPhone 14 mod...
With today’s introduction of the new iPhone 16e, Apple has discontinued the iPhone 14, 14 Pro, and SE. In response, Apple has dropped prices on unlocked, Certified Refurbished, iPhone 14 models to a... Read more
B&H has 16-inch M4 Max MacBook Pros on sa...
B&H Photo is offering a $360-$410 discount on new 16-inch MacBook Pros with M4 Max CPUs right now. B&H offers free 1-2 day shipping to most US addresses: – 16″ M4 Max MacBook Pro (36GB/1TB/... Read more
Amazon is offering a $100 discount on the M4...
Amazon has the M4 Pro Mac mini discounted $100 off MSRP right now. Shipping is free. Their price is the lowest currently available for this popular mini: – Mac mini M4 Pro (24GB/512GB): $1299, $100... Read more
B&H continues to offer $150-$220 discount...
B&H Photo has 14-inch M4 MacBook Pros on sale for $150-$220 off MSRP. B&H offers free 1-2 day shipping to most US addresses: – 14″ M4 MacBook Pro (16GB/512GB): $1449, $150 off MSRP – 14″ M4... Read more

Jobs Board

All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.