Node.js: Mòduls d'àudio

De Wikijoan
Dreceres ràpides: navegació, cerca

Contingut

Mòduls d'àudio

Mòdul speaker

Output PCM audio data to the speakers

npm install speaker

Here's an example of piping stdin to the speaker, which should be 2 channel, 16-bit audio at 44,100 samples per second (a.k.a CD quality audio).

prova1.js

script prova1.js:

var Speaker = require('speaker');

// Create the Speaker instance
var speaker = new Speaker({
  channels: 2,          // 2 channels
  bitDepth: 16,         // 16-bit samples
  sampleRate: 44100     // 44,100 Hz sample rate
});

// PCM data from stdin gets piped into the speaker
process.stdin.pipe(speaker);

Per fer la prova puc enviar al meu script soroll blanc:

$ cat /dev/urandom | node prova1.js

prova2.js

I ara ho implementem dins d'un servidor web fet amb Node.js: una url http que produeixi soroll pels altaveus. script prova2.js:

var http = require("http");
var Speaker = require('speaker');

http.createServer(function(request, response) {
  response.writeHead(200, {"Content-Type": "text/html"});
  response.write("Hola Mundo");  
  // PCM data from stdin gets piped into the speaker  
  process.stdin.pipe(speaker);  
  response.end();  
}).listen(8888);      
      
// Create the Speaker instance
var speaker = new Speaker({   
  channels: 2,          // 2 channels
  bitDepth: 16,         // 16-bit samples
  sampleRate: 44100     // 44,100 Hz sample rate
});
$ cat /dev/urandom | node prova2.js

i en el browser:

i la prova també la puc fer en un altre dispositiu:

i evidentment, l'altaveu que s'activa és el del servidor, no el del mòbil.

Per tant, és interessant en el sentit de què puc activar el so en el servidor a distància via web, de forma remota, i això obre moltes possibilitats d'interacció. De totes maneres, queda clar que el so es produeix en el servidor.

prova3.js

I ara el que volem fer és afegir interacció. En la url tindrem un botó per arrencar el so, i un altre botó per apagar-lo. El que queda clar és que els botons s'esdevindran en la part del client, i per tant jo he d'enviar informació al servidor per tal de què actuï en conseqüència. Com sempre, no confondre la part del client i del servidor.

If you want to call something in Node from the client side, you can use socket.io 
to transfer events to Node. Then catch those events, and return the correct data back 
to the client. Refer to the socket.io "How to Use".

Basant-nos en l'exemple del chat server vist a:

podem mirar de disparar el so amb un botó.

De moment, però, no ho farem amb http:

script prova3.js:

var io     = require( 'socket.io' )
var Speaker = require('speaker');

server = null;
server = io.listen( 8000 );

// Create the Speaker instance
var speaker = new Speaker({
  channels: 2,          // 2 channels
  bitDepth: 16,         // 16-bit samples
  sampleRate: 44100     // 44,100 Hz sample rate
});
      
 
server.sockets.on( 'connection', function( socket ) {
        socket.on( 'msg', function( data ) {
        if (data=='on') {
            // PCM data from stdin gets piped into the speaker
            process.stdin.pipe(speaker);
        } else {
            process.stdin.unpipe(speaker);
        }
        socket.emit( 'incMsg', data );
        console.log (data);
    });

});

speaker.html:

<input type="submit" id="btn_soundon" value="Sound ON" /> 
<input type="submit" id="btn_soundoff" value="Sound OFF" /> 
<p><textarea id="txt_output" rows="1" cols="20" readonly="readonly"></textarea></p>
Nota: on/off a la caixa de text no s'escriu en el cantó del client, sinó que és el servidor el que ho escriu
<script src="http://localhost:8000/socket.io/socket.io.js"></script>
<script>
 document.addEventListener( 'DOMContentLoaded', function() {
    var socket = io.connect( 'http://localhost:8000' );
    var btn_soundon = document.getElementById('btn_soundon');
    var btn_soundoff = document.getElementById('btn_soundoff');
    var txt_output = document.getElementById('txt_output');
    
    btn_soundon.addEventListener( 'click', function() {
        sendMsg('on');
    });
    
    btn_soundoff.addEventListener( 'click', function() {
        sendMsg('off');
    });
  
    function sendMsg(txt_message) {
        socket.emit( 'msg', txt_message );
        //alert(txt_message);
    }
    
    socket.on( 'incMsg', function( data ) { 
            txt_output.value = data; 
    });
    
});
</script>

Arrenquem el script i li envien soroll blanc:

$ cat /dev/urandom | node prova3.js

I ara ja podem utilitzar els botons on/off per arrencar o apagar el so.

prova4.js

El que aconseguim amb aquesta versió és escoltar el soroll blanc que enviem als altaveus amb un botó en una interfície totalment web. Per tant, puc fer un control remot per encendre i apagar els altaveus.

script prova4.js:

var http = require("http");
var Speaker = require('speaker');
var io     = require( 'socket.io' )

server = null;
server = io.listen( 8000 );

http.createServer(function(request, response) {
  response.writeHead(200, {"Content-Type": "text/html"});

  response.write("<input type='submit' id='btn_soundon' value='Sound ON' />");
  response.write("<input type='submit' id='btn_soundoff' value='Sound OFF' />");
  response.write("<p><textarea id='txt_output' rows='1' cols='20' readonly='readonly'></textarea></p>");
  response.write("Nota: on/off a la caixa de text no s'escriu en el cantó del client, sinó que és el servidor el que ho escriu");
  response.write("<script src='http://localhost:8000/socket.io/socket.io.js'></script>");
  response.write("<script>");
  response.write(" document.addEventListener( 'DOMContentLoaded', function() {");
  response.write(" 	 var socket = io.connect( 'http://localhost:8000' ); ");
  response.write("    var btn_soundon = document.getElementById('btn_soundon'); ");
  response.write("    var btn_soundoff = document.getElementById('btn_soundoff'); ");
  response.write("    var txt_output = document.getElementById('txt_output'); ");
  response.write("    btn_soundon.addEventListener( 'click', function() { sendMsg('on'); });");
  response.write("    btn_soundoff.addEventListener( 'click', function() { sendMsg('off'); });");

  response.write("    function sendMsg(txt_message) {  socket.emit( 'msg', txt_message ); }");
  response.write("    socket.on( 'incMsg', function( data ) { txt_output.value = data; }); ");
  response.write("});");
  response.write("</script>");

  response.end();  
}).listen(8888);   

// Create the Speaker instance
var speaker = new Speaker({
  channels: 2,          // 2 channels
  bitDepth: 16,         // 16-bit samples
  sampleRate: 44100     // 44,100 Hz sample rate
});
      
 
server.sockets.on( 'connection', function( socket ) {
        socket.on( 'msg', function( data ) {
        if (data=='on') {
            // PCM data from stdin gets piped into the speaker
            process.stdin.pipe(speaker);
        } else {
            process.stdin.unpipe(speaker);
        }
        socket.emit( 'incMsg', data );
        console.log (data);
    });

});
$ cat /dev/urandom | node prova4.js

Mòdul baudio

Generate audio streams with functions

Important veure el video de substack:

Per instal.lar el mòdul:

$ npm install baudio

Segueixo els exemples del video de substack:

La utilitat play la tinc en el paquet sox:

$ sudo apt-get install sox

I ara ja puc tocar soroll blanc (randomness):

$ play -c1 -r 44k -t s8 /dev/urandom

play WARN alsa: can't encode 8-bit Signed Integer PCM

/dev/urandom:

  Encoding: Signed PCM    
  Channels: 1 @ 8-bit    
Samplerate: 44000Hz

Baixa freqüència:

$ play -c1 -r 8k -t s8 /dev/urandom

Es pot utilitzar aplay (alsa player) en comptes de sox:

$ cat /dev/urandom | aplay -f S16_LE -c1 -r 8000 -t raw

script 004.sh:

#!/bin/bash

#square wave in bash
(while true; do
	echo -e '\x00\xff'
done) | play -c1 -r 8k -t s8 -

script perl per tocar una cançó:

I així és com sona la cançó del SubStack amb un format de U8 i un samplerate de 8000, utilitzant aplay (ALSA) i play (sox):

$ perl -e'%c=map{[a..z]->[log($_)/log(2)*8-48],$_}map 2**($_/8),6*8..8*8; print((((chr)x($_/16)).("\x00"x(256-$_)/16))x128)for map{$c{$_},0}"jklkjjj0kkk0j0g0g00jklkjjjjkkjkl"=~/./g' | aplay -f U8 -c1 -r 8k -t raw

$ perl -e'%c=map{[a..z]->[log($_)/log(2)*8-48],$_}map 2**($_/8),6*8..8*8; print((((chr)x($_/16)).("\x00"x(256-$_)/16))x128)for map{$c{$_},0}"jklkjjj0kkk0j0g0g00jklkjjjjkkjkl"=~/./g' | play -c1 -r8k -t s8 -

I ara ja puc tocar Una Plata d'Enciam, tenint en compte que l'escala c d e f g a b més o menys es correspon als bytes:

C D E F G A B
l k j h g d b"

Una Plata d'Enciam:

$ perl -e'%c=map{[a..z]->[log($_)/log(2)*8-48],$_}map 2**($_/8),6*8..8*8; print((((chr)x($_/16)).("\x00"x(256-$_)/16))x128)for map{$c{$_},0}"jhggddgddghhhhhgj0jhggddgddghjhgj"=~/./g' | aplay -f U8 -c1 -r 4k -t raw

I ara en el video ja es comença a utilitzar baudio:

script 007.sh:

#!/bin/bash
node -e "
    var baudio = require('baudio');
    var b = baudio(function (t) {
        return Math.sin(2 * Math.PI * t * 400);
    });
    b.pipe(process.stdout);
" | aplay -f S16_LE -c1 -r 44100 -t raw
$ ./007.sh 
Playing raw data 'stdin' : Signed 16 bit Little Endian, Rate 44100 Hz, Mono

En aquest cas amb aquest petit exemple de node el que fem es produir un sinusoide i l'enviem al stdout. Els bytes que s'envien es vomiten sobre el aplay.

Aquest script no cal que l'executi amb bash, sinó que és senzillament un script javascript:

script 007_baudio.js:

var baudio = require('baudio');
var b = baudio(function (t) {
    return Math.sin(2 * Math.PI * t * 400);
});
b.play();
$ node 007_baudio.js

script 007_node_v2.js:

var baudio = require('baudio');
var tau = Matj
var n = 0;
var b = baudio(function (t) {
    var x = Math.sin(t * 262 + Math.sin(n));
    n += Math.sin(t);
    return x;
});
b.play();

script 008_baudio.js:

var baudio = require('baudio');

var tau = 2*Math.PI;
var b = baudio(function (t) {
	return sin(400)+sin(401);
	function sin(freq) {
		return Math.sin(tau * t* freq);
	}
});

b.play();

Ona quadrada. script 009_baudio.js:

var baudio = require('baudio');

var tau = 2*Math.PI;
var b = baudio(function (t) {
	return square(400);
	function square(freq) {
		return Math.sin(tau * t * freq)<0 ? -1 : 1 ;
	}
});

b.play();

Ona dent de serra. script 010_sawtooth:

var baudio = require('baudio');

var tau = 2*Math.PI;
var b = baudio(function (t) {
	return sawtooth(400);
	function sawtooth(freq) {
		return t % (1/freq) * freq * 2 -1;
	}
});

b.play();

I puc fer inteferències de les diferents funcions que he vist. Script 011_sine_square_sawtooth.js:

var baudio = require('baudio');

var tau = 2*Math.PI;
var b = baudio(function (t) {
	return sin(400) + square(405) + sawtooth(408);

	function sin(freq) {
		return Math.sin(tau * t* freq);
	}

	function square(freq) {
		return Math.sin(tau * t * freq)<0 ? -1 : 1 ;
	}

	function sawtooth(freq) {
		return t % (1/freq) * freq * 2 -1;
	}
});

b.play();

I cada vegada es poden construir patrons més complicats com es veu en el video.

Ara ja puc construir una melodia, tenint en compte que les notes es construeixen a partir de fraccions. Recordar que una corda té una freqüencia natural, i que si l'escurço a la meitat de longitud, la seva freqüència natural serà el doble, és a dir, una octava amunt. I que si l'escurço a diferents fraccions, obtindré diferents notes.

script 301_melodymix.js:

var baudio = require('baudio');
var tau = 2*Math.PI;

