#                                                         -*- Perl -*-
# Copyright (c) 2001  Motoyuki Kasahara
#
# 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, 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.
#

#
# 音声 (WAVE) を収めたファイルを生成するクラス
#
package FreePWING::Sound;

require 5.005;
require Exporter;
use English;
use FileHandle;
use FreePWING::Tag;
use strict;
use integer;

use vars qw(@ISA
	    @EXPORT
	    @EXPORT_OK
	    $block_length
	    $tag_prefix);

@ISA = qw(Exporter);

#
# ブロックの長さ (バイト数)
#
$block_length = 2048;

#
# 書式:
#	new()
# メソッドの区分:
# 	public クラスメソッド。
# 説明:
# 	新しいオブジェクトを作る。
# 戻り値:
# 	作成したオブジェクトへのリファレンスを返す。
#
sub new {
    my $type = shift;
    my $new = {
	# バイナリファイルのハンドラ
	'handle' => FileHandle->new(),

	# バイナリファイル名
	'file_name' => '',

	# 書き込んだエントリ数
	'entry_count' => 0,

	# バイナリファイルのオフセット (これまでに書き込んだバイト数)
	'position' => 0,

	# タグ
	'tag' => FreePWING::Tag->new(),

	# 終了タグ (satomii)
	'end_tag_prefix' => 'sound-end',

	# 音声形式ファイル (satomii)
	'fmt_file_name' => '',
	'fmt_handle' => FileHandle->new(),

	# エラーメッセージ
	'error_message' => '',
    };
    return bless($new, $type);
}

#
# 書式:
#	open(file_name, [tag_file_name, [fmt_file_name]])
#           file_name
#		テキストファイルの名前。
#           tag_file_name
#		タグファイルの名前。
#           fmt_file_name
#		音声形式ファイルの名前。
# メソッドの区分:
# 	public インスタンスメソッド。
# 説明:
# 	書き込み用のテキストファイルを開く。
# 戻り値:
#	成功すれば 1 を返す。失敗すれば 0 を返す。
#
sub open {
    my $self = shift;
    my ($file_name, $tag_file_name, $fmt_file_name) = @ARG;

    #
    # ファイルを開く。
    #
    $self->{'file_name'} = $file_name;
    if (!$self->{'handle'}->open($self->{'file_name'}, 'w')) {
	$self->{'error_message'} =
	    "failed to open the file, $ERRNO: " . $self->{'file_name'};
	$self->close_internal();
	return 0;
    }
    binmode($self->{'handle'});

    #
    # タグファイルを開く。
    #
    if (defined($tag_file_name)
	&& !$self->{'tag'}->open($tag_file_name)) {
	$self->{'error_message'} = $self->{'tag'}->error_message();
	$self->close_internal();
        return 0;
    }

    #
    # 音声形式ファイルを開く。(satomii)
    #
    if (defined($fmt_file_name)) {
	$self->{'fmt_file_name'} = $fmt_file_name;
	if (!$self->{'fmt_handle'}->open($fmt_file_name, 'w')) {
	    $self->{'error_message'} =
		"failed to open the file, $ERRNO: $fmt_file_name";
	    $self->close_internal();
	    return 0;
	}
    }

    return 1;
}

#
# 書式:
#	close()
# メソッドの区分:
# 	public インスタンスメソッド。
# 説明:
# 	オブジェクトが開いているテキストファイル群を閉じる。
#	テキストファイルを開いていない場合は、何もしない。
# 戻り値:
#	成功すれば 1 を返す。失敗すれば 0 を返す。
#
sub close {
    my $self = shift;

    if (!$self->{'handle'}->fileno()) {
	return 1;
    }

    #
    # 半端なブロックの後方を "\0" で埋める。
    #
    my $pad_length = $block_length - $self->{'handle'}->tell() % $block_length;
    if ($pad_length < $block_length
	&& !$self->{'handle'}->print("\0" x $pad_length)) {
	$self->{'error_message'} =
	    "failed to write the file, $ERRNO: " . $self->{'file_name'};
	return 0;
    }
	
    #
    # ファイルを閉じる。
    #
    $self->close_internal();

    return 1;
}

