PDA

Просмотр полной версии : Как несколько раз озвучить заданный музыкальный интервал из mp3 файла на flex?


Zarion
28.06.2012, 22:34
Если бы музыкальный файл можно было бы проигрывать до конца, то проблемы бы практически не было. Достаточно выполнить команду вроде:

channel = sound.play(startPosition, countPlays);

и мы получили бы звук от startPosition до конца файла countPlays раз. Но проблема в том, что нужен звук не до конца файла, а до конкретной finishPosition.

Один раз это осуществить без проблем, с помощью таймера. Хотя надо сказать, что чувствительность у таймера не одна миллисекунда, а порядка 17-30 миллисекунд, что по само по себе не идеально, хотя терпимо. Вручную жать пимпочку, любое число раз, которая будет озвучивать данный музыкальный отрезок, тоже легко реализуемо. А вот программно запустить определенное число раз нужную пользовательскую функцию, типа:

playSound();
playSound();

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

Для решения данной задачи достаточно идей, а не кода. Если надо, я выложу свой образец программы.

Добавлено через 23 часа 37 минут
После некоторых экспериментов проблему удалось решить. С таймером идея оказалась не очень удачной, так как есть событие ENTER_FRAME, с помощью которого можно обрабатывать каждый фрейм звука, продолжительностью, по умолчанию, примерно от 20 до 35 миллисекунд (который, скорее всего, также привязан к системному таймеру), что соответствует от 50 до 30 кадрам в секунду. Пишем обработчик для этого события, который с одной стороны останавливает звук на нужной позиции, а с другой запускает повторное звучание фрагмента с заданной начальной позиции не более требуемого числа раз.

Пример теста программы, полностью удовлетворяющего меня, приведен ниже. С помощью кнопки "Browse mp3" загружаем произвольный музыкальный файл. После его загрузки в память становиться доступной кнопка "Play", нажав которую мы получим статическую информацию вроде:

startPosition = 10000
finishPosition = 50000
countPlays = 3

которая задается в программе непосредственно. Затем, после каждого проигрывания выводится сообщение типа:

2-я позиция звука: 50031 / 116767

Здесь мы видим слабую точность фреймового счетчика. В документации пишут о 16,7 миллисекундах. У нас где-то в два раза больше.

После третьего проигрывания (в данном случае) звук прекратится, что и требовалось получить. Раньше остановить все можно кнопкой "Stop". Не нажимайте только кнопку "Play" подряд несколько раз, а то мы получим какофонию нескольких звуков одновременно. Здесь в тесте нет особого желания обрабатывать подобную ситуацию.

Для любопытствующих, которые могут задаться вопросом, а зачем это нужно, скажу, что для озвучки коротких фрагментов аудиокниг, чтобы в промежутках между повторами можно было самому повторить текст диктора и набрать его на клавиатуре (русский перевод фрагмента текста также отображается), короче, для создания обучающей программы по собственному методу. Но это уже другая песня.

Пример кода:
<?xml version="1.0" encoding="utf-8"?>
<s:WindowedApplication
xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
backgroundColor="#BBBBBB"
creationComplete="onCreationComplete()">

<fx:Declarations>
</fx:Declarations>

<fx:Script><![CDATA[
import flash.events.*;

private var file:File;
private var filter:FileFilter = new FileFilter("*.mp3", "*.mp3;");

private var sound:Sound = null;
private var soundChannel:SoundChannel = null;
private var soundLength:int = 0;

private var count:int = 1;
private var startPosition:int = 10000;
private var finishPosition:int = 50000;
private var countPlays:int = 3;

protected function onCreationComplete():void {
// Центрирование окна приложения
nativeWindow.x = (Capabilities.screenResolutionX - width) / 2;
nativeWindow.y = (Capabilities.screenResolutionY - height) / 2;

//loader = new URLLoader();

//loader.dataFormat = URLLoaderDataFormat.BINARY;
//loader.addEventListener(Event.COMPLETE, onURLLoader);
//loader.load(new URLRequest("../assets/Lolita_Foreword.mp3"));
} // onCreationComplete()

private function onBrowseFile(event:MouseEvent):void {
file = new File();

file.addEventListener(Event.SELECT, onFileSelect);

file.browseForOpen("Open",[filter]);
} // onBrowseFile()

protected function onFileSelect(event:Event):void{
file.removeEventListener(Event.SELECT, onFileSelect);
file = File(event.currentTarget);

sound = new Sound();

sound.load(new URLRequest(file.url));
sound.addEventListener(Event.COMPLETE, onSoundComplete);
} // onFileSelect()

protected function onSoundComplete(event:Event):void {
playButton.enabled = true;
soundLength = sound.length;
playInfo.text = "startPosition: " + startPosition + "\n" +
"finishPosition: " + finishPosition + "\n" + "countPlays: " + countPlays;
} // onSoundComplete()

private function onPlaySound():void {
count = 1;

soundChannel = sound.play(startPosition);
addEventListener(Event.ENTER_FRAME, onEnterFrame);
} // onPlaySound()

protected function onEnterFrame(event:Event):void {
var position:int = soundChannel.position;

if(position >= finishPosition) {
playInfo.text = count + "-я позиция звука: " + position + " / " + soundLength;
soundChannel.stop();

if(count < countPlays) {
soundChannel = sound.play(startPosition);
count += 1;
}
}
} // onEnterFrame

protected function onStopSound():void {
count = 1;

if(soundChannel) {
playInfo.text = "Позиция звука: " + int(soundChannel.position) + " / " + soundLength;
soundChannel.stop();
removeEventListener(Event.ENTER_FRAME, onEnterFrame);
}
} // onStopSound()
]]></fx:Script>

<s:Label id="playInfo" verticalCenter="1" horizontalCenter="1" fontSize="24">Привет, Zarion!</s:Label>

<mx:HBox>
<s:Button id="browseButton" label="Browse mp3" click="onBrowseFile(event)"/>
<mx:Button id="playButton" label="Play" enabled="false" click="onPlaySound();"></mx:Button>
<mx:Button id="stopButton" label="Stop" click="onStopSound();"></mx:Button>
</mx:HBox>
</s:WindowedApplication>

Zarion
10.08.2012, 10:50
Хотя надо сказать, что чувствительность у таймера не одна миллисекунда, а порядка 17-30 миллисекунд, что по само по себе не идеально, хотя терпимо.
. . .
Здесь мы видим слабую точность фреймового счетчика. В документации пишут о 16,7 миллисекундах. У нас где-то в два раза больше.
Наконец-то удалось выяснить причины слабой чувствительности мультимедиа флэкса во времени. Понятно, что это ограничение самого мультимедийного движка флэкса, а не его таймера либо обработчика событий Event.ENTER_FRAME. Путем экспериментов удалось получить информацию, что точность таймера составляет порядка трех миллисекунд. Т.е. если вы даже выставите точность в одну миллисекунду:

_timer = new Timer(1);

все равно событие TimerEvent.TIMER будет срабатывать не чаще одного раза за три миллисекунды.

Этой точности было бы вполне достаточно, если бы мультимедийный движок флекса был напрямую привязан к таймеру, либо к событию Event.ENTER_FRAME. То, что это не так вызывает очень сильное удивление, тем более, что нигде об этом не пишут. В самом деле, флекс позволяет менять частоту кадров в пределах от 1 до 999, а в файле <ProgName>Config.xml можно вручную выставить любое значение в теге:

<default-frame-rate>1000</default-frame-rate>

только это может вызвать ошибку компиляции либо тупо проигнорировано, если значение default-frame-rate больше максимально возможного.

Чтобы выяснить максимально возможное значение частоты кадров, ставим в настройках программы значение 999 и во время компиляции вызываем код, вроде:

trace(stage.frameRate);

У меня, например, это значение получилось равным 231.01171875 (кадров в секунду), что составляет, примерно, 4.3 миллисекунды на один фрейм, это сопоставимо с величиной 3 миллисекунды для таймера. Таким образом, мы видим, что точность тайминга и фрейминга флэкса значительно выше, чем его мультимедийной раскадровки, которая реально составляет порядка 30 кадров в секунду независимо от значения stage.frameRate либо интервала таймера. Кстати, именно это значение, 30 fps, выставлено по умолчанию во FlashDevelop. Именно это значение дает точность порядка 33 миллисекунд, как раз то значение, которое у нас было получено экспериментально.

Но вот вопрос, а откуда оно берется? Иначе говоря, какое событие его генерит?

Как удалось выяснить, ответственным за эту величину является буферизация мультимедийного потока. В случае звука, конкретно имеем событие SampleDataEvent.SAMPLE_DATA, которое вызывается, когда буфер данных иссякает. Понятно теперь, почему этот процесс не зависит ни от тайминга, ни от фрейминга. Мы можем перехватывать это событие и писать в его обработчике нечто вроде:

// Минимальное значение буфера обеспечивает максимальную точность по времени
// (примерно, 30 кадров/сек)
_soundSrc.extract(bytes, 2048); // 2048 - 8192
//event.data.writeBytes(upOctave(bytes));
event.data.writeBytes(bytes);

Однако объемы данных меньше 2 КБ и более 8 Кб мы пересылать не можем, так как возникает ошибка компиляции. Именно это буфер данных, размером 2 КБ и ответственен за максимальную (30 fps) фактическую раскадровку звука, независимо от событий тайминга и фрейминга.

Наверное, эта информация должна быть где-то опубликована, чтобы было меньше непоняток со флексом.

Ну а что делать, если мы хотим работать со звуком в интервалах времени менее 33 миллисекунд?

Пожалуй, выход только один, привлекать сторонние инструменты. Например, работать непосредственно с байтами данных mp3-файла. Но для этого нужно знать формат mp3-файла. Однако это проблема вполне решаема, если посмотреть, скажем, исходники lame-3.99.5. Но тогда нам и флэкс особо не нужен, можно использовать тот же Си.