var melody = [0, -3/2, 1/5, 4/3, 7/5, 0, -5/6, 1/3]
	.map(function(x) { return Math.pow(2,x) })
	;

var b = baudio(function (t) {
	var m = melody[Math.floor(t*2%melody.length)];
	return ( sin(400*m) + square(800*m) + sawtooth(100*m) )/3;

	function sin(freq) {
		return Math.sin(tau * t* freq);
	}

	function square(freq) {
		return Math.sin(tau * t * freq)<0 ? -1 : 1 ;
	}

	function sawtooth(freq) {
		return t % (1/freq) * freq * 2 -1;
	}
});

b.play();

I ara el que puc fer és punctuate l'anterior exemple, i realment obtinc un so com els sons de 8 bits dels jocs dels 80.

script 302_melodymixpuctuated.js:

var baudio = require('baudio');
var tau = 2*Math.PI;

var melody = [0, -3/2, 1/5, 4/3, 7/5, 0, -5/6, 1/3]
	.map(function(x) { return Math.pow(2,x) })
	;

var b = baudio(function (t) {
	var m = melody[Math.floor(t*2%melody.length)];
	return ( sin(400*m) + square(800*m) + sawtooth(100*m) )/3 * (square(1)+square(4))/2;

	function sin(freq) {
		return Math.sin(tau * t* freq);
	}

	function square(freq) {
		return Math.sin(tau * t * freq)<0 ? -1 : 1 ;
	}

	function sawtooth(freq) {
		return t % (1/freq) * freq * 2 -1;
	}
});

b.play();

Les freqüències de les notes que ens interessen per tocar Una Plata d'Enciam, i les proporcions respecte el DO4:

DO		261.63	1
RE		293.66	1.122
MI		329.63	1.26
FA		349.23	1.335
SOL	392.00	1.498
LA		440.00	1.682
SI		493.88	1.888
DO		523.25	2

Ara ja puc tocar Una Plata d'Enciam. Script Una_Plata_DEnciam.js:

var baudio = require('baudio');
var tau = 2*Math.PI;

//var melody = [1, 1.122, 1.26, 1.335, 1.498, 1.682, 1.888, 2]
var melody = [1.26, 1.335, 1.498, 1.498, 1.682, 1.682, 1.498, 1.498, 1.682, 1.682, 1.5, 1.335, 1.335, 1.335, 1.335, 1.335, 1.5, 1.26, 1.26, 1.335, 1.5, 1.5, 1.682, 1.682, 1.5, 1.5, 1.682, 1.682, 1.5, 1.335, 1.335, 1.335, 1.5, 1.26, 1.26]
	.map(function(x) { return Math.pow(2,x) })
	;

var b = baudio(function (t) {
	var m = melody[Math.floor(t*2%melody.length)];
	return ( sin(400*m) + square(800*m) + sawtooth(100*m) )/3  * (square(2) + square(3))/2;

	function sin(freq) {
		return Math.sin(tau * t* freq);
	}

	function square(freq) {
		return Math.sin(tau * t * freq)<0 ? -1 : 1 ;
	}

	function sawtooth(freq) {
		return t % (1/freq) * freq * 2 -1;
	}
});

b.play();

Mòdul asynth

Live MIDI synthesizer

$ npm install asynth
...
asynth@0.1.1 ../node_modules/asynth
├── baudio@1.0.0 (inherits@1.0.0)
└── midi@0.6.0

Com veiem, asynth depèn de baudio, i també de midi. I és que amb node.js podem fer midi.

script 012_asynth.js:

var asynth = require('asynth');
var tau = 2*Math.PI;
var s = asynth(function (note,t) {
	//var freq = 440 * Math.pow(2, (note.key-49)/12);
	//return Math.sin(tau * t* freq);

    var freq = 440 * Math.pow(2, (note.key - 49) / 12);
    var x = Math.sin(2 * Math.PI * t * freq);
    var y = Math.sin(2 * Math.PI * t * (freq * 2));
    var z = Math.sin(2 * Math.PI * t * (freq / 2));
    return x * 0.6 + y * 0.2 + z * 0.2;

});

s.play();

Com diu la documentació, hem de conectar un sinte USB, i note.key és la nota que pitgem.