#
# 書式:
#	close_internal()
# メソッドの区分:
# 	private インスタンスメソッド。
# 説明:
#       close() の内部処理用メソッド。
#
sub close_internal {
    my $self = shift;

    if ($self->{'handle'}->fileno()) {
	$self->{'handle'}->close();
    }
    $self->{'tag'}->close_internal();

    #
    # 音声形式ファイルが開かれていれば閉じる。(satomii)
    #
    if ($self->{'fmt_handle'}->fileno()) {
	$self->{'fmt_handle'}->close();
    }
}

#
# 書式:
#	add_data(tag, file_name)
#	    tag
#		タグ名
#	    file_name
#		追加するバイナリファイル名
# メソッドの区分:
# 	public インスタンスメソッド。
# 説明:
# 	与えられた WAVE ファイルの内容を、データとして追加する。
# 戻り値:
#	成功すれば 1 を返す。失敗すれば 0 を返す。
#
sub add_data {
    my $self = shift;
    my ($tag, $file_name) = @ARG;

    #
    # タグを登録する。
    #
    if (!$self->{'tag'}->add_entry('sound:' . $tag, $self->{'position'})) {
	$self->{'error_message'} = $self->{'tag'}->error_message();
	$self->close_internal();
	return 0;
    }

    #
    # 指定されたファイルを開く。
    #
    my $handle = FileHandle->new();
    if (!$handle->open($file_name, 'r')) {
	$self->{'error_message'} =
	    "failed to open the file, $ERRNO: " . $file_name;
	$self->close_internal();
	return 0;
    }
    binmode($handle);

    #
    # 指定されたファイルの形式を調べて work/sndfmt に記録する。(satomii)
    #
    my ($data, $data_length);

    $data_length = $handle->read($data, 8);
    if ($data_length != 8) {
	$self->{'error_message'} = (!defined($data_length)) ?
	    "failed to read the file, $ERRNO: $file_name" :
	    "unknown sound format: $file_name";
	$self->close_internal();
	return 0;
    }

    if (substr($data, 0, 4) eq 'RIFF') {
	#
	# WAVE ファイル
	#
	$data_length = $handle->read($data, 4);
	if ($data_length != 4) {
	    $self->{'error_message'} = (!defined($data_length)) ?
		"failed to read the file, $ERRNO: $file_name" :
		"invalid RIFF data: $file_name";
	    $self->close_internal();
	    return 0;
	} elsif ($data ne 'WAVE') {
	    $self->{'error_message'} =
		"unknown RIFF type, $data: $file_name";
	    $self->close_internal();
	    return 0;
	}

	if ($self->{'fmt_handle'}->fileno()) {
	    my $header_position = $handle->tell();
	    my ($chunk_code, $chunk_length);

	    #
	    # fmt チャンクを探す。
	    #
	    while (1) {
		$data_length = $handle->read($data, 8);
		if ($data_length != 8) {
		    $self->{'error_message'} = do {
			if (!defined($data_length)) {
			    "failed to read the file, $ERRNO: $file_name";
			} elsif ($data_length == 0) {
			    "unexpected end of file: $file_name";
			} else {
			    "invalid WAVE data: $file_name";
			}
		    };
		    $self->close_internal();
		    return 0;
		}

		$chunk_code = substr($data, 0, 4);
		$chunk_length = unpack('L', substr($data, 4));

		$data_length = $handle->read($data, $chunk_length);
		if ($data_length != $chunk_length) {
		    $self->{'error_message'} = (!defined($data_length)) ?
			"failed to read the file, $ERRNO: $file_name" :
			"failed to read the $chunk_code chunk: $file_name";
		    $self->close_internal();
		    return 0;
		}

		if ($chunk_code ne 'fmt ') {
		    next;
		} elsif ($chunk_length < 16) {
		    $self->{'error_message'} =
			"invalid fmt chunk: $file_name";
		    $self->close_internal();
		    return 0;
		}

		#
		# チャンネル数、サンプリング周波数、ビット長を取得する。
		#
		my @fmt = unpack('S2L2S2', $data);
		if (!$self->{'fmt_handle'}->printf(
			 "%s\t01\t%d0%d%d\n",
			 $tag,
			 ($fmt[1] == 2) ? 1 : 0,
			 ($fmt[5] == 16) ? 0 : 1,
			 ($fmt[2] == 441000) ? 0 :
			 ($fmt[2] == 220500) ? 1 : 2)) {
		    $self->{'error_message'} =
			"failed to write the file, $ERRNO: "
			. $self->{'fmt_file_name'};
		    $self->close_internal();
		    return 0;
		}
		last;
	    }

	    #
	    # 読み出し位置を最初のサブチャンクの先頭に戻す。
	    #
	    if (!$handle->seek($header_position, 0)) {
		$self->{'error_message'} =
		    "failed to seek the file, $ERRNO: $file_name";
		$self->close_internal();
		return 0;
	    }
	}

    } elsif ($data eq "MThd\x00\x00\x00\x06") {
	#
	# MIDI ファイル (SMF)
	#
	if ($self->{'fmt_handle'}->fileno()) {
	    if (!$self->{'fmt_handle'}->print("$tag\t02\t0000\n")) {
		$self->{'error_message'} =
		    "failed to write the file, $ERRNO: "
		    . $self->{'fmt_file_name'};
		$self->close_internal();
		return 0;
	    }
	}

	#
	# 読み出し位置を先頭に戻す。
	# または最初のトラックチャンクの先頭にするべき? MIDI に対応したソフトが
	# ないためテストできず。JIS X 4081 の仕様書をお持ちの方、適当に修正して
	# ください。
	#
	if (!$handle->seek(0, 0)) {
	    $self->{'error_message'} =
		"failed to seek the file, $ERRNO: $file_name";
	    $self->close_internal();
	    return 0;
	}

    } else {
	$self->{'error_message'} = "unknown sound format: $file_name";
	$self->close_internal();
	return 0;
    }

    #
    # 音声データを読み込んでバイナリファイルに書き込む。
    #
    my $start_position = $self->{'position'};

    for (;;) {
	$data_length = $handle->read($data, $block_length);
	if (!defined($data_length)) {
	    $self->{'error_message'} =
		"failed to read the file, $ERRNO: $file_name";
	    $self->close_internal();
	    return 0;
	} elsif ($data_length == 0) {
	    last;
	}
	if (!$self->{'handle'}->print($data)) {
	    $self->{'error_message'} =
		"failed to write the file, $ERRNO: " . $self->{'file_name'};
	    $self->close_internal();
	    return 0;
	}
	$self->{'position'} += $data_length;
    }

    #
    # 指定されたファイルを閉じる。
    #
    $handle->close();
    $self->{'entry_count'}++;

    #
    # 終了タグを登録する。
    #
    if (defined($self->{'end_tag_prefix'})) {
	my ($end_tag) = $self->{'end_tag_prefix'} . ':' . $tag;
	my ($end_position) = $self->{'position'} - 1;
	if ($end_position < $start_position) {
	    $end_position = $start_position;
	}

	if (!$self->{'tag'}->add_entry($end_tag, $end_position)) {
	    $self->{'error_message'} = $self->{'tag'}->error_message();
	    $self->close_internal();
	    return 0;
	}
    }

    return 1;
}

