#! perl
#
# catAIFC.pl concatenates AIFC and RIFF WAVE files in a single output stream.
# All files MUST have identical audio formats and structures (channels, sample rates etc.). 
# ALL sound chunks are combined. Compressed files might not be concattenated
# correctly. Mixing AIFC and WAVE files will result in useless files.
# Note that ALL non-essential chunks are dropped.
# catAIFC.pl is fairly inefficient. It will pass over all files twice. Once to
# determine their length, once to actually output data.
#
# version 1.0
# 14 September 1999
#
# use:
# catAIFC.pl file file ... > concatenatedFile.aifc
#
# or
#
# require 'catAIFC.pl'; catAIFC(list of file names); # prints to $FileOutput
#
###############################################################################
#
# Author and Copyright:
# Rob van Son, © 1999
# Institute of Phonetic Sciences & IFOTT
# University of Amsterdam
# Herengracht 338
# NL-1016CG Amsterdam, The Netherlands 
# Email: Rob.van.Son@hum.uva.nl
#        rob.van.son@workmail.com
# WWW  : http://www.fon.hum.uva.nl/rob
#
# License for use and disclaimers
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
#
#########################################################################
#
# File pointer to write to (can be changed to a file)
my $FileOutput = STDOUT;

#
my $SoundFileType = 'AIFC';    # Current file type 
# Current Byte order (un)pack parameter (Big-Endian vs Little Endian)
my $EndianOrder = "N";      # (Normal Internet Byte-Order: Big-Endian)

# Get the header of the chunk
sub GetChunkInfo    # (FILE) -> ($ckID, $ckDataSize [, $formType])
{
    my $AudioFile = shift;
    #
    my @result = ();
    my $ckID;
    sysread($AudioFile, $ckID, 4) || return ();
    push (@result, $ckID);
    #
    my $ckDataSize;
    sysread($AudioFile, $ckDataSize, 4) || die "$AudioFile datasize read error: $!\n";
    $EndianOrder = "V" if $ckID eq 'RIFF';    # Non-Internet Byte Order
    $ckDataSize = unpack("$EndianOrder", $ckDataSize);   
    # The stored $ckDataSize is a SIGNED 32 bits long integer. Check for negative sizes
    die "Datasize < 0 error: $ckDataSize\n" if $ckDataSize > 2147483648;
    push (@result, $ckDataSize);
    #
    if($ckID eq 'FORM' || $ckID eq 'RIFF')
    {
        my $formType;
        sysread($AudioFile, $formType, 4) || die "$AudioFile formtype read error: $!\n";
        push (@result, $formType);
        $SoundFileType = $formType;
    };

    return @result;
};

# Get the header and the data in a chunk. Sound data are skipped unless an output
# file pointer is given
sub GetChunk     # (FILE [, OUTPUT]) -> ($ckID, $ckDataSize [, $formType], SSNDsize)[+ syswrite($Data)]
{
    my $AudioFile = shift;
    my $OutFile = shift || "";
    #
    my @result = GetChunkInfo($AudioFile);
    return () unless @result && $result[1] > 0;

    # DON'T read all sound samples
    my $ckData = "";
    if($result[0] eq 'SSND')
    { 
        my $DataSize = $result[1];   # Total number of bytes
        sysread($AudioFile, $ckData, 8)  # read first two LL
            || die "@result $AudioFile data read error: $!\n";
        $DataSize -= 8;              # Remove first two LL
        
        # Read through rest of block, write it to the output if necessary
        while($DataSize)
        {
            my $DataBlock = "";
            my $BlockSize = $DataSize > 1024 ? 1024 : $DataSize;
            my $ReadBlockSize = 0; 
            $ReadBlockSize = sysread($AudioFile, $DataBlock, $BlockSize);   # Read block of data
            die "@result $AudioFile Sampled data read error: $!\n" unless $ReadBlockSize;
            $DataSize -= $ReadBlockSize;
            syswrite($OutFile, $DataBlock, $ReadBlockSize) if $OutFile && $ReadBlockSize;
        };
        
    }
    elsif($result[0] eq 'data' && $SoundFileType eq 'WAVE')
    { 
        my $DataSize = $result[1];   # Total number of bytes
        
        # Read through rest of block, write it to the output if necessary
        while($DataSize)
        {
            my $DataBlock = "";
            my $BlockSize = $DataSize > 1024 ? 1024 : $DataSize;
            my $ReadBlockSize = 0; 
            $ReadBlockSize = sysread($AudioFile, $DataBlock, $BlockSize);   # Read block of data
            die "@result $AudioFile Sampled data read error: $!\n" unless $ReadBlockSize;
            $DataSize -= $ReadBlockSize;
            syswrite($OutFile, $DataBlock, $ReadBlockSize) if $OutFile && $ReadBlockSize;
        };
        
    }
    else    # Other blocks are read in total
    {
        sysread($AudioFile, $ckData, $result[1]) 
            || die "@result $AudioFile data read error: $!\n";
    };

    push(@result, $ckData);

    return @result;
}

