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

Tokkun Studio unveils alpha trailer for...
We are back on the MMORPG news train, and this time it comes from the sort of international developers Tokkun Studio. They are based in France and Japan, so it counts. Anyway, semantics aside, they have released an alpha trailer for the upcoming... | Read more »
Win a host of exclusive in-game Honor of...
To celebrate its latest Jujutsu Kaisen crossover event, Honor of Kings is offering a bounty of login and achievement rewards kicking off the holiday season early. [Read more] | Read more »
Miraibo GO comes out swinging hard as it...
Having just launched what feels like yesterday, Dreamcube Studio is wasting no time adding events to their open-world survival Miraibo GO. Abyssal Souls arrives relatively in time for the spooky season and brings with it horrifying new partners to... | Read more »
Ditch the heavy binders and high price t...
As fun as the real-world equivalent and the very old Game Boy version are, the Pokemon Trading Card games have historically been received poorly on mobile. It is a very strange and confusing trend, but one that The Pokemon Company is determined to... | Read more »
Peace amongst mobile gamers is now shatt...
Some of the crazy folk tales from gaming have undoubtedly come from the EVE universe. Stories of spying, betrayal, and epic battles have entered history, and now the franchise expands as CCP Games launches EVE Galaxy Conquest, a free-to-play 4x... | Read more »
Lord of Nazarick, the turn-based RPG bas...
Crunchyroll and A PLUS JAPAN have just confirmed that Lord of Nazarick, their turn-based RPG based on the popular OVERLORD anime, is now available for iOS and Android. Starting today at 2PM CET, fans can download the game from Google Play and the... | Read more »
Digital Extremes' recent Devstream...
If you are anything like me you are impatiently waiting for Warframe: 1999 whilst simultaneously cursing the fact Excalibur Prime is permanently Vault locked. To keep us fed during our wait, Digital Extremes hosted a Double Devstream to dish out a... | Read more »
The Frozen Canvas adds a splash of colou...
It is time to grab your gloves and layer up, as Torchlight: Infinite is diving into the frozen tundra in its sixth season. The Frozen Canvas is a colourful new update that brings a stylish flair to the Netherrealm and puts creativity in the... | Read more »
Back When AOL WAS the Internet – The Tou...
In Episode 606 of The TouchArcade Show we kick things off talking about my plans for this weekend, which has resulted in this week’s show being a bit shorter than normal. We also go over some more updates on our Patreon situation, which has been... | Read more »
Creative Assembly's latest mobile p...
The Total War series has been slowly trickling onto mobile, which is a fantastic thing because most, if not all, of them are incredibly great fun. Creative Assembly's latest to get the Feral Interactive treatment into portable form is Total War:... | Read more »

Price Scanner via MacPrices.net

Early Black Friday Deal: Apple’s newly upgrad...
Amazon has Apple 13″ MacBook Airs with M2 CPUs and 16GB of RAM on early Black Friday sale for $200 off MSRP, only $799. Their prices are the lowest currently available for these newly upgraded 13″ M2... Read more
13-inch 8GB M2 MacBook Airs for $749, $250 of...
Best Buy has Apple 13″ MacBook Airs with M2 CPUs and 8GB of RAM in stock and on sale on their online store for $250 off MSRP. Prices start at $749. Their prices are the lowest currently available for... Read more
Amazon is offering an early Black Friday $100...
Amazon is offering early Black Friday discounts on Apple’s new 2024 WiFi iPad minis ranging up to $100 off MSRP, each with free shipping. These are the lowest prices available for new minis anywhere... Read more
Price Drop! Clearance 14-inch M3 MacBook Pros...
Best Buy is offering a $500 discount on clearance 14″ M3 MacBook Pros on their online store this week with prices available starting at only $1099. Prices valid for online orders only, in-store... Read more
Apple AirPods Pro with USB-C on early Black F...
A couple of Apple retailers are offering $70 (28%) discounts on Apple’s AirPods Pro with USB-C (and hearing aid capabilities) this weekend. These are early AirPods Black Friday discounts if you’re... Read more
Price drop! 13-inch M3 MacBook Airs now avail...
With yesterday’s across-the-board MacBook Air upgrade to 16GB of RAM standard, Apple has dropped prices on clearance 13″ 8GB M3 MacBook Airs, Certified Refurbished, to a new low starting at only $829... Read more
Price drop! Apple 15-inch M3 MacBook Airs now...
With yesterday’s release of 15-inch M3 MacBook Airs with 16GB of RAM standard, Apple has dropped prices on clearance Certified Refurbished 15″ 8GB M3 MacBook Airs to a new low starting at only $999.... Read more
Apple has clearance 15-inch M2 MacBook Airs a...
Apple has clearance, Certified Refurbished, 15″ M2 MacBook Airs now available starting at $929 and ranging up to $410 off original MSRP. These are the cheapest 15″ MacBook Airs for sale today at... Read more
Apple drops prices on 13-inch M2 MacBook Airs...
Apple has dropped prices on 13″ M2 MacBook Airs to a new low of only $749 in their Certified Refurbished store. These are the cheapest M2-powered MacBooks for sale at Apple. Apple’s one-year warranty... Read more
Clearance 13-inch M1 MacBook Airs available a...
Apple has clearance 13″ M1 MacBook Airs, Certified Refurbished, now available for $679 for 8-Core CPU/7-Core GPU/256GB models. Apple’s one-year warranty is included, shipping is free, and each... Read more

Jobs Board

Seasonal Cashier - *Apple* Blossom Mall - J...
Seasonal Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Seasonal Fine Jewelry Commission Associate -...
…Fine Jewelry Commission Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) Read more
Seasonal Operations Associate - *Apple* Blo...
Seasonal Operations Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Read more
Hair Stylist - *Apple* Blossom Mall - JCPen...
Hair Stylist - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom Read more
Cashier - *Apple* Blossom Mall - JCPenney (...
Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom Mall Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.