Classic MacOS Sound Resource: format specification

Sound resources were introduced in Classic MacOS with the Sound Manager program. They can contain sound commands to generate sounds with given frequencies as well as sampled sound data. They are mostly found in resource forks, but can occasionally appear standalone or embedded in other files.

Application

Sound Manager

KS implementation details

License: MIT

References

This page hosts a formal specification of Classic MacOS Sound Resource using Kaitai Struct. This specification can be automatically translated into a variety of programming languages to get a parsing library.

Block diagram

Format specification in Kaitai Struct YAML

meta:
  id: mac_os_resource_snd
  title: Classic MacOS Sound Resource
  application: Sound Manager
  xref:
    mac-os-resource-type: 'snd '
    wikidata: Q7564684
  license: MIT
  endian: be
doc: |
  Sound resources were introduced in Classic MacOS with the Sound Manager program.
  They can contain sound commands to generate sounds with given frequencies as well as sampled sound data.
  They are mostly found in resource forks, but can occasionally appear standalone or embedded in other files.
doc-ref: "https://developer.apple.com/library/archive/documentation/mac/pdf/Sound/Sound_Manager.pdf"
seq:
  - id: format
    type: u2
  - id: num_data_formats
    -orig-id: number_of_data_formats
    type: u2
    if: format == 1
  - id: data_formats
    type: data_format
    repeat: expr
    repeat-expr: num_data_formats
    if: format == 1
  - id: reference_count
    type: u2
    if: format == 2
  - id: num_sound_commands
    -orig-id: number_of_sound_commands
    type: u2
  - id: sound_commands
    type: sound_command
    repeat: expr
    repeat-expr: num_sound_commands
instances:
  midi_note_to_frequency:
    value: |
      [
        8.18, 8.66, 9.18, 9.72, 10.30, 10.91, 11.56, 12.25,
        12.98, 13.75, 14.57, 15.43, 16.35, 17.32, 18.35, 19.45,
        20.60, 21.83, 23.12, 24.50, 25.96, 27.50, 29.14, 30.87,
        32.70, 34.65, 36.71, 38.89, 41.20, 43.65, 46.25, 49.00,
        51.91, 55.00, 58.27, 61.74, 65.41, 69.30, 73.42, 77.78,
        82.41, 87.31, 92.50, 98.00, 103.83, 110.00, 116.54, 123.47,
        130.81, 138.59, 146.83, 155.56, 164.81, 174.61, 185.00, 196.00,
        207.65, 220.00, 233.08, 246.94, 261.63, 277.18, 293.66, 311.13,
        329.63, 349.23, 369.99, 392.00, 415.30, 440.00, 466.16, 493.88,
        523.25, 554.37, 587.33, 622.25, 659.26, 698.46, 739.99, 783.99,
        830.61, 880.00, 932.33, 987.77, 1046.50, 1108.73, 1174.66, 1244.51,
        1318.51, 1396.91, 1479.98, 1567.98, 1661.22, 1760.00, 1864.66, 1975.53,
        2093.00, 2217.46, 2349.32, 2489.02, 2637.02, 2793.83, 2959.96, 3135.96,
        3322.44, 3520.00, 3729.31, 3951.07, 4186.01, 4434.92, 4698.64, 4978.03,
        5274.04, 5587.65, 5919.91, 6271.93, 6644.88, 7040.00, 7458.62, 7902.13,
        8372.02, 8869.84, 9397.27, 9956.06, 10548.08, 11175.30, 11839.82, 12543.85
      ]
    doc: |
      Lookup table to convert a MIDI note into a frequency in Hz
      The lookup table represents the formula (2 ** ((midi_note - 69) / 12)) * 440
    doc-ref: https://en.wikipedia.org/wiki/MIDI_tuning_standard