#
# 書式:
#	add_binary(tag, binary)
#	    tag
#		タグ名
#	    binary
#		追加するバイナリ
# メソッドの区分:
# 	public インスタンスメソッド。
# 説明:
# 	与えられたバイナリの内容を、データとして追加する。
# 戻り値:
#	成功すれば 1 を返す。失敗すれば 0 を返す。
#
sub add_binary {
    my $self = shift;
    my ($tag, $binary) = @ARG;
    my $pos = 0;

    #
    # タグを登録する。
    #
    if (!$self->{'tag'}->add_entry('sound:' . $tag, $self->{'position'})) {
	$self->{'error_message'} = $self->{'tag'}->error_message();
	$self->close_internal();
	return 0;
    }

    #
    # 指定されたファイルの形式を調べて work/sndfmt に記録する。(satomii)
    #
    my ($data, $data_length);

    $data = substr ($binary, 0, 8);
    if (length($data) != 8) {
	$self->{'error_message'} = "unknown sound format";
	$self->close_internal();
	return 0;
    }

    if (substr($data, 0, 4) eq 'RIFF') {
	#
	# WAVE ファイル
	#
	$data = substr($binary, 8, 4);
	$pos = 12;
	if (length($data) != 4) {
	    $self->{'error_message'} = "invalid RIFF data";
	    $self->close_internal();
	    return 0;
	} elsif ($data ne 'WAVE') {
	    $self->{'error_message'} =
		"unknown RIFF type, $data";
	    $self->close_internal();
	    return 0;
	}

	if ($self->{'fmt_handle'}->fileno()) {
	    my $header_position = $pos;
	    my ($chunk_code, $chunk_length);

	    #
	    # fmt チャンクを探す。
	    #
	    while (1) {
		$data = substr($binary, $pos, 8);
		$data_length = length($data);
		$pos += 8;
		if ($data_length != 8) {
		    $self->{'error_message'} = do {
			if ($data_length == 0) {
			    "unexpected end of data";
			} else {
			    "invalid WAVE data";
			}
		    };
		    $self->close_internal();
		    return 0;
		}

		$chunk_code = substr($data, 0, 4);
		$chunk_length = unpack('L', substr($data, 4));

		$data = substr($binary, $pos, $chunk_length);
		$pos += $chunk_length;
		if (length($data) != $chunk_length) {
		    $self->{'error_message'} = "failed to read the $chunk_code chunk";
		    $self->close_internal();
		    return 0;
		}

		if ($chunk_code ne 'fmt ') {
		    next;
		} elsif ($chunk_length < 16) {
		    $self->{'error_message'} =
			"invalid fmt chunk";
		    $self->close_internal();
		    return 0;
		}

		#
		# チャンネル数、サンプリング周波数、ビット長を取得する。
		#
		my @fmt = unpack('S2L2S2', $data);
		if (!$self->{'fmt_handle'}->printf(
			 "%s\t01\t%d0%d%d\n",
			 $tag,
			 ($fmt[1] == 2) ? 1 : 0,
			 ($fmt[5] == 16) ? 0 : 1,
			 ($fmt[2] == 441000) ? 0 :
			 ($fmt[2] == 220500) ? 1 : 2)) {
		    $self->{'error_message'} =
			"failed to write the file, $ERRNO: "
			. $self->{'fmt_file_name'};
		    $self->close_internal();
		    return 0;
		}
		last;
	    }

	    #
	    # 読み出し位置を最初のサブチャンクの先頭に戻す。
	    #
	    $pos = $header_position;
	}

    } elsif ($data eq "MThd\x00\x00\x00\x06") {
	#
	# MIDI ファイル (SMF)
	#
	if ($self->{'fmt_handle'}->fileno()) {
	    if (!$self->{'fmt_handle'}->print("$tag\t02\t0000\n")) {
		$self->{'error_message'} =
		    "failed to write the file, $ERRNO: "
		    . $self->{'fmt_file_name'};
		$self->close_internal();
		return 0;
	    }
	}

	#
	# 読み出し位置を先頭に戻す。
	# または最初のトラックチャンクの先頭にするべき? MIDI に対応したソフトが
	# ないためテストできず。JIS X 4081 の仕様書をお持ちの方、適当に修正して
	# ください。
	#
	$pos = 0;

    } else {
	$self->{'error_message'} = "unknown sound format";
	$self->close_internal();
	return 0;
    }

    #
    # 音声データを読み込んでバイナリファイルに書き込む。
    #
    my $start_position = $self->{'position'};

    $data = substr($binary, $pos);
    if (!$self->{'handle'}->print($data)) {
	$self->{'error_message'} =
	    "failed to write the file, $ERRNO: " . $self->{'file_name'};
	$self->close_internal();
	return 0;
    }
    $self->{'position'} += length($data);

    #
    #
    #
    $self->{'entry_count'}++;

    #
    # 終了タグを登録する。
    #
    if (defined($self->{'end_tag_prefix'})) {
	my ($end_tag) = $self->{'end_tag_prefix'} . ':' . $tag;
	my ($end_position) = $self->{'position'} - 1;
	if ($end_position < $start_position) {
	    $end_position = $start_position;
	}

	if (!$self->{'tag'}->add_entry($end_tag, $end_position)) {
	    $self->{'error_message'} = $self->{'tag'}->error_message();
	    $self->close_internal();
	    return 0;
	}
    }

    return 1;
}

######################################################################
# <インスタンス変数の値を返すメソッド群>
#
# 書式:
#	インスタンス変数名()
# メソッドの区分:
# 	public インスタンスメソッド。
# 戻り値:
#	インスタンス変数の値を返す。
#
sub file_name {
    my $self = shift;
    return $self->{'file_name'};
}

sub tag_file_name {
    my $self = shift;
    return $self->{'tag'}->file_name();
}

sub entry_count {
    my $self = shift;
    return $self->{'entry_count'};
}

sub error_message {
    my $self = shift;
    return $self->{'error_message'};
}

1;
