parent
0a02c0583b
commit
b9f2ae2bff
Binary file not shown.
@ -0,0 +1,44 @@ |
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> |
||||
<html> |
||||
<head> |
||||
<!-- swfobject is a commonly used library to embed Flash content --> |
||||
<script type="text/javascript" |
||||
src="https://ajax.googleapis.com/ajax/libs/swfobject/2.2/swfobject.js"></script> |
||||
|
||||
<!-- Setup the recorder interface --> |
||||
<script type="text/javascript" src="recorder.js"></script> |
||||
|
||||
<script> |
||||
function setup() { |
||||
Wami.setup("wami"); |
||||
} |
||||
|
||||
function record() { |
||||
status("Recording..."); |
||||
Wami.startRecording("https://wami-recorder.appspot.com/audio"); |
||||
} |
||||
|
||||
function play() { |
||||
Wami.startPlaying("https://wami-recorder.appspot.com/audio"); |
||||
} |
||||
|
||||
function stop() { |
||||
status(""); |
||||
Wami.stopRecording(); |
||||
Wami.stopPlaying(); |
||||
} |
||||
|
||||
function status(msg) { |
||||
document.getElementById('status').innerHTML = msg; |
||||
} |
||||
</script> |
||||
</head> |
||||
|
||||
<body onload="setup()"> |
||||
<input type="button" value="Record" onclick="record()"></input> |
||||
<input type="button" value="Stop" onclick="stop()"></input> |
||||
<input type="button" value="Play" onclick="play()"></input> |
||||
<div id="status"></div> |
||||
<div id="wami"></div> |
||||
</body> |
||||
</html> |
||||
|
After Width: | Height: | Size: 15 KiB |
@ -0,0 +1,349 @@ |
||||
var Wami = window.Wami || {}; |
||||
|
||||
// Upon a creation of a new Wami.GUI(options), we assume that a WAMI recorder
|
||||
// has been initialized.
|
||||
Wami.GUI = function(options) { |
||||
var RECORD_BUTTON = 1; |
||||
var PLAY_BUTTON = 2; |
||||
|
||||
setOptions(options); |
||||
setupDOM(); |
||||
|
||||
var recordButton, playButton; |
||||
var recordInterval, playInterval; |
||||
|
||||
function createDiv(id, style) { |
||||
var div = document.createElement("div"); |
||||
if (id) { |
||||
div.setAttribute('id', id); |
||||
} |
||||
if (style) { |
||||
div.style.cssText = style; |
||||
} |
||||
return div; |
||||
} |
||||
|
||||
function setOptions(options) { |
||||
if (!options.buttonUrl) { |
||||
options.buttonUrl = "buttons.png"; |
||||
} |
||||
|
||||
if (typeof options.listen == 'undefined' || options.listen) { |
||||
listen(); |
||||
} |
||||
} |
||||
|
||||
function setupDOM() { |
||||
var guidiv = createDiv(null, |
||||
"position: absolute; width: 214px; height: 137px;"); |
||||
document.getElementById(options.id).appendChild(guidiv); |
||||
|
||||
var rid = Wami.createID(); |
||||
var recordDiv = createDiv(rid, |
||||
"position: absolute; left: 40px; top: 25px"); |
||||
guidiv.appendChild(recordDiv); |
||||
|
||||
recordButton = new Button(rid, RECORD_BUTTON, options.buttonUrl); |
||||
recordButton.onstart = startRecording; |
||||
recordButton.onstop = stopRecording; |
||||
|
||||
recordButton.setEnabled(true); |
||||
|
||||
if (!options.singleButton) { |
||||
var pid = Wami.createID(); |
||||
var playDiv = createDiv(pid, |
||||
"position: absolute; right: 40px; top: 25px"); |
||||
guidiv.appendChild(playDiv); |
||||
|
||||
playButton = new Button(pid, PLAY_BUTTON, options.buttonUrl); |
||||
playButton.onstart = startPlaying; |
||||
playButton.onstop = stopPlaying; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* These methods are called on clicks from the GUI. |
||||
*/ |
||||
function startRecording() { |
||||
if (!options.recordUrl) { |
||||
alert("No record Url specified!"); |
||||
} |
||||
recordButton.setActivity(0); |
||||
playButton.setEnabled(false); |
||||
Wami.startRecording(options.recordUrl, |
||||
Wami.nameCallback(onRecordStart), Wami |
||||
.nameCallback(onRecordFinish), Wami |
||||
.nameCallback(onError)); |
||||
} |
||||
|
||||
function stopRecording() { |
||||
Wami.stopRecording(); |
||||
clearInterval(recordInterval); |
||||
recordButton.setEnabled(true); |
||||
} |
||||
|
||||
function startPlaying() { |
||||
if (!options.playUrl) { |
||||
alert('No play URL specified!'); |
||||
} |
||||
|
||||
playButton.setActivity(0); |
||||
recordButton.setEnabled(false); |
||||
|
||||
Wami.startPlaying(options.playUrl, Wami.nameCallback(onPlayStart), Wami |
||||
.nameCallback(onPlayFinish), Wami.nameCallback(onError)); |
||||
} |
||||
|
||||
function stopPlaying() { |
||||
Wami.stopPlaying(); |
||||
} |
||||
|
||||
this.setPlayUrl = function(url) { |
||||
options.playUrl = url; |
||||
} |
||||
|
||||
this.setRecordUrl = function(url) { |
||||
options.recordUrl = url; |
||||
} |
||||
|
||||
this.setPlayEnabled = function(val) { |
||||
playButton.setEnabled(val); |
||||
} |
||||
|
||||
this.setRecordEnabled = function(val) { |
||||
recordButton.setEnabled(val); |
||||
} |
||||
|
||||
/** |
||||
* Callbacks from the flash indicating certain events |
||||
*/ |
||||
|
||||
function onError(e) { |
||||
alert(e); |
||||
} |
||||
|
||||
function onRecordStart() { |
||||
recordInterval = setInterval(function() { |
||||
if (recordButton.isActive()) { |
||||
var level = Wami.getRecordingLevel(); |
||||
recordButton.setActivity(level); |
||||
} |
||||
}, 200); |
||||
if (options.onRecordStart) { |
||||
options.onRecordStart(); |
||||
} |
||||
} |
||||
|
||||
function onRecordFinish() { |
||||
playButton.setEnabled(true); |
||||
if (options.onRecordFinish) { |
||||
options.onRecordFinish(); |
||||
} |
||||
} |
||||
|
||||
function onPlayStart() { |
||||
playInterval = setInterval(function() { |
||||
if (playButton.isActive()) { |
||||
var level = Wami.getPlayingLevel(); |
||||
playButton.setActivity(level); |
||||
} |
||||
}, 200); |
||||
if (options.onPlayStart) { |
||||
options.onPlayStart(); |
||||
} |
||||
} |
||||
|
||||
function onPlayFinish() { |
||||
clearInterval(playInterval); |
||||
recordButton.setEnabled(true); |
||||
playButton.setEnabled(true); |
||||
if (options.onPlayFinish) { |
||||
options.onPlayFinish(); |
||||
} |
||||
} |
||||
|
||||
function listen() { |
||||
Wami.startListening(); |
||||
// Continually listening when the window is in focus allows us to
|
||||
// buffer a little audio before the users clicks, since sometimes
|
||||
// people talk too soon. Without "listening", the audio would record
|
||||
// exactly when startRecording() is called.
|
||||
window.onfocus = function() { |
||||
Wami.startListening(); |
||||
}; |
||||
|
||||
// Note that the use of onfocus and onblur should probably be replaced
|
||||
// with a more robust solution (e.g. jQuery's $(window).focus(...)
|
||||
window.onblur = function() { |
||||
Wami.stopListening(); |
||||
}; |
||||
} |
||||
|
||||
function Button(buttonid, type, url) { |
||||
var self = this; |
||||
self.active = false; |
||||
self.type = type; |
||||
|
||||
init(); |
||||
|
||||
// Get the background button image position
|
||||
// Index: 1) normal 2) pressed 3) mouse-over
|
||||
function background(index) { |
||||
if (index == 1) |
||||
return "-56px 0px"; |
||||
if (index == 2) |
||||
return "0px 0px"; |
||||
if (index == 3) |
||||
return "-112px 0"; |
||||
alert("Background not found: " + index); |
||||
} |
||||
|
||||
// Get the type of meter and its state
|
||||
// Index: 1) enabled 2) meter 3) disabled
|
||||
function meter(index, offset) { |
||||
var top = 5; |
||||
if (offset) |
||||
top += offset; |
||||
if (self.type == RECORD_BUTTON) { |
||||
if (index == 1) |
||||
return "-169px " + top + "px"; |
||||
if (index == 2) |
||||
return "-189px " + top + "px"; |
||||
if (index == 3) |
||||
return "-249px " + top + "px"; |
||||
} else { |
||||
if (index == 1) |
||||
return "-269px " + top + "px"; |
||||
if (index == 2) |
||||
return "-298px " + top + "px"; |
||||
if (index == 3) |
||||
return "-327px " + top + "px"; |
||||
} |
||||
alert("Meter not found: " + self.type + " " + index); |
||||
} |
||||
|
||||
function silhouetteWidth() { |
||||
if (self.type == RECORD_BUTTON) { |
||||
return "20px"; |
||||
} else { |
||||
return "29px"; |
||||
} |
||||
} |
||||
|
||||
function mouseHandler(e) { |
||||
var rightclick; |
||||
if (!e) |
||||
var e = window.event; |
||||
if (e.which) |
||||
rightclick = (e.which == 3); |
||||
else if (e.button) |
||||
rightclick = (e.button == 2); |
||||
|
||||
if (!rightclick) { |
||||
if (self.active && self.onstop) { |
||||
self.active = false; |
||||
self.onstop(); |
||||
} else if (!self.active && self.onstart) { |
||||
self.active = true; |
||||
self.onstart(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
function init() { |
||||
var div = document.createElement("div"); |
||||
var elem = document.getElementById(buttonid); |
||||
if (elem) { |
||||
elem.appendChild(div); |
||||
} else { |
||||
alert('Could not find element on page named ' + buttonid); |
||||
} |
||||
|
||||
self.guidiv = document.createElement("div"); |
||||
self.guidiv.style.width = '56px'; |
||||
self.guidiv.style.height = '63px'; |
||||
self.guidiv.style.cursor = 'pointer'; |
||||
self.guidiv.style.background = "url(" + url + ") no-repeat"; |
||||
self.guidiv.style.backgroundPosition = background(1); |
||||
div.appendChild(self.guidiv); |
||||
|
||||
// margin auto doesn't work in IE quirks mode
|
||||
// http://stackoverflow.com/questions/816343/why-will-this-div-img-not-center-in-ie8
|
||||
// text-align is a hack to force it to work even if you forget the
|
||||
// doctype.
|
||||
self.guidiv.style.textAlign = 'center'; |
||||
|
||||
self.meterDiv = document.createElement("div"); |
||||
self.meterDiv.style.width = silhouetteWidth(); |
||||
self.meterDiv.style.height = '63px'; |
||||
self.meterDiv.style.margin = 'auto'; |
||||
self.meterDiv.style.cursor = 'pointer'; |
||||
self.meterDiv.style.position = 'relative'; |
||||
self.meterDiv.style.background = "url(" + url + ") no-repeat"; |
||||
self.meterDiv.style.backgroundPosition = meter(2); |
||||
self.guidiv.appendChild(self.meterDiv); |
||||
|
||||
self.coverDiv = document.createElement("div"); |
||||
self.coverDiv.style.width = silhouetteWidth(); |
||||
self.coverDiv.style.height = '63px'; |
||||
self.coverDiv.style.margin = 'auto'; |
||||
self.coverDiv.style.cursor = 'pointer'; |
||||
self.coverDiv.style.position = 'relative'; |
||||
self.coverDiv.style.background = "url(" + url + ") no-repeat"; |
||||
self.coverDiv.style.backgroundPosition = meter(1); |
||||
self.meterDiv.appendChild(self.coverDiv); |
||||
|
||||
self.active = false; |
||||
self.guidiv.onmousedown = mouseHandler; |
||||
} |
||||
|
||||
self.isActive = function() { |
||||
return self.active; |
||||
} |
||||
|
||||
self.setActivity = function(level) { |
||||
self.guidiv.onmouseout = function() { |
||||
}; |
||||
self.guidiv.onmouseover = function() { |
||||
}; |
||||
self.guidiv.style.backgroundPosition = background(2); |
||||
self.coverDiv.style.backgroundPosition = meter(1, 5); |
||||
self.meterDiv.style.backgroundPosition = meter(2, 5); |
||||
|
||||
var totalHeight = 31; |
||||
var maxHeight = 9; |
||||
|
||||
// When volume goes up, the black image loses height,
|
||||
// creating the perception of the colored one increasing.
|
||||
var height = (maxHeight + totalHeight - Math.floor(level / 100 |
||||
* totalHeight)); |
||||
self.coverDiv.style.height = height + "px"; |
||||
} |
||||
|
||||
self.setEnabled = function(enable) { |
||||
var guidiv = self.guidiv; |
||||
self.active = false; |
||||
if (enable) { |
||||
self.coverDiv.style.backgroundPosition = meter(1); |
||||
self.meterDiv.style.backgroundPosition = meter(1); |
||||
guidiv.style.backgroundPosition = background(1); |
||||
guidiv.onmousedown = mouseHandler; |
||||
guidiv.onmouseover = function() { |
||||
guidiv.style.backgroundPosition = background(3); |
||||
}; |
||||
guidiv.onmouseout = function() { |
||||
guidiv.style.backgroundPosition = background(1); |
||||
}; |
||||
} else { |
||||
self.coverDiv.style.backgroundPosition = meter(3); |
||||
self.meterDiv.style.backgroundPosition = meter(3); |
||||
guidiv.style.backgroundPosition = background(1); |
||||
guidiv.onmousedown = null; |
||||
guidiv.onmouseout = function() { |
||||
}; |
||||
guidiv.onmouseover = function() { |
||||
}; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,52 @@ |
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> |
||||
<html> |
||||
<head> |
||||
<!-- swfobject is a commonly used library to embed Flash content --> |
||||
<script type="text/javascript" |
||||
src="https://ajax.googleapis.com/ajax/libs/swfobject/2.2/swfobject.js"></script> |
||||
|
||||
<!-- Setup the recorder interface --> |
||||
<script type="text/javascript" src="recorder.js"></script> |
||||
|
||||
<!-- GUI code... take it or leave it --> |
||||
<script type="text/javascript" src="gui.js"></script> |
||||
|
||||
<script> |
||||
function setupRecorder() { |
||||
Wami.setup({ |
||||
id : "wami", |
||||
onReady : setupGUI |
||||
}); |
||||
} |
||||
|
||||
function setupGUI() { |
||||
var gui = new Wami.GUI({ |
||||
id : "wami", |
||||
recordUrl : "https://wami-recorder.appspot.com/audio", |
||||
playUrl : "https://wami-recorder.appspot.com/audio" |
||||
}); |
||||
|
||||
gui.setPlayEnabled(false); |
||||
} |
||||
</script> |
||||
</head> |
||||
|
||||
<body onload="setupRecorder()"> |
||||
<div id="wami" style="margin-left: 100px;"></div> |
||||
<noscript>WAMI requires Javascript</noscript> |
||||
|
||||
<div |
||||
style="position: absolute; left: 400px; top: 20px; font-family: arial, sans-serif; font-size: 82%"> |
||||
Right-click to Download<br /> <br /> <a |
||||
href="https://wami-recorder.googlecode.com/hg/example/client/index.html">index.html</a><br /> |
||||
<a |
||||
href="https://wami-recorder.googlecode.com/hg/example/client/Wami.swf">Wami.swf</a><br /> |
||||
<a |
||||
href="https://wami-recorder.googlecode.com/hg/example/client/buttons.png">buttons.png</a><br /> |
||||
<a |
||||
href="https://wami-recorder.googlecode.com/hg/example/client/recorder.js">recorder.js</a><br /> |
||||
<a |
||||
href="https://wami-recorder.googlecode.com/hg/example/client/gui.js">gui.js</a><br /> |
||||
</div> |
||||
</body> |
||||
</html> |
||||
@ -0,0 +1,273 @@ |
||||
var Wami = window.Wami || {}; |
||||
|
||||
// Returns a (very likely) unique string with of random letters and numbers
|
||||
Wami.createID = function() { |
||||
return "wid" + ("" + 1e10).replace(/[018]/g, function(a) { |
||||
return (a ^ Math.random() * 16 >> a / 4).toString(16) |
||||
}); |
||||
} |
||||
|
||||
// Creates a named callback in WAMI and returns the name as a string.
|
||||
Wami.nameCallback = function(cb, cleanup) { |
||||
Wami._callbacks = Wami._callbacks || {}; |
||||
var id = Wami.createID(); |
||||
Wami._callbacks[id] = function() { |
||||
if (cleanup) { |
||||
Wami._callbacks[id] = null; |
||||
} |
||||
cb.apply(null, arguments); |
||||
}; |
||||
var named = "Wami._callbacks['" + id + "']"; |
||||
return named; |
||||
} |
||||
|
||||
// This method ensures that a WAMI recorder is operational, and that
|
||||
// the following API is available in the Wami namespace. All functions
|
||||
// must be named (i.e. cannot be anonymous).
|
||||
//
|
||||
// Wami.startPlaying(url, startfn = null, finishedfn = null, failedfn = null);
|
||||
// Wami.stopPlaying()
|
||||
//
|
||||
// Wami.startRecording(url, startfn = null, finishedfn = null, failedfn = null);
|
||||
// Wami.stopRecording()
|
||||
//
|
||||
// Wami.getRecordingLevel() // Returns a number between 0 and 100
|
||||
// Wami.getPlayingLevel() // Returns a number between 0 and 100
|
||||
//
|
||||
// Wami.hide()
|
||||
// Wami.show()
|
||||
//
|
||||
// Manipulate the WAMI recorder's settings. In Flash
|
||||
// we need to check if the microphone permission has been granted.
|
||||
// We might also set/return sample rate here, etc.
|
||||
//
|
||||
// Wami.getSettings();
|
||||
// Wami.setSettings(options);
|
||||
//
|
||||
// Optional way to set up browser so that it's constantly listening
|
||||
// This is to prepend audio in case the user starts talking before
|
||||
// they click-to-talk.
|
||||
//
|
||||
// Wami.startListening()
|
||||
//
|
||||
Wami.setup = function(options) { |
||||
if (Wami.startRecording) { |
||||
// Wami's already defined.
|
||||
if (options.onReady) { |
||||
options.onReady(); |
||||
} |
||||
return; |
||||
} |
||||
|
||||
// Assumes that swfobject.js is included if Wami.swfobject isn't
|
||||
// already defined.
|
||||
Wami.swfobject = Wami.swfobject || swfobject; |
||||
|
||||
if (!Wami.swfobject) { |
||||
alert("Unable to find swfobject to help embed the SWF."); |
||||
} |
||||
|
||||
var _options; |
||||
setOptions(options); |
||||
embedWamiSWF(_options.id, Wami.nameCallback(delegateWamiAPI)); |
||||
|
||||
function supportsTransparency() { |
||||
// Detecting the OS is a big no-no in Javascript programming, but
|
||||
// I can't think of a better way to know if wmode is supported or
|
||||
// not... since NOT supporting it (like Flash on Ubuntu) is a bug.
|
||||
return (navigator.platform.indexOf("Linux") == -1); |
||||
} |
||||
|
||||
function setOptions(options) { |
||||
// Start with default options
|
||||
_options = { |
||||
swfUrl : "Wami.swf", |
||||
onReady : function() { |
||||
Wami.hide(); |
||||
}, |
||||
onSecurity : checkSecurity, |
||||
onError : function(error) { |
||||
alert(error); |
||||
} |
||||
}; |
||||
|
||||
if (typeof options == 'undefined') { |
||||
alert('Need at least an element ID to place the Flash object.'); |
||||
} |
||||
|
||||
if (typeof options == 'string') { |
||||
_options.id = options; |
||||
} else { |
||||
_options.id = options.id; |
||||
} |
||||
|
||||
if (options.swfUrl) { |
||||
_options.swfUrl = options.swfUrl; |
||||
} |
||||
|
||||
if (options.onReady) { |
||||
_options.onReady = options.onReady; |
||||
} |
||||
|
||||
if (options.onLoaded) { |
||||
_options.onLoaded = options.onLoaded; |
||||
} |
||||
|
||||
if (options.onSecurity) { |
||||
_options.onSecurity = options.onSecurity; |
||||
} |
||||
|
||||
if (options.onError) { |
||||
_options.onError = options.onError; |
||||
} |
||||
|
||||
// Create a DIV for the SWF under _options.id
|
||||
|
||||
var container = document.createElement('div'); |
||||
container.style.position = 'absolute'; |
||||
_options.cid = Wami.createID(); |
||||
container.setAttribute('id', _options.cid); |
||||
|
||||
var swfdiv = document.createElement('div'); |
||||
var id = Wami.createID(); |
||||
swfdiv.setAttribute('id', id); |
||||
|
||||
container.appendChild(swfdiv); |
||||
document.getElementById(_options.id).appendChild(container); |
||||
|
||||
_options.id = id; |
||||
} |
||||
|
||||
function checkSecurity() { |
||||
var settings = Wami.getSettings(); |
||||
if (settings.microphone.granted) { |
||||
_options.onReady(); |
||||
} else { |
||||
// Show any Flash settings panel you want:
|
||||
// http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/system/SecurityPanel.html
|
||||
Wami.showSecurity("privacy", "Wami.show", Wami |
||||
.nameCallback(_options.onSecurity), Wami |
||||
.nameCallback(_options.onError)); |
||||
} |
||||
} |
||||
|
||||
// Embed the WAMI SWF and call the named callback function when loaded.
|
||||
function embedWamiSWF(id, initfn) { |
||||
var flashVars = { |
||||
visible : false, |
||||
loadedCallback : initfn |
||||
} |
||||
|
||||
var params = { |
||||
allowScriptAccess : "always" |
||||
} |
||||
|
||||
if (supportsTransparency()) { |
||||
params.wmode = "transparent"; |
||||
} |
||||
|
||||
if (typeof console !== 'undefined') { |
||||
flashVars.console = true; |
||||
} |
||||
|
||||
var version = '10.0.0'; |
||||
document.getElementById(id).innerHTML = "WAMI requires Flash " |
||||
+ version |
||||
+ " or greater<br />https://get.adobe.com/flashplayer/"; |
||||
|
||||
// This is the minimum size due to the microphone security panel
|
||||
Wami.swfobject.embedSWF(_options.swfUrl, id, 214, 137, version, null, |
||||
flashVars, params); |
||||
|
||||
// Without this line, Firefox has a dotted outline of the flash
|
||||
Wami.swfobject.createCSS("#" + id, "outline:none"); |
||||
} |
||||
|
||||
// To check if the microphone settings were 'remembered', we
|
||||
// must actually embed an entirely new Wami client and check
|
||||
// whether its microphone is granted. If it is, it was remembered.
|
||||
function checkRemembered(finishedfn) { |
||||
var id = Wami.createID(); |
||||
var div = document.createElement('div'); |
||||
div.style.top = '-999px'; |
||||
div.style.left = '-999px'; |
||||
div.setAttribute('id', id); |
||||
var body = document.getElementsByTagName('body').item(0); |
||||
body.appendChild(div); |
||||
|
||||
var fn = Wami.nameCallback(function() { |
||||
var swf = document.getElementById(id); |
||||
Wami._remembered = swf.getSettings().microphone.granted; |
||||
Wami.swfobject.removeSWF(id); |
||||
eval(finishedfn + "()"); |
||||
}); |
||||
|
||||
embedWamiSWF(id, fn); |
||||
} |
||||
|
||||
// Attach all the audio methods to the Wami namespace in the callback.
|
||||
function delegateWamiAPI() { |
||||
var recorder = document.getElementById(_options.id); |
||||
|
||||
function delegate(name) { |
||||
Wami[name] = function() { |
||||
return recorder[name].apply(recorder, arguments); |
||||
} |
||||
} |
||||
delegate('startPlaying'); |
||||
delegate('stopPlaying'); |
||||
delegate('startRecording'); |
||||
delegate('stopRecording'); |
||||
delegate('startListening'); |
||||
delegate('stopListening'); |
||||
delegate('getRecordingLevel'); |
||||
delegate('getPlayingLevel'); |
||||
delegate('setSettings'); |
||||
|
||||
// Append extra information about whether mic settings are sticky
|
||||
Wami.getSettings = function() { |
||||
var settings = recorder.getSettings(); |
||||
settings.microphone.remembered = Wami._remembered; |
||||
return settings; |
||||
} |
||||
|
||||
Wami.showSecurity = function(panel, startfn, finishedfn, failfn) { |
||||
// Flash must be on top for this.
|
||||
var container = document.getElementById(_options.cid); |
||||
|
||||
var augmentedfn = Wami.nameCallback(function() { |
||||
checkRemembered(finishedfn); |
||||
container.style.cssText = "position: absolute;"; |
||||
}); |
||||
|
||||
container.style.cssText = "position: absolute; z-index: 99999"; |
||||
|
||||
recorder.showSecurity(panel, startfn, augmentedfn, failfn); |
||||
} |
||||
|
||||
Wami.show = function() { |
||||
if (!supportsTransparency()) { |
||||
recorder.style.visibility = "visible"; |
||||
} |
||||
} |
||||
|
||||
Wami.hide = function() { |
||||
// Hiding flash in all the browsers is tricky. Please read:
|
||||
// https://code.google.com/p/wami-recorder/wiki/HidingFlash
|
||||
if (!supportsTransparency()) { |
||||
recorder.style.visibility = "hidden"; |
||||
} |
||||
} |
||||
|
||||
// If we already have permissions, they were previously 'remembered'
|
||||
Wami._remembered = recorder.getSettings().microphone.granted; |
||||
|
||||
if (_options.onLoaded) { |
||||
_options.onLoaded(); |
||||
} |
||||
|
||||
if (!_options.noSecurityCheck) { |
||||
checkSecurity(); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,12 @@ |
||||
Each directory (or sub-directory of 'gae') contains an example server |
||||
in a particular language designed to handle audio incoming from the |
||||
Wami recorder. |
||||
|
||||
The PHP example requires the configuration of a PHP-enabled |
||||
web-server, while the python server can be run on any machine with |
||||
python installed. |
||||
|
||||
If you do not wish to host your own server, you can try out the Google |
||||
App Engine (GAE) examples. The GAE python example stores data in a |
||||
"blobstore", for which Google provides a handy web-based management |
||||
console. |
||||
@ -0,0 +1,9 @@ |
||||
<?php |
||||
# Save the audio to a URL-accessible directory for playback. |
||||
parse_str($_SERVER['QUERY_STRING'], $params); |
||||
$name = isset($params['name']) ? $params['name'] : 'output.wav'; |
||||
$content = file_get_contents('php://input'); |
||||
$fh = fopen($name, 'w') or die("can't open file"); |
||||
fwrite($fh, $content); |
||||
fclose($fh); |
||||
?> |
||||
@ -0,0 +1,53 @@ |
||||
# Run from the commandline: |
||||
# |
||||
# python server.py |
||||
# POST audio to http://localhost:9000 |
||||
# GET audio from http://localhost:9000 |
||||
# |
||||
# A simple server to collect audio using python. To be more secure, |
||||
# you might want to check the file names and place size restrictions |
||||
# on the incoming data. |
||||
|
||||
import cgi |
||||
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer |
||||
|
||||
class WamiHandler(BaseHTTPRequestHandler): |
||||
dirname = "/tmp/" |
||||
|
||||
def do_GET(self): |
||||
f = open(self.get_name()) |
||||
self.send_response(200) |
||||
self.send_header('content-type','audio/x-wav') |
||||
self.end_headers() |
||||
self.wfile.write(f.read()) |
||||
f.close() |
||||
|
||||
def do_POST(self): |
||||
f = open(self.get_name(), "wb") |
||||
# Note that python's HTTPServer doesn't support chunked transfer. |
||||
# Thus, it requires a content-length. |
||||
length = int(self.headers.getheader('content-length')) |
||||
print "POST of length " + str(length) |
||||
f.write(self.rfile.read(length)) |
||||
f.close(); |
||||
|
||||
def get_name(self): |
||||
filename = 'output.wav'; |
||||
qs = self.path.split('?',1); |
||||
if len(qs) == 2: |
||||
params = cgi.parse_qs(qs[1]) |
||||
if params['name']: |
||||
filename = params['name'][0]; |
||||
return WamiHandler.dirname + filename |
||||
|
||||
def main(): |
||||
try: |
||||
server = HTTPServer(('', 9000), WamiHandler) |
||||
print 'Started server...' |
||||
server.serve_forever() |
||||
except KeyboardInterrupt: |
||||
print 'Stopping server' |
||||
server.socket.close() |
||||
|
||||
if __name__ == '__main__': |
||||
main() |
||||
@ -0,0 +1,9 @@ |
||||
<?xml version="1.0" encoding="UTF-8" ?> |
||||
<!-- This is just a Google gadget to toss an example onto the code.google.com page. --> |
||||
<Module> |
||||
<ModulePrefs title="Wami Recorder Example" |
||||
author="Ian McGraw" |
||||
author_email="wami-toolkit@csail.mit.edu" |
||||
description="A Google gadget to exemplify the simplest wami-recorder use-case." /> |
||||
<Content type="url" href="https://wami-recorder.appspot.com/client/index.html"/> |
||||
</Module> |
||||
@ -0,0 +1,171 @@ |
||||
/* |
||||
* Copyright (c) 2011 |
||||
* Spoken Language Systems Group |
||||
* MIT Computer Science and Artificial Intelligence Laboratory |
||||
* Massachusetts Institute of Technology |
||||
* |
||||
* Permission is hereby granted, free of charge, to any person |
||||
* obtaining a copy of this software and associated documentation |
||||
* files (the "Software"), to deal in the Software without |
||||
* restriction, including without limitation the rights to use, copy, |
||||
* modify, merge, publish, distribute, sublicense, and/or sell copies |
||||
* of the Software, and to permit persons to whom the Software is |
||||
* furnished to do so, subject to the following conditions: |
||||
* |
||||
* The above copyright notice and this permission notice shall be |
||||
* included in all copies or substantial portions of the Software. |
||||
* |
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
* SOFTWARE. |
||||
*/ |
||||
package edu.mit.csail.wami.audio |
||||
{ |
||||
import edu.mit.csail.wami.utils.External; |
||||
|
||||
import flash.utils.ByteArray; |
||||
import flash.utils.Endian; |
||||
|
||||
/** |
||||
* This container is better for streaming, because it explicitly |
||||
* says what to do when the length of the audio is unknown. It's typically |
||||
* associated with mu-law compression (which wouldn't be too hard too implement) |
||||
* but here we're using linear PCM. |
||||
*/ |
||||
public class AuContainer implements IAudioContainer |
||||
{ |
||||
public function isLengthRequired():Boolean { |
||||
return false; |
||||
} |
||||
|
||||
public function toByteArray(format:AudioFormat, length:int = -1):ByteArray |
||||
{ |
||||
var dataLength:uint = 0xffffffff; |
||||
if (length > -1) |
||||
{ |
||||
dataLength = length; |
||||
} |
||||
|
||||
if (format.endian != Endian.BIG_ENDIAN) |
||||
{ |
||||
throw new Error("AU is a container for big endian data"); |
||||
} |
||||
// http://en.wikipedia.org/wiki/Au_file_format |
||||
var header:ByteArray = new ByteArray(); |
||||
header.endian = format.endian; |
||||
header.writeUTFBytes(".snd"); |
||||
header.writeInt(24); // Data offset |
||||
header.writeInt(dataLength); |
||||
|
||||
var bits:uint = getEncodingFromBits(format); |
||||
header.writeInt(bits); |
||||
header.writeInt(format.rate); |
||||
header.writeInt(format.channels); |
||||
header.position = 0; |
||||
External.debugBytes(header); |
||||
return header; |
||||
} |
||||
|
||||
private function getEncodingFromBits(format:AudioFormat):uint |
||||
{ |
||||
if (format.bits == 16) |
||||
{ |
||||
return 3; |
||||
} |
||||
else if (format.bits == 24) |
||||
{ |
||||
return 4; |
||||
} |
||||
else if (format.bits == 32) |
||||
{ |
||||
return 5; |
||||
} |
||||
|
||||
throw new Error("Bits not supported"); |
||||
} |
||||
|
||||
private function getBitsFromEncoding(encoding:uint):uint |
||||
{ |
||||
if (encoding == 3) |
||||
{ |
||||
return 16; |
||||
} |
||||
else if (encoding == 4) |
||||
{ |
||||
return 24; |
||||
} |
||||
else if (encoding == 5) |
||||
{ |
||||
return 32; |
||||
} |
||||
|
||||
throw new Error("Encoding not supported: " + encoding); |
||||
} |
||||
|
||||
public function fromByteArray(header:ByteArray):AudioFormat |
||||
{ |
||||
if (header.bytesAvailable < 24) |
||||
{ |
||||
return notAu(header, "Header not yet long enough for Au"); |
||||
} |
||||
|
||||
var b:ByteArray = new ByteArray(); |
||||
header.readBytes(b, 0, 24); |
||||
External.debugBytes(b); |
||||
header.position = 0; |
||||
|
||||
header.endian = Endian.BIG_ENDIAN; // Header is big-endian |
||||
|
||||
var magic:String = header.readUTFBytes(4); |
||||
if (magic != ".snd") |
||||
{ |
||||
return notAu(header, "Not an AU header, first bytes should be .snd"); |
||||
} |
||||
|
||||
var dataOffset:uint = header.readInt(); |
||||
var dataLength:uint = header.readInt(); |
||||
|
||||
if (header.bytesAvailable < dataOffset - 12) |
||||
{ |
||||
return notAu(header, "Header of length " + header.bytesAvailable + " not long enough yet to include offset of length " + dataOffset); |
||||
} |
||||
|
||||
var encoding:uint = header.readInt(); |
||||
|
||||
var bits:uint; |
||||
try { |
||||
bits = getBitsFromEncoding(encoding); |
||||
} catch (e:Error) { |
||||
return notAu(header, e.message); |
||||
} |
||||
|
||||
var rate:uint = header.readInt(); |
||||
var channels:uint = header.readInt(); |
||||
|
||||
header.position = dataOffset; |
||||
|
||||
var format:AudioFormat; |
||||
try |
||||
{ |
||||
format = new AudioFormat(rate, channels, bits, Endian.BIG_ENDIAN); |
||||
} catch (e:Error) |
||||
{ |
||||
return notAu(header, e.message); |
||||
} |
||||
|
||||
return format; |
||||
} |
||||
|
||||
private function notAu(header:ByteArray, msg:String):AudioFormat |
||||
{ |
||||
External.debug("Not Au: " + msg); |
||||
header.position = 0; |
||||
return null; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,158 @@ |
||||
/* |
||||
* Copyright (c) 2011 |
||||
* Spoken Language Systems Group |
||||
* MIT Computer Science and Artificial Intelligence Laboratory |
||||
* Massachusetts Institute of Technology |
||||
* |
||||
* Permission is hereby granted, free of charge, to any person |
||||
* obtaining a copy of this software and associated documentation |
||||
* files (the "Software"), to deal in the Software without |
||||
* restriction, including without limitation the rights to use, copy, |
||||
* modify, merge, publish, distribute, sublicense, and/or sell copies |
||||
* of the Software, and to permit persons to whom the Software is |
||||
* furnished to do so, subject to the following conditions: |
||||
* |
||||
* The above copyright notice and this permission notice shall be |
||||
* included in all copies or substantial portions of the Software. |
||||
* |
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
* SOFTWARE. |
||||
*/ |
||||
package edu.mit.csail.wami.audio |
||||
{ |
||||
import flash.utils.Endian; |
||||
|
||||
/** |
||||
* This class keeps track of all the information that defines the |
||||
* audio format independent of the actual audio container |
||||
* (e.g. .wav or .au) |
||||
*/ |
||||
public class AudioFormat |
||||
{ |
||||
// Allow 8 and 16 kHz as well. |
||||
public static var allrates:Boolean = false; |
||||
|
||||
public var channels:uint; |
||||
public var rate:uint; |
||||
public var bits:uint; |
||||
public var endian:String; |
||||
|
||||
public function AudioFormat(rate:uint, channels:uint, bits:uint, endian:String) |
||||
{ |
||||
this.rate = rate; |
||||
this.channels = channels; |
||||
this.endian = endian; |
||||
this.bits = bits; |
||||
|
||||
validate(); |
||||
} |
||||
|
||||
// flash.media.Microphone quasi-rounds sample rates in kHz |
||||
public static function toRoundedRate(rate:uint):uint |
||||
{ |
||||
if (rate == 5512) |
||||
{ |
||||
return 5; |
||||
} |
||||
else if (rate == 8000) |
||||
{ |
||||
return 8; |
||||
} |
||||
else if (rate == 11025) |
||||
{ |
||||
return 11; |
||||
} |
||||
else if (rate == 16000) |
||||
{ |
||||
return 16; |
||||
} |
||||
else if (rate == 22050) |
||||
{ |
||||
return 22; |
||||
} |
||||
else if (rate == 44100) |
||||
{ |
||||
return 44; |
||||
} |
||||
|
||||
throw new Error("Unsupported sample rate in Hz: " + rate); |
||||
} |
||||
|
||||
|
||||
public static function fromRoundedRate(rate:uint):uint |
||||
{ |
||||
if (rate == 5) |
||||
{ |
||||
return 5512; |
||||
} |
||||
else if (rate == 8) |
||||
{ |
||||
return 8000; |
||||
} |
||||
else if (rate == 11) |
||||
{ |
||||
return 11025; |
||||
} |
||||
else if (rate == 16) |
||||
{ |
||||
return 16000; |
||||
} |
||||
else if (rate == 22) |
||||
{ |
||||
return 22050; |
||||
} |
||||
else if (rate == 44) |
||||
{ |
||||
return 44100; |
||||
} |
||||
|
||||
throw new Error("Unsupported sample rate rounded in kHz: " + rate); |
||||
} |
||||
|
||||
public function validate():void |
||||
{ |
||||
if (bits != 8 && bits != 16 && bits != 32) |
||||
{ |
||||
throw new Error("Unsupported number of bits per sample: " + bits); |
||||
} |
||||
|
||||
if (channels != 1 && channels != 2) |
||||
{ |
||||
throw new Error("Unsupported number of channels: " + channels); |
||||
} |
||||
|
||||
if (endian != Endian.BIG_ENDIAN && endian != Endian.LITTLE_ENDIAN) |
||||
{ |
||||
throw new Error("Unsupported endian type: " + endian); |
||||
} |
||||
|
||||
var msg:String = ""; |
||||
if (rate < 100) |
||||
{ |
||||
throw new Error("Rate should be in Hz"); |
||||
} |
||||
else if (rate != 5512 && rate != 8000 && rate != 11025 && rate != 16000 && rate != 22050 && rate != 44100) |
||||
{ |
||||
msg = "Sample rate of " + rate + " is not supported."; |
||||
msg += " See flash.media.Microphone documentation." |
||||
throw new Error(msg); |
||||
} |
||||
else if (!allrates && (rate == 8000 || rate == 16000 || rate == 11025)) { |
||||
msg = "8kHz and 16kHz are supported for recording but not playback. 11kHz doesn't work in Ubuntu."; |
||||
msg += " Enable all rates via a parameter passed into the Flash." |
||||
throw new Error(msg); |
||||
} |
||||
} |
||||
|
||||
public function toString():String |
||||
{ |
||||
return "Rate: " + rate + " Channels " + channels + " Bits: " + bits + " Endian: " + endian; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,125 @@ |
||||
/* |
||||
* Copyright (c) 2011 |
||||
* Spoken Language Systems Group |
||||
* MIT Computer Science and Artificial Intelligence Laboratory |
||||
* Massachusetts Institute of Technology |
||||
* |
||||
* Permission is hereby granted, free of charge, to any person |
||||
* obtaining a copy of this software and associated documentation |
||||
* files (the "Software"), to deal in the Software without |
||||
* restriction, including without limitation the rights to use, copy, |
||||
* modify, merge, publish, distribute, sublicense, and/or sell copies |
||||
* of the Software, and to permit persons to whom the Software is |
||||
* furnished to do so, subject to the following conditions: |
||||
* |
||||
* The above copyright notice and this permission notice shall be |
||||
* included in all copies or substantial portions of the Software. |
||||
* |
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
* SOFTWARE. |
||||
*/ |
||||
package edu.mit.csail.wami.audio |
||||
{ |
||||
import edu.mit.csail.wami.audio.AudioFormat; |
||||
import edu.mit.csail.wami.audio.IAudioContainer; |
||||
import edu.mit.csail.wami.utils.External; |
||||
import edu.mit.csail.wami.utils.Pipe; |
||||
|
||||
import flash.utils.ByteArray; |
||||
import flash.utils.Endian; |
||||
|
||||
/** |
||||
* Convert WAVE data coming in to the float-based format flash uses. |
||||
*/ |
||||
public class DecodePipe extends Pipe |
||||
{ |
||||
private var format:AudioFormat; |
||||
private var header:ByteArray = new ByteArray(); |
||||
private var containers:Vector.<IAudioContainer>; |
||||
|
||||
public function DecodePipe(containers:Vector.<IAudioContainer>) { |
||||
if (containers.length == 0) { |
||||
throw new Error("Must have at least one container."); |
||||
} |
||||
this.containers = containers; |
||||
} |
||||
|
||||
override public function write(bytes:ByteArray):void |
||||
{ |
||||
if (format == null) |
||||
{ |
||||
// Try to get header by parsing from each container |
||||
bytes.readBytes(header, header.length, bytes.length); |
||||
for each (var container:IAudioContainer in containers) { |
||||
format = container.fromByteArray(header); |
||||
if (format != null) { |
||||
// Put the leftover bytes back |
||||
bytes = new ByteArray(); |
||||
header.readBytes(bytes); |
||||
External.debug("Format: " + format); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (format != null && bytes.bytesAvailable) |
||||
{ |
||||
bytes.endian = format.endian; |
||||
super.write(decode(bytes)); |
||||
} |
||||
} |
||||
|
||||
private function decode(bytes:ByteArray):ByteArray |
||||
{ |
||||
var decoded:ByteArray = new ByteArray(); |
||||
while (bytes.bytesAvailable) |
||||
{ |
||||
var sample1:Number = getSample(bytes); |
||||
var sample2:Number = sample1; |
||||
if (format.channels == 2) |
||||
{ |
||||
sample2 = getSample(bytes); |
||||
} |
||||
|
||||
// cheap way to upsample |
||||
var repeat:uint = 44100 / format.rate; |
||||
while (repeat-- > 0) { |
||||
decoded.writeFloat(sample1); |
||||
decoded.writeFloat(sample2); |
||||
} |
||||
} |
||||
decoded.position = 0; |
||||
return decoded; |
||||
} |
||||
|
||||
private function getSample(bytes:ByteArray):Number |
||||
{ |
||||
var sample:Number; |
||||
|
||||
if (format.bits == 8) |
||||
{ |
||||
sample = bytes.readByte()/0x7f; |
||||
} |
||||
else if (format.bits == 16) |
||||
{ |
||||
sample = bytes.readShort()/0x7fff; |
||||
} |
||||
else if (format.bits == 32) |
||||
{ |
||||
sample = bytes.readInt()/0x7fffffff; |
||||
} |
||||
else |
||||
{ |
||||
throw new Error("Unsupported bits per sample: " + format.bits); |
||||
} |
||||
|
||||
return sample; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,126 @@ |
||||
/* |
||||
* Copyright (c) 2011 |
||||
* Spoken Language Systems Group |
||||
* MIT Computer Science and Artificial Intelligence Laboratory |
||||
* Massachusetts Institute of Technology |
||||
* |
||||
* Permission is hereby granted, free of charge, to any person |
||||
* obtaining a copy of this software and associated documentation |
||||
* files (the "Software"), to deal in the Software without |
||||
* restriction, including without limitation the rights to use, copy, |
||||
* modify, merge, publish, distribute, sublicense, and/or sell copies |
||||
* of the Software, and to permit persons to whom the Software is |
||||
* furnished to do so, subject to the following conditions: |
||||
* |
||||
* The above copyright notice and this permission notice shall be |
||||
* included in all copies or substantial portions of the Software. |
||||
* |
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
* SOFTWARE. |
||||
*/ |
||||
package edu.mit.csail.wami.audio |
||||
{ |
||||
import edu.mit.csail.wami.audio.AudioFormat; |
||||
import edu.mit.csail.wami.audio.IAudioContainer; |
||||
import edu.mit.csail.wami.utils.Pipe; |
||||
|
||||
import flash.utils.ByteArray; |
||||
|
||||
/** |
||||
* Convert float format to raw audio accoring to the audio format passed in. |
||||
*/ |
||||
public class EncodePipe extends Pipe |
||||
{ |
||||
private var format:AudioFormat; |
||||
private var container:IAudioContainer; |
||||
|
||||
// Buffer if container requires length. In this case, |
||||
// we cannot write the data to the sink until the very end. |
||||
private var buffer:ByteArray; |
||||
private var headerWritten:Boolean; |
||||
|
||||
function EncodePipe(format:AudioFormat, container:IAudioContainer) |
||||
{ |
||||
this.format = format; |
||||
this.container = container; |
||||
this.buffer = new ByteArray(); |
||||
headerWritten = false; |
||||
} |
||||
|
||||
override public function write(bytes:ByteArray):void |
||||
{ |
||||
var transcoded:ByteArray = new ByteArray(); |
||||
transcoded.endian = format.endian; |
||||
|
||||
while (bytes.bytesAvailable >= 4) |
||||
{ |
||||
var sample:int; |
||||
if (format.bits == 16) |
||||
{ |
||||
sample = bytes.readFloat()*0x7fff; |
||||
transcoded.writeShort(sample); |
||||
if (format.channels == 2) |
||||
{ |
||||
transcoded.writeShort(sample); |
||||
} |
||||
} |
||||
else if (format.bits == 32) |
||||
{ |
||||
sample = bytes.readFloat()*0x7fffffff; |
||||
transcoded.writeInt(sample); |
||||
if (format.channels == 2) |
||||
{ |
||||
transcoded.writeInt(sample); |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
throw new Error("Unsupported bits per sample: " + format.bits); |
||||
} |
||||
} |
||||
transcoded.position = 0; |
||||
handleEncoded(transcoded); |
||||
} |
||||
|
||||
private function handleEncoded(bytes:ByteArray):void { |
||||
if (container == null) { |
||||
// No container, just stream it on |
||||
super.write(bytes); |
||||
return; |
||||
} |
||||
|
||||
if (container.isLengthRequired()) |
||||
{ |
||||
buffer.writeBytes(bytes, bytes.position, bytes.bytesAvailable); |
||||
return; |
||||
} |
||||
|
||||
if (!headerWritten) |
||||
{ |
||||
var header:ByteArray = container.toByteArray(format); |
||||
super.write(header); |
||||
headerWritten = true; |
||||
} |
||||
super.write(bytes); |
||||
} |
||||
|
||||
override public function close():void |
||||
{ |
||||
if (container != null && container.isLengthRequired()) |
||||
{ |
||||
// Write the audio (including the header). |
||||
buffer.position = 0; |
||||
super.write(container.toByteArray(format, buffer.length)); |
||||
super.write(buffer); |
||||
} |
||||
|
||||
super.close(); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,54 @@ |
||||
/* |
||||
* Copyright (c) 2011 |
||||
* Spoken Language Systems Group |
||||
* MIT Computer Science and Artificial Intelligence Laboratory |
||||
* Massachusetts Institute of Technology |
||||
* |
||||
* Permission is hereby granted, free of charge, to any person |
||||
* obtaining a copy of this software and associated documentation |
||||
* files (the "Software"), to deal in the Software without |
||||
* restriction, including without limitation the rights to use, copy, |
||||
* modify, merge, publish, distribute, sublicense, and/or sell copies |
||||
* of the Software, and to permit persons to whom the Software is |
||||
* furnished to do so, subject to the following conditions: |
||||
* |
||||
* The above copyright notice and this permission notice shall be |
||||
* included in all copies or substantial portions of the Software. |
||||
* |
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
* SOFTWARE. |
||||
*/ |
||||
package edu.mit.csail.wami.audio |
||||
{ |
||||
import flash.utils.ByteArray; |
||||
|
||||
/** |
||||
* There are a number of ways to store raw audio. WAV is a container from |
||||
* Microsoft. AU is a container from Sun Microsystems. This interface |
||||
* helps us separate the container format from the audio format itself. |
||||
*/ |
||||
public interface IAudioContainer |
||||
{ |
||||
function toByteArray(format:AudioFormat, length:int = -1):ByteArray; |
||||
|
||||
/** |
||||
* If successful, the position is left at the first byte after |
||||
* the header. If the bytes do not represent the expected container |
||||
* header null is returned and the position is returned to 0. |
||||
*/ |
||||
function fromByteArray(bytes:ByteArray):AudioFormat; |
||||
|
||||
/** |
||||
* Some containers (e.g. WAV) require the length of the data to be specified, |
||||
* and thus are not amenable to streaming. Others (e.g. AU) have well |
||||
* defined ways of dealing with data of unknown length. |
||||
*/ |
||||
function isLengthRequired():Boolean; |
||||
} |
||||
} |
||||
@ -0,0 +1,131 @@ |
||||
/* |
||||
* Copyright (c) 2011 |
||||
* Spoken Language Systems Group |
||||
* MIT Computer Science and Artificial Intelligence Laboratory |
||||
* Massachusetts Institute of Technology |
||||
* |
||||
* Permission is hereby granted, free of charge, to any person |
||||
* obtaining a copy of this software and associated documentation |
||||
* files (the "Software"), to deal in the Software without |
||||
* restriction, including without limitation the rights to use, copy, |
||||
* modify, merge, publish, distribute, sublicense, and/or sell copies |
||||
* of the Software, and to permit persons to whom the Software is |
||||
* furnished to do so, subject to the following conditions: |
||||
* |
||||
* The above copyright notice and this permission notice shall be |
||||
* included in all copies or substantial portions of the Software. |
||||
* |
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
* SOFTWARE. |
||||
*/ |
||||
package edu.mit.csail.wami.audio |
||||
{ |
||||
import edu.mit.csail.wami.utils.External; |
||||
|
||||
import flash.utils.ByteArray; |
||||
import flash.utils.Endian; |
||||
|
||||
/** |
||||
* This class builds a WAVE header formt the audio format. |
||||
*/ |
||||
public class WaveContainer implements IAudioContainer |
||||
{ |
||||
public function isLengthRequired():Boolean { |
||||
return true; |
||||
} |
||||
|
||||
public function toByteArray(audioFormat:AudioFormat, length:int = -1):ByteArray |
||||
{ |
||||
// https://ccrma.stanford.edu/courses/422/projects/WaveFormat/ |
||||
var id:String = (audioFormat.endian == Endian.LITTLE_ENDIAN) ? "RIFF" : "RIFX"; |
||||
var bytesPerSample:uint = audioFormat.channels*audioFormat.bits/8; |
||||
|
||||
var header:ByteArray = new ByteArray(); |
||||
|
||||
// Little-endian is generally the way to go for WAVs |
||||
header.endian = Endian.LITTLE_ENDIAN; |
||||
header.writeUTFBytes(id); |
||||
header.writeInt(length > 0 ? 36 + length : 0); |
||||
header.writeUTFBytes("WAVE"); |
||||
header.writeUTFBytes("fmt "); |
||||
header.writeInt(16); |
||||
header.writeShort(1); |
||||
header.writeShort(audioFormat.channels); |
||||
header.writeInt(audioFormat.rate); |
||||
header.writeInt(audioFormat.rate*bytesPerSample); |
||||
header.writeShort(bytesPerSample); |
||||
header.writeShort(audioFormat.bits); |
||||
header.writeUTFBytes('data'); |
||||
header.writeInt(length); |
||||
header.position = 0; |
||||
return header; |
||||
} |
||||
|
||||
public function fromByteArray(header:ByteArray):AudioFormat |
||||
{ |
||||
if (header.bytesAvailable < 44) { |
||||
var msg:String = "This header is not yet long enough "; |
||||
msg += "(need 44 bytes only have " + header.bytesAvailable + ")." |
||||
return notWav(header, msg); |
||||
} |
||||
|
||||
var endian:String = Endian.LITTLE_ENDIAN; |
||||
var chunkID:String = header.readUTFBytes(4); |
||||
if (chunkID == "RIFX") |
||||
{ |
||||
endian = Endian.BIG_ENDIAN; |
||||
} |
||||
else if (chunkID != "RIFF") |
||||
{ |
||||
return notWav(header, "Does not look like a WAVE header: " + chunkID); |
||||
} |
||||
|
||||
header.endian = Endian.LITTLE_ENDIAN; // Header is little-endian |
||||
var totalLength:uint = header.readInt() + 8; |
||||
var waveFmtStr:String = header.readUTFBytes(8); // "WAVEfmt " |
||||
if (waveFmtStr != "WAVEfmt ") |
||||
{ |
||||
return notWav(header, "RIFF header, but not a WAV."); |
||||
} |
||||
var subchunkSize:uint = header.readUnsignedInt(); // 16 |
||||
var audioFormat:uint = header.readShort(); // 1 |
||||
if (audioFormat != 1) { |
||||
return notWav(header, "Currently we only support linear PCM"); |
||||
} |
||||
var channels:uint = header.readShort(); |
||||
var rate:uint = header.readInt(); |
||||
var bps:uint = header.readInt(); |
||||
var bytesPerSample:uint = header.readShort(); |
||||
var bits:uint = header.readShort(); |
||||
var dataStr:String = header.readUTFBytes(4); // "data" |
||||
var length:uint = header.readInt(); |
||||
|
||||
var format:AudioFormat; |
||||
try |
||||
{ |
||||
format = new AudioFormat(rate, channels, bits, endian); |
||||
} catch (e:Error) |
||||
{ |
||||
return notWav(header, e.message); |
||||
} |
||||
return format; |
||||
} |
||||
|
||||
/** |
||||
* Emit error message for debugging, reset the ByteArray and |
||||
* return null. |
||||
*/ |
||||
private function notWav(header:ByteArray, msg:String):AudioFormat |
||||
{ |
||||
External.debug("Not WAV: " + msg); |
||||
header.position = 0; |
||||
return null; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,104 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<mx:Application xmlns:fx="http://ns.adobe.com/mxml/2009" |
||||
xmlns:mx="library://ns.adobe.com/flex/mx" |
||||
minWidth="1" minHeight="1" backgroundAlpha="0" |
||||
applicationComplete="applicationCompleteHandler(event)"> |
||||
<!--- |
||||
The WAMI recorder is not meant to be seen, except for when the security panel |
||||
is in view. There is, however, an interface for the times you want to run it |
||||
in a debugger. |
||||
--> |
||||
<fx:Script> |
||||
<![CDATA[ |
||||
import edu.mit.csail.wami.utils.External; |
||||
|
||||
import mx.controls.Button; |
||||
import mx.controls.Label; |
||||
import mx.core.UIComponent; |
||||
|
||||
private function applicationCompleteHandler(event:Event):void { |
||||
External.debug('WAMI Flash Version 1.0: ' + Security.sandboxType); |
||||
Security.allowInsecureDomain("*"); |
||||
var params:WamiParams = new WamiParams(LoaderInfo(this.root.loaderInfo).parameters); |
||||
|
||||
var audio:WamiAudio = new WamiAudio(params); |
||||
var settings:FlashSettings = new FlashSettings(stage); |
||||
|
||||
if (params.visible) |
||||
{ |
||||
showDebugInterface(params, audio); |
||||
} |
||||
|
||||
External.call(params.loadedCallback); |
||||
} |
||||
|
||||
private function showDebugInterface(params:WamiParams, audio:WamiAudio):void |
||||
{ |
||||
this.horizontalScrollPolicy = "off"; |
||||
this.verticalScrollPolicy = "off"; |
||||
|
||||
var label:Label = new Label(); |
||||
var interval:uint; |
||||
|
||||
var recordButton:Button = new Button(); |
||||
recordButton.label = "Record"; |
||||
addElement(recordButton); |
||||
recordButton.addEventListener(MouseEvent.CLICK, function(evt:MouseEvent):void |
||||
{ |
||||
audio.startRecording(params.testRecordUrl); |
||||
label.setStyle("color", "#500000"); |
||||
interval = setInterval(function():void |
||||
{ |
||||
label.text = "Activity: " + audio.getRecordingLevel(); |
||||
}, 250); |
||||
}); |
||||
|
||||
var stopButton:Button = new Button(); |
||||
stopButton.label = "Stop"; |
||||
addElement(stopButton); |
||||
stopButton.addEventListener(MouseEvent.CLICK, function(evt:MouseEvent):void |
||||
{ |
||||
if (interval) clearInterval(interval); |
||||
interval = 0; |
||||
label.text = ""; |
||||
audio.stopRecording(); |
||||
audio.stopPlaying(); |
||||
}); |
||||
|
||||
var playButton:Button = new Button(); |
||||
playButton.label = "Play"; |
||||
addElement(playButton); |
||||
var uic:UIComponent = new UIComponent(); |
||||
var video:Video = new Video(); |
||||
uic.addChild(video); |
||||
addChild(uic); |
||||
playButton.addEventListener(MouseEvent.CLICK, function(evt:MouseEvent):void |
||||
{ |
||||
function ns_onMetaData(item:Object):void { |
||||
trace("metaData"); |
||||
// Resize video instance. |
||||
video.width = item.width; |
||||
video.height = item.height; |
||||
// Center video instance on Stage. |
||||
video.x = (stage.stageWidth - video.width) / 2; |
||||
video.y = (stage.stageHeight - video.height) / 2; |
||||
} |
||||
|
||||
function ns_onCuePoint(item:Object):void { |
||||
trace("cuePoint"); |
||||
trace(item.name + "\t" + item.time); |
||||
} |
||||
|
||||
audio.startPlaying(params.testPlayUrl); |
||||
label.setStyle("color", "#000500"); |
||||
interval = setInterval(function():void |
||||
{ |
||||
label.text = "Activity: " + audio.getPlayingLevel(); |
||||
}, 250); |
||||
}); |
||||
|
||||
addChild(label); |
||||
} |
||||
]]> |
||||
</fx:Script> |
||||
</mx:Application> |
||||
@ -0,0 +1,109 @@ |
||||
/* |
||||
* Copyright (c) 2011 |
||||
* Spoken Language Systems Group |
||||
* MIT Computer Science and Artificial Intelligence Laboratory |
||||
* Massachusetts Institute of Technology |
||||
* |
||||
* Permission is hereby granted, free of charge, to any person |
||||
* obtaining a copy of this software and associated documentation |
||||
* files (the "Software"), to deal in the Software without |
||||
* restriction, including without limitation the rights to use, copy, |
||||
* modify, merge, publish, distribute, sublicense, and/or sell copies |
||||
* of the Software, and to permit persons to whom the Software is |
||||
* furnished to do so, subject to the following conditions: |
||||
* |
||||
* The above copyright notice and this permission notice shall be |
||||
* included in all copies or substantial portions of the Software. |
||||
* |
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
* SOFTWARE. |
||||
*/ |
||||
package edu.mit.csail.wami.client |
||||
{ |
||||
import edu.mit.csail.wami.play.IPlayer; |
||||
import edu.mit.csail.wami.play.WamiPlayer; |
||||
import edu.mit.csail.wami.record.IRecorder; |
||||
import edu.mit.csail.wami.record.WamiRecorder; |
||||
import edu.mit.csail.wami.utils.External; |
||||
|
||||
import flash.display.MovieClip; |
||||
|
||||
public class WamiAudio extends MovieClip |
||||
{ |
||||
private var recorder:IRecorder; |
||||
private var player:IPlayer; |
||||
|
||||
private var checkSettingsIntervalID:int = 0; |
||||
private var checkSettingsInterval:int = 1000; |
||||
|
||||
function WamiAudio(params:WamiParams) |
||||
{ |
||||
recorder = new WamiRecorder(params.getMicrophone(), params); |
||||
player = new WamiPlayer(); |
||||
|
||||
External.addCallback("startListening", startListening); |
||||
External.addCallback("stopListening", stopListening); |
||||
External.addCallback("startRecording", startRecording); |
||||
External.addCallback("stopRecording",stopRecording); |
||||
External.addCallback("getRecordingLevel", getRecordingLevel); |
||||
|
||||
External.addCallback("startPlaying",startPlaying); |
||||
External.addCallback("stopPlaying",stopPlaying); |
||||
External.addCallback("getPlayingLevel", getPlayingLevel); |
||||
} |
||||
|
||||
internal function startPlaying(url:String, |
||||
startedCallback:String = null, |
||||
finishedCallback:String = null, |
||||
failedCallback:String = null):void |
||||
{ |
||||
recorder.stop(true); |
||||
player.start(url, new WamiListener(startedCallback, finishedCallback, failedCallback)); |
||||
} |
||||
|
||||
internal function stopPlaying():void |
||||
{ |
||||
player.stop(); |
||||
} |
||||
|
||||
internal function getPlayingLevel():int |
||||
{ |
||||
return player.level(); |
||||
} |
||||
|
||||
private function startListening(paddingMillis:uint = 200):void |
||||
{ |
||||
recorder.listen(paddingMillis); |
||||
} |
||||
|
||||
private function stopListening():void |
||||
{ |
||||
recorder.unlisten(); |
||||
} |
||||
|
||||
internal function startRecording(url:String, |
||||
startedCallback:String = null, |
||||
finishedCallback:String = null, |
||||
failedCallback:String = null):void |
||||
{ |
||||
recorder.start(url, new WamiListener(startedCallback, finishedCallback, failedCallback)); |
||||
} |
||||
|
||||
internal function stopRecording():void |
||||
{ |
||||
recorder.stop(); |
||||
} |
||||
|
||||
internal function getRecordingLevel():int |
||||
{ |
||||
return recorder.level(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
@ -0,0 +1,61 @@ |
||||
/* |
||||
* Copyright (c) 2011 |
||||
* Spoken Language Systems Group |
||||
* MIT Computer Science and Artificial Intelligence Laboratory |
||||
* Massachusetts Institute of Technology |
||||
* |
||||
* Permission is hereby granted, free of charge, to any person |
||||
* obtaining a copy of this software and associated documentation |
||||
* files (the "Software"), to deal in the Software without |
||||
* restriction, including without limitation the rights to use, copy, |
||||
* modify, merge, publish, distribute, sublicense, and/or sell copies |
||||
* of the Software, and to permit persons to whom the Software is |
||||
* furnished to do so, subject to the following conditions: |
||||
* |
||||
* The above copyright notice and this permission notice shall be |
||||
* included in all copies or substantial portions of the Software. |
||||
* |
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
* SOFTWARE. |
||||
*/ |
||||
package edu.mit.csail.wami.client |
||||
{ |
||||
import edu.mit.csail.wami.utils.External; |
||||
import edu.mit.csail.wami.utils.StateListener; |
||||
|
||||
/** |
||||
* Translates audio events into Javascript callbacks. |
||||
*/ |
||||
public class WamiListener implements StateListener |
||||
{ |
||||
private var startCallback:String, finishedCallback:String, failedCallback:String; |
||||
|
||||
function WamiListener(startCallback:String, finishedCallback:String, failedCallback:String) |
||||
{ |
||||
this.startCallback = startCallback; |
||||
this.finishedCallback = finishedCallback; |
||||
this.failedCallback = failedCallback; |
||||
} |
||||
|
||||
public function started():void |
||||
{ |
||||
External.call(startCallback); |
||||
} |
||||
|
||||
public function finished():void |
||||
{ |
||||
External.call(finishedCallback); |
||||
} |
||||
|
||||
public function failed(error:Error):void |
||||
{ |
||||
External.call(failedCallback, error.message); |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,180 @@ |
||||
/* |
||||
* Copyright (c) 2011 |
||||
* Spoken Language Systems Group |
||||
* MIT Computer Science and Artificial Intelligence Laboratory |
||||
* Massachusetts Institute of Technology |
||||
* |
||||
* Permission is hereby granted, free of charge, to any person |
||||
* obtaining a copy of this software and associated documentation |
||||
* files (the "Software"), to deal in the Software without |
||||
* restriction, including without limitation the rights to use, copy, |
||||
* modify, merge, publish, distribute, sublicense, and/or sell copies |
||||
* of the Software, and to permit persons to whom the Software is |
||||
* furnished to do so, subject to the following conditions: |
||||
* |
||||
* The above copyright notice and this permission notice shall be |
||||
* included in all copies or substantial portions of the Software. |
||||
* |
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
* SOFTWARE. |
||||
*/ |
||||
package edu.mit.csail.wami.client |
||||
{ |
||||
import edu.mit.csail.wami.audio.AudioFormat; |
||||
import edu.mit.csail.wami.utils.External; |
||||
|
||||
import flash.media.Microphone; |
||||
import flash.utils.ByteArray; |
||||
import flash.utils.Endian; |
||||
|
||||
/** |
||||
* A class documents the possible parameters and sets a few defaults. |
||||
* The defaults are set up to stream to localhost. |
||||
*/ |
||||
public class WamiParams |
||||
{ |
||||
private var mic:Microphone; |
||||
|
||||
// Show the debug interface. |
||||
public var visible:Boolean = true; |
||||
|
||||
// Append this many milliseconds of audio before |
||||
// and after calls to startRecording/stopRecording. |
||||
public var paddingMillis:uint = 250; |
||||
|
||||
// Send the audio using multiple HTTP Posts. |
||||
public var stream:Boolean = false; |
||||
|
||||
// The URLs used in the debugging interface. |
||||
public var testRecordUrl:String = "https://wami-recorder.appspot.com/audio"; |
||||
public var testPlayUrl:String = "https://wami-recorder.appspot.com/audio"; |
||||
|
||||
// Callbacks for loading the client. |
||||
public var loadedCallback:String; |
||||
public var format:AudioFormat; |
||||
|
||||
public function WamiParams(params:Object):void |
||||
{ |
||||
mic = Microphone.getMicrophone(); |
||||
|
||||
External.addCallback("setSettings", setSettings); |
||||
External.addCallback("getSettings", getSettings); |
||||
|
||||
if (params.stream != undefined) |
||||
{ |
||||
stream = params.stream == "true"; |
||||
} |
||||
|
||||
if (params.visible != undefined) |
||||
{ |
||||
visible = params.visible == "true"; |
||||
} |
||||
|
||||
if (params.console != undefined) { |
||||
External.debugToConsole = params.console == "true"; |
||||
} |
||||
|
||||
// Override to allow recording at 8000 and 16000 as well. |
||||
// Note that playback at these sample-rates will be sped up. |
||||
if (params.allrates != undefined) { |
||||
AudioFormat.allrates = params.allrates == "true"; |
||||
} |
||||
|
||||
loadedCallback = params.loadedCallback; |
||||
|
||||
var rate:uint = 22050; |
||||
if (params.rate != undefined) |
||||
{ |
||||
rate = uint(params.rate); |
||||
} |
||||
format = new AudioFormat(rate, 1, 16, Endian.LITTLE_ENDIAN); |
||||
} |
||||
|
||||
public function getMicrophone():Microphone { |
||||
return mic; |
||||
} |
||||
|
||||
// Settings (including microphone security) are passed back here. |
||||
internal function getSettings():Object |
||||
{ |
||||
var json:Object = { |
||||
"container" : (stream) ? "au" : "wav", |
||||
"encoding" : "pcm", |
||||
"signed" : true, |
||||
"sampleSize" : format.bits, |
||||
"bigEndian" : format.endian == Endian.BIG_ENDIAN, |
||||
"sampleRate" : format.rate, |
||||
"numChannels" : format.channels, |
||||
"interleaved" : true, |
||||
"microphone" : { |
||||
"granted" : (mic != null && !mic.muted) |
||||
} |
||||
}; |
||||
|
||||
return json; |
||||
} |
||||
|
||||
internal function setSettings(json:Object):void |
||||
{ |
||||
if (json) |
||||
{ |
||||
// For now the type also specifies streaming or not. |
||||
if (json.container == "au") |
||||
{ |
||||
stream = true; |
||||
} |
||||
else if (json.container == "wav") |
||||
{ |
||||
stream = false; |
||||
} |
||||
|
||||
if (json.encoding) |
||||
{ |
||||
throw new Error("Encodings such as mu-law could be implemented."); |
||||
} |
||||
|
||||
if (json.signed) |
||||
{ |
||||
throw new Error("Not implemented yet."); |
||||
} |
||||
|
||||
if (json.bigEndian) |
||||
{ |
||||
throw new Error("Automatically determined."); |
||||
} |
||||
|
||||
if (json.numChannels) |
||||
{ |
||||
format.channels = json.numChannels; |
||||
} |
||||
|
||||
if (json.sampleSize) |
||||
{ |
||||
format.bits = json.sampleSize; |
||||
} |
||||
|
||||
if (json.interleaved) |
||||
{ |
||||
throw new Error("Always true."); |
||||
} |
||||
|
||||
if (json.microphone) |
||||
{ |
||||
throw new Error("Only the user can change the microphone security settings."); |
||||
} |
||||
|
||||
if (json.sampleRate) |
||||
{ |
||||
format.rate = json.sampleRate; |
||||
} |
||||
|
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,39 @@ |
||||
/* |
||||
* Copyright (c) 2011 |
||||
* Spoken Language Systems Group |
||||
* MIT Computer Science and Artificial Intelligence Laboratory |
||||
* Massachusetts Institute of Technology |
||||
* |
||||
* Permission is hereby granted, free of charge, to any person |
||||
* obtaining a copy of this software and associated documentation |
||||
* files (the "Software"), to deal in the Software without |
||||
* restriction, including without limitation the rights to use, copy, |
||||
* modify, merge, publish, distribute, sublicense, and/or sell copies |
||||
* of the Software, and to permit persons to whom the Software is |
||||
* furnished to do so, subject to the following conditions: |
||||
* |
||||
* The above copyright notice and this permission notice shall be |
||||
* included in all copies or substantial portions of the Software. |
||||
* |
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
* SOFTWARE. |
||||
*/ |
||||
package edu.mit.csail.wami.play |
||||
{ |
||||
import edu.mit.csail.wami.utils.StateListener; |
||||
|
||||
public interface IPlayer |
||||
{ |
||||
function start(url:String, listener:StateListener):void; |
||||
function stop():void; |
||||
|
||||
// Audio level (between 0 and 100) |
||||
function level():int; |
||||
} |
||||
} |
||||
@ -0,0 +1,179 @@ |
||||
/* |
||||
* Copyright (c) 2011 |
||||
* Spoken Language Systems Group |
||||
* MIT Computer Science and Artificial Intelligence Laboratory |
||||
* Massachusetts Institute of Technology |
||||
* |
||||
* Permission is hereby granted, free of charge, to any person |
||||
* obtaining a copy of this software and associated documentation |
||||
* files (the "Software"), to deal in the Software without |
||||
* restriction, including without limitation the rights to use, copy, |
||||
* modify, merge, publish, distribute, sublicense, and/or sell copies |
||||
* of the Software, and to permit persons to whom the Software is |
||||
* furnished to do so, subject to the following conditions: |
||||
* |
||||
* The above copyright notice and this permission notice shall be |
||||
* included in all copies or substantial portions of the Software. |
||||
* |
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
* SOFTWARE. |
||||
*/ |
||||
package edu.mit.csail.wami.play |
||||
{ |
||||
import edu.mit.csail.wami.audio.AuContainer; |
||||
import edu.mit.csail.wami.audio.DecodePipe; |
||||
import edu.mit.csail.wami.audio.IAudioContainer; |
||||
import edu.mit.csail.wami.audio.WaveContainer; |
||||
import edu.mit.csail.wami.utils.BytePipe; |
||||
import edu.mit.csail.wami.utils.External; |
||||
import edu.mit.csail.wami.utils.Pipe; |
||||
import edu.mit.csail.wami.utils.StateListener; |
||||
|
||||
import flash.events.Event; |
||||
import flash.events.HTTPStatusEvent; |
||||
import flash.events.IOErrorEvent; |
||||
import flash.events.ProgressEvent; |
||||
import flash.events.SampleDataEvent; |
||||
import flash.events.SecurityErrorEvent; |
||||
import flash.media.Sound; |
||||
import flash.media.SoundChannel; |
||||
import flash.net.URLLoader; |
||||
import flash.net.URLLoaderDataFormat; |
||||
import flash.net.URLRequest; |
||||
import flash.net.URLRequestMethod; |
||||
import flash.utils.ByteArray; |
||||
|
||||
public class WamiPlayer implements IPlayer |
||||
{ |
||||
private var currentChannel:SoundChannel = null; |
||||
private var currentAudio:ByteArray; |
||||
private var listener:StateListener; |
||||
|
||||
public function start(url:String, listener:StateListener):void |
||||
{ |
||||
this.listener = listener; |
||||
var loader:URLLoader = new URLLoader(); |
||||
loader.dataFormat = URLLoaderDataFormat.BINARY; |
||||
|
||||
loader.addEventListener(Event.COMPLETE, completeHandler); |
||||
loader.addEventListener(Event.OPEN, openHandler); |
||||
loader.addEventListener(ProgressEvent.PROGRESS, progressHandler); |
||||
loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler); |
||||
loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, httpStatusHandler); |
||||
loader.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler); |
||||
|
||||
var request:URLRequest = new URLRequest(url); |
||||
request.method = URLRequestMethod.GET; |
||||
|
||||
try { |
||||
loader.load(request); |
||||
} catch (error:Error) { |
||||
listener.failed(error); |
||||
} |
||||
|
||||
function completeHandler(event:Event):void { |
||||
listener.started(); |
||||
|
||||
loader.removeEventListener(Event.COMPLETE, completeHandler); |
||||
loader.removeEventListener(Event.OPEN, openHandler); |
||||
loader.removeEventListener(ProgressEvent.PROGRESS, progressHandler); |
||||
loader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler); |
||||
loader.removeEventListener(HTTPStatusEvent.HTTP_STATUS, httpStatusHandler); |
||||
loader.removeEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler) |
||||
|
||||
play(loader.data); |
||||
} |
||||
|
||||
function openHandler(event:Event):void { |
||||
External.debug("openHandler: " + event); |
||||
} |
||||
|
||||
function progressHandler(event:ProgressEvent):void { |
||||
//External.debug("progressHandler loaded:" + event.bytesLoaded + " total: " + event.bytesTotal); |
||||
} |
||||
|
||||
function securityErrorHandler(event:SecurityErrorEvent):void { |
||||
listener.failed(new Error("Security error while playing: " + event.errorID)); |
||||
} |
||||
|
||||
function httpStatusHandler(event:HTTPStatusEvent):void { |
||||
External.debug("httpStatusHandler: " + event); |
||||
} |
||||
|
||||
function ioErrorHandler(event:IOErrorEvent):void { |
||||
listener.failed(new Error("IO error while playing: " + event.errorID)); |
||||
} |
||||
} |
||||
|
||||
public function stop():void |
||||
{ |
||||
if (currentChannel != null) |
||||
{ |
||||
External.debug("Stop playing."); |
||||
currentChannel.removeEventListener(Event.SOUND_COMPLETE, stop); |
||||
currentChannel.stop(); |
||||
External.debug("Listener finished."); |
||||
listener.finished(); |
||||
currentChannel = null; |
||||
} |
||||
} |
||||
|
||||
public function level():int |
||||
{ |
||||
if (currentChannel != null) { |
||||
return 100 * ((currentChannel.leftPeak + currentChannel.rightPeak) / 2.0); |
||||
} |
||||
|
||||
return 0; |
||||
} |
||||
|
||||
protected function play(audio:ByteArray):void |
||||
{ |
||||
stop(); // Make sure we're stopped |
||||
|
||||
var containers:Vector.<IAudioContainer> = new Vector.<IAudioContainer>(); |
||||
containers.push(new WaveContainer()); |
||||
containers.push(new AuContainer()); |
||||
|
||||
External.debug("Playing audio of " + audio.length + " bytes."); |
||||
var decoder:Pipe = new DecodePipe(containers); |
||||
var pipe:BytePipe = new BytePipe(); |
||||
decoder.setSink(pipe); |
||||
decoder.write(audio); |
||||
decoder.close(); |
||||
|
||||
currentAudio = pipe.getByteArray(); |
||||
External.debug("Playing audio with " + currentAudio.length/4 + " samples."); |
||||
|
||||
var sound:Sound = new Sound(); |
||||
sound.addEventListener(SampleDataEvent.SAMPLE_DATA, handleSampleEvent); |
||||
|
||||
currentChannel = sound.play(); |
||||
currentChannel.addEventListener(Event.SOUND_COMPLETE, function(event:Event):void { |
||||
sound.removeEventListener(SampleDataEvent.SAMPLE_DATA, handleSampleEvent); |
||||
stop(); |
||||
}); |
||||
} |
||||
|
||||
private function handleSampleEvent(event:SampleDataEvent):void |
||||
{ |
||||
if (currentAudio == null) return; |
||||
|
||||
var MAX_SAMPLES_PER_EVENT:uint = 4000; |
||||
var count:uint = 0; |
||||
// External.debug("Audio " + currentAudio.bytesAvailable + " " + event.data.endian); |
||||
while (currentAudio.bytesAvailable && count < MAX_SAMPLES_PER_EVENT) |
||||
{ |
||||
event.data.writeFloat(currentAudio.readFloat()); |
||||
event.data.writeFloat(currentAudio.readFloat()); |
||||
count += 1; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,49 @@ |
||||
/* |
||||
* Copyright (c) 2011 |
||||
* Spoken Language Systems Group |
||||
* MIT Computer Science and Artificial Intelligence Laboratory |
||||
* Massachusetts Institute of Technology |
||||
* |
||||
* Permission is hereby granted, free of charge, to any person |
||||
* obtaining a copy of this software and associated documentation |
||||
* files (the "Software"), to deal in the Software without |
||||
* restriction, including without limitation the rights to use, copy, |
||||
* modify, merge, publish, distribute, sublicense, and/or sell copies |
||||
* of the Software, and to permit persons to whom the Software is |
||||
* furnished to do so, subject to the following conditions: |
||||
* |
||||
* The above copyright notice and this permission notice shall be |
||||
* included in all copies or substantial portions of the Software. |
||||
* |
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
* SOFTWARE. |
||||
*/ |
||||
package edu.mit.csail.wami.record |
||||
{ |
||||
import edu.mit.csail.wami.utils.StateListener; |
||||
|
||||
public interface IRecorder |
||||
{ |
||||
// Start and stop recording. Calling start while recording |
||||
// or calling stop when not recording should have no effect. |
||||
function start(url:String, listener:StateListener):void; |
||||
function stop(force:Boolean = false):void; |
||||
|
||||
// It can be helpful to buffer a certain amount of audio to |
||||
// prepend (and append) to the audio collected between start |
||||
// and stop. This means, Flash needs to constantly listen. |
||||
// There are other times when it's obvious no recording will |
||||
// be done, and so listening is unnecesary. |
||||
function listen(paddingMillis:uint):void; |
||||
function unlisten():void; |
||||
|
||||
// Audio level (between 0 and 100) |
||||
function level():int; |
||||
} |
||||
} |
||||
@ -0,0 +1,111 @@ |
||||
/* |
||||
* Copyright (c) 2011 |
||||
* Spoken Language Systems Group |
||||
* MIT Computer Science and Artificial Intelligence Laboratory |
||||
* Massachusetts Institute of Technology |
||||
* |
||||
* Permission is hereby granted, free of charge, to any person |
||||
* obtaining a copy of this software and associated documentation |
||||
* files (the "Software"), to deal in the Software without |
||||
* restriction, including without limitation the rights to use, copy, |
||||
* modify, merge, publish, distribute, sublicense, and/or sell copies |
||||
* of the Software, and to permit persons to whom the Software is |
||||
* furnished to do so, subject to the following conditions: |
||||
* |
||||
* The above copyright notice and this permission notice shall be |
||||
* included in all copies or substantial portions of the Software. |
||||
* |
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
* SOFTWARE. |
||||
*/ |
||||
package edu.mit.csail.wami.record |
||||
{ |
||||
import edu.mit.csail.wami.utils.External; |
||||
import edu.mit.csail.wami.utils.Pipe; |
||||
import edu.mit.csail.wami.utils.StateListener; |
||||
|
||||
import flash.utils.ByteArray; |
||||
|
||||
public class MultiPost extends Pipe implements StateListener |
||||
{ |
||||
private var url:String; |
||||
private var contentType:String = null; |
||||
private var partIndex:int = 0; |
||||
private var timeoutMillis:int; |
||||
private var totalBytes:int = 0; |
||||
private var totalPostsMade:int = 0; |
||||
private var totalPostsDone:int = 0; |
||||
private var error:Error = null; |
||||
private var listener:StateListener; |
||||
|
||||
/** |
||||
* Does of POST of the data passed in to every call to "write" |
||||
*/ |
||||
public function MultiPost(url:String, type:String, timeoutMillis:int, listener:StateListener) |
||||
{ |
||||
this.url = url; |
||||
this.contentType = type; |
||||
this.timeoutMillis = timeoutMillis; |
||||
this.listener = listener; |
||||
} |
||||
|
||||
override public function write(bytes:ByteArray):void |
||||
{ |
||||
if (getError() != null) { |
||||
throw getError(); |
||||
} |
||||
|
||||
var type:String = contentType.replace("%s", partIndex++); |
||||
var post:Pipe = new SinglePost(url, type, timeoutMillis, this); |
||||
post.write(bytes); |
||||
post.close(); |
||||
totalBytes += bytes.length; |
||||
totalPostsMade++; |
||||
} |
||||
|
||||
// A final POST containing a -1 signifies the end of the MultiPost stream. |
||||
override public function close():void |
||||
{ |
||||
External.debug("Total multi-posted bytes: " + totalBytes); |
||||
var arr:ByteArray = new ByteArray(); |
||||
arr.writeInt(-1); |
||||
arr.position = 0; |
||||
write(arr); |
||||
super.close(); |
||||
} |
||||
|
||||
public function started():void |
||||
{ |
||||
// nothing to do |
||||
} |
||||
|
||||
public function finished():void |
||||
{ |
||||
totalPostsDone++; |
||||
checkFinished(); |
||||
} |
||||
|
||||
public function failed(error:Error):void |
||||
{ |
||||
this.error = error; |
||||
} |
||||
|
||||
public function getError():Error |
||||
{ |
||||
return error; |
||||
} |
||||
|
||||
private function checkFinished():void |
||||
{ |
||||
if (totalPostsDone == totalPostsMade && super.isClosed()) { |
||||
listener.finished(); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,159 @@ |
||||
/* |
||||
* Copyright (c) 2011 |
||||
* Spoken Language Systems Group |
||||
* MIT Computer Science and Artificial Intelligence Laboratory |
||||
* Massachusetts Institute of Technology |
||||
* |
||||
* Permission is hereby granted, free of charge, to any person |
||||
* obtaining a copy of this software and associated documentation |
||||
* files (the "Software"), to deal in the Software without |
||||
* restriction, including without limitation the rights to use, copy, |
||||
* modify, merge, publish, distribute, sublicense, and/or sell copies |
||||
* of the Software, and to permit persons to whom the Software is |
||||
* furnished to do so, subject to the following conditions: |
||||
* |
||||
* The above copyright notice and this permission notice shall be |
||||
* included in all copies or substantial portions of the Software. |
||||
* |
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
* SOFTWARE. |
||||
*/ |
||||
package edu.mit.csail.wami.record |
||||
{ |
||||
import edu.mit.csail.wami.utils.External; |
||||
import edu.mit.csail.wami.utils.Pipe; |
||||
import edu.mit.csail.wami.utils.StateListener; |
||||
|
||||
import flash.events.Event; |
||||
import flash.events.HTTPStatusEvent; |
||||
import flash.events.IOErrorEvent; |
||||
import flash.events.ProgressEvent; |
||||
import flash.events.SecurityErrorEvent; |
||||
import flash.net.URLLoader; |
||||
import flash.net.URLRequest; |
||||
import flash.net.URLRequestMethod; |
||||
import flash.utils.ByteArray; |
||||
import flash.utils.setInterval; |
||||
|
||||
/** |
||||
* Write data and POST on close. |
||||
*/ |
||||
public class SinglePost extends Pipe |
||||
{ |
||||
private var url:String; |
||||
private var contentType:String = null; |
||||
private var listener:StateListener; |
||||
|
||||
private var finished:Boolean = false; |
||||
private var buffer:ByteArray = new ByteArray(); |
||||
private var timeoutMillis:int; |
||||
|
||||
public function SinglePost(url:String, type:String, timeoutMillis:int, listener:StateListener) |
||||
{ |
||||
this.url = url; |
||||
this.contentType = type; |
||||
this.listener = listener; |
||||
this.timeoutMillis = timeoutMillis; |
||||
} |
||||
|
||||
override public function write(bytes:ByteArray):void |
||||
{ |
||||
bytes.readBytes(buffer, buffer.length, bytes.bytesAvailable); |
||||
} |
||||
|
||||
override public function close():void |
||||
{ |
||||
buffer.position = 0; |
||||
External.debug("POST " + buffer.length + " bytes of type " + contentType); |
||||
buffer.position = 0; |
||||
var loader:URLLoader = new URLLoader(); |
||||
|
||||
loader.addEventListener(Event.COMPLETE, completeHandler); |
||||
loader.addEventListener(Event.OPEN, openHandler); |
||||
loader.addEventListener(ProgressEvent.PROGRESS, progressHandler); |
||||
loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler); |
||||
loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, httpStatusHandler); |
||||
loader.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler); |
||||
|
||||
var request:URLRequest = new URLRequest(url); |
||||
request.method = URLRequestMethod.POST; |
||||
request.contentType = contentType; |
||||
request.data = buffer; |
||||
if (buffer.bytesAvailable == 0) { |
||||
External.debug("Note that flash does a GET request if bytes.length == 0"); |
||||
} |
||||
|
||||
try { |
||||
loader.load(request); |
||||
} catch (error:Error) { |
||||
if (listener) |
||||
{ |
||||
listener.failed(error); |
||||
} |
||||
} |
||||
|
||||
super.close(); |
||||
} |
||||
|
||||
private function completeHandler(event:Event):void { |
||||
External.debug("POST: completeHandler"); |
||||
var loader:URLLoader = URLLoader(event.target); |
||||
loader.removeEventListener(Event.COMPLETE, completeHandler); |
||||
loader.removeEventListener(Event.OPEN, openHandler); |
||||
loader.removeEventListener(ProgressEvent.PROGRESS, progressHandler); |
||||
loader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler); |
||||
loader.removeEventListener(HTTPStatusEvent.HTTP_STATUS, httpStatusHandler); |
||||
loader.removeEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler); |
||||
listener.finished(); |
||||
finished = true; |
||||
} |
||||
|
||||
private function openHandler(event:Event):void { |
||||
External.debug("POST openHandler: " + event); |
||||
setInterval(checkFinished, timeoutMillis); |
||||
} |
||||
|
||||
private function checkFinished():void { |
||||
if (!finished && listener) { |
||||
listener.failed(new Error("POST is taking too long.")); |
||||
} |
||||
finished = true; |
||||
} |
||||
|
||||
private function progressHandler(event:ProgressEvent):void { |
||||
External.debug("POST progressHandler loaded:" + event.bytesLoaded + " total: " + event.bytesTotal); |
||||
} |
||||
|
||||
private function securityErrorHandler(event:SecurityErrorEvent):void { |
||||
if (!finished && listener) |
||||
{ |
||||
listener.failed(new Error("Record security error: " + event.errorID)); |
||||
} |
||||
finished = true; |
||||
} |
||||
|
||||
private function httpStatusHandler(event:HTTPStatusEvent):void { |
||||
// Apparently the event.status can be zero in some environments where nothing is wrong: |
||||
// http://johncblandii.com/2008/04/flex-3-firefox-beta-3-returns-0-for-http-status-codes.html |
||||
if (!finished && listener && event.status != 200 && event.status != 0) |
||||
{ |
||||
listener.failed(new Error("HTTP status error: " + event.status)); |
||||
} |
||||
finished = true; |
||||
} |
||||
|
||||
private function ioErrorHandler(event:IOErrorEvent):void { |
||||
if (!finished && listener) |
||||
{ |
||||
listener.failed(new Error("Record IO error: " + event.errorID)); |
||||
} |
||||
finished = true; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,269 @@ |
||||
/* |
||||
* Copyright (c) 2011 |
||||
* Spoken Language Systems Group |
||||
* MIT Computer Science and Artificial Intelligence Laboratory |
||||
* Massachusetts Institute of Technology |
||||
* |
||||
* Permission is hereby granted, free of charge, to any person |
||||
* obtaining a copy of this software and associated documentation |
||||
* files (the "Software"), to deal in the Software without |
||||
* restriction, including without limitation the rights to use, copy, |
||||
* modify, merge, publish, distribute, sublicense, and/or sell copies |
||||
* of the Software, and to permit persons to whom the Software is |
||||
* furnished to do so, subject to the following conditions: |
||||
* |
||||
* The above copyright notice and this permission notice shall be |
||||
* included in all copies or substantial portions of the Software. |
||||
* |
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
* SOFTWARE. |
||||
*/ |
||||
package edu.mit.csail.wami.record |
||||
{ |
||||
import edu.mit.csail.wami.audio.AuContainer; |
||||
import edu.mit.csail.wami.audio.AudioFormat; |
||||
import edu.mit.csail.wami.audio.EncodePipe; |
||||
import edu.mit.csail.wami.audio.IAudioContainer; |
||||
import edu.mit.csail.wami.audio.WaveContainer; |
||||
import edu.mit.csail.wami.client.WamiParams; |
||||
import edu.mit.csail.wami.utils.BytePipe; |
||||
import edu.mit.csail.wami.utils.External; |
||||
import edu.mit.csail.wami.utils.Pipe; |
||||
import edu.mit.csail.wami.utils.StateListener; |
||||
|
||||
import flash.events.SampleDataEvent; |
||||
import flash.events.StatusEvent; |
||||
import flash.media.Microphone; |
||||
import flash.media.SoundCodec; |
||||
import flash.utils.Endian; |
||||
import flash.utils.clearInterval; |
||||
import flash.utils.setInterval; |
||||
|
||||
public class WamiRecorder implements IRecorder |
||||
{ |
||||
private static var CHUNK_DURATION_MILLIS:Number = 200; |
||||
|
||||
private var mic:Microphone = null; |
||||
private var params:WamiParams; |
||||
private var audioPipe:Pipe; |
||||
|
||||
// For adding some audio padding to start and stop. |
||||
private var circularBuffer:BytePipe; |
||||
private var stopInterval:uint; |
||||
private var paddingMillis:uint = 0; // initially 0, but listen changes it. |
||||
private var listening:Boolean = false; |
||||
|
||||
// To determine if the amount of audio recorded matches up with |
||||
// the length of time we've recorded (i.e. not dropping any frames) |
||||
private var handled:uint; |
||||
private var startTime:Date; |
||||
private var stopTime:Date; |
||||
private var listener:StateListener; |
||||
|
||||
public function WamiRecorder(mic:Microphone, params:WamiParams) |
||||
{ |
||||
this.params = params; |
||||
this.circularBuffer = new BytePipe(getPaddingBufferSize()); |
||||
this.mic = mic; |
||||
mic.addEventListener(StatusEvent.STATUS, onMicStatus); |
||||
|
||||
if (getChunkSize() <= 0) |
||||
{ |
||||
throw new Error("Desired duration is too small, even for streaming chunks: " + getChunkSize()); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* The WAMI recorder can listen constantly, keeping a buffer of the last |
||||
* few milliseconds of audio. Often people start talking before they click the |
||||
* button, so we prepend paddingMillis milliseconds to the audio. |
||||
*/ |
||||
public function listen(paddingMillis:uint):void { |
||||
if (!listening) { |
||||
this.paddingMillis = paddingMillis; |
||||
mic.rate = AudioFormat.toRoundedRate(params.format.rate); |
||||
mic.codec = SoundCodec.NELLYMOSER; // Just to clarify 5, 8, 11, 16, 22 and 44 kHz |
||||
mic.setSilenceLevel(0, 10000); |
||||
mic.addEventListener(SampleDataEvent.SAMPLE_DATA, sampleHandler); |
||||
External.debug("Listening..."); |
||||
listening = true; |
||||
} |
||||
} |
||||
|
||||
public function unlisten():void { |
||||
if (listening) { |
||||
mic.removeEventListener(SampleDataEvent.SAMPLE_DATA, sampleHandler); |
||||
listening = false; |
||||
if (paddingMillis > 0) { |
||||
circularBuffer = new BytePipe(getPaddingBufferSize()); |
||||
} |
||||
External.debug("Unlistening."); |
||||
} |
||||
} |
||||
|
||||
protected function onMicStatus(event:StatusEvent):void |
||||
{ |
||||
External.debug("status: " + event.code); |
||||
if (event.code == "Microphone.Unmuted") |
||||
{ |
||||
listen(this.paddingMillis); |
||||
} else if (event.code == "Microphone.Muted") { |
||||
unlisten(); |
||||
} |
||||
} |
||||
|
||||
public function start(url:String, listener:StateListener):void |
||||
{ |
||||
// Forces security if mic is still muted in debugging mode. |
||||
listen(this.paddingMillis); |
||||
|
||||
// Flash might be able to decide on a different sample rate |
||||
// than the one you suggest depending on your audio card... |
||||
params.format.rate = AudioFormat.fromRoundedRate(mic.rate); |
||||
External.debug("Recording at rate: " + params.format.rate); |
||||
|
||||
stop(true); |
||||
audioPipe = createAudioPipe(url, listener); |
||||
|
||||
if (paddingMillis > 0) { |
||||
// Prepend a small amount of audio we've already recorded. |
||||
circularBuffer.close(); |
||||
audioPipe.write(circularBuffer.getByteArray()); |
||||
circularBuffer = new BytePipe(getPaddingBufferSize()); |
||||
} |
||||
|
||||
listener.started(); |
||||
|
||||
handled = 0; |
||||
startTime = new Date(); |
||||
} |
||||
|
||||
public function createAudioPipe(url:String, listener:StateListener):Pipe |
||||
{ |
||||
this.listener = listener; |
||||
|
||||
var post:Pipe; |
||||
var container:IAudioContainer; |
||||
if (params.stream) |
||||
{ |
||||
// The chunk parameter is something I made up. It would need |
||||
// to be handled on the server-side to piece all the chunks together. |
||||
post = new MultiPost(url, "audio/basic; chunk=%s", 3*1000, listener); |
||||
params.format.endian = Endian.BIG_ENDIAN; |
||||
container = new AuContainer(); |
||||
} |
||||
else |
||||
{ |
||||
post = new SinglePost(url, "audio/x-wav", 30*1000, listener); |
||||
container = new WaveContainer(); |
||||
} |
||||
|
||||
// Setup the audio pipes. A transcoding pipe converts floats |
||||
// to shorts and passes them on to a chunking pipe, which spits |
||||
// out chunks to a pipe that possibly adds a WAVE header |
||||
// before passing the chunks on to a pipe that does HTTP posts. |
||||
var pipe:Pipe = new EncodePipe(params.format, container); |
||||
pipe.setSink(new ChunkPipe(getChunkSize())) |
||||
.setSink(post); |
||||
|
||||
return pipe; |
||||
} |
||||
|
||||
internal function sampleHandler(evt:SampleDataEvent):void |
||||
{ |
||||
evt.data.position = 0; |
||||
try |
||||
{ |
||||
if (audioPipe) |
||||
{ |
||||
audioPipe.write(evt.data); |
||||
handled += evt.data.length / 4; |
||||
} |
||||
else if (paddingMillis > 0) |
||||
{ |
||||
circularBuffer.write(evt.data); |
||||
} |
||||
} |
||||
catch (error:Error) |
||||
{ |
||||
audioPipe = null; |
||||
stop(true); |
||||
listener.failed(error); |
||||
} |
||||
} |
||||
|
||||
public function stop(force:Boolean = false):void |
||||
{ |
||||
clearInterval(stopInterval); |
||||
|
||||
if (force) |
||||
{ |
||||
reallyStop(); |
||||
} |
||||
else |
||||
{ |
||||
stopInterval = setInterval(function():void { |
||||
clearInterval(stopInterval); |
||||
reallyStop(); |
||||
}, paddingMillis); |
||||
} |
||||
} |
||||
|
||||
public function level():int |
||||
{ |
||||
if (!audioPipe) return 0; |
||||
return mic.activityLevel; |
||||
} |
||||
|
||||
private function reallyStop():void |
||||
{ |
||||
if (!audioPipe) return; |
||||
|
||||
try { |
||||
audioPipe.close(); |
||||
} catch(error:Error) { |
||||
listener.failed(error); |
||||
} |
||||
|
||||
audioPipe = null; |
||||
validateAudioLength(); |
||||
|
||||
if (this.paddingMillis == 0) { |
||||
// No need if we're not padding the audio |
||||
unlisten(); |
||||
} |
||||
} |
||||
|
||||
private function validateAudioLength():void |
||||
{ |
||||
stopTime = new Date(); |
||||
var seconds:Number = ((stopTime.time - startTime.time + paddingMillis) / 1000.0); |
||||
var expectedSamples:uint = uint(seconds*params.format.rate); |
||||
External.debug("Expected Samples: " + expectedSamples + " Actual Samples: " + handled); |
||||
startTime = null; |
||||
stopTime = null; |
||||
} |
||||
|
||||
private function getBytesPerSecond():uint |
||||
{ |
||||
return params.format.channels * (params.format.bits/8) * params.format.rate; |
||||
} |
||||
|
||||
private function getChunkSize():uint |
||||
{ |
||||
return params.stream ? getBytesPerSecond() * CHUNK_DURATION_MILLIS / 1000.0 : int.MAX_VALUE; |
||||
} |
||||
|
||||
private function getPaddingBufferSize():uint |
||||
{ |
||||
return uint(getBytesPerSecond()*params.paddingMillis/1000.0); |
||||
} |
||||
|
||||
} |
||||
} |
||||
@ -0,0 +1,93 @@ |
||||
/* |
||||
* Copyright (c) 2011 |
||||
* Spoken Language Systems Group |
||||
* MIT Computer Science and Artificial Intelligence Laboratory |
||||
* Massachusetts Institute of Technology |
||||
* |
||||
* Permission is hereby granted, free of charge, to any person |
||||
* obtaining a copy of this software and associated documentation |
||||
* files (the "Software"), to deal in the Software without |
||||
* restriction, including without limitation the rights to use, copy, |
||||
* modify, merge, publish, distribute, sublicense, and/or sell copies |
||||
* of the Software, and to permit persons to whom the Software is |
||||
* furnished to do so, subject to the following conditions: |
||||
* |
||||
* The above copyright notice and this permission notice shall be |
||||
* included in all copies or substantial portions of the Software. |
||||
* |
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
* SOFTWARE. |
||||
*/ |
||||
package edu.mit.csail.wami.utils |
||||
{ |
||||
import flash.utils.ByteArray; |
||||
|
||||
/** |
||||
* Accumulates the written bytes into an array. If a max number of bytes |
||||
* is specified this will act as a circular buffer. |
||||
*/ |
||||
public class BytePipe extends Pipe |
||||
{ |
||||
private var buffer:ByteArray = new ByteArray(); |
||||
private var done:Boolean = false; |
||||
private var maxBytes:uint; |
||||
private var start:uint = 0; |
||||
|
||||
function BytePipe(maxBytes:uint = uint.MAX_VALUE):void |
||||
{ |
||||
this.maxBytes = maxBytes; |
||||
} |
||||
|
||||
override public function write(bytes:ByteArray):void |
||||
{ |
||||
if (maxBytes <= 0) return; // no room! |
||||
|
||||
var available:uint = Math.min(maxBytes - buffer.length, bytes.bytesAvailable); |
||||
if (available > 0) |
||||
{ |
||||
bytes.readBytes(buffer, buffer.length, available); |
||||
} |
||||
|
||||
while (bytes.bytesAvailable) |
||||
{ |
||||
// Read bytes into the circular buffer. |
||||
available = Math.min(buffer.length - start, bytes.bytesAvailable); |
||||
bytes.readBytes(buffer, start, available); |
||||
start = (start + available) % maxBytes; |
||||
} |
||||
|
||||
buffer.position = 0; |
||||
} |
||||
|
||||
override public function close():void |
||||
{ |
||||
super.close(); |
||||
done = true; |
||||
} |
||||
|
||||
public function getByteArray():ByteArray |
||||
{ |
||||
if (!done) |
||||
{ |
||||
throw new Error("BytePipe should be done before accessing byte array."); |
||||
} |
||||
|
||||
var array:ByteArray = new ByteArray(); |
||||
buffer.position = start; |
||||
buffer.readBytes(array); |
||||
buffer.position = 0; |
||||
if (start > 0) |
||||
{ |
||||
buffer.readBytes(array, array.length, start); |
||||
} |
||||
array.position = 0; |
||||
return array; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,144 @@ |
||||
/* |
||||
* Copyright (c) 2011 |
||||
* Spoken Language Systems Group |
||||
* MIT Computer Science and Artificial Intelligence Laboratory |
||||
* Massachusetts Institute of Technology |
||||
* |
||||
* Permission is hereby granted, free of charge, to any person |
||||
* obtaining a copy of this software and associated documentation |
||||
* files (the "Software"), to deal in the Software without |
||||
* restriction, including without limitation the rights to use, copy, |
||||
* modify, merge, publish, distribute, sublicense, and/or sell copies |
||||
* of the Software, and to permit persons to whom the Software is |
||||
* furnished to do so, subject to the following conditions: |
||||
* |
||||
* The above copyright notice and this permission notice shall be |
||||
* included in all copies or substantial portions of the Software. |
||||
* |
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
* SOFTWARE. |
||||
*/ |
||||
package edu.mit.csail.wami.utils |
||||
{ |
||||
import flash.external.ExternalInterface; |
||||
import flash.utils.ByteArray; |
||||
|
||||
/** |
||||
* Make external calls only if available. |
||||
*/ |
||||
public class External |
||||
{ |
||||
public static var debugToConsole:Boolean = false; |
||||
|
||||
public static function call(functionName:String, ... arguments):void |
||||
{ |
||||
if (ExternalInterface.available && functionName) |
||||
{ |
||||
try |
||||
{ |
||||
trace("External.call: " + functionName + "(" + arguments + ")"); |
||||
ExternalInterface.call(functionName, arguments); |
||||
} |
||||
catch (e:Error) |
||||
{ |
||||
trace("Error calling external function: " + e.message); |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
trace("No ExternalInterface - External.call: " + functionName + "(" + arguments + ")"); |
||||
} |
||||
} |
||||
|
||||
public static function addCallback(functionName:String, closure:Function):void |
||||
{ |
||||
if (ExternalInterface.available && functionName) |
||||
{ |
||||
try |
||||
{ |
||||
External.debug("External.addCallback: " + functionName); |
||||
ExternalInterface.addCallback(functionName, closure); |
||||
} |
||||
catch (e:Error) |
||||
{ |
||||
External.debug("Error calling external function: " + e.message); |
||||
} |
||||
} |
||||
else |
||||
{ |
||||
External.debug("No ExternalInterface - External.addCallback: " + functionName); |
||||
} |
||||
} |
||||
|
||||
public static function debug(msg:String):void |
||||
{ |
||||
if (debugToConsole) { |
||||
ExternalInterface.call("console.log", "FLASH: " + msg); |
||||
} |
||||
else |
||||
{ |
||||
trace(msg); |
||||
} |
||||
} |
||||
|
||||
public static function debugBytes(bytes:ByteArray):void |
||||
{ |
||||
debug(bytesToHex(bytes)); |
||||
} |
||||
|
||||
public static function bytesToHex(bytes:ByteArray):String |
||||
{ |
||||
var position:int = bytes.position; |
||||
var count:int = 0; |
||||
var str:String = "<"; |
||||
while (bytes.bytesAvailable) |
||||
{ |
||||
if (count%4 == 0) |
||||
{ |
||||
str += " 0x"; |
||||
} |
||||
var byte:uint = bytes.readUnsignedByte(); |
||||
var nib1:uint = byte/16; |
||||
var nib2:uint = byte%16; |
||||
str += getHex(nib1) + getHex(nib2); |
||||
count++; |
||||
} |
||||
str += " >"; |
||||
|
||||
// Reset position |
||||
bytes.position = position; |
||||
return str; |
||||
} |
||||
|
||||
private static function getHex(nibble:uint):String |
||||
{ |
||||
switch(nibble) |
||||
{ |
||||
case 0: return '0'; |
||||
case 1: return '1'; |
||||
case 2: return '2'; |
||||
case 3: return '3'; |
||||
case 4: return '4'; |
||||
case 5: return '5'; |
||||
case 6: return '6'; |
||||
case 7: return '7'; |
||||
case 8: return '8'; |
||||
case 9: return '9'; |
||||
case 10: return 'a'; |
||||
case 11: return 'b'; |
||||
case 12: return 'c'; |
||||
case 13: return 'd'; |
||||
case 14: return 'e'; |
||||
case 15: return 'f'; |
||||
} |
||||
|
||||
return "ERROR(" + nibble + ")"; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,35 @@ |
||||
/* |
||||
* Copyright (c) 2011 |
||||
* Spoken Language Systems Group |
||||
* MIT Computer Science and Artificial Intelligence Laboratory |
||||
* Massachusetts Institute of Technology |
||||
* |
||||
* Permission is hereby granted, free of charge, to any person |
||||
* obtaining a copy of this software and associated documentation |
||||
* files (the "Software"), to deal in the Software without |
||||
* restriction, including without limitation the rights to use, copy, |
||||
* modify, merge, publish, distribute, sublicense, and/or sell copies |
||||
* of the Software, and to permit persons to whom the Software is |
||||
* furnished to do so, subject to the following conditions: |
||||
* |
||||
* The above copyright notice and this permission notice shall be |
||||
* included in all copies or substantial portions of the Software. |
||||
* |
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS |
||||
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN |
||||
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
* SOFTWARE. |
||||
*/ |
||||
package edu.mit.csail.wami.utils |
||||
{ |
||||
public interface StateListener |
||||
{ |
||||
function started():void; |
||||
function finished():void; |
||||
function failed(error:Error):void; |
||||
} |
||||
} |
||||
Loading…
Reference in new issue