sub catAIFC        # (file names) -> single AIFC file to $FileOutput
{
my @Files = @_;

# Start autoflush
select($FileOutput) || die "$!\n";;
$| = 1;

# First, determine the total size of the sound files 
my @formInfo;
my @verInfo;
my @RIFFfmt;
my $TotalNumberOfSampleFrames = 0;
my $TotalSoundDataSize = 0;
my ($numChannels, $numSampleFrames, $sampleSize, $rest);
foreach $AudioFile (@Files)
{
    open(INPUT, "<$AudioFile") || die "<$AudioFile not opened $!\n";
    @formInfo = GetChunkInfo(INPUT);
    @verInfo = GetChunk(INPUT) unless $SoundFileType eq 'WAVE';  # AIFC version
    my @DataChunk = ();
    while(@DataChunk = GetChunk(INPUT))
    {
        if($DataChunk[0] eq 'COMM')
	{
            ($numChannels, $numSampleFrames, $sampleSize, $rest) =
		unpack("s${EndianOrder}sa*", $DataChunk[2]);
	    $TotalNumberOfSampleFrames += $numSampleFrames;
        }
        elsif($DataChunk[0] eq 'SSND')
	{
	    $TotalSoundDataSize += $DataChunk[1] - 8;
        }
        elsif($DataChunk[0] eq 'data' && $SoundFileType eq 'WAVE')
	{
	    $TotalSoundDataSize += $DataChunk[1];
        }
        elsif($DataChunk[0] eq 'fmt ' && $SoundFileType eq 'WAVE')
        {
            @RIFFfmt = @DataChunk;
        };        
    };
    # Close

    close(INPUT);
};

# Construct the COMMON block
my $CommonBlock = "";
unless($SoundFileType eq 'WAVE')
{
    $CommonBlock = 
      pack("s${EndianOrder}sa*", $numChannels, $TotalNumberOfSampleFrames, $sampleSize, $rest);
};

# Write     FORM, VER, and COMM and SSND blocks
if($SoundFileType eq 'WAVE')     # Non-Internet
{
    $formInfo[1] = 0 + 8+$RIFFfmt[1]   +    8+$TotalSoundDataSize;
    syswrite($FileOutput, pack("a4Va4", @formInfo), 12);   # RIFF chunk
    syswrite($FileOutput, pack("a4Va*", @RIFFfmt), length($RIFFfmt[2])+8);         # fmt Block
    syswrite($FileOutput, pack("a4V", 'data', $TotalSoundDataSize), 8);            # sound data
}
else                             # Internet
{
    $formInfo[1] = 0 + 12   +    26  +     8+16+$TotalSoundDataSize;
    syswrite($FileOutput, pack("a4${EndianOrder}a4", @formInfo), 12);   # Form chunk
    syswrite($FileOutput, pack("a4${EndianOrder}${EndianOrder}", @verInfo), 12);    # version chunk
    syswrite($FileOutput, pack("a4${EndianOrder}a*", 'COMM', length($CommonBlock), $CommonBlock), 
                 length($CommonBlock)+8);         # Common Block
    syswrite($FileOutput, pack("a4${EndianOrder}${EndianOrder}${EndianOrder}", 'SSND', $TotalSoundDataSize+8, 0, 0), 16);
};
#  
# Write all samples to output
foreach $AudioFile (@Files)
{
    open(INPUT, "<$AudioFile") || die "<$AudioFile not opened $!\n";
    @formInfo = GetChunkInfo(INPUT);
    my @DataChunk = ();
    while(@DataChunk = GetChunk(INPUT, $FileOutput)){   };
    # Close
    close(INPUT)
};
}

unless(caller())
{
    catAIFC(@ARGV);
};

1; # Make require happy


=head1 NAME

catAIFC.pl - concatenates AIFC and RIFF (wav) files 

=head1 DESCRIPTION

catAIFC.pl concatenates AIFC and RIFF WAVE files in a single output stream.
 
=head1 README

catAIFC.pl concatenates AIFC and RIFF WAVE files in a single output stream.
All files MUST have identical audio formats and structures (channels, sample 
rates etc.). ALL sound chunks are combined. Compressed files might not be 
concattenated correctly. Mixing AIFC and WAVE files will result in useless 
files. Note that ALL non-essential chunks are dropped.
catAIFC.pl is fairly inefficient. It will pass over all files twice. Once to
determine their length, once to actually output data.

=head1 PREREQUISITES

=head1 COREQUISITES

=pod OSNAMES

Unix

=pod SCRIPT CATEGORIES

Audio: AIFC
Audio: RIFF
Web

=cut
