added per-channel low-pass filter
sounds better, but still clicks and pops a lot. i'll need a volume envelope lowpass filter, i guess.
This commit is contained in:
parent
15a848d797
commit
7de75f6977
2 changed files with 74 additions and 23 deletions
22
LICENSE
Normal file
22
LICENSE
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015 Andy Sloane <andy@a1k0n.net>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
||||||
69
xm.js
69
xm.js
|
|
@ -56,6 +56,28 @@ function GetEnvelope(env, ticks) {
|
||||||
return y0;
|
return y0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return 2-pole Butterworth lowpass filter coefficients for
|
||||||
|
// center frequncy f_c (relative to sampling frequency)
|
||||||
|
function FilterCoeffs(f_c) {
|
||||||
|
// if (f_c > 0.5) { // we can't lowpass above the nyquist frequency...
|
||||||
|
// return [1, 0, 0];
|
||||||
|
// }
|
||||||
|
var wct = Math.sqrt(2) * Math.PI * f_c;
|
||||||
|
var e = Math.exp(-wct);
|
||||||
|
var c = e * Math.cos(wct);
|
||||||
|
var s = e * Math.sin(wct);
|
||||||
|
var gain = (1 - 2*c + c*c + s*s) / 2;
|
||||||
|
return [gain, 2*c, -c*c - s*s];
|
||||||
|
}
|
||||||
|
|
||||||
|
function Filter(x, coef, state) {
|
||||||
|
var y = coef[0] * (x + state[0]) + coef[1]*state[1] + coef[2]*state[2];
|
||||||
|
state[0] = x;
|
||||||
|
state[2] = state[1];
|
||||||
|
state[1] = y;
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
var cur_songpos = -1, cur_pat = -1, cur_row = 64, cur_ticksamp = 0;
|
var cur_songpos = -1, cur_pat = -1, cur_row = 64, cur_ticksamp = 0;
|
||||||
var cur_tick = 6;
|
var cur_tick = 6;
|
||||||
function next_row(f_smp) {
|
function next_row(f_smp) {
|
||||||
|
|
@ -107,6 +129,7 @@ function next_row(f_smp) {
|
||||||
channelinfo[i].off = 0;
|
channelinfo[i].off = 0;
|
||||||
channelinfo[i].release = 0;
|
channelinfo[i].release = 0;
|
||||||
channelinfo[i].envtick = 0;
|
channelinfo[i].envtick = 0;
|
||||||
|
channelinfo[i].filter = FilterCoeffs(channelinfo[i].doff / 2);
|
||||||
// console.log("channel", i, r[i][0], channelinfo[i]);
|
// console.log("channel", i, r[i][0], channelinfo[i]);
|
||||||
// if there's an instrument and a note, set the volume
|
// if there's an instrument and a note, set the volume
|
||||||
if (r[i][0] != -1) {
|
if (r[i][0] != -1) {
|
||||||
|
|
@ -140,21 +163,22 @@ function next_tick(f_smp) {
|
||||||
next_row(f_smp);
|
next_row(f_smp);
|
||||||
}
|
}
|
||||||
for (var j = 0; j < nchan; j++) {
|
for (var j = 0; j < nchan; j++) {
|
||||||
var inst = channelinfo[j].inst;
|
var ch = channelinfo[j];
|
||||||
|
var inst = ch.inst;
|
||||||
if (inst === undefined) continue;
|
if (inst === undefined) continue;
|
||||||
if (inst.env_vol !== undefined) {
|
if (inst.env_vol !== undefined) {
|
||||||
channelinfo[j].env_vol = GetEnvelope(inst.env_vol, channelinfo[j].envtick);
|
ch.env_vol = GetEnvelope(inst.env_vol, ch.envtick);
|
||||||
if (inst.env_vol_type & 2) { // sustain loop?
|
if (inst.env_vol_type & 2) { // sustain loop?
|
||||||
// if we're sustaining a note, leave env_vol alone
|
// if we're sustaining a note, leave env_vol alone
|
||||||
if (channelinfo[j].release == 0) {
|
if (ch.release == 0) {
|
||||||
if (channelinfo[j].envtick >= inst.env_vol[inst.env_vol_sustain*2]) {
|
if (ch.envtick >= inst.env_vol[inst.env_vol_sustain*2]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
channelinfo[j].envtick++;
|
ch.envtick++;
|
||||||
} else {
|
} else {
|
||||||
channelinfo[j].env_vol = 64;
|
ch.env_vol = 64;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -176,52 +200,55 @@ function audio_cb(e) {
|
||||||
}
|
}
|
||||||
var tickduration = Math.min(buflen, ticklen - cur_ticksamp);
|
var tickduration = Math.min(buflen, ticklen - cur_ticksamp);
|
||||||
for (var j = 0; j < nchan; j++) {
|
for (var j = 0; j < nchan; j++) {
|
||||||
var inst = channelinfo[j].inst;
|
var ch = channelinfo[j];
|
||||||
|
var inst = ch.inst;
|
||||||
if (inst === undefined) continue;
|
if (inst === undefined) continue;
|
||||||
var samp = inst.sampledata;
|
var samp = inst.sampledata;
|
||||||
var sample_end = inst.len;
|
var sample_end = inst.len;
|
||||||
var looplen = 0;
|
var looplen = 0;
|
||||||
var loop = false;
|
var loop = false;
|
||||||
if (inst.type & 3) {
|
if ((inst.type & 3) == 1) { // todo: support pingpong
|
||||||
loop = true;
|
loop = true;
|
||||||
looplen = inst.looplen;
|
looplen = inst.looplen;
|
||||||
sample_end = looplen + inst.loop;
|
sample_end = looplen + inst.loop;
|
||||||
}
|
}
|
||||||
var samplen = inst.len;
|
var samplen = inst.len;
|
||||||
var env_vol = channelinfo[j].env_vol / 64.0;
|
var env_vol = ch.env_vol / 64.0;
|
||||||
var volL = env_vol * channelinfo[j].volL / 64.0;
|
var volL = env_vol * ch.volL / 64.0;
|
||||||
var volR = env_vol * channelinfo[j].volR / 64.0;
|
var volR = env_vol * ch.volR / 64.0;
|
||||||
if (volL < 0) volL = 0;
|
if (volL < 0) volL = 0;
|
||||||
if (volR < 0) volR = 0;
|
if (volR < 0) volR = 0;
|
||||||
if (volR == 0 && volL == 0)
|
if (volR == 0 && volL == 0)
|
||||||
continue;
|
continue;
|
||||||
var k = channelinfo[j].off;
|
var k = ch.off;
|
||||||
var dk = channelinfo[j].doff;
|
var dk = ch.doff;
|
||||||
// console.log(j, offset, channelinfo[j]);
|
// console.log(j, offset, ch);
|
||||||
for (var i = offset; i < offset+tickduration; i++) {
|
for (var i = offset; i < offset+tickduration; i++) {
|
||||||
var kk = k|0;
|
var kk = k|0;
|
||||||
var si = samp[kk];
|
var si = samp[kk];
|
||||||
|
/*
|
||||||
// bilinear filtering
|
// bilinear filtering
|
||||||
var sj = si;
|
var sj = si;
|
||||||
if (kk < sample_end-1)
|
if (kk < sample_end-1)
|
||||||
sj = samp[kk+1];
|
sj = samp[kk+1];
|
||||||
var t = k - kk;
|
var t = k - kk;
|
||||||
si = t * sj + (1 - t) * si;
|
si = t * sj + (1 - t) * si;
|
||||||
dataL[i] += volL * si;
|
*/
|
||||||
dataR[i] += volR * si;
|
dataL[i] += Filter(volL * si, ch.filter, ch.filterstate[0]);
|
||||||
|
dataR[i] += Filter(volR * si, ch.filter, ch.filterstate[1]);
|
||||||
k += dk;
|
k += dk;
|
||||||
if (k >= sample_end) { // TODO: implement pingpong looping
|
if (k >= sample_end) { // TODO: implement pingpong looping
|
||||||
if (loop) {
|
if (loop) {
|
||||||
k -= looplen;
|
k -= looplen;
|
||||||
} else {
|
} else {
|
||||||
// kill sample
|
// kill sample
|
||||||
channelinfo[j].inst = undefined;
|
ch.inst = undefined;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
channelinfo[j].off = k;
|
ch.off = k;
|
||||||
channelinfo[j].doff = dk;
|
ch.doff = dk;
|
||||||
}
|
}
|
||||||
offset += tickduration;
|
offset += tickduration;
|
||||||
cur_ticksamp += tickduration;
|
cur_ticksamp += tickduration;
|
||||||
|
|
@ -269,7 +296,9 @@ function playXM(arrayBuf) {
|
||||||
tempo = dv.getUint16(0x4c, true);
|
tempo = dv.getUint16(0x4c, true);
|
||||||
bpm = dv.getUint16(0x4e, true);
|
bpm = dv.getUint16(0x4e, true);
|
||||||
for (var i = 0; i < nchan; i++) {
|
for (var i = 0; i < nchan; i++) {
|
||||||
channelinfo.push({})
|
channelinfo.push({
|
||||||
|
filterstate: [new Float32Array(3), new Float32Array(3)]
|
||||||
|
})
|
||||||
}
|
}
|
||||||
console.log("header len " + hlen);
|
console.log("header len " + hlen);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue