I'm trying to manage to apply an generated image of an audio graph for every div that a music url is found with a Google Chrome Extension.
However, the process of downloading the music from the url and processing the image, takes enough time that all of the images keep applying to the last div.
I'm trying to apply the images to each div as throughout the JQuery's each request. All the div's have the /renderload.gif gif playing, but only the last div flashes as the images finished processing one by one.
Example being that the src is being set to /renderload.gif for all 1,2,3,4,5
but once the sound blob was downloaded and image was generated, only 4-5 gets the images and it continues on loading the queue, repeating the issue.
Here's an example of what I'm trying to deal with.
Here's my latest attempts to add queueing to avoid lag by loading all the audios at once, but it seems the issue still persists.
// context.js
function Queue(){
var queue = [];
var offset = 0;
this.getLength = function(){
return (queue.length - offset);
}
this.isEmpty = function(){
return (queue.length == 0);
}
this.setEmpty = function(){
queue = [];
return true;
}
this.enqueue = function(item){
queue.push(item);
}
this.dequeue = function(){
if (queue.length == 0) return undefined;
var item = queue[offset];
if (++ offset * 2 >= queue.length){
queue = queue.slice(offset);
offset = 0;
}
return item;
}
this.peek = function(){
return (queue.length > 0 ? queue[offset] : undefined);
}
}
var audioqueue=new Queue();
var init=0;
var current=0;
var finished=0;
function RunGraphs(x) {
if (x==init) {
if (audioqueue.isEmpty()==false) {
current++;
var das=audioqueue.dequeue();
var divparent=das.find(".original-image");
var songurl=das.find(".Mpcs").find('span').attr("data-url");
console.log("is song url "+songurl);
console.log("is data here "+divparent.attr("title"));
divparent.css('width','110px');
divparent.attr('src','https://i.pinimg.com/originals/a4/f2/cb/a4f2cb80ff2ae2772e80bf30e9d78d4c.gif');
var blob = null;
var xhr = new XMLHttpRequest();
xhr.open("GET",songurl,true);
xhr.responseType = "blob";//force the HTTP response, response-type header to be blob
xhr.onload = function() {
blob = xhr.response;//xhr.response is now a blob object
console.log(blob);
SCWFRobloxAudioTool.generate(blob, {
canvas_width: 110,
canvas_height: 110,
bar_width: 1,
bar_gap : .2,
wave_color: "#ecb440",
download: false,
onComplete: function(png, pixels) {
if (init == x) {
divparent.attr('src',png);
finished++;
}
}
});
}
xhr.send();
OnHold(x);
}
}
}
function OnHold(x) {
if (x==init) {
if (current > finished+7) {
setTimeout(function(){
OnHold(x)
},150)
} else {
RunGraphs(x)
}
}
}
if (window.location.href.includes("/lib?Ct=DevOnly")){
functionlist=[];
current=0;
finished=0;
init++;
audioqueue.setEmpty();
$(".CATinner").each(function(index) {
(function(x){
audioqueue.enqueue(x);
}($(this)));
});
RunGraphs(init);
};
The SCWFAudioTool is from this github repository.
Soundcloud Waveform Generator
The Queue.js from a search request, slightly modified to have setEmpty support.Queue.js
Please read the edit part of the post
I mad a usable minimal example of your code in order to check your Queue and defer method. there seems to be no error that i can find (i don't have the html file and cant check for missing files. Please do that yourself by adding the if (this.status >= 200 && this.status < 400) check to the onload callback):
// context.js
function Queue(){
var queue = [];
var offset = 0;
this.getLength = function(){
return (queue.length - offset);
}
this.isEmpty = function(){
return (queue.length == 0);
}
this.setEmpty = function(){
queue = [];
return true;
}
this.enqueue = function(item){
queue.push(item);
}
this.dequeue = function(){
if (queue.length == 0) return undefined;
var item = queue[offset];
if (++ offset * 2 >= queue.length){
queue = queue.slice(offset);
offset = 0;
}
return item;
}
this.peek = function(){
return (queue.length > 0 ? queue[offset] : undefined);
}
}
var audioqueue=new Queue();
var init=0;
var current=0;
var finished=0;
function RunGraphs(x) {
if (x==init) {
if (audioqueue.isEmpty()==false) {
current++;
var songurl = audioqueue.dequeue();
console.log("is song url "+songurl);
var blob = null;
var xhr = new XMLHttpRequest();
xhr.open("GET",songurl,true);
xhr.responseType = "blob";//force the HTTP response, response-type header to be blob
xhr.onload = function() {
if (this.status >= 200 && this.status < 400) {
blob = xhr.response;//xhr.response is now a blob object
console.log('OK');
finished++;
} else {
console.log('FAIL');
}
}
xhr.send();
OnHold(x);
}
}
}
function OnHold(x) {
if (x==init) {
if (current > finished+7) {
setTimeout(function(){
OnHold(x)
},150)
} else {
RunGraphs(x)
}
}
}
var demoObject = new Blob(["0".repeat(1024*1024*2)]); // 2MB Blob
var demoObjectURL = URL.createObjectURL(demoObject);
if (true){
functionlist=[];
current=0;
finished=0;
init++;
audioqueue.setEmpty();
for(var i = 0; i < 20; i++)
audioqueue.enqueue(demoObjectURL);
RunGraphs(init);
};
Therefore if there are no errors left concerning missing files the only errors i can think off is related to the SCWFRobloxAudioTool.generate method.
Please check if the callback gets triggered correctly and that no errors accrue during conversion.
If you provide additional additional info, data or code i can look into this problem.
EDIT:
I looked into the 'SoundCloudWaveform' program and i think i see the problem:
The module is not made to handle multiple queries at once (there is only one global setting object. So every attempt to add another query to the api will override the callback of the previous one, and since the fileReader is a async call only the latest added callback will be executed.)
Please consider using an oop attempt of this api:
window.AudioContext = window.AudioContext || window.webkitAudioContext;
Array.prototype.max = function() {
return Math.max.apply(null, this);
};
function SoundCloudWaveform (){
this.settings = {
canvas_width: 453,
canvas_height: 66,
bar_width: 3,
bar_gap : 0.2,
wave_color: "#666",
download: false,
onComplete: function(png, pixels) {}
}
this.generate = function(file, options) {
// preparing canvas
this.settings.canvas = document.createElement('canvas');
this.settings.context = this.settings.canvas.getContext('2d');
this.settings.canvas.width = (options.canvas_width !== undefined) ? parseInt(options.canvas_width) : this.settings.canvas_width;
this.settings.canvas.height = (options.canvas_height !== undefined) ? parseInt(options.canvas_height) : this.settings.canvas_height;
// setting fill color
this.settings.wave_color = (options.wave_color !== undefined) ? options.wave_color : this.settings.wave_color;
// setting bars width and gap
this.settings.bar_width = (options.bar_width !== undefined) ? parseInt(options.bar_width) : this.settings.bar_width;
this.settings.bar_gap = (options.bar_gap !== undefined) ? parseFloat(options.bar_gap) : this.settings.bar_gap;
this.settings.download = (options.download !== undefined) ? options.download : this.settings.download;
this.settings.onComplete = (options.onComplete !== undefined) ? options.onComplete : this.settings.onComplete;
// read file buffer
var reader = new FileReader();
var _this = this;
reader.onload = function(event) {
var audioContext = new AudioContext()
audioContext.decodeAudioData(event.target.result, function(buffer) {
audioContext.close();
_this.extractBuffer(buffer);
});
};
reader.readAsArrayBuffer(file);
}
this.extractBuffer = function(buffer) {
buffer = buffer.getChannelData(0);
var sections = this.settings.canvas.width;
var len = Math.floor(buffer.length / sections);
var maxHeight = this.settings.canvas.height;
var vals = [];
for (var i = 0; i < sections; i += this.settings.bar_width) {
vals.push(this.bufferMeasure(i * len, len, buffer) * 10000);
}
for (var j = 0; j < sections; j += this.settings.bar_width) {
var scale = maxHeight / vals.max();
var val = this.bufferMeasure(j * len, len, buffer) * 10000;
val *= scale;
val += 1;
this.drawBar(j, val);
}
if (this.settings.download) {
this.generateImage();
}
this.settings.onComplete(this.settings.canvas.toDataURL('image/png'), this.settings.context.getImageData(0, 0, this.settings.canvas.width, this.settings.canvas.height));
// clear canvas for redrawing
this.settings.context.clearRect(0, 0, this.settings.canvas.width, this.settings.canvas.height);
},
this.bufferMeasure = function(position, length, data) {
var sum = 0.0;
for (var i = position; i <= (position + length) - 1; i++) {
sum += Math.pow(data[i], 2);
}
return Math.sqrt(sum / data.length);
},
this.drawBar = function(i, h) {
this.settings.context.fillStyle = this.settings.wave_color;
var w = this.settings.bar_width;
if (this.settings.bar_gap !== 0) {
w *= Math.abs(1 - this.settings.bar_gap);
}
var x = i + (w / 2),
y = this.settings.canvas.height - h;
this.settings.context.fillRect(x, y, w, h);
},
this.generateImage = function() {
var image = this.settings.canvas.toDataURL('image/png');
var link = document.createElement('a');
link.href = image;
link.setAttribute('download', '');
link.click();
}
}
console.log(new SoundCloudWaveform());
Also consider simply using an array for the queue:
function Queue(){
var queue = [];
var offset = 0;
this.getLength = function(){
return (queue.length - offset);
}
this.isEmpty = function(){
return (queue.length == 0);
}
this.setEmpty = function(){
queue = [];
return true;
}
this.enqueue = function(item){
queue.push(item);
}
this.dequeue = function(){
if (queue.length == 0) return undefined;
var item = queue[offset];
if (++ offset * 2 >= queue.length){
queue = queue.slice(offset);
offset = 0;
}
return item;
}
this.peek = function(){
return (queue.length > 0 ? queue[offset] : undefined);
}
}
var q = new Queue();
q.enqueue(1)
q.enqueue(2)
q.enqueue(3)
console.log(q.dequeue());
console.log(q.dequeue());
console.log(q.dequeue());
console.log(q.dequeue());
var q2 = [];
q2.push(1)
q2.push(2)
q2.push(3)
console.log(q2.shift());
console.log(q2.shift());
console.log(q2.shift());
console.log(q2.shift());
It prevents confusion ant the speedup of it is minimal in your application.
On your open method on the xhr object, set the parameter to true, also... try using the onload() instead of onloadend(). Good Luck!
var xmlhttp = new XMLHttpRequest(),
method = 'GET',
url = 'https://developer.mozilla.org/';
xmlhttp.open(method, url, true);
xmlhttp.onload = function () {
// Do something with the retrieved data
};
xmlhttp.send();
Related
I have been searching for all available solutions
This is the best so far i could find but it doesn't work as it should be
It works for first few images then stops. Then i refresh page, works for few more images and then stops again.
So basically what i want to achieve is, give like a 100 images to a function, it starts downloading them 1 by 1
So browser caches those images and those images are not downloaded on other pages and instantly displayed
I want this images to be cached on mobile as well
Here my javascript code that i call. Actually i have more than 100 images but didn't put all here
I accept both jquery and raw javascript solutions doesn't matter
(function() {
'use strict';
var preLoader = function(images, options) {
this.options = {
pipeline: true,
auto: true,
/* onProgress: function(){}, */
/* onError: function(){}, */
onComplete: function() {}
};
options && typeof options == 'object' && this.setOptions(options);
this.addQueue(images);
this.queue.length && this.options.auto && this.processQueue();
};
preLoader.prototype.setOptions = function(options) {
// shallow copy
var o = this.options,
key;
for (key in options) options.hasOwnProperty(key) && (o[key] = options[key]);
return this;
};
preLoader.prototype.addQueue = function(images) {
// stores a local array, dereferenced from original
this.queue = images.slice();
return this;
};
preLoader.prototype.reset = function() {
// reset the arrays
this.completed = [];
this.errors = [];
return this;
};
preLoader.prototype.load = function(src, index) {
console.log("downloading image " + src);
var image = new Image(),
self = this,
o = this.options;
// set some event handlers
image.onerror = image.onabort = function() {
this.onerror = this.onabort = this.onload = null;
self.errors.push(src);
o.onError && o.onError.call(self, src);
checkProgress.call(self, src);
o.pipeline && self.loadNext(index);
};
image.onload = function() {
this.onerror = this.onabort = this.onload = null;
// store progress. this === image
self.completed.push(src); // this.src may differ
checkProgress.call(self, src, this);
o.pipeline && self.loadNext(index);
};
// actually load
image.src = src;
return this;
};
preLoader.prototype.loadNext = function(index) {
// when pipeline loading is enabled, calls next item
index++;
this.queue[index] && this.load(this.queue[index], index);
return this;
};
preLoader.prototype.processQueue = function() {
// runs through all queued items.
var i = 0,
queue = this.queue,
len = queue.length;
// process all queue items
this.reset();
if (!this.options.pipeline)
for (; i < len; ++i) this.load(queue[i], i);
else this.load(queue[0], 0);
return this;
};
function checkProgress(src, image) {
// intermediate checker for queue remaining. not exported.
// called on preLoader instance as scope
var args = [],
o = this.options;
// call onProgress
o.onProgress && src && o.onProgress.call(this, src, image, this.completed.length);
if (this.completed.length + this.errors.length === this.queue.length) {
args.push(this.completed);
this.errors.length && args.push(this.errors);
o.onComplete.apply(this, args);
}
return this;
}
if (typeof define === 'function' && define.amd) {
// we have an AMD loader.
define(function() {
return preLoader;
});
} else {
this.preLoader = preLoader;
}
}).call(this);
// Usage:
$(window).load(function() {
new preLoader([
'//static.pokemonpets.com/images/attack_animations/absorb1.png',
'//static.pokemonpets.com/images/attack_animations/bleeding1.png',
'//static.pokemonpets.com/images/attack_animations/bug_attack1.png',
'//static.pokemonpets.com/images/attack_animations/bug_attack2.png',
'//static.pokemonpets.com/images/attack_animations/bug_boost1.png',
'//static.pokemonpets.com/images/attack_animations/burned1.png',
'//static.pokemonpets.com/images/attack_animations/change_weather_cloud.png',
'//static.pokemonpets.com/images/attack_animations/confused1.png',
'//static.pokemonpets.com/images/attack_animations/copy_all_enemy_moves.png',
'//static.pokemonpets.com/images/attack_animations/copy_last_move_enemy.png',
'//static.pokemonpets.com/images/attack_animations/cringed1.png',
'//static.pokemonpets.com/images/attack_animations/critical1.png',
'//static.pokemonpets.com/images/attack_animations/cure_all_status_problems.png',
'//static.pokemonpets.com/images/attack_animations/dark_attack1.png',
'//static.pokemonpets.com/images/attack_animations/dark_attack2.png',
'//static.pokemonpets.com/images/attack_animations/dark_attack3.png',
'//static.pokemonpets.com/images/attack_animations/dark_boost1.png',
'//static.pokemonpets.com/images/attack_animations/double_effect.png',
'//static.pokemonpets.com/images/attack_animations/dragon_attack1.png',
'//static.pokemonpets.com/images/attack_animations/dragon_attack2.png',
'//static.pokemonpets.com/images/attack_animations/dragon_attack3.png',
'//static.pokemonpets.com/images/attack_animations/dragon_attack4.png'
]);
});
Something like this?
I do not have time to make it pretty.
const pref = "https://static.pokemonpets.com/images/attack_animations/";
const defa = "https://imgplaceholder.com/20x20/000/fff/fa-image";
const images=['absorb1','bleeding1','bug_attack1','bug_attack2','bug_boost1','burned1','change_weather_cloud','confused1','copy_all_enemy_moves','copy_last_move_enemy','cringed1','critical1','cure_all_status_problems','dark_attack1','dark_attack2','dark_attack3','dark_boost1','double_effect','dragon_attack1','dragon_attack2','dragon_attack3','dragon_attack4'];
let cnt = 0;
function loadIt() {
if (cnt >= images.length) return;
$("#imagecontainer > img").eq(cnt).attr("src",pref+images[cnt]+".png"); // preload next
cnt++;
}
const $cont = $("#container");
const $icont = $("#imagecontainer");
// setting up test images
$.each(images,function(_,im) {
$cont.append('<img src="'+defa+'" id="'+im+'"/>'); // actual images
$icont.append('<img src="'+defa+'" data-id="'+im+'"/>'); // preload images
});
$("#imagecontainer > img").on("load",function() {
if (this.src.indexOf("imgplaceholder") ==-1) { // not for the default image
$("#"+$(this).attr("data-id")).attr("src",this.src); // copy preloaded
loadIt(); // run for next entry
}
})
loadIt(); // run
#imagecontainer img { height:20px }
#container img { height:100px }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="container"></div>
<hr/>
<div id="imagecontainer"></div>
I have a problem because I can not add php session to post in javascript, or do not know how to do it, here is my code which is a problem.
if (!this.movesAvailable()) {
var xmlhttp = null;
this.over = true; // Game over!
xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET", "http://obiektywnywaper.pl/2048/post.php?user=&wynik="+self.score, true);
xmlhttp.send();
alert(self.score);
}
I tried something like this, but it does not work
if (!this.movesAvailable()) {
var xmlhttp = null;
var user=<?echo $_SESSION['user'];?>;
this.over = true; // Game over!
xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET", "http://obiektywnywaper.pl/2048/post.php?user="+user+"&wynik="+self.score, true);
xmlhttp.send();
alert(self.score);
}
I add also the js file
function GameManager(size, InputManager, Actuator, ScoreManager) {
this.size = size; // Size of the grid
this.inputManager = new InputManager;
this.scoreManager = new ScoreManager;
this.actuator = new Actuator;
this.startTiles = 2;
this.inputManager.on("move", this.move.bind(this));
this.inputManager.on("restart", this.restart.bind(this));
this.inputManager.on("keepPlaying", this.keepPlaying.bind(this));
this.inputManager.on("showInfo", this.showInfo.bind(this));
this.inputManager.on("hideInfo", this.hideInfo.bind(this));
this.setup();
}
// Restart the game
GameManager.prototype.restart = function () {
this.actuator.continue();
this.setup();
};
// Keep playing after winning
GameManager.prototype.keepPlaying = function () {
this.keepPlaying = true;
this.actuator.continue();
};
GameManager.prototype.showInfo = function () {
this.actuator.showInfo();
};
GameManager.prototype.hideInfo = function () {
this.actuator.hideInfo();
};
GameManager.prototype.isGameTerminated = function () {
if (this.over || (this.won && !this.keepPlaying)) {
return true;
} else {
return false;
}
};
// Set up the game
GameManager.prototype.setup = function () {
this.grid = new Grid(this.size);
this.score = 0;
this.over = false;
this.won = false;
this.keepPlaying = false;
// Add the initial tiles
this.addStartTiles();
// Update the actuator
this.actuate();
};
// Set up the initial tiles to start the game with
GameManager.prototype.addStartTiles = function () {
for (var i = 0; i < this.startTiles; i++) {
this.addRandomTile();
}
};
// Adds a tile in a random position
GameManager.prototype.addRandomTile = function () {
if (this.grid.cellsAvailable()) {
var value = Math.random() < 0.9 ? 2 : 4;
var tile = new Tile(this.grid.randomAvailableCell(), value);
this.grid.insertTile(tile);
}
};
// Sends the updated grid to the actuator
GameManager.prototype.actuate = function () {
if (this.scoreManager.get() < this.score) {
this.scoreManager.set(this.score);
}
this.actuator.actuate(this.grid, {
score: this.score,
over: this.over,
won: this.won,
bestScore: this.scoreManager.get(),
terminated: this.isGameTerminated()
});
};
// Save all tile positions and remove merger info
GameManager.prototype.prepareTiles = function () {
this.grid.eachCell(function (x, y, tile) {
if (tile) {
tile.mergedFrom = null;
tile.savePosition();
}
});
};
// Move a tile and its representation
GameManager.prototype.moveTile = function (tile, cell) {
this.grid.cells[tile.x][tile.y] = null;
this.grid.cells[cell.x][cell.y] = tile;
tile.updatePosition(cell);
};
// Move tiles on the grid in the specified direction
GameManager.prototype.move = function (direction) {
// 0: up, 1: right, 2:down, 3: left
var self = this;
if (this.isGameTerminated()) return; // Don't do anything if the game's over
var cell, tile;
var vector = this.getVector(direction);
var traversals = this.buildTraversals(vector);
var moved = false;
// Save the current tile positions and remove merger information
this.prepareTiles();
// Traverse the grid in the right direction and move tiles
traversals.x.forEach(function (x) {
traversals.y.forEach(function (y) {
cell = { x: x, y: y };
tile = self.grid.cellContent(cell);
if (tile) {
var positions = self.findFarthestPosition(cell, vector);
var next = self.grid.cellContent(positions.next);
// Only one merger per row traversal?
if (next && next.value === tile.value && !next.mergedFrom) {
var merged = new Tile(positions.next, tile.value * 2);
merged.mergedFrom = [tile, next];
self.grid.insertTile(merged);
self.grid.removeTile(tile);
// Converge the two tiles' positions
tile.updatePosition(positions.next);
// Update the score
self.score += merged.value;
// The mighty 2048 tile
if (merged.value === 2048) self.won = true;
} else {
self.moveTile(tile, positions.farthest);
}
if (!self.positionsEqual(cell, tile)) {
moved = true; // The tile moved from its original cell!
}
}
});
});
if (moved) {
this.addRandomTile();
if (!this.movesAvailable()) {
var xmlhttp = null;
this.over = true; // Game over!
xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET", "http://obiektywnywaper.pl/2048/post.php?user=&wynik="+self.score, true);
xmlhttp.send();
alert(self.score);
}
this.actuate();
}
};
// Get the vector representing the chosen direction
GameManager.prototype.getVector = function (direction) {
// Vectors representing tile movement
var map = {
0: { x: 0, y: -1 }, // up
1: { x: 1, y: 0 }, // right
2: { x: 0, y: 1 }, // down
3: { x: -1, y: 0 } // left
};
return map[direction];
};
// Build a list of positions to traverse in the right order
GameManager.prototype.buildTraversals = function (vector) {
var traversals = { x: [], y: [] };
for (var pos = 0; pos < this.size; pos++) {
traversals.x.push(pos);
traversals.y.push(pos);
}
// Always traverse from the farthest cell in the chosen direction
if (vector.x === 1) traversals.x = traversals.x.reverse();
if (vector.y === 1) traversals.y = traversals.y.reverse();
return traversals;
};
GameManager.prototype.findFarthestPosition = function (cell, vector) {
var previous;
// Progress towards the vector direction until an obstacle is found
do {
previous = cell;
cell = { x: previous.x + vector.x, y: previous.y + vector.y };
} while (this.grid.withinBounds(cell) &&
this.grid.cellAvailable(cell));
return {
farthest: previous,
next: cell // Used to check if a merge is required
};
};
GameManager.prototype.movesAvailable = function () {
return this.grid.cellsAvailable() || this.tileMatchesAvailable();
};
// Check for available matches between tiles (more expensive check)
GameManager.prototype.tileMatchesAvailable = function () {
var self = this;
var tile;
for (var x = 0; x < this.size; x++) {
for (var y = 0; y < this.size; y++) {
tile = this.grid.cellContent({ x: x, y: y });
if (tile) {
for (var direction = 0; direction < 4; direction++) {
var vector = self.getVector(direction);
var cell = { x: x + vector.x, y: y + vector.y };
var other = self.grid.cellContent(cell);
if (other && other.value === tile.value) {
return true; // These two tiles can be merged
}
}
}
}
}
return false;
};
GameManager.prototype.positionsEqual = function (first, second) {
return first.x === second.x && first.y === second.y;
};
I don't know php, but classic asp works in similar fashion as far as I know. If you set the variable outside the scope of the function, and also include apostrophes, it should work.
var user="<?php echo $_SESSION['user'];?>"; //possibly declared outside of scope
Edited as per #developerwjk's comment about the php-syntax
So I am creating a piano through web audio and am having trouble implementing a volume control. Whenever a key is clicked, the volume control should dictate at what volume it is played through. I have used the code from html5rocks and modified it to my own uses. Basically instead of a VolumeSample array I have all of my soundclips loaded into a BUFFERS array. Whenever I try to manipulate the slider and change the gain of the clip, I get an 'cannot read property 'gain' of null. I am testing it through the debugger and everything runs fine up until the this.gainNode.gain.value = fraction * fraction; portion of my code. Just take a look at my code and hopefully you can see what I am missing. I'd like to call attention to the playSounds(buffer) method, which is where create and connect the gain node, and the method changeVolume at the bottom, which is where the actualy change in gain node happens:
var context;
var bufferLoader;
var BUFFERS = {};
var VolumeMain = {};
var LowPFilter = {FREQ_MUL: 7000,
QUAL_MUL: 30};
var BUFFERS_TO_LOAD = {
Down1: 'mp3/0C.mp3',
Down2: 'mp3/0CS.mp3',
Down3: 'mp3/0D.mp3',
Down4: 'mp3/0DS.mp3',
Down5: 'mp3/0E.mp3',
Down6: 'mp3/0F.mp3',
Down7: 'mp3/0FS.mp3',
Down8: 'mp3/0G.mp3',
Down9: 'mp3/0GS.mp3',
Down10: 'mp3/0A.mp3',
Down11: 'mp3/0AS.mp3',
Down12: 'mp3/0B.mp3',
Up13: 'mp3/1C.mp3',
Up14: 'mp3/1CS.mp3',
Up15: 'mp3/1D.mp3',
Up16: 'mp3/1DS.mp3',
Up17: 'mp3/1E.mp3',
Up18: 'mp3/1F.mp3',
Up19: 'mp3/1FS.mp3',
Up20: 'mp3/1G.mp3',
Up21: 'mp3/1GS.mp3',
Up22: 'mp3/1A.mp3',
Up23: 'mp3/1AS.mp3',
Up24: 'mp3/1B.mp3',
Beat1: 'mp3/beat1.mp3',
Beat2: 'mp3/beat2.mp3'
};
function loadBuffers() {
var names = [];
var paths = [];
for (var name in BUFFERS_TO_LOAD) {
var path = BUFFERS_TO_LOAD[name];
names.push(name);
paths.push(path);
}
bufferLoader = new BufferLoader(context, paths, function(bufferList) {
for (var i = 0; i < bufferList.length; i++) {
var buffer = bufferList[i];
var name = names[i];
BUFFERS[name] = buffer;
}
});
bufferLoader.load();
}
document.addEventListener('DOMContentLoaded', function() {
try {
// Fix up prefixing
window.AudioContext = window.AudioContext || window.webkitAudioContext;
context = new AudioContext();
}
catch(e) {
alert("Web Audio API is not supported in this browser");
}
loadBuffers();
});
function playSound(buffer) {
var source = context.createBufferSource();
source.buffer = buffer;
var filter1 = context.createBiquadFilter();
filter1.type = 0;
filter1.frequency.value = 5000;
var gainNode = context.createGain();
source.connect(gainNode);
source.connect(filter1);
gainNode.connect(context.destination);
filter1.connect(context.destination);
source.start(0);
}
//volume control
VolumeMain.gainNode = null;
VolumeMain.changeVolume = function(element) {
var volume = element.value;
var fraction = parseInt(element.value) / parseInt(element.max);
this.gainNode.gain.value = fraction * fraction; //error occurs here
};
// Start off by initializing a new context.
context = new (window.AudioContext || window.webkitAudioContext)();
if (!context.createGain)
context.createGain = context.createGainNode;
if (!context.createDelay)
context.createDelay = context.createDelayNode;
if (!context.createScriptProcessor)
context.createScriptProcessor = context.createJavaScriptNode;
// shim layer with setTimeout fallback
window.requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function( callback ){
window.setTimeout(callback, 1000 / 60);
};
})();
function BufferLoader(context, urlList, callback) {
this.context = context;
this.urlList = urlList;
this.onload = callback;
this.bufferList = new Array();
this.loadCount = 0;
}
BufferLoader.prototype.loadBuffer = function(url, index) {
// Load buffer asynchronously
var request = new XMLHttpRequest();
request.open("GET", url, true);
request.responseType = "arraybuffer";
var loader = this;
request.onload = function() {
// Asynchronously decode the audio file data in request.response
loader.context.decodeAudioData(
request.response,
function(buffer) {
if (!buffer) {
alert('error decoding file data: ' + url);
return;
}
loader.bufferList[index] = buffer;
if (++loader.loadCount == loader.urlList.length)
loader.onload(loader.bufferList);
},
function(error) {
console.error('decodeAudioData error', error);
}
);
}
request.onerror = function() {
alert('BufferLoader: XHR error');
}
request.send();
};
BufferLoader.prototype.load = function() {
for (var i = 0; i < this.urlList.length; ++i)
this.loadBuffer(this.urlList[i], i);
}
LowPFilter.changeFrequency = function(element) {
// Clamp the frequency between the minimum value (40 Hz) and half of the
// sampling rate.
var minValue = 40;
var maxValue = context.sampleRate / 2;
// Logarithm (base 2) to compute how many octaves fall in the range.
var numberOfOctaves = Math.log(maxValue / minValue) / Math.LN2;
// Compute a multiplier from 0 to 1 based on an exponential scale.
var multiplier = Math.pow(2, numberOfOctaves * (element.value - 1.0));
// Get back to the frequency value between min and max.
this.filter1.frequency.value = maxValue * multiplier;
};
LowPFilter.changeQuality = function(element) {
this.filter1.Q.value = element.value * this.QUAL_MUL;
};
LowPFilter.toggleFilter = function(element) {
this.source.disconnect(0);
this.filter1.disconnect(0);
// Check if we want to enable the filter.
if (element.checked) {
// Connect through the filter.
this.source.connect(this.filter1);
this.filter1.connect(context.destination);
} else {
// Otherwise, connect directly.
this.source.connect(context.destination);
}
};
function Beat1() {
this.isPlaying = false;
};
Beat1.prototype.play = function() {
this.gainNode = context.createGain();
this.source = context.createBufferSource();
this.source.buffer = BUFFERS.Beat1;
// Connect source to a gain node
this.source.connect(this.gainNode);
// Connect gain node to destination
this.gainNode.connect(context.destination);
// Start playback in a loop
this.source.loop = true;
this.source[this.source.start ? 'start' : 'noteOn'](0);
};
Beat1.prototype.changeVolume = function(element) {
var volume = element.value;
var fraction = parseInt(element.value) / parseInt(element.max);
// Let's use an x*x curve (x-squared) since simple linear (x) does not
// sound as good.
this.gainNode.gain.value = fraction * fraction;
};
Beat1.prototype.stop = function() {
this.source[this.source.stop ? 'stop' : 'noteOff'](0);
};
Beat1.prototype.toggle = function() {
this.isPlaying ? this.stop() : this.play();
this.isPlaying = !this.isPlaying;
};
function Beat2() {
this.isPlaying = false;
};
Beat2.prototype.play = function() {
this.gainNode = context.createGain();
this.source = context.createBufferSource();
this.source.buffer = BUFFERS.Beat2;
// Connect source to a gain node
this.source.connect(this.gainNode);
// Connect gain node to destination
this.gainNode.connect(context.destination);
// Start playback in a loop
this.source.loop = true;
this.source[this.source.start ? 'start' : 'noteOn'](0);
};
Beat2.prototype.changeVolume = function(element) {
var volume = element.value;
var fraction = parseInt(element.value) / parseInt(element.max);
// Let's use an x*x curve (x-squared) since simple linear (x) does not
// sound as good.
this.gainNode.gain.value = fraction * fraction;
};
Beat2.prototype.stop = function() {
this.source[this.source.stop ? 'stop' : 'noteOff'](0);
};
Beat2.prototype.toggle = function() {
this.isPlaying ? this.stop() : this.play();
this.isPlaying = !this.isPlaying;
};
This is where I create the piano and check which key was clicked and play the appropriate sound (seperate JS file):
// keyboard creation function
window.onload = function () {
// Keyboard Height
var keyboard_height = 120;
// Keyboard Width
var keyboard_width = 980;
// White Key Color
var white_color = 'white';
// Black Key Color
var black_color = 'black';
// Number of octaves
var octaves = 2;
// ID of containing Div
var div_id = 'keyboard';
//------------------------------------------------------------
var paper = Raphael(div_id, keyboard_width, keyboard_height);
// Define white key specs
var white_width = keyboard_width / 14;
// Define black key specs
var black_width = white_width/2;
var black_height = keyboard_height/1.6;
var repeat = 0;
var keyboard_keys = [];
//define white and black key names
var wkn = ['C', 'D', 'E', 'F', 'G', 'A', 'B'];
var bkn = ['Csharp', 'Dsharp', 'Fsharp', 'Gsharp', 'Asharp'];
//create octave groups
for (i=0;i<octaves;i++) {
//create white keys first
for (var w=0; w <= 6 ; w++) {
keyboard_keys[wkn[w]+i] = paper.rect(white_width*(repeat + w), 0, white_width, keyboard_height).attr("fill", white_color);
};
//set multiplier for black key placement
var bw_multiplier = 1.5;
//then black keys on top
for (var b=0; b <= 4 ; b++) {
keyboard_keys[bkn[b]+i] = paper.rect((white_width*repeat) + (black_width*bw_multiplier), 0, black_width, black_height).attr("fill", black_color);
bw_multiplier = (b == 1) ? bw_multiplier + 4 : bw_multiplier + 2;
};
repeat = repeat + 7;
}
for (var i in keyboard_keys) {
(function (st) {
st.node.onclick = function(event) {
var newColor = '#'+(0x1000000+(Math.random())*0xffffff).toString(16).substr(1,6);
st.animate({fill:newColor}, 100);
var testKey = st.paper.getElementByPoint(event.pageX, event.pageY);
var indexOfKey = testKey.id;
if (indexOfKey == 0)
{
playSound(BUFFERS.Down1);
}
else if (indexOfKey == 1)
{
playSound(BUFFERS.Down3);
}
else if (indexOfKey == 2)
{
playSound(BUFFERS.Down5);
}
else if (indexOfKey == 3)
{
playSound(BUFFERS.Down6);
}
else if (indexOfKey == 4)
{
playSound(BUFFERS.Down8);
}
else if (indexOfKey == 5)
{
playSound(BUFFERS.Down10);
}
else if (indexOfKey == 6)
{
playSound(BUFFERS.Down12);
}
else if (indexOfKey == 7)
{
playSound(BUFFERS.Down2);
}
else if (indexOfKey == 8)
{
playSound(BUFFERS.Down4);
}
else if (indexOfKey == 9)
{
playSound(BUFFERS.Down7);
}
else if (indexOfKey == 10)
{
playSound(BUFFERS.Down9);
}
else if (indexOfKey == 11)
{
playSound(BUFFERS.Down11);
}
else if (indexOfKey == 12)
{
playSound(BUFFERS.Up13);
}
else if (indexOfKey == 13)
{
playSound(BUFFERS.Up15);
}
else if (indexOfKey == 14)
{
playSound(BUFFERS.Up17);
}
else if (indexOfKey == 15)
{
playSound(BUFFERS.Up18);
}
else if (indexOfKey == 16)
{
playSound(BUFFERS.Up20);
}
else if (indexOfKey == 17)
{
playSound(BUFFERS.Up22);
}
else if (indexOfKey == 18)
{
playSound(BUFFERS.Up24);
}
else if (indexOfKey == 19)
{
playSound(BUFFERS.Up14);
}
else if (indexOfKey == 20)
{
playSound(BUFFERS.Up16)
}
else if (indexOfKey == 21)
{
playSound(BUFFERS.Up19);
}
else if (indexOfKey == 22)
{
playSound(BUFFERS.Up21);
}
else
{
playSound(BUFFERS.Up23);
}
};
})(keyboard_keys[i]);
}
};
Here's where I define the range slider for the volume control in my HTML (don't worry it is formatted correctly on in my code):
<div id="keyboard">
<script>
loadBuffers();
var beat1 = new Beat1();
var beat2 = new Beat2();
</script>
</div>
<div>Volume: <input type="range" min="0" max="100" value="100" oninput="VolumeMain.changeVolume(this);" /></div>
<div>Low Pass Filter on: <input type="checkbox" checked="false" oninput="LowPFilter.toggleFilter(this);" />
Frequency: <input type="range" min="0" max="1" step="0.01" value="1" oninput="LowPFilter.changeFrequency(this);" />
Quality: <input type="range" min="0" max="1" step="0.01" value="0" oninput="LowPFilter.changeQuality(this);" /></div>
<div>Beat 1: <input type="button" onclick="beat1.toggle();" value="Play/Pause"/>
Volume: <input type="range" min="0" max="100" value="100" onchange="beat1.changeVolume(this);"></div>
<div>Beat 2: <input type="button" onclick="beat2.toggle();" value="Play/Pause"/>
Volume: <input type="range" min="0" max="100" value="100" onchange="beat2.changeVolume(this);"></div>
</div>
This issue seems to be that the Volume control being used for the keyboard itself is somehow not being able to detect which sound buffer to use and modify. The code you supplied is good when you know exactly which source you are going to be adjusting the volume for, like in the case of my Beat1 and Beat 2 (those volume controls both work fine). I need the code to be able to modify the volume of any source in the buffer array. I'm using the Raphael package to create the keyboard, if that helps (it probably doesn't). I would call attention to the playSound(buffer) method and the VolumeMain.changeVolume functions. None of the LowPFilter methods work either but once we figure out how to adjust the volume for any given source that method's problem will also be fixed.
Edit (update). This removes the error and allows you to access the gainNode value
var gainNode = context.createGain();
function playSound(buffer) {
var source = context.createBufferSource();
source.buffer = buffer;
var filter1 = context.createBiquadFilter();
filter1.type = 0;
filter1.frequency.value = 5000;
source.connect(gainNode);
source.connect(filter1);
gainNode.connect(context.destination);
filter1.connect(context.destination);
source.start(audioContext.currentTime);
}
//volume control
VolumeMain.changeVolume = function(element) {
var volume = element.value;
var fraction = parseInt(element.value) / parseInt(element.max);
gainNode.gain.value = fraction * fraction;
console.log(gainNode.gain.value); // Console log of gain value when slider is moved
};
Previous reply
I don't really understand the problem but if you just want a piece of code as an example of setting up a gain node with an HTML range slider here's an example with an oscillator. You might want to do a little spike test and see if something like this works in your code with an oscillator and then try and apply it to your audio buffer code.
http://jsfiddle.net/vqb9dmrL/
<input id="gainSlider" type="range" min="0" max="1" step="0.05" value="0.5"/>
var audioContext = new webkitAudioContext();
var osc = audioContext.createOscillator();
osc.start(audioContext.cueentTime);
var gainChan1 = audioContext.createGain();
osc.connect(gainChan1);
gainChan1.connect(audioContext.destination);
var gainSlider = document.getElementById("gainSlider");
gainSlider.addEventListener('change', function() {
gainChan1.gain.value = this.value;
});
I'm trying to find the memory leak in the example here:
document.addEventListener('DOMContentLoaded', bindTabs);
function bindTabs(){
var lastTab,
tabs = document.querySelectorAll(".tabs li"),
i;
function selectTab(){
activateTab(this);
loadContent(this.attributes["data-load"].value);
}
function activateTab(tab){
if (lastTab) {
lastTab.classList.remove("active");
}
(lastTab = tab).classList.add("active");
}
[].forEach.call(tabs, function(el){
el.addEventListener('click', selectTab, false);
});
}
function loadContent(url){
getContent(url).then(function(data){
prepareContent(data);
prepareGallery();
})
}
var lastDiv;
function prepareContent(data){
var content = document.getElementById("content"),
div = document.createElement("div");
div.innerHTML = data;
if (lastDiv) {
content.removeChild(lastDiv);
}
content.appendChild(div);
lastDiv = div;
}
function prepareGallery(){
var width = 426,
forward = false,
position = 0,
elements = document.querySelectorAll(".gallery li"),
number = elements.length;
function checkDirection() {
if (position === 0) {
forward = true;
} else if (position === (number - 1)) {
forward = false;
}
}
function changeLeftProperty() {
var value = (-1 * width * position) + "px";
[].forEach.call(elements, function(el) {
el.style.left = value;
});
}
function advance(){
position = position + (forward ? 1 : -1);
}
function move(){
checkDirection();
advance();
changeLeftProperty();
}
setInterval(move, 2000);
}
function getContent(url){
var callbacks = [],
xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function(e){
if (xhr.status == 200) {
callbacks.forEach(function(cb){
cb(xhr.response);
})
}
}
xhr.send()
return {
then: function(fn){
callbacks.push(fn)
}
}
}
I profiled the page by taking heap snapshots and running the timeline but unfortunately I wasn't able to find anything.
Can you suggest me where could be the problem and what is the exact memory leak (if any)?
There is setInterval(move, 2000); in prepareGallery that will be called every click, and there is no clearInterval.
Here is the module i am working on:
var FeatureRotator = (function($,global) {
var self = {},
currentFeature = 0,
images = [],
imagePrefix = "/public/images/features/",
timer = null,
totalImages = 0,
initialFeature,
interval,
blendSpeed,
element = null,
img1 = null,
img2 = null;
function setVisibleImage(iid) {
$("#img1").attr('src',images[iid].src).css('opacity',1);
$("#img2").css('opacity',0);
$(".active").removeClass("active");
$("#f"+iid).addClass("active");
}
function setCurrentImage(id) {
currentFeature = id;
setVisibleImage(id);
}
function doHoverIn(position) {
if (currentFeature === position) {
self.pause();
} else {
setCurrentImage(global.parseInt(position, 10));
self.pause();
}
}
function doHoverOut(position) {
self.unpause();
}
self.init = function(options,callback) {
var i = 0,
tempImg = null;
interval = options.interval || 5000;
blendSpeed = options.blendSpeed || 500;
element = options.element;
initialFeature = options.initialFeature || 0;
img1 = $("<img/>").attr('id','img1');
img2 = $("<img/>").attr('id','img2').css('opacity','0').css('margin-top',-options.height);
$(element).append(img1).append(img2);
totalImages = $(".feature").size();
for (i = 0;i < totalImages; i++) {
tempImg = new global.Image();
tempImg.src = imagePrefix +"feature_" + i + ".png";
images.push(tempImg);
$("#f"+i).css('background-image',
'url("'+imagePrefix+"feature_"+i+"_thumb.png"+'")')
.hover(doHoverIn($(this).attr('position'))
, doHoverOut($(this).attr('position'))
).attr('position',i);
}
setVisibleImage(initialFeature);
if (options.autoStart) {
self.start();
}
if (callback !== null) {
callback();
}
};
function updateImage() {
var active = $("#img1").css('opacity') === 1 ? "#img1" : "#img2";
var nextFeature = (currentFeature === totalImages-1 ? 0 : currentFeature+1);
if (active === "#img1") {
$("#img2").attr('src',images[nextFeature].src);
$("#img2").fadeTo(blendSpeed, 1);
$("#img1").fadeTo(blendSpeed, 0);
} else {
$("#img1").attr('src',images[nextFeature].src);
$("#img1").fadeTo(blendSpeed, 1);
$("#img2").fadeTo(blendSpeed, 0);
}
$("#f"+currentFeature).removeClass("active");
$("#f"+nextFeature).addClass("active");
currentFeature = nextFeature;
}
self.start = function() {
currentFeature = initialFeature;
setVisibleImage(currentFeature);
timer = global.setInterval(function(){
updateImage();
}, interval);
};
self.pause = function() {
global.clearTimeout(timer);
};
self.unpause = function() {
timer = global.setInterval(function(){
updateImage();
}, interval);
};
return self;
}(this.jQuery, this));
And here is how it is used on the page:
<script type="text/javascript">
// ...
$(function() {
FeatureRotator.init({
interval:5000,
element:'#intro',
autoStart:true,
height:177,
blendSpeed:1000,
initialFeature:0
});
});
</script>
The problem is, when setVisibleImage is called from the init method, the value of iid is NaN. I've stepped through the debugger and verified that 'initialFeature' is 0 when the setVisibleImage function is called, but alas, the value doesn't make it over there.
Can anyone help me determine what the problem is? I've run the code through JSLint, and it came back clean.
UPDATE
Ok here is my updated code, which works now except the fading doesnt work, the image just flips to the next one and doesn't fade smoothly anymore:
var FeatureRotator = (function($,global) {
var self = {},
currentFeature = 0,
images = [],
imagePrefix = "/public/images/features/",
timer = null,
totalImages = 0,
initialFeature = 0,
interval,
blendSpeed;
function setVisibleImage(iid) {
$("#img1").attr('src',images[iid].src).css('opacity',1);
$("#img2").css('opacity',0);
$(".active").removeClass("active");
$("#f"+iid).addClass("active");
}
function setCurrentImage(id) {
currentFeature = id;
setVisibleImage(id);
}
function doHoverIn(obj) {
var position = global.parseInt(obj.target.attributes["position"].value,10);
if (currentFeature === position) {
self.pause();
} else {
setCurrentImage(global.parseInt(position, 10));
self.pause();
}
}
function doHoverOut() {
self.unpause();
}
self.init = function(options,callback) {
var i = 0,
tempImg = null,
element = null,
img1 = null,
img2 = null;
interval = options.interval || 5000;
blendSpeed = options.blendSpeed || 500;
element = options.element;
initialFeature = options.initialFeature || 0;
img1 = $("<img/>").attr('id','img1');
img2 = $("<img/>").attr('id','img2').css('opacity','0').css('margin-top',-options.height);
$(element).append(img1).append(img2);
totalImages = $(".feature").size();
for (i = 0;i < totalImages; i++) {
tempImg = new global.Image();
tempImg.src = imagePrefix +"feature_" + i + ".png";
images.push(tempImg);
$("#f"+i).css('background-image','url("'+imagePrefix+"feature_"+i+"_thumb.png"+'")')
.hover(doHoverIn, doHoverOut)
.attr('position',i);
}
setVisibleImage(initialFeature);
if (options.autoStart) {
self.start();
}
if (typeof callback === "function") {
callback();
}
};
function updateImage() {
var active = $("#img1").css('opacity') === 1 ? "#img1" : "#img2";
var nextFeature = (currentFeature === totalImages-1 ? 0 : currentFeature+1);
if (active === "#img1") {
$("#img2").attr('src',images[nextFeature].src);
$("#img2").fadeTo(blendSpeed, 1);
$("#img1").fadeTo(blendSpeed, 0);
} else {
$("#img1").attr('src',images[nextFeature].src);
$("#img1").fadeTo(blendSpeed, 1);
$("#img2").fadeTo(blendSpeed, 0);
}
$("#f"+currentFeature).removeClass("active");
$("#f"+nextFeature).addClass("active");
currentFeature = nextFeature;
}
self.start = function() {
currentFeature = initialFeature;
setVisibleImage(currentFeature);
timer = global.setInterval(function(){
updateImage();
}, interval);
};
self.stop = function() {
global.clearTimeout(timer);
};
self.pause = function() {
global.clearTimeout(timer);
};
self.unpause = function() {
timer = global.setInterval(function(){
updateImage();
}, interval);
};
return self;
}(this.jQuery, this));
Since you're getting NaN, I'm guessing it is actually taking place from this line:
.hover(doHoverIn($(this).attr('position'))
...which calls this:
setCurrentImage(global.parseInt(position, 10)); // note the parseInt()
...which calls this:
setVisibleImage(id);
So the position being passed to parseInt is coming from $(this).attr('position'), which is likely an value that can't be parsed into a Number, so you get NaN.
Check out the value of that attribute in first line of the block for the for statement.
for (i = 0;i < totalImages; i++) {
console.log( $(this).attr('position') ); // verify the value of position
// ...