types:
  data_format:
    seq:
      - id: id
        type: u2
        enum: data_type
      - id: options
        type: u4
        doc: contains initialisation options for the SndNewChannel function
    instances:
      wave_init_channel_mask:
        value: 0x07
        doc: wave table only, Sound Manager 2.0 and earlier
      wave_init:
        value: options & wave_init_channel_mask
        enum: wave_init_option
        if: id == data_type::wave_table_synth
      init_pan_mask:
        -orig-id: initPanMask
        value: 0x0003
        doc: mask for right/left pan values
      pan_init:
        value: options & init_pan_mask
        enum: init_option
      init_stereo_mask:
        -orig-id: initStereoMask
        value: 0x00C0
        doc: mask for mono/stereo values
      stereo_init:
        value: options & init_stereo_mask
        enum: init_option
      init_comp_mask:
        -orig-id: initCompMask
        value: 0xFF00
        doc: mask for compression IDs
      comp_init:
        value: options & init_comp_mask
        enum: init_option
  sound_command:
    seq:
      - id: is_data_offset
        type: b1
      - id: cmd
        type: b15
        enum: cmd_type
      - id: param1
        type: u2
      - id: param2
        type: u4
    instances:
      sound_header:
        pos: param2
        type: sound_header
        if: is_data_offset and cmd == cmd_type::buffer_cmd
  sound_header:
    seq:
      - size: 0
        if: start_ofs < 0 # invoking the `start_ofs` value instance to save the current `_io.pos`
      - id: sample_ptr
        -orig-id: samplePtr
        type: u4
        doc: pointer to samples (or 0 if samples follow data structure)
      - id: num_samples
        -orig-id: length
        type: u4
        doc: number of samples
        if: sound_header_type == sound_header_type::standard
      - id: num_channels
        -orig-id: numChannels
        type: u4
        doc: number of channels in sample
        if: sound_header_type == sound_header_type::extended or sound_header_type == sound_header_type::compressed
      - id: sample_rate
        -orig-id: sampleRate
        type: unsigned_fixed_point
        doc: The rate at which the sample was originally recorded.
      - id: loop_start
        -orig-id: loopStart
        type: u4
        doc: loop point beginning
      - id: loop_end
        -orig-id: loopEnd
        type: u4
        doc: loop point ending
      - id: encode
        type: u1
        enum: sound_header_type
        doc: sample's encoding option
      - id: midi_note
        type: u1
        doc: base frequency of sample, expressed as MIDI note values, 60 is middle C
        -orig-id: baseFrequency
      - id: extended_or_compressed
        type: extended_or_compressed
        if: sound_header_type == sound_header_type::extended or sound_header_type == sound_header_type::compressed
      - id: sample_area
        -orig-id: sampleArea
        size: |
          sound_header_type == sound_header_type::standard ? num_samples :
          sound_header_type == sound_header_type::extended ? extended_or_compressed.num_frames * num_channels * extended_or_compressed.bits_per_sample / 8 :
          _io.size - _io.pos
        doc: sampled-sound data
        if: sample_ptr == 0
    instances:
      start_ofs:
        value: _io.pos
      base_freqeuncy:
        value: _root.midi_note_to_frequency[midi_note]
        #TODO: If https://github.com/kaitai-io/kaitai_struct/issues/216 is implemented:
        #TODO: value: (2 ** ((midi_note - 69) / 12)) * 440
        if: midi_note >= 0 and midi_note < 128
        doc: |
          base frequency of sample in Hz
          Calculated with the formula (2 ** ((midi_note - 69) / 12)) * 440
        doc-ref: https://en.wikipedia.org/wiki/MIDI_tuning_standard
      sound_header_type:
        pos: start_ofs + 20
        type: u1
        enum: sound_header_type
  extended_or_compressed:
    seq:
      - id: num_frames
        type: u4
      - id: aiff_sample_rate
        size: 10
        doc: rate of original sample (Extended80)
      - id: marker_chunk
        -orig-id: markerChunk
        type: u4
        doc: reserved
      - id: extended
        type: extended
        if: _parent.sound_header_type == sound_header_type::extended
      - id: compressed
        type: compressed
        if: _parent.sound_header_type == sound_header_type::compressed
      - id: bits_per_sample
        -orig-id: sampleSize
        type: u2
        doc: number of bits per sample
      - id: reserved
        -orig-id: futureUse1, futureUse2, futureUse3, futureUse4
        size: 14
        doc: reserved
        if: _parent.sound_header_type == sound_header_type::extended
  extended:
    seq:
      - id: instrument_chunk_ptr
        -orig-id: instrumentChunks
        type: u4
        doc: pointer to instrument info
      - id: aes_recording_ptr
        -orig-id: AESRecording
        type: u4
        doc: pointer to audio info
  compressed:
    seq:
      - id: format
        size: 4
        type: str
        encoding: ASCII
        doc: data format type
      - id: reserved
        -orig-id: futureUse2
        size: 4
      - id: state_vars_ptr
        -orig-id: stateVars
        type: u4
        doc: pointer to StateBlock
      - id: left_over_samples_ptr
        -orig-id: leftOverSamples
        type: u4
        doc: pointer to LeftOverBlock
      - id: compression_id
        -orig-id: compressionID
        type: s2
        doc: ID of compression algorithm
      - id: packet_size
        -orig-id: packetSize
        type: u2
        doc: number of bits per packet
      - id: synthesizer_id
        -orig-id: snthID
        type: u2
        doc: |
          Latest Sound Manager documentation specifies this field as:
          This field is unused. You should set it to 0.
          Inside Macintosh (Volume VI, 1991) specifies it as:
          Indicates the resource ID number of the 'snth' resource that was used to compress the packets contained in the compressed sound header.
        doc-ref: "https://vintageapple.org/inside_o/pdf/Inside_Macintosh_Volume_VI_1991.pdf Page 22-49, absolute page number 1169 in the PDF"
    instances:
      compression_type:
        value: compression_id
        enum: compression_type_enum
  unsigned_fixed_point:
    seq:
      - id: integer_part
        type: u2
      - id: fraction_part
        type: u2
    instances:
      value:
        value: integer_part + fraction_part/65535.0
