Classic MacOS Sound Resource: Perl parsing library

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.

Perl source code to parse Classic MacOS Sound Resource

MacOsResourceSnd.pm

# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild

use strict;
use warnings;
use IO::KaitaiStruct 0.009_000;
use Encode;

########################################################################
package MacOsResourceSnd;

our @ISA = 'IO::KaitaiStruct::Struct';

sub from_file {
    my ($class, $filename) = @_;
    my $fd;

    open($fd, '<', $filename) or return undef;
    binmode($fd);
    return new($class, IO::KaitaiStruct::Stream->new($fd));
}

our $CMD_TYPE_NULL_CMD = 0;
our $CMD_TYPE_QUIET_CMD = 3;
our $CMD_TYPE_FLUSH_CMD = 4;
our $CMD_TYPE_RE_INIT_CMD = 5;
our $CMD_TYPE_WAIT_CMD = 10;
our $CMD_TYPE_PAUSE_CMD = 11;
our $CMD_TYPE_RESUME_CMD = 12;
our $CMD_TYPE_CALL_BACK_CMD = 13;
our $CMD_TYPE_SYNC_CMD = 14;
our $CMD_TYPE_EMPTY_CMD = 15;
our $CMD_TYPE_AVAILABLE_CMD = 24;
our $CMD_TYPE_VERSION_CMD = 25;
our $CMD_TYPE_TOTAL_LOAD_CMD = 26;
our $CMD_TYPE_LOAD_CMD = 27;
our $CMD_TYPE_FREQ_DURATION_CMD = 40;
our $CMD_TYPE_REST_CMD = 41;
our $CMD_TYPE_FREQ_CMD = 42;
our $CMD_TYPE_AMP_CMD = 43;
our $CMD_TYPE_TIMBRE_CMD = 44;
our $CMD_TYPE_GET_AMP_CMD = 45;
our $CMD_TYPE_VOLUME_CMD = 46;
our $CMD_TYPE_GET_VOLUME_CMD = 47;
our $CMD_TYPE_WAVE_TABLE_CMD = 60;
our $CMD_TYPE_PHASE_CMD = 61;
our $CMD_TYPE_SOUND_CMD = 80;
our $CMD_TYPE_BUFFER_CMD = 81;
our $CMD_TYPE_RATE_CMD = 82;
our $CMD_TYPE_GET_RATE_CMD = 85;

our $SOUND_HEADER_TYPE_STANDARD = 0;
our $SOUND_HEADER_TYPE_COMPRESSED = 254;
our $SOUND_HEADER_TYPE_EXTENDED = 255;

our $DATA_TYPE_SQUARE_WAVE_SYNTH = 1;
our $DATA_TYPE_WAVE_TABLE_SYNTH = 3;
our $DATA_TYPE_SAMPLED_SYNTH = 5;

our $WAVE_INIT_OPTION_CHANNEL0 = 4;
our $WAVE_INIT_OPTION_CHANNEL1 = 5;
our $WAVE_INIT_OPTION_CHANNEL2 = 6;
our $WAVE_INIT_OPTION_CHANNEL3 = 7;

our $INIT_OPTION_CHAN_LEFT = 2;
our $INIT_OPTION_CHAN_RIGHT = 3;
our $INIT_OPTION_NO_INTERP = 4;
our $INIT_OPTION_NO_DROP = 8;
our $INIT_OPTION_MONO = 128;
our $INIT_OPTION_STEREO = 192;
our $INIT_OPTION_MACE3 = 768;
our $INIT_OPTION_MACE6 = 1024;

our $COMPRESSION_TYPE_ENUM_VARIABLE_COMPRESSION = -2;
our $COMPRESSION_TYPE_ENUM_FIXED_COMPRESSION = -1;
our $COMPRESSION_TYPE_ENUM_NOT_COMPRESSED = 0;
our $COMPRESSION_TYPE_ENUM_TWO_TO_ONE = 1;
our $COMPRESSION_TYPE_ENUM_EIGHT_TO_THREE = 2;
our $COMPRESSION_TYPE_ENUM_THREE_TO_ONE = 3;
our $COMPRESSION_TYPE_ENUM_SIX_TO_ONE = 4;

sub new {
    my ($class, $_io, $_parent, $_root) = @_;
    my $self = IO::KaitaiStruct::Struct->new($_io);

    bless $self, $class;
    $self->{_parent} = $_parent;
    $self->{_root} = $_root || $self;;

    $self->_read();

    return $self;
}