var s = asynth(function (note,t) {
...

La variable s és una instància de baudio.

A mi no em funciona, dóna un error quan executo el script.

Mòdul webaudio. browserify

És un mètode que es fa servir en el video per tal, de forma ràpida, poder veure els exemples en un browser.

El primer és instal.lar el mòdul webaudio

$ npm install webaudio
$ npm install -g browserify

browserify s'instal.la amb l'opció -g (TBD)

$ browserify -r webaudio:baudio 008_baudio.js > bundle.js

I es crea el fitxer bundle.js, que conté:

require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
var baudio = require('baudio');

var tau = 2*Math.PI;
var b = baudio(function (t) {
	return sin(400)+sin(401);
	function sin(freq) {
		return Math.sin(tau * t* freq);
	}
});

b.play();

},{"baudio":"dAEa01"}],"dAEa01":[function(require,module,exports){
module.exports = function (context, fn) {
	
    if (typeof context === 'function') {
      var Context = window.AudioContext || window.webkitAudioContext;
      if (!Context) throw new Error('AudioContext not supported');
      fn = context;
      context = new Context();
    }

    var self = context.createScriptProcessor(2048, 1, 1);

	  self.fn = fn
	
	  self.i = self.t = 0
	
  	window._SAMPLERATE = self.sampleRate = self.rate = context.sampleRate;

		self.duration = Infinity;
		
		self.recording = false;

	  self.onaudioprocess = function(e){
	    var output = e.outputBuffer.getChannelData(0)
			,   input = e.inputBuffer.getChannelData(0);
			self.tick(output, input);
	  };
	
		self.tick = function (output, input) { // a fill-a-buffer function

		    output = output || self._buffer;
		
		    input = input || []

		    for (var i = 0; i < output.length; i += 1) {

		        self.t = self.i / self.rate;
		
		        self.i += 1;

						output[i] = self.fn(self.t, self.i, input[i]);
						
		        if(self.i >= self.duration) {
			    		self.stop()
			    		break;
		        }

		    }

		    return output
		};
		
		self.stop = function(){
			self.disconnect();
			
		  self.playing = false;
		
		  if(self.recording) {
  		}
		};

		self.play = function(opts){

		  if (self.playing) return;

		  self.connect(self.context.destination);
		
		  self.playing = true;

		// this timeout seems to be the thing that keeps the audio from clipping #WTFALEART

		  setTimeout(function(){this.node.disconnect()}, 100000000000)

		  return
		};

		self.record = function(){

		};
		
		self.reset = function(){
			self.i = self.t = 0
		};
		
		self.createSample = function(duration){
			self.reset();
	    var buffer = self.context.createBuffer(1, duration, self.context.sampleRate)
			var blob = buffer.getChannelData(0);
	    self.tick(blob);
			return buffer
		};

    return self;
};

function mergeArgs (opts, args) {
    Object.keys(opts || {}).forEach(function (key) {
        args[key] = opts[key];
    });
    
    return Object.keys(args).reduce(function (acc, key) {
        var dash = key.length === 1 ? '-' : '--';
        return acc.concat(dash + key, args[key]);
    }, []);
}

function signed (n) {
    if (isNaN(n)) return 0;
    var b = Math.pow(2, 15);
    return n > 0
        ? Math.min(b - 1, Math.floor((b * n) - 1))
        : Math.max(-b, Math.ceil((b * n) - 1))
    ;
}

},{}],"baudio":[function(require,module,exports){
module.exports=require('dAEa01');
},{}]},{},[1])
;

I creem el fitxer index.html que sigui similar a:

<script src="bundle.js"></script>
<script>
//var webaudio = require('webaudio');
//var baudio = require('baudio');
</script>

és a dir, que inclogui bundle.js.

I aleshores ara executem en un navegador aquest fitxer:

$ xdg-open index.html

xdg-open senzillament el que fa és obrir el fitxer en un navegador web.

Al final no funciona (no sento res...) però és una cosa similar a això (TBD).

Si no funciona sempre es pot fer de la manera com s'ha vist més amunt: un servidor web.


creat per Joan Quintana Compte, desembre 2013

Eines de l'usuari
Espais de noms
Variants
Accions
Navegació
IES Jaume Balmes
Màquines recreatives
CNC
Informàtica musical
joanillo.org Planet
Eines