The SndKit lets you access the sound hardware with a minimum of effort. Recording and playback of sound are particularly easy; the software manages data buffering, communication with the UNIX devices, synchronization with the operating system, and other such necessities. It's designed to accommodate both casual use of sound effects as well as detailed examination and manipulation of sound data.
The SndKit consists of three main classes: Snd, SndView, and SndMeter.
The Snd class provides a number of methods that let you access, modify, and perform sound data. The methods fall into four categories:
Locating and storing sounds
Recording and playback
Editing
Sound data manipulation
While a Snd object uses the SndSoundStruct structure to represent its sound, you only need to be familiar with this structure if you're directly manipulating sound data.
Each Snd object represents a single sound. The Snd class provides four ways to install a sound in a Snd object. You can:
Record a sound using the CODEC microphone input.
Read sound data from a soundfile or Mach-O sound segment.
Retrieve a sound from the pasteboard.
Sound recording (and playback) is described in the next section. Described here are the methods that let you read sounds from a soundfile or Mach-O segment and retrieve them from the pasteboard. As a shortcut to finding sounds, the Snd class provides a global naming mechanism that lets you identify and locate sounds by name.
Also described here are methods that let you store your sound by writing it to a soundfile or placing it on the pasteboard.
Soundfiles are files on a disk that contain sound data. By convention, soundfile names are given a “.snd” extension. To read a soundfile into a Snd object, simply create the object and send it the readSoundfile: message:
#import <SndKit/SndKit.h> /* you must import this file */ . . . Snd *aSound = [[Snd alloc] init]; /* create a Snd object */ int result = [aSound readSoundfile: @"KneeSqueak.snd"]; /* read a file */ |
The data in the named soundfile is read into the Snd object. The given file name is a complete UNIX pathname and must include the extension; in the example, the soundfile is searched for in the current working directory. Like many of the Snd methods, readSoundfile: returns an error code; the complete list of errors codes is given in the description of the SNDSoundError() C function in SndKit Function References. Success is indicated by the code SND_ERR_NONE.
These two operations, initializing a Snd object and reading a soundfile, are combined in the initFromSoundfile: method:
Snd *aSound = [[Snd alloc] initFromSoundfile: @"KneeSqueak.snd"]; |
The method returns nil if the soundfile isn't found or if it can't be read. You can read a new soundfile into an existing Snd object at any time; the object's old sound is discarded.
Apple provides a number of short sound effects (useful as system beeps) that are stored in the directory /System/Library/Sounds. You can audition a soundfile by running the sndplay program from a Terminal shell window. For example:
sndplay /System/Library/Sounds/Frog.aiff |
Writing a soundfile from the data in a sound object is done by invoking the writeSoundfile: method:
[mySound writeSoundfile: @"FleaSigh.snd"]; |
Even if the Snd object contains fragmented data, the data in the soundfile will be compact. However, the Snd object's data will remain fragmented.
Placing a Snd object on the pasteboard lets you copy its data between running applications. To place a Snd on the pasteboard, invoke the writeToPasteboard method:
[mySound writeToPasteboard]; |
The object's data is compacted (if it's fragmented) and copied. The copy is then placed on the pasteboard.
To read data from the pasteboard into a Snd, invoke the initFromPasteboard: method:
id mySound = [[Snd alloc] initFromPasteboard]; |
The sound data currently on the pasteboard is copied into the receiver of the message. Since the pasteboard can contain only one sound at a time, the method doesn't require an argument to further identify the sound. If there isn't a sound on the pasteboard, initFromPasteboard returns nil.
The Snd class maintains an application-wide list of named Snd objects called the named Sound list. The addName:Sound: class method lets you name a Snd object and add it to the named Sound list:
/* Add a Snd to the named Sound list. */ id namedSound = [Snd addName: @"PopTop" sound: mySound]; /* Check for failure. */ if (namedSound == nil) . . . |
The names in the named Sound list are unique; if you try to add a Snd by a name that's already in use, the effort is denied and nil is returned.
You can also name a Snd and place it on the named Sound list by sending setName: to the object:
id namedSound = [mySound setName: @"RedRover"]; |
setName: can be used to change the name of a Snd that's already on the named Sound list.
The name method retrieves a Snd object's name, whether given in a setName: message or through the addName:sound: method.
Named Snds are visible to your entire application. To retrieve a named Snd and load a copy of its data into a new Snd object, invoke the findSoundFor:method:
id newRedButton = [Snd findSoundFor: @"RedButton"]; |
If findSoundFor: fails to find the Snd in the named Sound list, it gives its argument (the Snd name) a “.snd” suffix and looks for a soundfile in these directories (in order):
~/Library/Sounds/
/LocalLibrary/Sounds/ (on NeXTStep)
/NextLibrary/Sounds/ (on NeXTStep)
/Library/Sounds/ (on MacOS X)
/System/Library/Sounds/ (on MacOS X)
(~ represents the user's home directory.)
A Snd found through findSoundFor: is automatically added to the named Sound list.
To remove a named Snd from the named Sound list, invoke removeSoundForName:, passing the name of the object that you want to remove. Removing a named Snd neither frees the Snd nor changes the object's notion of its name (which it stores as an instance variable).
Identifying and locating Snds through the named Sound list is generally the most efficient way to access sound data. The data in a named Snd is shared by all the objects that retrieve it.
To record a sound into a Snd object, simply create the object and send it the record message:
id mySound = [[Snd alloc] init]; int errorCode = [mySound record]; |
Currently, the record method always records from the CODEC microphone input. The method returns immediately while the recording is performed by a background thread.
The value returned by record indicates the success or failure of the attempt to begin recording; SND_ERR_NONE indicates success.
The recording continues until the Snd object receives the stop message or until the Snd object can accommodate no more data. By default, the receiver of the record message is always set to accommodate ten minutes of 8 kHz mu-law sound (the type of sound data sent from the CODEC). You can set the size of the Snd object, prior to recording, to specify a different recording length. This is done through the setDataSize:dataFormat:samplingRate:channelCount:infoSize: method.
To play a sound, send the play message to the Snd object:
int errorCode = [mySound play]; |
Like recording, playback is performed by a background thread and the play method returns an error code. Playback continues until the entire sound is played or until the Snd object that initiated the playback receives the stop message.
A single Snd object can only perform one recording or playback operation at a time, thus the function of the stop method is never ambiguous: If the Snd is playing, stop stops the playback; if it's recording, it stops the recording.
You can temporarily suspend a playback or recording by sending the pause message to a Snd object. Like stop, the pause message halts whatever activity the Snd is currently engaged in; however, unlike stop, the Snd doesn't forget where it was. This allows the resume message to cause the Snd to continue its activity from the place at which it was paused.
The record, play, pause, resume, and stop methods (and the analogous action methods described in the next section) should only be used if you have a running NSApplication object. To create a command-line program (similar to sndrecord or sndplay), you can use methods to create Snd objects and read sound data, but you should use the C functions SNDStartRecording(), SNDStartPlaying(), and SNDStop() to perform the Snd.
The Snd class methods record:, play:, pause:, resume:, and stop: are designed to be used as part of the target/action mechanism described in the Openstep/Cocoa Concepts manual. Briefly, this mechanism lets you assign a selected message (the action) and an object id (the target) to a NSControl object such that when the user acts on the NSControl, the action message is sent to the target object. In the following example, the three methods are assigned as action messages to three different NSControl objects―in this case, NSButtons. The same Snd object is assigned as the NSButton's target:
/* Create a Snd object ... */ id mySound = [[Snd alloc] init]; /* ... and three NSButtons. */ id recordButton = [[NSButton alloc] init], playButton = [[NSButton alloc] init], stopButton = [[NSButton alloc] init]; /* Set the action messages. */ [recordButton setAction: @selector(record:)]; [playButton setAction: @selector(play:)]; [stopButton setAction: @selector(stop:)]; /* Set the targets. */ [recordButton setTarget: mySound]; [playButton setTarget: mySound]; [stopButton setTarget: mySound]; |
In response to the user's clicking the different NSButtons, the Snd object starts recording, starts playing, or stops one of these operations.
A Snd can have a delegate object. A Snd's delegate receives, asynchronously, the following messages as the Snd records or plays:
willPlay: is sent just before the Snd begins playing.
didPlay: is sent when the Snd finishes playing.
willRecord: is sent just before recording.
didRecord: is sent after recording.
hadError: is sent if playback or recording generates an error.
To set a Snd's delegate object, invoke the setDelegate: method:
[mySound setDelegate:SoundDelegate]; |
A message is sent to the delegate only if the delegate implements the method that the message invokes.
The Snd class defines methods that support cut, copy, and paste operations for sampled sound data:
copySamples:at:count: replaces the Snd's data with a copy of a portion of the data in its first argument, which must also be a Snd object.
insertSamples:at: inserts a copy of the first argument's sound data into the receiving Snd object.
deleteSamplesAt:count: deletes a portion of the Snd's data.
These methods all return SNDSoundError() type error codes (recall that SND_ERROR_NONE indicates success).
The operations described here are also implemented in a more convenient form in the SndView class; for example, replacing a portion of a Snd object with a portion of another Snd object requires all three methods listed above. By operating on a user-defined selection and using the pasteboard, the SndView implements this operation in a single paste: method. The SndView methods are less general than those in Snd, but if you want to include a simple graphic sound editor in your application, you should use the SndView methods rather than these. |
Deleting a portion of a Snd's data is direct; you simply invoke deleteSamplesAt:count:. For example:
/* Delete the beginning of mySound. */ int eCode = [mySound deleteSamplesAt:0 count:1000]; |
The first 1000 samples are deleted from the receiver of the message. The first argument specifies the beginning of the deletion in samples from the beginning of the data (counting from sample 0); the second argument is the number of samples to delete.
Copying a portion of one Snd and pasting it into another―or into itself, for that matter―requires the use of both copySamples:at:count and insertSamples:at:. In the following example, the beginning of mySound is repeated:
/* Create a stutter at the beginning of mySound. */ id tmpSound = [[Snd alloc] init]; int errorCode = [tmpSound copySamples: mySound at: 0 count: 1000]; if (errorCode == SND_ERROR_NONE) errorCode = [mySound insertSamples: tmpSound at: 0]; [tmpSound free]; |
First, the data in tmpSound is completely replaced by a copy of the first 1000 samples in mySound. Note that the copySamples:at:count method doesn't remove any data from its first argument, it simply copies the specified range of samples from the first argument into the receiver. Next, tmpSound is prepended to mySound, creating a repetition of the first 1000 samples in mySound. The insertSamples: method inserts a copy of the argument into the receiver. Thus, the argument can be freed after inserting.
The two Snd objects involved in the insertSamples:at: method (the receiver and the first argument) must be compatible: They must have the same format, sampling rate, and channel count. If possible, the data that's inserted into the receiver of insertSamples:at: is automatically converted to be compatible with the data already in the receiver (see the description of the SNDConvertSound() C function in SndKit Function References for a list of the conversions that are supported). An error code indicating that the insertion failed is returned if the two Snds aren't compatible or if the inserted data can't be converted.
Replacing is like copying and pasting, except that a region of the pasted-into Snd is destroyed to accommodate the new data. In the following example, the beginning of oneSound is replaced with a copy of the beginning of twoSound:
/* Replace the beginning of oneSound with that of twoSound. */ int tmpCode = [tmpSound copySamples:twoSound at:0 count:1000]; int inCode; if (tmpCode == SND_ERROR_NONE) { int oneCode = [oneSound deleteSamplesAt:0 count:1000]; if (oneCode == SND_ERROR_NONE) inCode = [oneSound insertSamples:tmpSound at:0]; } [tmpSound free]; /* Check inCode before performing further manipulations. */ . . . |
The editing methods described above only work on Snds that contain sampled data. The isEditable method is provided to quickly determine whether a Snd object can be edited. The method returns YES if the object can be edited, NO if it can't.
The compatibleWith: method takes a Snd object as its argument and returns YES if the argument and the receiver are compatible. (The method also returns YES if one of the objects is empty; in other words, it's OK to insert samples into an empty object.) This method is useful prior to invoking the insertSound:at: method.
Another handy method is sampleCount, which returns the number of sample frames contained in the receiver. A sample frame is a channel-independent count of the samples in a Snd. For example, sending sampleCount to a two-channel Snd that contains three seconds worth of data returns the same value as sending it to a one-channel Snd that also contains three seconds of data (given that the two Snds have the same sampling rate), even though the two-channel Snd actually contains twice as much data.
The Snd class defines three more editing methods:
copy returns a new Snd object that's a copy of the receiver.
copySound: takes a Snd object as an argument and replaces the data in the receiver with the data in its argument. Since the entire range of data in the receiver is replaced, it needn't be editable, nor must the two Snds be compatible.
deleteSamples can only be sent to an editable Snd. It deletes the receiver's sound data.
A Snd's data is normally contiguous in memory. However, when you edit a Snd object, its data can become fragmented, or discontiguous. Fragmentation is explained in the description of the SndSoundStruct, earlier in this chapter. Briefly, fragmentation lets you edit Snds without incurring the cost of moving large sections of data in memory. However, fragmented Snds can be less efficient to play. The needsCompacting and compactSamples methods are provided to determine if a Snd is fragmented and to compact it. Note that compacting a large Snd that has been mercilessly fragmented can take a noticeably long time.
The SndView class provides a mechanism for displaying the sound data contained in a Snd object. While SndView inherits from the Application Kit's NSView class, it implements a number of methods that are also defined in Snd, such as play:, record:, and stop:. In addition, it implements editing methods such as cut:, copy:, and paste:.
SndViews are designed to be used within a NSScrollView. While you can create a SndView without placing it in a NSScrollView, its utility―particularly as it's used to display a large Snd―is limited.
To display a sound, you create a new SndView with a particular frame, give it a Snd object to display (through setSound:), and then send the display message to the SndView:
/* Create a new SndView object. */ id mySoundView = [[SndView alloc] initFrame: &svRect]; /* Set its Snd object. */ [mySoundView setSound:mySound]; /* Display the Snd object's sound data. */ [mySoundView display]; |
In the example, svRect is a previously defined NSRect. If autodisplaying is turned on (as set through NSView's setAutodisplay: method), you needn't send the display message; simply setting the Snd will cause the SndView to be displayed.
For most complete sounds, the length of the Snd's data in samples is greater than the horizontal length of the SndView in display units. The SndView employs a reduction factor to determine the ratio of samples to display units and plots the minimum and maximum amplitude values of the samples within that ratio. For example, a reduction factor of 10.0 means that the minimum and maximum values among the first ten samples are plotted in the first display unit, the minimum and maximum values of the next ten samples are displayed in the second display unit and so on. You can set the reduction factor through the setReductionFactor: method.
Changing the reduction factorchanges the time scale of the object. As you increase the reduction factor, more “sound-per-inch” is displayed. Of course, since more samples are used in computing the average amplitude, the resolution in a SndView with a heightened reduction factor is degraded. Conversely, reducing the reduction factor displays fewer samples per display unit but with an improved resolution. You should be aware that changing the reduction factor on a large sound can take a noticeably long time.
In a SndView, time runs from left to right; amplitude is represented on the y-axis, with 0.0 amplitude in the (vertical) center. When you set a SndView's Snd, the amplitude data that's displayed is automatically scaled to fit within the given height of the SndView.
The manner in which a SndView's horizontal dimension is computed depends on the object's autoscale flag. If autoscaling is turned off, the length of a SndView's frame is resized to fit the length of the Snd object's data while maintaining a constant reduction factor. In other words, a SndView that's displaying a Snd that contains 10000 samples will be twice as long as one with a Snd that contains 5000 samples, given the same reduction factor in either SndView.
Whenever the displayed data changes, due to editing or recording, the SndView is resized to fit the length of the new data. This is particularly useful in a SndView that's inside a NSScrollView: The NSScrollView determines the portion of data that's actually displayed, while the SndView maintains a constant time scale. Changing the reduction factor with autoscaling turned off causes the SndView to zoom in or out on the displayed data.
You can enable autoscaling by sending the message:
/* Enable autoscale. */ [mySoundView setAutoscale:YES]; |
With autoscale enabled, the SndView's frame size is maintained regardless of the length of the SndView's Snd data. Instead, the reduction factor is recomputed so the length of the data will fit within the frame. When autoscaling is on, invoking setReductionFactor: has no effect.
A SndView can display a sound as a continuous waveform, such as you would see on an oscilloscope, or as an outline of its maximum and minimum amplitudes. You set a SndView's display mode by sending it the setDisplayMode: message with one of the following SndKit constants as an argument:
Table 2-3. Display Mode Constants
Constant | Meaning |
---|---|
SK_DISPLAY_WAVE | Waveform display |
SK_DISPLAY_MINMAX | Amplitude outline display |
Waveform display is the default.
The SndView class provides a selection mechanism. You can selectively enable the selection mechanism for each SndView object by sending the setEnabled:YES message. When you drag in an enabled SndView display, the selected region is highlighted. The method getSelection:size: returns, by reference, the number of the first sample and the number of samples in the selection.