Разработка

В исполняемой части программы открываем команду запуска MPV с нужными параметрами, записываем адреса в полученный дескриптор, после чего дескриптор закроем:

open my $mpv, '|-', 'mpv --really-quiet --gapless-audio --playlist=-';
speakFile($mpv, $_) for @ARGV;
close $mpv;

Записью адресов в дескриптор заведует процедура speakFile. Она получает дескриптор и имя текстового файла. Процедура вызывается для каждого имени файла из командной строки.

Процедура speakFile открывает для чтения файл, чьё имя она получает как второй параметр. Затем файл читается по строкам (каждая прочитанная строка оказывается в переменной $line). Особо отметим, что, поскольку текстовый файл открывается с опцией '<:utf8', строка в переменной $line будет рассматриваться как символьная, а не как октетная. Это нам как раз на руку, поскольку это облегчит нам задачу разбиения найденных в строке слов на группы. Напомним, что из слов формируются группы, в каждой из которых не более ста символов (не октетов!). Для каждой группы формируется URL.

Сосредоточим внимание на самой трудной части процедуры:

while($line=~m/(\p{L}+)/g)
{
	if(length($query)+length($1)<100)
	{
		$query.=' ' if $query;
		$query.=$1;
	}
	else
	{
		speakWords($mpv, $query);
		$query=$1;
	}
}

В цикле строка $line сопоставляется с регулярным выражением m/(\p{L}+)/g. Метасимвол \p{L} обозначает символьный класс букв, независимо от языка. Таким образом, группа, из которой состоит регулярное выражение, должна захватить в переменную $1 одно слово. Обратите внимание на флаг g (от global), записанный сразу после шаблона. При многократных сопоставлениях строки $line с шаблоном, помеченным этим флагом, поиск будет начинаться не с начала строки каждый раз, а сразу после того места, где ранее уже что-то было найдено. Это позволит в цикле извлекать слово за словом:

while($line=~m/(…)/g)
{
	сделать что-то с $1
}

В отсутствие флага цикл стал бы бесконечным: при каждом его проходе в $1 попадало бы самое первое слово, найденное в строке $line.

Что же мы станем делать с очередным словом, попавшим в $1? Мы заготовили буфер $query, к которому новое слово будет добавлено через пробел, если только такое добавление не противоречит лимиту в сто символов, включая пробел. Если же противоречит, значит, буфер заполнен под завязку, а тогда он передаётся процедуре speakWords, после чего новое слово заменяет собой старое содержимое буфера. Мы специально позаботились, чтобы при добавлении первого слова в пустой буфер перед словом пробел не вставлялся — там он не нужен.

Итак, вся процедура speakFile в сборе:

sub speakFile($$)
{
	my $mpv=shift;
	open my $file, '<:utf8', shift;
	my $query='';
	while(my $line=<$file>)
	{
		chomp $line;
		while($line=~m/(\p{L}+)/g)
		{
			if(length($query)+length($1)<100)
			{
				$query.=' ' if $query;
				$query.=$1;
			}
			else
			{
				speakWords($mpv, $query);
				$query=$1;
			}
		}
	}
	speakWords($mpv, $query) if $query;
}

Последний вызов speakWords обрабатывает самые последние слова, если такие ещё останутся в буфере.

Процедура speakWords получает два параметра: дескриптор, в который добавляется очередной URL, и текст (очередная группа слов, разделённых пробелами), который станет частью адреса.

sub speakWords($$)
{
	my $mpv=shift;
	my $text=urlEncode(shift);
	$mpv->print("http://translate.google.com/translate_tts?client=tw-ob&tl=ru&ie=UTF-8&q=$text\n");
}

Текст подвергается %-кодированию и включается в адрес URL (вызов urlEncode), а тот, в свою очередь, записывается в дескриптор.

Важной частью программы будет процентное кодирование текста, которое мы оформим как процедуру urlEncode. Процедура получает строку и возвращает результат её процентного кодирования.

sub urlEncode($)
{
	my $text=shift;
	utf8::encode($text);
	my $result;
	for(split //, $text)
	{
		if(m/\w/)
		{
			$result.=$_;
		}
		elsif($_ eq ' ')
		{
			$result.='+';
		}
		else
		{
			$result.=sprintf('%%%2X', ord);
		}
	}
	return $result;
}

Следует учесть, что при процентном кодировании строка рассматривается как последовательность октетов, а не символов. В то же время текстовый файл, который программа будет читать вслух, предполагается закодированным в UTF-8. Поэтому первое, что следует сделать с переданным в процедуру параметром — преобразовать символьную строку в октетную. Для этого и понадобился вызов utf8::encode($word). Теперь мы уверены, что split //, $text породит последовательность октетов, а не символов.

Дальше всё просто. В зависимости от очередного октета последовательности к строке $result добавляется его закодированный вариант. Для латинских букв и цифр октет добавляется без всяких изменений. Пробел превращается в плюс. Все остальные октеты подвергаются %-кодированию. Мы использовали встроенную процедуру sprintf (см. раздел «Форматированный вывод»). Процедура ord возвращает порядковый номер символа или октета (в данном случае — октета), переданного как параметр, или, по умолчанию, содержащегося в переменной $_.

Теперь программа готова.

Информатика-54© А. Н. Швец