sub _read {
    my ($self) = @_;

    $self->{format} = $self->{_io}->read_u2be();
    if ($self->format() == 1) {
        $self->{num_data_formats} = $self->{_io}->read_u2be();
    }
    if ($self->format() == 1) {
        $self->{data_formats} = ();
        my $n_data_formats = $self->num_data_formats();
        for (my $i = 0; $i < $n_data_formats; $i++) {
            push @{$self->{data_formats}}, MacOsResourceSnd::DataFormat->new($self->{_io}, $self, $self->{_root});
        }
    }
    if ($self->format() == 2) {
        $self->{reference_count} = $self->{_io}->read_u2be();
    }
    $self->{num_sound_commands} = $self->{_io}->read_u2be();
    $self->{sound_commands} = ();
    my $n_sound_commands = $self->num_sound_commands();
    for (my $i = 0; $i < $n_sound_commands; $i++) {
        push @{$self->{sound_commands}}, MacOsResourceSnd::SoundCommand->new($self->{_io}, $self, $self->{_root});
    }
}

sub midi_note_to_frequency {
    my ($self) = @_;
    return $self->{midi_note_to_frequency} if ($self->{midi_note_to_frequency});
    $self->{midi_note_to_frequency} = [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];
    return $self->{midi_note_to_frequency};
}

sub format {
    my ($self) = @_;
    return $self->{format};
}

sub num_data_formats {
    my ($self) = @_;
    return $self->{num_data_formats};
}

sub data_formats {
    my ($self) = @_;
    return $self->{data_formats};
}

sub reference_count {
    my ($self) = @_;
    return $self->{reference_count};
}

sub num_sound_commands {
    my ($self) = @_;
    return $self->{num_sound_commands};
}

sub sound_commands {
    my ($self) = @_;
    return $self->{sound_commands};
}

########################################################################
package MacOsResourceSnd::Extended;

our @ISA = 'IO::KaitaiStruct::Struct';

sub from_file {
    my ($class, $filename) = @_;
    my $fd;

    open($fd, '<', $filename) or return undef;
    binmode($fd);
    return new($class, IO::KaitaiStruct::Stream->new($fd));
}

sub new {
    my ($class, $_io, $_parent, $_root) = @_;
    my $self = IO::KaitaiStruct::Struct->new($_io);

    bless $self, $class;
    $self->{_parent} = $_parent;
    $self->{_root} = $_root || $self;;

    $self->_read();

    return $self;
}

sub _read {
    my ($self) = @_;

    $self->{instrument_chunk_ptr} = $self->{_io}->read_u4be();
    $self->{aes_recording_ptr} = $self->{_io}->read_u4be();
}

sub instrument_chunk_ptr {
    my ($self) = @_;
    return $self->{instrument_chunk_ptr};
}

sub aes_recording_ptr {
    my ($self) = @_;
    return $self->{aes_recording_ptr};
}

########################################################################
package MacOsResourceSnd::SoundHeader;

our @ISA = 'IO::KaitaiStruct::Struct';

sub from_file {
    my ($class, $filename) = @_;
    my $fd;

    open($fd, '<', $filename) or return undef;
    binmode($fd);
    return new($class, IO::KaitaiStruct::Stream->new($fd));
}

sub new {
    my ($class, $_io, $_parent, $_root) = @_;
    my $self = IO::KaitaiStruct::Struct->new($_io);

    bless $self, $class;
    $self->{_parent} = $_parent;
    $self->{_root} = $_root || $self;;

    $self->_read();

    return $self;
}