enums:
  data_type:
    0x01: square_wave_synth
    0x03: wave_table_synth
    0x05: sampled_synth
  wave_init_option:
    0x04:
      id: channel0
      -orig-id: waveInitChannel0
      doc: Play sounds through the first wave-table channel
    0x05:
      id: channel1
      -orig-id: waveInitChannel1
      doc: Play sounds through the second wave-table channel
    0x06:
      id: channel2
      -orig-id: waveInitChannel2
      doc: Play sounds through the third wave-table channel
    0x07:
      id: channel3
      -orig-id: waveInitChannel3
      doc: Play sounds through the fourth wave-table channel
  init_option:
    0x0002:
      id: chan_left
      -orig-id: initChanLeft
      doc: left stereo channel
    0x0003:
      id: chan_right
      -orig-id: initChanRight
      doc: right stereo channel
    0x0004:
      id: no_interp
      -orig-id: initNoInterp
      doc: no linear interpolation
    0x0008:
      id: no_drop
      -orig-id: initNoDrop
      doc: no drop-sample conversion
    0x0080:
      id: mono
      -orig-id: initMono
      doc: monophonic channel
    0x00C0:
      id: stereo
      -orig-id: initStereo
      doc: stereo channel
    0x0300:
      id: mace3
      -orig-id: initMACE3
      doc: MACE 3:1
    0x0400:
      id: mace6
      -orig-id: initMACE6
      doc: MACE 6:1

  cmd_type:
    0:
      id: null_cmd
      -orig-id: nullCmd
      doc: do nothing
    3:
      id: quiet_cmd
      -orig-id: quietCmd
      doc: stop a sound that is playing
    4:
      id: flush_cmd
      -orig-id: flushCmd
      doc: flush a sound channel
    5:
      id: re_init_cmd
      -orig-id: reInitCmd
      doc: reinitialize a sound channel
    10:
      id: wait_cmd
      -orig-id: waitCmd
      doc: suspend processing in a channel
    11:
      id: pause_cmd
      -orig-id: pauseCmd
      doc: pause processing in a channel
    12:
      id: resume_cmd
      -orig-id: resumeCmd
      doc: resume processing in a channel
    13:
      id: call_back_cmd
      -orig-id: callBackCmd
      doc: execute a callback procedure
    14:
      id: sync_cmd
      -orig-id: syncCmd
      doc: synchronize channels
    15:
      id: empty_cmd
      -orig-id: emptyCmd
      doc: |
        If no other commands are pending in the sound channel after a
        resumeCmd command, the Sound Manager sends an emptyCmd command.
        The emptyCmd command is sent only by the Sound Manager and
        should not be issued by your application.
    24:
      id: available_cmd
      -orig-id: availableCmd
      doc: see if initialization options are supported
    25:
      id: version_cmd
      -orig-id: versionCmd
      doc: determine version
    26:
      id: total_load_cmd
      -orig-id: totalLoadCmd
      doc: report total CPU load
    27:
      id: load_cmd
      -orig-id: loadCmd
      doc: report CPU load for a new channel
    40:
      id: freq_duration_cmd
      -orig-id: freqDurationCmd
      doc: play a note for a duration
    41:
      id: rest_cmd
      -orig-id: restCmd
      doc: rest a channel for a duration
    42:
      id: freq_cmd
      -orig-id: freqCmd
      doc: change the pitch of a sound
    43:
      id: amp_cmd
      -orig-id: ampCmd
      doc: change the amplitude of a sound
    44:
      id: timbre_cmd
      -orig-id: timbreCmd
      doc: change the timbre of a sound
    45:
      id: get_amp_cmd
      -orig-id: getAmpCmd
      doc: get the amplitude of a sound
    46:
      id: volume_cmd
      -orig-id: volumeCmd
      doc: set volume
    47:
      id: get_volume_cmd
      -orig-id: getVolumeCmd
      doc: get volume
    60:
      id: wave_table_cmd
      -orig-id: waveTableCmd
      doc: install a wave table as a voice
    61:
      id: phase_cmd
      -orig-id: phaseCmd
      doc: Not documented
    80:
      id: sound_cmd
      -orig-id: soundCmd
      doc: install a sampled sound as a voice
    81:
      id: buffer_cmd
      -orig-id: bufferCmd
      doc: play a sampled sound
    82:
      id: rate_cmd
      -orig-id: rateCmd
      doc: set the pitch of a sampled sound
    85:
      id: get_rate_cmd
      -orig-id: getRateCmd
      doc: get the pitch of a sampled sound
  sound_header_type:
    0x00: standard
    0xFF: extended
    0xFE: compressed
  compression_type_enum:
    -2: variable_compression
    -1: fixed_compression
    0: not_compressed
    1: two_to_one
    2: eight_to_three
    3: three_to_one
    4: six_to_one