Related
I made extensive 2 days research on the topic, but there is no really well explained piece that would work.
So the flow is following:
load mp3 (store bought) cbr 320 into wavesurfer
apply all the changes you need
download processed result back to mp3 file (without usage of server)
Ive seen online apps that can do that, nothing is transmitted to server, all happens in the browser.
when we inspect wavesurfer, we have access to these:
The goal would be to use already available references from wavesurfer to produce the download mp3.
from my understanding this can be done with MediaRecorder, WebCodecs API or some libraries like lamejs.
Ive tried to find working example of how to do it with two first methods but without luck. I also tried to do it with lamejs using their example provided on the git but i am getting errors from the lib that are hard to debug, most likely related to providing wrong input.
So far i only managed to download wav file using following script:
handleCopyRegion = (region, instance) => {
var segmentDuration = region.end - region.start;
var originalBuffer = instance.backend.buffer;
var emptySegment = instance.backend.ac.createBuffer(
originalBuffer.numberOfChannels,
Math.ceil(segmentDuration * originalBuffer.sampleRate),
originalBuffer.sampleRate
);
for (var i = 0; i < originalBuffer.numberOfChannels; i++) {
var chanData = originalBuffer.getChannelData(i);
var emptySegmentData = emptySegment.getChannelData(i);
var mid_data = chanData.subarray(
Math.ceil(region.start * originalBuffer.sampleRate),
Math.ceil(region.end * originalBuffer.sampleRate)
);
emptySegmentData.set(mid_data);
}
return emptySegment;
};
bufferToWave = (abuffer, offset, len) => {
var numOfChan = abuffer.numberOfChannels,
length = len * numOfChan * 2 + 44,
buffer = new ArrayBuffer(length),
view = new DataView(buffer),
channels = [],
i,
sample,
pos = 0;
// write WAVE header
setUint32(0x46464952); // "RIFF"
setUint32(length - 8); // file length - 8
setUint32(0x45564157); // "WAVE"
setUint32(0x20746d66); // "fmt " chunk
setUint32(16); // length = 16
setUint16(1); // PCM (uncompressed)
setUint16(numOfChan);
setUint32(abuffer.sampleRate);
setUint32(abuffer.sampleRate * 2 * numOfChan); // avg. bytes/sec
setUint16(numOfChan * 2); // block-align
setUint16(16); // 16-bit (hardcoded in this demo)
setUint32(0x61746164); // "data" - chunk
setUint32(length - pos - 4); // chunk length
// write interleaved data
for (i = 0; i < abuffer.numberOfChannels; i++)
channels.push(abuffer.getChannelData(i));
while (pos < length) {
for (i = 0; i < numOfChan; i++) {
// interleave channels
sample = Math.max(-1, Math.min(1, channels[i][offset])); // clamp
sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767) | 0; // scale to 16-bit signed int
view.setInt16(pos, sample, true); // update data chunk
pos += 2;
}
offset++; // next source sample
}
// create Blob
return new Blob([buffer], { type: "audio/wav" });
function setUint16(data) {
view.setUint16(pos, data, true);
pos += 2;
}
function setUint32(data) {
view.setUint32(pos, data, true);
pos += 4;
}
};
const cutSelection = this.handleCopyRegion(
this.wavesurfer.regions.list.cut,
this.wavesurfer
);
const blob = this.bufferToWave(cutSelection, 0, cutSelection.length);
// you can now download wav from the blob
Is there a way to avoid making wav and right away make mp3 and download it, or if not make mp3 from that wav, if so how it can be done?
I mainly tried to use wavesurfer.backend.buffer as input, because this reference is AudioBuffer and accessing .getChannelData(0|1) gives you left and right channels. But didnt accomplish anything, maybe i am thinking wrong.
Alright, here is the steps we need to do:
Get buffer data from the wavesurfer player
Analyze the buffer to get the number of Channels(STEREO or MONO), channels data and Sample rate.
Use lamejs library to convert buffer to the MP3 blob file
Then we can get that download link from blob
Here is a quick DEMO
and also the JS code:
function downloadMp3() {
var MP3Blob = analyzeAudioBuffer(wavesurfer.backend.buffer);
console.log('here is your mp3 url:');
console.log(URL.createObjectURL(MP3Blob));
}
function analyzeAudioBuffer(aBuffer) {
let numOfChan = aBuffer.numberOfChannels,
btwLength = aBuffer.length * numOfChan * 2 + 44,
btwArrBuff = new ArrayBuffer(btwLength),
btwView = new DataView(btwArrBuff),
btwChnls = [],
btwIndex,
btwSample,
btwOffset = 0,
btwPos = 0;
setUint32(0x46464952); // "RIFF"
setUint32(btwLength - 8); // file length - 8
setUint32(0x45564157); // "WAVE"
setUint32(0x20746d66); // "fmt " chunk
setUint32(16); // length = 16
setUint16(1); // PCM (uncompressed)
setUint16(numOfChan);
setUint32(aBuffer.sampleRate);
setUint32(aBuffer.sampleRate * 2 * numOfChan); // avg. bytes/sec
setUint16(numOfChan * 2); // block-align
setUint16(16); // 16-bit
setUint32(0x61746164); // "data" - chunk
setUint32(btwLength - btwPos - 4); // chunk length
for (btwIndex = 0; btwIndex < aBuffer.numberOfChannels; btwIndex++)
btwChnls.push(aBuffer.getChannelData(btwIndex));
while (btwPos < btwLength) {
for (btwIndex = 0; btwIndex < numOfChan; btwIndex++) {
// interleave btwChnls
btwSample = Math.max(-1, Math.min(1, btwChnls[btwIndex][btwOffset])); // clamp
btwSample = (0.5 + btwSample < 0 ? btwSample * 32768 : btwSample * 32767) | 0; // scale to 16-bit signed int
btwView.setInt16(btwPos, btwSample, true); // write 16-bit sample
btwPos += 2;
}
btwOffset++; // next source sample
}
let wavHdr = lamejs.WavHeader.readHeader(new DataView(btwArrBuff));
//Stereo
let data = new Int16Array(btwArrBuff, wavHdr.dataOffset, wavHdr.dataLen / 2);
let leftData = [];
let rightData = [];
for (let i = 0; i < data.length; i += 2) {
leftData.push(data[i]);
rightData.push(data[i + 1]);
}
var left = new Int16Array(leftData);
var right = new Int16Array(rightData);
//STEREO
if (wavHdr.channels===2)
return bufferToMp3(wavHdr.channels, wavHdr.sampleRate, left,right);
//MONO
else if (wavHdr.channels===1)
return bufferToMp3(wavHdr.channels, wavHdr.sampleRate, data);
function setUint16(data) {
btwView.setUint16(btwPos, data, true);
btwPos += 2;
}
function setUint32(data) {
btwView.setUint32(btwPos, data, true);
btwPos += 4;
}
}
function bufferToMp3(channels, sampleRate, left, right = null) {
var buffer = [];
var mp3enc = new lamejs.Mp3Encoder(channels, sampleRate, 128);
var remaining = left.length;
var samplesPerFrame = 1152;
for (var i = 0; remaining >= samplesPerFrame; i += samplesPerFrame) {
if (!right)
{
var mono = left.subarray(i, i + samplesPerFrame);
var mp3buf = mp3enc.encodeBuffer(mono);
}
else {
var leftChunk = left.subarray(i, i + samplesPerFrame);
var rightChunk = right.subarray(i, i + samplesPerFrame);
var mp3buf = mp3enc.encodeBuffer(leftChunk,rightChunk);
}
if (mp3buf.length > 0) {
buffer.push(mp3buf);//new Int8Array(mp3buf));
}
remaining -= samplesPerFrame;
}
var d = mp3enc.flush();
if(d.length > 0){
buffer.push(new Int8Array(d));
}
var mp3Blob = new Blob(buffer, {type: 'audio/mpeg'});
//var bUrl = window.URL.createObjectURL(mp3Blob);
// send the download link to the console
//console.log('mp3 download:', bUrl);
return mp3Blob;
}
Let me know if you have any question about the code
I want to do the equivalent of ArrayBuffer.slice without actually copying the contents. The use case is converting a very large (50mb) ArrayBuffer into a string
Below you can see that I am using new Uint16Array(buffer, start, chunkSize)). This copies the value from the ArrayBuffer, which is slow. Any ideas on how to make this more performant?
function arrayBufferToStr(buffer: ArrayBuffer) {
// Use chunks to not go over call stack
// chunks of 1024 bytes * 64
const chunkSize = 1024 * 64
// To right before the last chunk so the last `String.fromCharCode`
// can skip passing a byteLength argument
const endCondition = buffer.byteLength - (chunkSize * 2)
let str = ""
let start = 0
for (start = 0; start < endCondition; start += chunkSize * 2) {
str += String.fromCharCode.apply(null, new Uint16Array(buffer, start, chunkSize))
}
const view = new Uint16Array(buffer, start)
buffer = null
str += String.fromCharCode.apply(null, view)
return str
}
I want to convert image (png/jpeg) to ICO using javascript in frontend.
On searching the web, I came across this code on github: https://gist.github.com/twolfson/7656254 but unfortunately it uses fs module of nodejs (+ the code is very difficult to compehend).
Can someone tell guide me on what should I search/or a way through which I can convert png/jpeg to ico using javascript in frontend?
Alternates I have tried?
Used this repo: https://github.com/fiahfy/ico-convert but they use sharp and sharp isn't supported on client side
On googling, I got this Mozilla post, with examples, which provides the following code for conversion to ICO format (limited to Firefox browser only),
A way to convert a canvas to an ico (Mozilla only)
This uses -moz-parse to convert the canvas to ico. Windows XP doesn't
support converting from PNG to ico, so it uses bmp instead. A download
link is created by setting the download attribute. The value of the
download attribute is the name it will use as the file name.
The code:
var canvas = document.getElementById('canvas');
var d = canvas.width;
ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.moveTo(d / 2, 0);
ctx.lineTo(d, d);
ctx.lineTo(0, d);
ctx.closePath();
ctx.fillStyle = 'yellow';
ctx.fill();
function blobCallback(iconName) {
return function(b) {
var a = document.createElement('a');
a.textContent = 'Download';
document.body.appendChild(a);
a.style.display = 'block';
a.download = iconName + '.ico';
a.href = window.URL.createObjectURL(b);
}
}
canvas.toBlob(blobCallback('passThisString'),
'image/vnd.microsoft.icon',
'-moz-parse-options:format=bmp;bpp=32'
);
Apart from that, I'd found no other ways to convert png/jpeg into ICO format. Alternatively, you can do the conversion on the server-side by using any of the following modules:
to-ico
image-to-icon
png-to-ico
Should you want to support every browser and have only a PNG image, the .ICO file format supports embedded PNG images as long as they are smaller than 256x256. Based on the ICO file format, I've been able to construct an ICO using a small PNG image and a hex editor. This can be replicated in JavaScript. This is my testing image file:
To convert it into an ICO, I prepended the following hex data, little-endian encoded (the bytes in the values are reversed):
00 00 01 00 - File header. Says "This file is an ICO."
01 00 - There is one image in this file.
9C - This image is 0x9C pixels wide. **This should be variable**
77 - This image is 0x77 pixels tall. **This should be variable**
00 - There is not a limited color pallette.
00 - Reserved value.
01 00 - There is one color plane in this image.
18 00 - There are 0x18 bits per pixel (24 bits per pixel is standard RGB encoding)
8A 06 00 00 - This image is 0x0000068A large. **This should be variable**
16 00 00 00 - There were 0x16 bytes before this point.
[PNG data here]
This successfully created an ISO file from the PNG. You can create a simple JavaScript script for this prepending. Looking at the PNG specification, the first 8 bytes are a header, followed by 8 bytes of IHDR chunk metadata, which starts with a 4-byte little-endian width and a 4-byte little-endian height. This can be used in our script to discover the PNG's width and height. Something like:
function pngToIco(icoFile, pngData) {
icoFile = "\x00\x00\x01\x00\x01\x00"; // First 6 bytes are constant
icoFile += pngData[15+4]; // PNG width byte
icoFile += pngData[15+8]; // PNG height byte
// Make sure PNG is less than 256x256
if (pngData[15+1] || pngData[15+2] || pngData[15+3]) {
console.log("Width over 255!"); return;
}
if (pngData[15+5] || pngData[15+6] || pngData[15+7]) {
console.log("Height over 255!"); return;
}
// Add more (probably constant) information
icoFile += "\x00\x00\x01\x00\x18\x00";
// Add encoded length
var lenBytes = pngData.length;
for (var i=0; i<4; i++) {
icoFile += String.fromCharCode(lenBytes % 256);
lenBytes >>= 4;
}
// We had 0x16 bytes before now
icoFile += "\x16\x00\x00\x00";
// Now add the png data
icoFile += pngData;
// Now we have a valid ico file!
return icoFile;
}
Here's another solution if you're willing to bend the rules of this question being a JavaScript question. If your web browser supports WebAssembly (most modern browsers do), you could use a version of the well-known library ImageMagick cross-compiled into WebAssembly. Here is what I found: https://github.com/KnicKnic/WASM-ImageMagick
This library takes in image data from a sourceBytes buffer, and returns a transformed or converted image. According to the documentation, you can use it with a syntax similar to ImageMagick's terminal syntax, with a bit of extra code (copied from documentation and modified):
<script type='module'>
import * as Magick from 'https://knicknic.github.io/wasm-imagemagick/magickApi.js';
async function converPNGToIco(pngData) {
var icoData = await Magick.Call([{ 'name': 'srcFile.png', 'content': pngData }], ["convert", "srcFile.png", "-resize", "200x200", "outFile.ico"]);
// do stuff with icoData
}
</script>
Here is a pure JavaScript version (inspired by #id01) that converts an array of png image data (each images as an array of bytes) and returns an array of bytes for .ico file
function pngToIco( images )
{
let icoHead = [ //.ico header
0, 0, // Reserved. Must always be 0 (2 bytes)
1, 0, // Specifies image type: 1 for icon (.ICO) image, 2 for cursor (.CUR) image. Other values are invalid. (2 bytes)
images.length & 255, (images.length >> 8) & 255 // Specifies number of images in the file. (2 bytes)
],
icoBody = [],
pngBody = [];
for(let i = 0, num, pngHead, pngData, offset = 0; i < images.length; i++)
{
pngData = Array.from( images[i] );
pngHead = [ //image directory (16 bytes)
0, // Width 0-255, should be 0 if 256 pixels (1 byte)
0, // Height 0-255, should be 0 if 256 pixels (1 byte)
0, // Color count, should be 0 if more than 256 colors (1 byte)
0, // Reserved, should be 0 (1 byte)
1, 0, // Color planes when in .ICO format, should be 0 or 1, or the X hotspot when in .CUR format (2 bytes)
32, 0 // Bits per pixel when in .ICO format, or the Y hotspot when in .CUR format (2 bytes)
];
num = pngData.length;
for (let i = 0; i < 4; i++)
pngHead[pngHead.length] = ( num >> ( 8 * i )) & 255; // Size of the bitmap data in bytes (4 bytes)
num = icoHead.length + (( pngHead.length + 4 ) * images.length ) + offset;
for (let i = 0; i < 4; i++)
pngHead[pngHead.length] = ( num >> ( 8 * i )) & 255; // Offset in the file (4 bytes)
offset += pngData.length;
icoBody = icoBody.concat(pngHead); // combine image directory
pngBody = pngBody.concat(pngData); // combine actual image data
}
return icoHead.concat(icoBody, pngBody);
}
Example how to use by converting multiple canvas images into .ico:
function draw(canvas)
{
var ctx = canvas.getContext('2d');
ctx.beginPath();
var p = Math.min(canvas.width, canvas.height);
ctx.fillStyle = "yellow";
ctx.lineWidth = p / 16 / 1.5;
ctx.lineCap = "round";
ctx.arc(50*p/100, 50*p/100, 39*p/100, 0, Math.PI * 2, true); // Outer circle
ctx.save();
var radgrad = ctx.createRadialGradient(50*p/100,50*p/100,0,50*p/100,50*p/100,50*p/100);
radgrad.addColorStop(0, 'rgba(128,128,128,1)');
radgrad.addColorStop(0.3, 'rgba(128,128,128,.8)');
radgrad.addColorStop(0.5, 'rgba(128,128,128,.5)');
radgrad.addColorStop(0.8, 'rgba(128,128,128,.2)');
radgrad.addColorStop(1, 'rgba(128,128,128,0)');
ctx.fillStyle = radgrad;
ctx.fillRect(0, 0, p, p);
ctx.restore();
ctx.fill();
ctx.moveTo(77*p/100, 50*p/100);
ctx.arc(50*p/100, 50*p/100, 27*p/100, 0, Math.PI, false); // Mouth (clockwise)
ctx.moveTo(41*p/100, 42*p/100);
ctx.arc(38*p/100, 42*p/100, 3*p/100, 0, Math.PI * 2, true); // Left eye
ctx.moveTo(65*p/100, 42*p/100);
ctx.arc(62*p/100, 42*p/100, 3*p/100, 0, Math.PI * 2, true); // Right eye
ctx.stroke();
}
let images = [];//array of image data. each image as an array of bytes
for(let i= 0, a = [16,24,32,48,64,128,256,512,1024]; i < a.length; i++)
{
let canvas = document.createElement("canvas");
canvas.width = a[i];
canvas.height = a[i];
draw(canvas);
// Convert canvas to Blob, then Blob to ArrayBuffer.
canvas.toBlob(function (blob) {
const reader = new FileReader();
reader.addEventListener('loadend', () => {
images[i] = new Uint8Array(reader.result);
if (images.length == a.length)//all canvases converted to png
{
let icoData = pngToIco(images), //array of bytes
type = "image/x-ico",
blob = new Blob([new Uint8Array(icoData)], {type: type}),
a = document.getElementById("download");
a.download = "smile.ico";
a.href = window.URL.createObjectURL(blob);
a.dataset.downloadurl = [type, a.download, a.href].join(':');
document.getElementById("img").src = a.href;
}
});
reader.readAsArrayBuffer(blob);
}, "image/png");
document.body.appendChild(canvas);
}
function pngToIco( images )
{
let icoHead = [ //.ico header
0, 0, // Reserved. Must always be 0 (2 bytes)
1, 0, // Specifies image type: 1 for icon (.ICO) image, 2 for cursor (.CUR) image. Other values are invalid. (2 bytes)
images.length & 255, (images.length >> 8) & 255 // Specifies number of images in the file. (2 bytes)
],
icoBody = [],
pngBody = [];
for(let i = 0, num, pngHead, pngData, offset = 0; i < images.length; i++)
{
pngData = Array.from( images[i] );
pngHead = [ //image directory (16 bytes)
0, // Width 0-255, should be 0 if 256 pixels (1 byte)
0, // Height 0-255, should be 0 if 256 pixels (1 byte)
0, // Color count, should be 0 if more than 256 colors (1 byte)
0, // Reserved, should be 0 (1 byte)
1, 0, // Color planes when in .ICO format, should be 0 or 1, or the X hotspot when in .CUR format (2 bytes)
32, 0 // Bits per pixel when in .ICO format, or the Y hotspot when in .CUR format (2 bytes)
];
num = pngData.length;
for (let i = 0; i < 4; i++)
pngHead[pngHead.length] = ( num >> ( 8 * i )) & 255; // Size of the bitmap data in bytes (4 bytes)
num = icoHead.length + (( pngHead.length + 4 ) * images.length ) + offset;
for (let i = 0; i < 4; i++)
pngHead[pngHead.length] = ( num >> ( 8 * i )) & 255; // Offset in the file (4 bytes)
offset += pngData.length;
icoBody = icoBody.concat(pngHead); // combine image directory
pngBody = pngBody.concat(pngData); // combine actual image data
}
return icoHead.concat(icoBody, pngBody);
}
<a id="download">download image <img id="img" width="128"></a>
<br>
https://jsfiddle.net/vanowm/b657yksg/
I wrote an ES6 module that pack PNG files into ICO container: PNG2ICOjs. Everything works in modern browsers without any dependency.
import { PngIcoConverter } from "../src/png2icojs.js";
// ...
const inputs = [...files].map(file => ({
png: file
}));
// Result is a Blob
const resultBlob1 = await converter.convertToBlobAsync(inputs); // Default mime type is image/x-icon
const resultBlob2 = await converter.convertToBlobAsync(inputs, "image/your-own-mime");
// Result is an Uint8Array
const resultArr = await converter.convertAsync(inputs);
this should works
github url:https://github.com/egy186/icojs
<input type="file" id="input-file" />
<script>
document.getElementById('input-file').addEventListener('change', function (evt) {
// use FileReader for converting File object to ArrayBuffer object
var reader = new FileReader();
reader.onload = function (e) {
ICO.parse(e.target.result).then(function (images) {
// logs images
console.dir(images);
})
};
reader.readAsArrayBuffer(evt.target.files[0]);
}, false);
</script>
Fully working example.
Here is JSFiddle: click (use it because this site's Code snippet do not allow me to click on a[download] link), but other code is working in code snippet - you can open links in new tab to see it.
var MyBlobBuilder = function() {
this.parts = [];
}
MyBlobBuilder.prototype.append = function(part) {
this.parts.push(part);
this.blob = undefined; // Invalidate the blob
};
MyBlobBuilder.prototype.write = function(part) {
this.append(part);
}
MyBlobBuilder.prototype.getBlob = function(atype) {
if (!this.blob) {
this.blob = new Blob(this.parts, {
type: !atype ? "text/plain" : atype
});
}
return this.blob;
};
const img = document.getElementById('input'),
a = document.getElementById('a'),
a2 = document.getElementById('a2'),
file1 = document.getElementById('file');
let imgSize = 0,
imgBlob;
img.onload = e => {
fetch(img.src).then(resp => resp.blob())
.then(blob => {
imgBlob = blob;
imgSize = blob.size;
});
};
function convertToIco(imgSize, imgBlob) {
let file = new MyBlobBuilder(),
buff;
// Write out the .ico header [00, 00]
// Reserved space
buff = new Uint8Array([0, 0]).buffer;
file.write(buff, 'binary');
// Indiciate ico file [01, 00]
buff = new Uint8Array([1, 0]).buffer;
file.write(buff, 'binary');
// Indiciate 1 image [01, 00]
buff = new Uint8Array([1, 0]).buffer;
file.write(buff, 'binary');
// Image is 50 px wide [32]
buff = new Uint8Array([img.width < 256 ? img.width : 0]).buffer;
file.write(buff, 'binary');
// Image is 50 px tall [32]
buff = new Uint8Array([img.height < 256 ? img.height : 0]).buffer;
file.write(buff, 'binary');
// Specify no color palette [00]
// TODO: Not sure if this is appropriate
buff = new Uint8Array([0]).buffer;
file.write(buff, 'binary');
// Reserved space [00]
// TODO: Not sure if this is appropriate
buff = new Uint8Array([0]).buffer;
file.write(buff, 'binary');
// Specify 1 color plane [01, 00]
// TODO: Not sure if this is appropriate
buff = new Uint8Array([1, 0]).buffer;
file.write(buff, 'binary');
// Specify 32 bits per pixel (bit depth) [20, 00]
// TODO: Quite confident in this one
buff = new Uint8Array([32, 0]).buffer;
file.write(buff, 'binary');
// Specify image size in bytes
// DEV: Assuming LE means little endian [84, 01, 00, 00] = 388 byte
// TODO: Semi-confident in this one
buff = new Uint32Array([imgSize]).buffer;
file.write(buff, 'binary');
// Specify image offset in bytes
// TODO: Not that confident in this one [16]
buff = new Uint32Array([22]).buffer;
file.write(buff, 'binary');
// Dump the .png
file.write(imgBlob, 'binary');
return file.getBlob('image/vnd.microsoft.icon');
}
function test() {
const ico = convertToIco(imgSize, imgBlob);
let url = window.URL.createObjectURL(ico);
a.href = url;
document.getElementById('result1').style.display = '';
}
file1.addEventListener('change', () => {
var file = file1.files[0];
var fileReader = new FileReader();
fileReader.onloadend = function(e) {
var arrayBuffer = e.target.result;
const blobFile = new Blob([arrayBuffer]);
const ico = convertToIco(blobFile.size, blobFile);
let url = window.URL.createObjectURL(ico);
a2.href = url;
document.getElementById('result2').style.display = '';
}
fileReader.readAsArrayBuffer(file);
});
.section {
padding: 4px;
border: 1px solid gray;
margin: 4px;
}
h3 {
margin: 0 0 4px 0;
}
<div class="section">
<h3>Convert img[src] to ICO</h3>
<div>
Example (PNG): <img src="https://gist.githubusercontent.com/twolfson/7656254/raw/c5d0dcedc0e212f177695f08d685af5aad9ff865/sprite1.png" id="input" />
</div>
<div>
<button onclick="test()">Conver to ICO</button>
</div>
<div id="result1" style="display:none">
Save ICO: <a id="a" href="#" download="example.ico">click me</a>
</div>
</div>
<div class="section">
<h3>Convert input[type=file] to ICO</h3>
<input type="file" id="file" />
<div id="result2" style="display:none">
Save ICO: <a id="a2" href="#" download="example.ico">click me</a>
</div>
</div>
P.S. Used documentation: Wikipedia.
I'm building a simple html component that will allow a user to record a message and then immediately play it back.
The aim is to allow them to discard the recording if they don't like it before saving the recording.
The messages will be less than 60 seconds.
So I have a UI that allows me to record audio into an array of AudioBuffers.
I'm now trying to push those buffers back into a second audio control so a user can play the recording back.
Here is a codepen.
This is the core playback function which I assume is at the heart of the problem.
The solution must work within a browser on android and ios and not use third party libraries (to make your life really difficult :)
function _playback(){
let context = new AudioContext();
var dest = context.createMediaStreamDestination();
let source = context.createBufferSource(); // creates a sound source
source.buffer = _appendBuffer(context, this._audioBuffer);
source.connect(dest);
let player = window.getElementById("playback");
player.srcObject = dest.stream;
player.play();
}
The key issue is that the audio doesn't playback even though the player control shows time progressing.
I also have questions about whether I should be using two players (one for recording and one for playback) or I can just use a single audio element?
So this one was a bit of a nightmare so I though I would post the full script here.
I started with the famous recorder.js but had some problems adapting it to my needs. I ended up doing a major refactor to make the code easier to understand.
Unlike recorder.js this code doesn't use worker threads (which I really didn't need).
/**
* Allows recording via the devices microphone.
*/
class Recorder
{
/**
* Constraints setup for mono.
* Currently now to modify them.
* It should be noted that these settings are ignored on most
* systems and we get stereo at a 44K sampleRate regardless of these settings.
*/
static _constraints = {
audio:
{
channelCount: 1,
mimeType: 'audio/wav',
sampleRate: 8192,
sampleSize: 8,
autoGainControl: true,
noiseSuppression: true,
echoCancellation: true,
}
};
constructor(desiredChannels)
{
this._desiredChannels = desiredChannels;
this._reset();
}
/*
* Start recording.
*
* errorCallback(e) - a function that is called if the start fails.
*
*/
start(errorCallback)
{
this._reset();
this._context = new AudioContext();
// request permission and if given
// wire our audio control to the media stream.
navigator
.mediaDevices
.getUserMedia(Recorder._constraints)
.then((stream) => this._wireRecordingStream(stream))
.catch(e => errorCallback(e));
// TODO: consider giving the user the ability to select an input device.
}
/*
* Stops a currently active recording.
*/
stop()
{
if (this._context != null)
{
this._context.close();
this._context = null;
}
}
/**
* check if the user's phone supports media api
*/
hasGetUserMedia()
{
return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia);
}
/**
* returns a Blob containing a wav file of the recording.
*/
getWav()
{
if (this._mergedChannelData == null)
this._mergeChannels();
let wav = new Wav(this._mergedChannelData, this._actualChannelCount, this._actualSampleRate);
return wav.getBlob();
}
/**
* resets the Recorder so we can restart the recording.
*/
_reset()
{
this._channels = null;
this._actualChannelCount = -1;
// this will be updated when the recording starts to the actual rate.
this._actualSampleRate = -1;
// after _mergeChannels is called this will contain
// a single float32 array of the underlying channel
// data interleaved to create a single audio stream.
this._mergedChannelData = null;
}
_initChannelBuffers(actualChannels)
{
if (this._channels == null)
{
this._channels = [];
this._actualChannelCount = actualChannels;
for (var i = 0; i < actualChannels; i++)
{
this._channels.push(new Channel());
}
}
}
/**
* The start() method uses this method to initialise the media stream
* and wire up the 'onaudioprocess'or to capture the recording.
*/
_wireRecordingStream(stream)
{
// https://developers.google.com/web/fundamentals/media/recording-audio/
// Setup recording.
this._source = this._context.createMediaStreamSource(stream);
this._node = (this._context.createScriptProcessor || this._context.createJavaScriptNode)
.call(this._context, 4096, this._desiredChannels, this._desiredChannels); // 4K buffer and we prefer a single (mono) channel.
// the context may have ignored our preferred sample rate.
this._actualSampleRate = this._context.sampleRate;
this._node.onaudioprocess = (e) => this._storeAudio(e.inputBuffer);
this._source.connect(this._node);
this._node.connect(this._context.destination);
}
/**
* This is the callback for 'onaudioprocess' where we store the recorded data
* to each channel buffer.
*/
_storeAudio(inputBuffer)
{
this._initChannelBuffers(inputBuffer.numberOfChannels);
for (var i = 0; i < this._actualChannelCount; i++)
{
this._channels[i].storeAudioPacket(inputBuffer.getChannelData(i));
}
}
// Merges all channels into a single float32Array.
// Channels are merged by interleaving data packet from each channel into a single stream.
_mergeChannels()
{
if (this._actualChannelCount === 2)
{
this._mergedChannelData = this._interleave(this._channels[0], this._channels[1]);
}
else
{
this._mergedChannelData = this._channels[0].getAudioData();
}
}
/**
** interleaves two channel buffers into a single float32 array.
*/
_interleave(lhsChannel, rhsChannel)
{
let length = lhsChannel.getLength() + rhsChannel.getLength();
let result = new Float32Array(length);
let index = 0;
let inputIndex = 0;this._channels
let lhsData = lhsChannel.getAudioData();
let rhsData = rhsChannel.getAudioData();
while (index < length)
{
result[index++] = lhsData[inputIndex];
result[index++] = rhsData[inputIndex];
inputIndex++;
}
return result;
}
}
/**
* Used to buffer audio data for a single channel.
*/
class Channel
{
constructor()
{
/**
* the total no of Float32's stored in all of the audio packets.
*/
this._length = 0;
// an array of audio packets (Float32Array) captured as the recording progresses.
//
this._audioPackets = [];
// If flatten has been called this will be a Float32Array
// contain all of the combined audio packets as a single array.
this._flattened = null;
}
getLength()
{
return this._length;
}
/**
* returns a single audio packet stored at the given index.
*/
getAudioPacket(index)
{
return this._audioPackets[index];
}
/**
* returns the entire underlying data (Float32s) as a single Float32 array
* If it hasn't already been done this method will call flatten to
* combine all of the packets into a singl data array.
*/
getAudioData()
{
if (this._flattened == null)
this._flatten();
return this._flattened;
}
// Stores an audioPacket (Float32Array) to _audioPackets
storeAudioPacket(audioPacket)
{
this._audioPackets.push(new Float32Array(audioPacket));
this._length += audioPacket.length;
}
/**
* coalesce all of the _audioPackets into a single float32Array
*/
_flatten()
{
this._flattened = new Float32Array(this._length);
let offset = 0;
for (let i = 0; i < this._audioPackets.length; i++)
{
this._flattened.set(this._audioPackets[i], offset);
offset += this._audioPackets[i].length;
}
}
}
/**
* The logic for creating a wav file (well just the data structure actually) from
* a stream of audioData
*
* audioData - Float32Array containing the interleaved data from all channels.
* channelCount - the number of channels interleaved into the audioData
* sampleRate - the sampleRate of the audioData.
*/
class Wav
{
/**
* expects a single float32array from which it will create a wav file.
*/
constructor(audioData, channelCount, sampleRate)
{
this._audioData = audioData;
this._channelCount = channelCount;
this._sampleRate = sampleRate;
}
/**
* returns the wav file as a blob.
*/
getBlob()
{
let wav = this._encodeAsWAV();
let audioBlob = new Blob([wav], { type: "audio/wav" });
return audioBlob;
}
/**
* Encodes _audioData into a wav file by adding the
* standard wav header.
*/
_encodeAsWAV()
{
let audioData = this._audioData;
var wavBuffer = new ArrayBuffer(44 + audioData.length * 2);
var view = new DataView(wavBuffer);
/* RIFF identifier */
this._writeString(view, 0, 'RIFF');
/* RIFF chunk length */
view.setUint32(4, 36 + audioData.length * 2, true);
/* RIFF type */
this._writeString(view, 8, 'WAVE');
/* format chunk identifier */
this._writeString(view, 12, 'fmt ');
/* format chunk length */
view.setUint32(16, 16, true);
/* sample format (raw) */
view.setUint16(20, 1, true);
/* channel count */
view.setUint16(22, this._channelCount, true);
/* sample rate */
view.setUint32(24, this._sampleRate, true);
/* byte rate (sample rate * block align) */
view.setUint32(28, this._sampleRate * 4, true);
/* block align (channel count * bytes per sample) */
view.setUint16(32, this._channelCount * 2, true);
/* bits per sample */
view.setUint16(34, 16, true);
/* data chunk identifier */
this._writeString(view, 36, 'data');
/* data chunk length */
view.setUint32(40, audioData.length * 2, true);
this._floatTo16BitPCM(view, 44, audioData);
return view;
}
_floatTo16BitPCM(output, offset, input)
{
for (var i = 0; i < input.length; i++, offset += 2)
{
var s = Math.max(-1, Math.min(1, input[i]));
output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}
}
_writeString(view, offset, string)
{
for (var i = 0; i < string.length; i++)
{
view.setUint8(offset + i, string.charCodeAt(i));
}
}
}
I have to create a animation for waves . I need to control the speed of the waves depends on the availability of the data. Is it possible to speed up the waves. I'm using canvas for the waves.
Thanks in advance
Fiddle:https://jsfiddle.net/Chaitanya_Kumar/6ztr0Lfh/
function animate() {
if (x > data.length - 1) {
return;
}
if (continueAnimation) {
requestAnimationFrame(animate);
}
if (x++ < panAtX) {
var temp = data[x];
var final = constant-(temp);
ctx.fillRect(x, final, 1, 1);
ctx.lineTo(x, final);
ctx.stroke();
} else {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath(); // reset the path
for (var xx = 0; xx < panAtX; xx++) {
var y = data[x - panAtX + xx];
var final = constant - (y);
ctx.fillRect(xx, final, 1, 1);
ctx.lineTo(xx, final);
}
ctx.stroke();
}
}
Sub sampling data
Below is an example of data sampling. It uses linear interpolation to subsample a data source and display that data on a rolling graph display.
Regularly interval data.
The data from your question and fiddle suggest that you have a constant sample rate interval and that you want to vary the display rate of that data. This is what I have done in the demo below.
About the demo
The graph is a real-time display of the data and its speed from left to right is dependent on the rate at which you call the sample function.
displayBuffer.readFrom(dataSource, dataSpeed, samplesPerFrame)
displayBuffer is the object that holds the displayable data
dataSource is the source of data and has a read and seek function and a readPos You seek to a position dataSource.seek(0.01); move ahead 0.01 data samples and then read the data dataSource.read(); and the linear interpolated value is returned.
This allows you to speed up or slow down data streaming from the source data.
The data reader object
//------------------------------------------------------------------------------
// data reader reads from a data source
const dataReader = {
readPos : 0,
seek(amount){ // moves read pos forward or back
if(this.data.length === 0){
this.readPos = 0;
return 0;
}
this.readPos += amount;
this.readPos = this.readPos < 0 ? 0 :this.readPos >= this.data.length ? this.data.length - 1 : this.readPos;
return this.readPos;
},
// this function reads the data at read pos. It is a linear interpolation of the
// data and does nor repressent what the actual data may be at fractional read positions
read(){
var fraction = this.readPos % 1;
var whole = Math.floor(this.readPos);
var v1 = this.data[Math.min(this.data.length-1,whole)];
var v2 = this.data[Math.min(this.data.length-1,whole + 1)];
return (v2 - v1) * fraction + v1;
},
}
Timestamped data source.
The demo can be adapted by adding to the dataReader.
If your data sample rate is irregular than you will need to add a timestamp for each sample. You then add a timeSeek function that is similare to seek but uses the slope between time samples to calculate the read position for a given time. It will require sampling of each sample from the current sampled time to the next (in the seek direction) making CPU cycles needed to seek indeterminant.
The following is an example seekTime that finds the readPos (from above dataReader object) for time shifted forward by the timeShift argument. the object's readTime and readPos properties are updated and the next read() call will return the data at dataSource.readTime.
readTime : 0, // current seeked time
seekTime(timeShift){ // Example is forward seek only
if(this.timeStamps.length === 0){
this.readPos = 0;
return 0;
}
this.readTime += timeShift; // set new seeked time
var readPos = Math.floor(this.readPos);
// move read pos forward until at correct sample
while(this.timeStamps[readPos] > this.readTime &&
readPos++ < this.timeStamps.length);
// Warning you could be past end of buffer
// you will need to check and set seek time to the last
// timestamp value and exit. Code below the following line
// will crash if you dont vet here.
//if(readPos === this.timeStamps.length)
// now readPos points to the first timeStamp less than the needed
// time position. The next read position should be a time ahead of the
// needed time
var t1 = this.timeStamps[readPos]; // time befor seekTime
var t2 = this.timeStamps[readPos+1]; // time after seekTime
// warning divide by zero if data bad
var fraction = (this.readTime-t1)/(t2-t1)); // get the sub sample fractional location for required time.
this.readPos = readPos + fraction;
return this.readPos;
},
Warning I have omitted all safety checks. You should check for buffer end, bad time shift values. If time stamped data has bad samples you will get a divide by zero that will make the dataReader return only NaN from that point on and throw for any reads. So vet for safety.
Note For the above time stamped function to work you will need to ensure that for each data sample there is a corresponding timeStamp. If there is not a one to one matching time stamp of each sample the above code will not work.
Changes to the dataDisplay are simple. Just change the seek call in the function
dataDisplay.readFrom(dataSource,speed,samples) to dataSource.seekTime(speed / samples) the speed now represents time rather than samples. (or I just overwrite the seek() function with seekTime() if I have time stamps) this allows the dataDisplay object to handle both timeStamped and regular interval data as is.
Demo
The example samples random data and displays it at variable speed and sampling rates. Use left right to set display speed. The framerate is 60fps thereabouts but you can make the speed variable scaled to the time between frames.
var ctx = canvas.getContext("2d");
window.focus();
//==============================================================================
// the current data read speed
var dataSpeed = 1;
var samplesPerFrame = 1;
requestAnimationFrame(mainLoop); // start animation when code has been parsed and executed
//------------------------------------------------------------------------------
// data reader reads from a data source
const dataReader = {
readPos : 0,
seek(amount){ // moves read pos forward or back
if(this.data.length === 0){
this.readPos = 0;
return 0;
}
this.readPos += amount;
this.readPos = this.readPos < 0 ? 0 :this.readPos >= this.data.length ? this.data.length - 1 : this.readPos;
return this.readPos;
},
// this function reads the data at read pos. It is a linear interpolation of the
// data and does nor repressent what the actual data may be at fractional read positions
read(){
var fraction = this.readPos % 1;
var whole = Math.floor(this.readPos);
var v1 = this.data[Math.min(this.data.length-1,whole)];
var v2 = this.data[Math.min(this.data.length-1,whole + 1)];
return (v2 - v1) * fraction + v1;
},
}
//------------------------------------------------------------------------------
// Create a data source and add a dataReader to it
const dataSource = Object.assign({
data : [],
},dataReader
);
// fill the data source with random data
for(let i = 0; i < 100000; i++ ){
// because random data looks the same if sampled every 1000 or 1 unit I have added
// two waves to the data that will show up when sampling at high rates
var wave = Math.sin(i / 10000) * 0.5;
wave += Math.sin(i / 1000) * 0.5;
// high frequency data shift
var smallWave = Math.sin(i / 100) * (canvas.height / 5);
// get a gaussian distributed random value
dataSource.data[i] = Math.floor(smallWave + ((wave + Math.random()+Math.random()+Math.random()+Math.random()+Math.random()) / 5) * canvas.height);
}
//------------------------------------------------------------------------------
// Data displayer used to display a data source
const dataDisplay = {
writePos : 0,
width : 0,
color : "black",
lineWidth : 1,
// this function sets the display width which limits the data buffer
// when it is called all buffers are reset
setDisplayWidth(width){
this.data.length = 0;
this.width = width;
this.writePos = 0;
if(this.lastRead === undefined){
this.lastRead = {};
}
this.lastRead.mean = 0;
this.lastRead.max = 0;
this.lastRead.min = 0;
},
// this draws the buffered data scrolling from left to right
draw(){
var data = this.data; // to save my self from writing this a zillion times
const ch = canvas.height / 2;
if(data.length > 0){ // only if there is something to draw
ctx.beginPath();
ctx.lineWidth = this.lineWidth;
ctx.strokeStyle = this.color;
ctx.lineJoin = "round";
if(data.length < this.width){ // when buffer is first filling draw from start
ctx.moveTo(0, data[0])
for(var i = 1; i < data.length; i++){
ctx.lineTo(i, data[i])
}
}else{ // buffer is full and write position is chasing the tail end
ctx.moveTo(0, data[this.writePos])
for(var i = 1; i < data.length; i++){
ctx.lineTo(i, data[(this.writePos + i) % data.length]);
}
}
ctx.stroke();
}
},
// this reads data from a data source (that has dataReader functionality)
// Speed is in data units,
// samples is number of samples per buffer write.
// samples is only usefull if speed > 1 and lets you see the
// mean, min, and max of the data over the speed unit
// If speed < 1 and sample > 1 the data is just a linear interpolation
// so the lastRead statistics are meaningless (sort of)
readFrom(dataSource,speed,samples){ // samples must be a whole positive number
samples = Math.floor(samples);
var value = 0;
var dataRead;
var min;
var max;
for(var i = 0; i < samples; i ++){ // read samples
dataSource.seek(speed / samples); // seek to next sample
dataRead = dataSource.read(); // read the sample
if(i === 0){
min = dataRead;
max = dataRead;
}else{
min = Math.min(dataRead,min);
max = Math.min(dataRead,max);
}
value += dataRead;
}
// write the samples data and statistics.
this.lastRead.min = min;
this.lastRead.max = max;
this.lastRead.delta = value / samples - this.lastRead.mean;
this.lastRead.mean = value / samples;
this.data[this.writePos] = value / samples;
this.writePos += 1;
this.writePos %= this.width;
}
}
// display data buffer
var displayBuffer = Object.assign({ // this data is displayed at 1 pixel per frame
data : [], // but data is written into it at a variable speed
},
dataDisplay // add display functionality
);
//------------------------------------------------------------------------------
// for control
const keys = {
ArrowLeft : false,
ArrowRight : false,
ArrowUp : false,
ArrowDown : false,
}
function keyEvent(event){
if(keys[event.code] !== undefined){
event.preventDefault();
keys[event.code] = true;
}
}
addEventListener("keydown",keyEvent);
//------------------------------------------------------------------------------
function mainLoop(time){
ctx.clearRect(0,0,canvas.width,canvas.height);
if(canvas.width !== displayBuffer.width){
displayBuffer.setDisplayWidth(canvas.width);
}
displayBuffer.readFrom(dataSource,dataSpeed,samplesPerFrame);
displayBuffer.draw();
//-----------------------------------------------------------------------------
// rest is display UI and stuff like that
ctx.font = "16px verdana";
ctx.fillStyle = "black";
//var dataValue =displayBuffer.lastRead.mean.toFixed(2);
//var delta = displayBuffer.lastRead.delta.toFixed(4);
var readPos = dataSource.readPos.toFixed(4);
//if(displayBuffer.lastRead.delta > 0){ delta = "+" + delta }
// ctx.fillText("Data : " + dataValue + " ( " +delta +" )" ,4,18);
ctx.setTransform(0.9,0,0,0.89,4,18);
ctx.fillText("Speed : " + dataSpeed.toFixed(3) + ", Sample rate :" +samplesPerFrame + ", Read # "+readPos ,0,0);
ctx.setTransform(0.7,0,0,0.7,4,32);
if(samplesPerFrame === 1){
ctx.fillText("Keyboard speed -left, +right Sample rate +up",0,0);
}else{
ctx.fillText("Keyboard speed -left, +right Sample rate -down, +up",0,0);
}
ctx.setTransform(1,0,0,1,0,0);
if(keys.ArrowLeft){
keys.ArrowLeft = false;
if(dataSpeed > 1){
dataSpeed -= 1;
}else{
dataSpeed *= 1/1.2;
}
}
if(keys.ArrowRight){
keys.ArrowRight = false;
if(dataSpeed >= 1){
dataSpeed += 1;
}else{
dataSpeed *= 1.2;
if(dataSpeed > 1){ dataSpeed = 1 }
}
}
if(keys.ArrowUp){
keys.ArrowUp = false;
samplesPerFrame += 1;
}
if(keys.ArrowDown){
keys.ArrowDown = false;
samplesPerFrame -= 1;
samplesPerFrame = samplesPerFrame < 1 ? 1 : samplesPerFrame;
}
requestAnimationFrame(mainLoop);
}
canvas {
border : 2px black solid;
}
<canvas id=canvas width=512 height=200></canvas>
Reading and displaying data this way is quick and simple. It is easy it add grid markings and data processing to the data source and display data. The demo (regular interval data) can easily handle displaying large data sources while zooming in and out on data. Note that for timeStamped data the above seekTime function is not suitable for large datasets. You will need to index such data for more effective seek times.