sub _read {
    my ($self) = @_;

    if ($self->start_ofs() < 0) {
        $self->{_unnamed0} = $self->{_io}->read_bytes(0);
    }
    $self->{sample_ptr} = $self->{_io}->read_u4be();
    if ($self->sound_header_type() == $MacOsResourceSnd::SOUND_HEADER_TYPE_STANDARD) {
        $self->{num_samples} = $self->{_io}->read_u4be();
    }
    if ( (($self->sound_header_type() == $MacOsResourceSnd::SOUND_HEADER_TYPE_EXTENDED) || ($self->sound_header_type() == $MacOsResourceSnd::SOUND_HEADER_TYPE_COMPRESSED)) ) {
        $self->{num_channels} = $self->{_io}->read_u4be();
    }
    $self->{sample_rate} = MacOsResourceSnd::UnsignedFixedPoint->new($self->{_io}, $self, $self->{_root});
    $self->{loop_start} = $self->{_io}->read_u4be();
    $self->{loop_end} = $self->{_io}->read_u4be();
    $self->{encode} = $self->{_io}->read_u1();
    $self->{midi_note} = $self->{_io}->read_u1();
    if ( (($self->sound_header_type() == $MacOsResourceSnd::SOUND_HEADER_TYPE_EXTENDED) || ($self->sound_header_type() == $MacOsResourceSnd::SOUND_HEADER_TYPE_COMPRESSED)) ) {
        $self->{extended_or_compressed} = MacOsResourceSnd::ExtendedOrCompressed->new($self->{_io}, $self, $self->{_root});
    }
    if ($self->sample_ptr() == 0) {
        $self->{sample_area} = $self->{_io}->read_bytes(($self->sound_header_type() == $MacOsResourceSnd::SOUND_HEADER_TYPE_STANDARD ? $self->num_samples() : ($self->sound_header_type() == $MacOsResourceSnd::SOUND_HEADER_TYPE_EXTENDED ? int((($self->extended_or_compressed()->num_frames() * $self->num_channels()) * $self->extended_or_compressed()->bits_per_sample()) / 8) : ($self->_io()->size() - $self->_io()->pos()))));
    }
}

sub start_ofs {
    my ($self) = @_;
    return $self->{start_ofs} if ($self->{start_ofs});
    $self->{start_ofs} = $self->_io()->pos();
    return $self->{start_ofs};
}

sub base_freqeuncy {
    my ($self) = @_;
    return $self->{base_freqeuncy} if ($self->{base_freqeuncy});
    if ( (($self->midi_note() >= 0) && ($self->midi_note() < 128)) ) {
        $self->{base_freqeuncy} = @{$self->_root()->midi_note_to_frequency()}[$self->midi_note()];
    }
    return $self->{base_freqeuncy};
}

sub sound_header_type {
    my ($self) = @_;
    return $self->{sound_header_type} if ($self->{sound_header_type});
    my $_pos = $self->{_io}->pos();
    $self->{_io}->seek(($self->start_ofs() + 20));
    $self->{sound_header_type} = $self->{_io}->read_u1();
    $self->{_io}->seek($_pos);
    return $self->{sound_header_type};
}

sub _unnamed0 {
    my ($self) = @_;
    return $self->{_unnamed0};
}

sub sample_ptr {
    my ($self) = @_;
    return $self->{sample_ptr};
}

sub num_samples {
    my ($self) = @_;
    return $self->{num_samples};
}

sub num_channels {
    my ($self) = @_;
    return $self->{num_channels};
}

sub sample_rate {
    my ($self) = @_;
    return $self->{sample_rate};
}

sub loop_start {
    my ($self) = @_;
    return $self->{loop_start};
}

sub loop_end {
    my ($self) = @_;
    return $self->{loop_end};
}

sub encode {
    my ($self) = @_;
    return $self->{encode};
}

sub midi_note {
    my ($self) = @_;
    return $self->{midi_note};
}

sub extended_or_compressed {
    my ($self) = @_;
    return $self->{extended_or_compressed};
}

sub sample_area {
    my ($self) = @_;
    return $self->{sample_area};
}

########################################################################
package MacOsResourceSnd::UnsignedFixedPoint;

our @ISA = 'IO::KaitaiStruct::Struct';

sub from_file {
    my ($class, $filename) = @_;
    my $fd;

    open($fd, '<', $filename) or return undef;
    binmode($fd);
    return new($class, IO::KaitaiStruct::Stream->new($fd));
}

sub new {
    my ($class, $_io, $_parent, $_root) = @_;
    my $self = IO::KaitaiStruct::Struct->new($_io);

    bless $self, $class;
    $self->{_parent} = $_parent;
    $self->{_root} = $_root || $self;;

    $self->_read();

    return $self;
}

sub _read {
    my ($self) = @_;

    $self->{integer_part} = $self->{_io}->read_u2be();
    $self->{fraction_part} = $self->{_io}->read_u2be();
}

sub value {
    my ($self) = @_;
    return $self->{value} if ($self->{value});
    $self->{value} = ($self->integer_part() + ($self->fraction_part() / 65535.0));
    return $self->{value};
}

sub integer_part {
    my ($self) = @_;
    return $self->{integer_part};
}

sub fraction_part {
    my ($self) = @_;
    return $self->{fraction_part};
}

########################################################################
package MacOsResourceSnd::SoundCommand;

our @ISA = 'IO::KaitaiStruct::Struct';

