I need to save microphone input to use later in an AudioElement. I do this to get microphone input:
window.navigator.getUserMedia(audio: true).then((MediaStream stream) {
# what should go here?
});
What should I do to save the audio?
There are many horrible stupid examples out there where you are able to play the current audio recording in the current browser window. Is there ever a use case for this. For video I can imaging that one want to build a Skype like application and have a preview window to see if you look stupid on the video, but audio ...
I found one good post though: From microphone to .WAV with: getUserMedia and Web Audio
I have ported a part of the code in the linked article that shows how to get hold of the data.
import 'dart:html';
import 'dart:async';
import 'dart:web_audio';
void main() {
window.navigator.getUserMedia(video: true, audio: true).then((MediaStream stream) {
var context = new AudioContext();
GainNode volume = context.createGain();
MediaStreamAudioSourceNode audioInput = context.createMediaStreamSource(stream);
audioInput.connectNode(volume);
int bufferSize = 2048;
ScriptProcessorNode recorder = context.createJavaScriptNode(bufferSize, 2, 2);
recorder.onAudioProcess.listen((AudioProcessingEvent e) {
print('recording');
var left = e.inputBuffer.getChannelData(0);
var right = e.inputBuffer.getChannelData(1);
print(left);
// process Data
});
volume.connectNode(recorder);
recorder.connectNode(context.destination);
/**
* [How to get a file or blob from an object URL?](http://stackoverflow.com/questions/11876175)
* [Convert blob URL to normal URL](http://stackoverflow.com/questions/14952052/convert-blob-url-to-normal-url)
* Doesn't work as it seems blob urls are not supported in Dart
*/
// String url = Url.createObjectUrlFromStream(stream);
// var xhr = new HttpRequest();
// xhr.responseType = 'blob';
// xhr.onLoad.listen((ProgressEvent e) {
// print(xhr.response);
// var recoveredBlog = xhr.response;
// var reader = new FileReader();
//
// reader.onLoad.listen((e) {
// var blobAsDataUrl = reader.result;
// reader.readAsDataUrl(blobAsDataUrl);
// });
// });
// xhr.open('GET', url);
// xhr.send();
/**
* only for testing purposes
**/
// var audio = document.querySelector('audio') as AudioElement;
// audio.controls = true;
// audio.src = url;
});
}
Thanks to Günter Zöchbauer for pointing to this JS solution. I have rewrote the code in Dart and it works.
import 'dart:html';
import 'dart:async';
import 'dart:web_audio';
import 'dart:typed_data';
bool recording;
List leftchannel;
List rightchannel;
int recordingLength;
int sampleRate;
void main() {
leftchannel = [];
rightchannel = [];
recordingLength = 0;
sampleRate = 44100;
recording = true;
// add stop button
ButtonElement stopBtn = new ButtonElement()
..text = 'Stop'
..onClick.listen((_) {
// stop recording
recording = false;
// we flat the left and right channels down
var leftBuffer = mergeBuffers ( leftchannel, recordingLength );
var rightBuffer = mergeBuffers ( rightchannel, recordingLength );
// we interleave both channels together
var interleaved = interleave( leftBuffer, rightBuffer );
// we create our wav file
var buffer = new Uint8List(44 + interleaved.length * 2);
ByteData view = new ByteData.view(buffer);
// RIFF chunk descriptor
writeUTFBytes(view, 0, 'RIFF');
view.setUint32(4, 44 + interleaved.length * 2, Endianness.LITTLE_ENDIAN);
writeUTFBytes(view, 8, 'WAVE');
// FMT sub-chunk
writeUTFBytes(view, 12, 'fmt ');
view.setUint32(16, 16, Endianness.LITTLE_ENDIAN);
view.setUint16(20, 1, Endianness.LITTLE_ENDIAN);
// stereo (2 channels)
view.setUint16(22, 2, Endianness.LITTLE_ENDIAN);
view.setUint32(24, sampleRate, Endianness.LITTLE_ENDIAN);
view.setUint32(28, sampleRate * 4, Endianness.LITTLE_ENDIAN);
view.setUint16(32, 4, Endianness.LITTLE_ENDIAN);
view.setUint16(34, 16, Endianness.LITTLE_ENDIAN);
// data sub-chunk
writeUTFBytes(view, 36, 'data');
view.setUint32(40, interleaved.length * 2, Endianness.LITTLE_ENDIAN);
// write the PCM samples
var lng = interleaved.length;
var index = 44;
var volume = 1;
for (var i = 0; i < lng; i++){
view.setInt16(index, (interleaved[i] * (0x7FFF * volume)).truncate(), Endianness.LITTLE_ENDIAN);
index += 2;
}
// our final binary blob
var blob = new Blob ( [ view ] , 'audio/wav' );
// let's save it locally
String url = Url.createObjectUrlFromBlob(blob);
AnchorElement link = new AnchorElement()
..href = url
..text = 'download'
..download = 'output.wav';
document.body.append(link);
});
document.body.append(stopBtn);
window.navigator.getUserMedia(audio: true).then((MediaStream stream) {
var context = new AudioContext();
GainNode volume = context.createGain();
MediaStreamAudioSourceNode audioInput = context.createMediaStreamSource(stream);
audioInput.connectNode(volume);
int bufferSize = 2048;
ScriptProcessorNode recorder = context.createJavaScriptNode(bufferSize, 2, 2);
recorder.onAudioProcess.listen((AudioProcessingEvent e) {
if (!recording) return;
print('recording');
var left = e.inputBuffer.getChannelData(0);
var right = e.inputBuffer.getChannelData(1);
print(left);
// process Data
leftchannel.add(new Float32List.fromList(left));
rightchannel.add(new Float32List.fromList(right));
recordingLength += bufferSize;
});
volume.connectNode(recorder);
recorder.connectNode(context.destination);
});
}
void writeUTFBytes(ByteData view, offset, String string){
var lng = string.length;
for (var i = 0; i < lng; i++){
view.setUint8(offset + i, string.codeUnitAt(i));
}
}
Float32List interleave(leftChannel, rightChannel){
var length = leftChannel.length + rightChannel.length;
var result = new Float32List(length);
var inputIndex = 0;
for (var index = 0; index < length; ){
result[index++] = leftChannel[inputIndex];
result[index++] = rightChannel[inputIndex];
inputIndex++;
}
return result;
}
List mergeBuffers(channelBuffer, recordingLength){
List result = new List();
var offset = 0;
var lng = channelBuffer.length;
for (var i = 0; i < lng; i++){
var buffer = channelBuffer[i];
result.addAll(buffer);
}
return result;
}
You can pull the code from github here.
Related
I'd want to create a GIF that counts down for 60 seconds. I could use photoshop but don't want to go through the hassle of creating new layers for each number.
I'm looking for a way to automatically generate a GIF (or images that I can combine into a GIF after the fact) that counts down from 60 to 0.
I'll accept any answer that fulfills these requirements.
I post this AIR code as an education exercise to the reader. The base idea is to use ActionScript to render text via the TextField clas within a Sprite, use Flash's ability to render any DisplayObject to static bitmap data and then use a 3rd-party open-source lib to convert each rendered frame to a gif.
Note: You could save each BitmapData frame to a file so you could use an external gif creation tool.
package {
import flash.display.BitmapData;
import flash.display.Sprite;
import flash.utils.ByteArray;
import flash.text.TextField;
import flash.text.TextFormat;
import flash.filesystem.File;
import flash.filesystem.FileStream;
import flash.filesystem.FileMode;
import org.bytearray.gif.encoder.GIFEncoder;
import org.bytearray.gif.player.GIFPlayer;
public class Main extends Sprite {
var defaultFormat:TextFormat = new TextFormat();
var background:Sprite = new Sprite();
var countdownText = new TextField();
var fsize:int = 125;
var xsize:int = 100;
var ysize:int = 100;
public function Main():void {
defaultFormat.font = "Arial";
defaultFormat.size = fsize;
defaultFormat.color = 0xffffff;
var encoder:GIFEncoder = new GIFEncoder();
encoder.setRepeat(0);
encoder.setDelay(1000);
encoder.start();
setupCounterDisplay();
var startFrom:uint = 60;
var startColor:uint = 255;
for (var i:int = startFrom; i > -1; i--) {
var colorRGB:uint = (startColor / startFrom) * i;
encoder.addFrame(createCounterDisplay(i, ( colorRGB << 16 ) | ( colorRGB << 8 ) | colorRGB ) );
}
encoder.finish();
removeChild(background);
saveGIF("CounterDown.gif", encoder.stream);
playGIF(encoder.stream);
}
private function playGIF(data:ByteArray):void {
data.position = 0;
var player:GIFPlayer = new GIFPlayer();
player.loadBytes(data);
addChild(player);
}
private function saveGIF(fileName:String, data:ByteArray):void {
var outFile:File = File.desktopDirectory;
outFile = outFile.resolvePath(fileName);
var outStream:FileStream = new FileStream();
outStream.open(outFile, FileMode.WRITE);
outStream.writeBytes(data, 0, data.length);
outStream.close();
}
private function padString(string:String, padChar:String, finalLength:int, padLeft:Boolean = true):String {
while (string.length < finalLength) {
string = padLeft ? padChar + string : string + padChar;
}
return string;
}
private function setupCounterDisplay():void {
var xsize:int = 100;
var ysize:int = 100;
background.graphics.beginFill(0x000000, 1);
background.graphics.drawCircle(xsize, ysize, ysize);
background.graphics.endFill();
countdownText.defaultTextFormat = defaultFormat;
countdownText.border = true;
countdownText.borderColor = 0xff0000;
background.addChild(countdownText);
this.addChild(background);
}
private function createCounterDisplay(num:int, color:uint):BitmapData {
background.graphics.beginFill(0x000000, 1);
background.graphics.drawCircle(xsize, ysize, ysize);
background.graphics.endFill();
defaultFormat.color = color;
countdownText.defaultTextFormat = defaultFormat;
countdownText.text = padString(num.toString(), "0", 2);
countdownText.autoSize = "center";
countdownText.x = countdownText.width / 5;
countdownText.y = countdownText.height / 5;
var bitmap:BitmapData = new BitmapData(countdownText.width * 1.5, countdownText.height * 1.5, true);
bitmap.draw(background);
return bitmap;
}
}
}
Gif library via : https://code.google.com/p/as3gif/wiki/How_to_use
I've seen this question asked here already : Web Audio API Memory Leaks on Mobile Platforms but there doesn't seem to be any response to it. I've tried lots of different variations - setting variables to null once I'm finished with them, or declaring the variables within scope, but it appears that the AudioContext (or OfflineAudioContext ) is not being Garbage collected after each operation. I'm using this with Phonegap, on an IOS, so the browser is safari. Any ideas how to solve this memory leak?
Here is my code:
function readVocalsToBuffer(file){
console.log('readVocalsToBuffer');
var reader = new FileReader();
reader.onloadend = function(evt){
var x = audioContext.decodeAudioData(evt.target._result, function(buffer){
if(!buffer){
console.log('error decoding file to Audio Buffer');
return;
}
window.voiceBuffer = buffer;
loadBuffers();
});
}
reader.readAsArrayBuffer(file);
}
function loadBuffers(){
console.log('loadBuffers');
try{
var bufferLoader = new BufferLoader(
audioContext,
[
"."+window.srcSong
],
createOffLineContext
);
bufferLoader.load()
}
catch(e){
console.log(e.message);
}
}
function createOffLineContext(bufferList){
console.log('createOfflineContext');
offline = new webkitOfflineAudioContext(2, window.voiceBuffer.length, 44100);
var vocalSource = offline.createBufferSource();
vocalSource.buffer = window.voiceBuffer;
vocalSource.connect(offline.destination);
var backing = offline.createBufferSource();
backing.buffer = bufferList[0];
backing.connect(offline.destination);
vocalSource.start(0);
backing.start(0);
offline.oncomplete = function(ev){
vocalSource.stop(0);
backing.stop(0);
vocalSource.disconnect(0);
backing.disconnect(0);
delete vocalSource;
delete backing;
delete window.voiceBuffer;
window.renderedFile = ev.renderedBuffer;
var bufferR = ev.renderedBuffer.getChannelData(0);
var bufferL = ev.renderedBuffer.getChannelData(1);
var interleaved = interleave(bufferL, bufferR);
var dataview = encodeWAV(interleaved);
window.audioBlob = new Blob([dataview], {type: 'Wav'});
saveFile();
}
offline.startRendering();
}
function interleave(inputL, inputR){
console.log('interleave');
var length = inputL.length + inputR.length;
var result = new Float32Array(length);
var index = 0,
inputIndex = 0;
while (index < length){
result[index++] = inputL[inputIndex];
result[index++] = inputR[inputIndex];
inputIndex++;
}
return result;
}
function saveFile(){
offline = null;
console.log('saveFile');
delete window.renderedFile;
window.requestFileSystem(LocalFileSystem.PERSISTENT, 0, onFSSuccess, fail);
}
**I have an air app for iOS I have been developing. I am trying to capture a picture, save the file to the storage directory (not Camera Roll), and save the file name in an sqlite db.
I have tried so many different variations of this, but when it comes to writing the filestream to save the app hangs. Testing on iPad 3. Does ANYONE have a suggestion? This has been driving me nuts for days. I have searched the web but I am stumped.**
public var temp:File; // File Object to save name in database
protected function selectPicture():void
{
myCam = new CameraUI();
myCam.addEventListener(MediaEvent.COMPLETE, onComplete);
myCam.launch(MediaType.IMAGE);
}
protected function onComplete(event:MediaEvent):void {
//imageProblem.source = event.data.file.url;
var cameraUI:CameraUI = event.target as CameraUI;
var mediaPromise:MediaPromise = event.data;
var mpLoader:Loader = new Loader();
mpLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, onMediaPromiseLoaded);
mpLoader.loadFilePromise(mediaPromise);
}
private function onMediaPromiseLoaded(e:Event):void
{
var mpLoaderInfo:LoaderInfo = e.target as LoaderInfo;
mpLoaderInfo.removeEventListener(Event.COMPLETE, onMediaPromiseLoaded);
this.imageProblem.source = mpLoaderInfo.loader;
var stream:FileStream = new FileStream();
stream.addEventListener(Event.COMPLETE, showComplete);
stream.addEventListener(IOErrorEvent.IO_ERROR, showError);
try{
this.messages.text = "Starting";
stream.open( temp, FileMode.WRITE );
stream.writeBytes(mpLoaderInfo.bytes);
stream.close();
}catch(e:Error){
this.messages.text = e.message;
}
}
protected function showError(e:IOErrorEvent):void{
this.messages.text = e.toString();
}
protected function showComplete(e:Event):void{
this.messages.text = "Completed Writing";
this.imgName.text = temp.url;
imagefile = temp;
deleteFlag = 1;
}
Application hangs due to you are trying to use file operation in Sync mode.
You need to use Async Mode operation instead of sync mode file operation.
stream.openAsync( temp, FileMode.WRITE );
Try with this
var stream:FileStream = new FileStream();
stream.addEventListener(Event.COMPLETE, showComplete);
stream.addEventListener(IOErrorEvent.IO_ERROR, showError);
stream.openAsync( temp, FileMode.WRITE );
stream.writeBytes(mpLoaderInfo.bytes);
stream.close();
Note when using async operation you need not use try catch.For handling error listen IOErrorEvent will catch if any error occurs.
I finally got this to work, I added comments below in the code to explain why it wasn't working.
public var temp:File;
protected function selectPicture():void
{
myCam = new CameraUI();
myCam.addEventListener(MediaEvent.COMPLETE, onComplete);
myCam.launch(MediaType.IMAGE);
}
protected function onComplete(event:MediaEvent):void {
//imageProblem.source = event.data.file.url;
var cameraUI:CameraUI = event.target as CameraUI;
var mediaPromise:MediaPromise = event.data;
var mpLoader:Loader = new Loader();
mpLoader.contentLoaderInfo.addEventListener(Event.COMPLETE, onMediaPromiseLoaded);
mpLoader.loadFilePromise(mediaPromise);
}
private function onMediaPromiseLoaded(e:Event):void
{
var mpLoaderInfo:LoaderInfo = e.target as LoaderInfo;
mpLoaderInfo.removeEventListener(Event.COMPLETE, onMediaPromiseLoaded);
this.imageProblem.source = mpLoaderInfo.loader;
/// Here was the solution
var bitmapDataA:BitmapData = new BitmapData(mpLoaderInfo.width, mpLoaderInfo.height);
bitmapDataA.draw(mpLoaderInfo.content,null,null,null,null,true);
/// I had to cast the loaderInfo as BitmapData
var bitmapDataB:BitmapData = resizeimage(bitmapDataA, int(mpLoaderInfo.width / 4), int(mpLoaderInfo.height/ 4)); // function to shrink the image
var c:CameraRoll = new CameraRoll();
c.addBitmapData(bitmapDataB);
var now:Date = new Date();
var f:File = File.applicationStorageDirectory.resolvePath("IMG" + now.seconds + now.minutes + ".jpg");
var stream:FileStream = new FileStream()
stream.open(f, FileMode.WRITE);
// Then had to redraw and encode as a jpeg before writing the file
var bytes:ByteArray = new ByteArray();
bytes = bitmapDataB.encode(new Rectangle(0,0, int(mpLoaderInfo.width / 4) , int(mpLoaderInfo.height / 4)), new JPEGEncoderOptions(80), bytes);
stream.writeBytes(bytes,0,bytes.bytesAvailable);
stream.close();
this.imgName.text = f.url;
imagefile = f;
deleteFlag = 1;
}
I have this script where I can print the movie that i indicated in the code.
function print_page()
{
var my_pj = new PrintJob();
var myResult = my_pj.start();
if (myResult)
{
myResult = my_pj.addPage("img_mc", null, {printAsBitmap:true}, 1);
my_pj.send();
delete my_pj;
trace("ok");
}
}
I need to know, how to print some image that is outside the flahs... clicking in the MC.... but i need in AS2...
ty for help!
This is the only way i sucess print external image with PrintJob() and AS2
#include "mc_tween2.as"
_root.myholder.loadMovie("teste_labirinto.jpg");
_root.myholder._x = 0; //i set the x and y position of image out of the screen area!
_root.myholder._y = 990; //
function print_page()
{
var my_pj = new PrintJob();
var myResult = my_pj.start();
if (myResult)
{
my_pj.orientation = portrait;
myResult = my_pj.addPage("myholder", {xMin:0, xMax:600, yMin:0, yMax:800}, {printAsBitmap:true}, 1);
my_pj.my_pj.send();
delete my_pj;
trace("ok");
}
}
btn.onRelease = function()
{
print_page();
};
When I programmatically create a PlotChart starting w/ no series and adding series w/ own scaling and verticalAxes (and Renderers but not necessary for behavior error), the axes and data show, but the default axis controls all scaling and the other axes are erroneous. Thus, the yValue magnitudes do not correspond to their associated axes and mixing series w/ grossly different orders of magnitude squishes all but the largest into indistinguishable floor values. I cannot null out the default vertical axis as it gets an null pointer error. [Click anywhere on the chart to add the next series.]
package
{
/*
* Attempt to dynamically create graph w/ varying series. Having difficulty getting axis to render correctly based
* on sets of series. Having trouble getting more than one series. I can't munge the yValue property because in my
* actual code all of the data elements are instances of the same class with properties akin to series, xValue, and yValue.
*
*/
import flash.events.MouseEvent;
import mx.charts.AxisRenderer;
import mx.charts.DateTimeAxis;
import mx.charts.LinearAxis;
import mx.charts.PlotChart;
import mx.charts.chartClasses.Series;
import mx.charts.events.ChartEvent;
import mx.charts.series.PlotSeries;
import mx.collections.ArrayCollection;
public class GraphSample extends PlotChart
{
public function GraphSample()
{
super();
this.selectionMode = "single";
this.percentWidth = 80;
this.series = new Array();
this.verticalAxisRenderers = new Array();
this.showDataTips = true;
}
protected override function createChildren():void {
super.createChildren();
// Create the horizontal axis
var hAxis:DateTimeAxis = new DateTimeAxis();
hAxis.dataUnits = "days";
this.horizontalAxis = hAxis;
// NOTE: I do not want an automatically created verticalAxis, but can't seem to avoid it
// Add vertical axis renderer
var axis:AxisRenderer = new AxisRenderer();
axis.axis = this.verticalAxis;
axis.horizontal = false;
axis.setStyle("showLabels", true);
axis.setStyle("showLine", true);
this.verticalAxisRenderers.push(axis);
this.verticalAxisRenderers = this.verticalAxisRenderers;
this.addEventListener(ChartEvent.CHART_CLICK, addSeries);
}
private var seriesColors:Array = [0x888888, 0xFFC833, 0xFF6433, 0xE73399,
0x8133CC, 0x346B9B, 0x33C399, 0x98F133];
private var seriesColorCursor:int = 0;
private const ONE_DAY:Number = (1000 * 60 * 60 * 24);
private const TODAY:Date = new Date();
private function addSeries(event:MouseEvent):void {
var dimension:Object = newDimension();
var series:Series = new PlotSeries();
series.displayName = dimension.name;
series.dataFunction = genericFieldRetriever;
series.setStyle("fill", seriesColors[seriesColorCursor++]);
if (seriesColorCursor > seriesColors.length) seriesColorCursor %= seriesColors.length;
series.dataProvider = dimension.elements;
this.series.push(series);
var seriesAxis:LinearAxis = new LinearAxis();
series.setAxis("verticalAxis", seriesAxis);
var min:Number = Number.MAX_VALUE, max:Number = 0;
for each (var ele:Object in series.dataProvider) {
if (ele.value > max) max = ele.value;
if (ele.value < min) min = ele.value;
}
seriesAxis.minimum = min * 0.85;
seriesAxis.maximum = max * 1.2;
seriesAxis.displayName = series.displayName;
seriesAxis.title = series.displayName;
// Add vertical axis renderer
var axis:AxisRenderer = new AxisRenderer();
axis.axis = seriesAxis;
axis.horizontal = false;
axis.setStyle("color", series.getStyle("fill"));
axis.setStyle("showLabels", true);
axis.setStyle("showLine", true);
this.verticalAxisRenderers.push(axis);
this.verticalAxisRenderers = this.verticalAxisRenderers;
// also strokes?
// http://www.flexdeveloper.eu/forums/flex-charting/creating-linecolumn-series-dynamically/
this.series = this.series;
this.invalidateSeriesStyles();
if (names.length == 0) this.removeEventListener(ChartEvent.CHART_CLICK, addSeries);
}
/**
* A fake of the server data retrieval mechanism.
* #return wrapper w/ name and elements set where each element has a data and value.
*/
private function newDimension():Object {
var result:Object = new Object();
result['name'] = names.shift();
var elements:ArrayCollection = new ArrayCollection();
var thisBase:int = base.shift();
var thisMax:int = maxAdd.shift();
for (var date:Date = new Date(TODAY.getTime() - 10 * ONE_DAY); date.getTime() <= TODAY.getTime(); date = new Date(date.getTime() + ONE_DAY)) {
var element:Object = new Object();
element['date'] = date;
element['value'] = thisBase + Math.random() * thisMax;
elements.addItem(element);
}
result['elements'] = elements;
return result;
}
private var names:Array = ['fat', 'carbs', 'steps', 'walking miles', 'pounds', 'cycling miles', 'running miles', 'skiing hours', 'lifting mins'];
private var base:Array = [0, 20, 1000, 0, 100, 0, 0, 0, 0];
private var maxAdd:Array = [100, 200, 9000, 4, 200, 40, 8, 3, 75];
private function genericFieldRetriever(series:Series, item:Object, fieldName:String):Object {
if(fieldName == 'yValue')
return(item.value);
else if(fieldName == "xValue")
return(item.date);
else
return null;
}
}
}
The problem was the line
series.setAxis("verticalAxis", seriesAxis);
That is not equivalent to
series.verticalAxis = seriesAxis;
Once I changed that, everything works perfectly, but it cannot handle the abstract Series class b/c that class does not have the setter for verticalAxis.