sub from_file {
    my ($class, $filename) = @_;
    my $fd;

    open($fd, '<', $filename) or return undef;
    binmode($fd);
    return new($class, IO::KaitaiStruct::Stream->new($fd));
}

sub new {
    my ($class, $_io, $_parent, $_root) = @_;
    my $self = IO::KaitaiStruct::Struct->new($_io);

    bless $self, $class;
    $self->{_parent} = $_parent;
    $self->{_root} = $_root || $self;;

    $self->_read();

    return $self;
}

sub _read {
    my ($self) = @_;

    $self->{is_data_offset} = $self->{_io}->read_bits_int_be(1);
    $self->{cmd} = $self->{_io}->read_bits_int_be(15);
    $self->{_io}->align_to_byte();
    $self->{param1} = $self->{_io}->read_u2be();
    $self->{param2} = $self->{_io}->read_u4be();
}

sub sound_header {
    my ($self) = @_;
    return $self->{sound_header} if ($self->{sound_header});
    if ( (($self->is_data_offset()) && ($self->cmd() == $MacOsResourceSnd::CMD_TYPE_BUFFER_CMD)) ) {
        my $_pos = $self->{_io}->pos();
        $self->{_io}->seek($self->param2());
        $self->{sound_header} = MacOsResourceSnd::SoundHeader->new($self->{_io}, $self, $self->{_root});
        $self->{_io}->seek($_pos);
    }
    return $self->{sound_header};
}

sub is_data_offset {
    my ($self) = @_;
    return $self->{is_data_offset};
}

sub cmd {
    my ($self) = @_;
    return $self->{cmd};
}

sub param1 {
    my ($self) = @_;
    return $self->{param1};
}

sub param2 {
    my ($self) = @_;
    return $self->{param2};
}

########################################################################
package MacOsResourceSnd::Compressed;

our @ISA = 'IO::KaitaiStruct::Struct';

sub from_file {
    my ($class, $filename) = @_;
    my $fd;

    open($fd, '<', $filename) or return undef;
    binmode($fd);
    return new($class, IO::KaitaiStruct::Stream->new($fd));
}

sub new {
    my ($class, $_io, $_parent, $_root) = @_;
    my $self = IO::KaitaiStruct::Struct->new($_io);

    bless $self, $class;
    $self->{_parent} = $_parent;
    $self->{_root} = $_root || $self;;

    $self->_read();

    return $self;
}

sub _read {
    my ($self) = @_;

    $self->{format} = Encode::decode("ASCII", $self->{_io}->read_bytes(4));
    $self->{reserved} = $self->{_io}->read_bytes(4);
    $self->{state_vars_ptr} = $self->{_io}->read_u4be();
    $self->{left_over_samples_ptr} = $self->{_io}->read_u4be();
    $self->{compression_id} = $self->{_io}->read_s2be();
    $self->{packet_size} = $self->{_io}->read_u2be();
    $self->{synthesizer_id} = $self->{_io}->read_u2be();
}

sub compression_type {
    my ($self) = @_;
    return $self->{compression_type} if ($self->{compression_type});
    $self->{compression_type} = $self->compression_id();
    return $self->{compression_type};
}

sub format {
    my ($self) = @_;
    return $self->{format};
}

sub reserved {
    my ($self) = @_;
    return $self->{reserved};
}

sub state_vars_ptr {
    my ($self) = @_;
    return $self->{state_vars_ptr};
}

sub left_over_samples_ptr {
    my ($self) = @_;
    return $self->{left_over_samples_ptr};
}

sub compression_id {
    my ($self) = @_;
    return $self->{compression_id};
}

sub packet_size {
    my ($self) = @_;
    return $self->{packet_size};
}

sub synthesizer_id {
    my ($self) = @_;
    return $self->{synthesizer_id};
}

########################################################################
package MacOsResourceSnd::ExtendedOrCompressed;

our @ISA = 'IO::KaitaiStruct::Struct';

sub from_file {
    my ($class, $filename) = @_;
    my $fd;

    open($fd, '<', $filename) or return undef;
    binmode($fd);
    return new($class, IO::KaitaiStruct::Stream->new($fd));
}

sub new {
    my ($class, $_io, $_parent, $_root) = @_;
    my $self = IO::KaitaiStruct::Struct->new($_io);

    bless $self, $class;
    $self->{_parent} = $_parent;
    $self->{_root} = $_root || $self;;

    $self->_read();

    return $self;
}

sub _read {
    my ($self) = @_;

    $self->{num_frames} = $self->{_io}->read_u4be();
    $self->{aiff_sample_rate} = $self->{_io}->read_bytes(10);
    $self->{marker_chunk} = $self->{_io}->read_u4be();
    if ($self->_parent()->sound_header_type() == $MacOsResourceSnd::SOUND_HEADER_TYPE_EXTENDED) {
        $self->{extended} = MacOsResourceSnd::Extended->new($self->{_io}, $self, $self->{_root});
    }
    if ($self->_parent()->sound_header_type() == $MacOsResourceSnd::SOUND_HEADER_TYPE_COMPRESSED) {
        $self->{compressed} = MacOsResourceSnd::Compressed->new($self->{_io}, $self, $self->{_root});
    }
    $self->{bits_per_sample} = $self->{_io}->read_u2be();
    if ($self->_parent()->sound_header_type() == $MacOsResourceSnd::SOUND_HEADER_TYPE_EXTENDED) {
        $self->{reserved} = $self->{_io}->read_bytes(14);
    }
}

sub num_frames {
    my ($self) = @_;
    return $self->{num_frames};
}

sub aiff_sample_rate {
    my ($self) = @_;
    return $self->{aiff_sample_rate};
}

sub marker_chunk {
    my ($self) = @_;
    return $self->{marker_chunk};
}

sub extended {
    my ($self) = @_;
    return $self->{extended};
}

sub compressed {
    my ($self) = @_;
    return $self->{compressed};
}

sub bits_per_sample {
    my ($self) = @_;
    return $self->{bits_per_sample};
}

sub reserved {
    my ($self) = @_;
    return $self->{reserved};
}

########################################################################
package MacOsResourceSnd::DataFormat;

our @ISA = 'IO::KaitaiStruct::Struct';

sub from_file {
    my ($class, $filename) = @_;
    my $fd;

    open($fd, '<', $filename) or return undef;
    binmode($fd);
    return new($class, IO::KaitaiStruct::Stream->new($fd));
}

sub new {
    my ($class, $_io, $_parent, $_root) = @_;
    my $self = IO::KaitaiStruct::Struct->new($_io);

    bless $self, $class;
    $self->{_parent} = $_parent;
    $self->{_root} = $_root || $self;;

    $self->_read();

    return $self;
}

sub _read {
    my ($self) = @_;

    $self->{id} = $self->{_io}->read_u2be();
    $self->{options} = $self->{_io}->read_u4be();
}

sub init_pan_mask {
    my ($self) = @_;
    return $self->{init_pan_mask} if ($self->{init_pan_mask});
    $self->{init_pan_mask} = 3;
    return $self->{init_pan_mask};
}

sub wave_init_channel_mask {
    my ($self) = @_;
    return $self->{wave_init_channel_mask} if ($self->{wave_init_channel_mask});
    $self->{wave_init_channel_mask} = 7;
    return $self->{wave_init_channel_mask};
}

sub init_stereo_mask {
    my ($self) = @_;
    return $self->{init_stereo_mask} if ($self->{init_stereo_mask});
    $self->{init_stereo_mask} = 192;
    return $self->{init_stereo_mask};
}

sub wave_init {
    my ($self) = @_;
    return $self->{wave_init} if ($self->{wave_init});
    if ($self->id() == $MacOsResourceSnd::DATA_TYPE_WAVE_TABLE_SYNTH) {
        $self->{wave_init} = ($self->options() & $self->wave_init_channel_mask());
    }
    return $self->{wave_init};
}

sub pan_init {
    my ($self) = @_;
    return $self->{pan_init} if ($self->{pan_init});
    $self->{pan_init} = ($self->options() & $self->init_pan_mask());
    return $self->{pan_init};
}

sub init_comp_mask {
    my ($self) = @_;
    return $self->{init_comp_mask} if ($self->{init_comp_mask});
    $self->{init_comp_mask} = 65280;
    return $self->{init_comp_mask};
}

sub stereo_init {
    my ($self) = @_;
    return $self->{stereo_init} if ($self->{stereo_init});
    $self->{stereo_init} = ($self->options() & $self->init_stereo_mask());
    return $self->{stereo_init};
}

sub comp_init {
    my ($self) = @_;
    return $self->{comp_init} if ($self->{comp_init});
    $self->{comp_init} = ($self->options() & $self->init_comp_mask());
    return $self->{comp_init};
}

sub id {
    my ($self) = @_;
    return $self->{id};
}

sub options {
    my ($self) = @_;
    return $self->{options};
}

1;