fix vibrato, implement 6xy
This commit is contained in:
parent
17123cc5f1
commit
b82a350011
1 changed files with 55 additions and 35 deletions
90
xm.js
90
xm.js
|
|
@ -51,9 +51,11 @@ var tempo = 4;
|
||||||
// Return 2-pole Butterworth lowpass filter coefficients for
|
// Return 2-pole Butterworth lowpass filter coefficients for
|
||||||
// center frequncy f_c (relative to sampling frequency)
|
// center frequncy f_c (relative to sampling frequency)
|
||||||
function FilterCoeffs(f_c) {
|
function FilterCoeffs(f_c) {
|
||||||
// if (f_c > 0.5) { // we can't lowpass above the nyquist frequency...
|
// if (f_c > 0.5) { // we can't lowpass above the nyquist frequency...
|
||||||
// return [1, 0, 0];
|
// return [1, 0, 0];
|
||||||
// }
|
// }
|
||||||
|
// what happens instead is the filter wraps around to an alias frequency,
|
||||||
|
// and that also works OK, though it isn't strictly right... FIXME
|
||||||
var wct = Math.sqrt(2) * Math.PI * f_c;
|
var wct = Math.sqrt(2) * Math.PI * f_c;
|
||||||
var e = Math.exp(-wct);
|
var e = Math.exp(-wct);
|
||||||
var c = e * Math.cos(wct);
|
var c = e * Math.cos(wct);
|
||||||
|
|
@ -75,11 +77,6 @@ function PeriodForNote(ch, note) {
|
||||||
return 1920 - note*16 - ch.inst.fine / 8.0;
|
return 1920 - note*16 - ch.inst.fine / 8.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function UpdateChannelNote(ch, note) {
|
|
||||||
ch.period = PeriodForNote(ch, note);
|
|
||||||
UpdateChannelPeriod(ch, ch.period);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
||||||
var patdisplay = [];
|
var patdisplay = [];
|
||||||
|
|
@ -183,7 +180,7 @@ function next_row() {
|
||||||
ch.vibratopos = 0;
|
ch.vibratopos = 0;
|
||||||
ch.env_vol = new EnvelopeFollower(inst.env_vol);
|
ch.env_vol = new EnvelopeFollower(inst.env_vol);
|
||||||
ch.env_pan = new EnvelopeFollower(inst.env_pan);
|
ch.env_pan = new EnvelopeFollower(inst.env_pan);
|
||||||
UpdateChannelNote(ch, note);
|
ch.period = PeriodForNote(ch, note);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var debug = document.getElementById("debug");
|
var debug = document.getElementById("debug");
|
||||||
|
|
@ -259,12 +256,14 @@ function next_tick() {
|
||||||
for (var j = 0; j < nchan; j++) {
|
for (var j = 0; j < nchan; j++) {
|
||||||
var ch = channelinfo[j];
|
var ch = channelinfo[j];
|
||||||
var inst = ch.inst;
|
var inst = ch.inst;
|
||||||
|
ch.periodoffset = 0;
|
||||||
if (ch.effectfn) {
|
if (ch.effectfn) {
|
||||||
ch.effectfn(ch);
|
ch.effectfn(ch);
|
||||||
}
|
}
|
||||||
if (inst === undefined) continue;
|
if (inst === undefined) continue;
|
||||||
ch.volE = ch.env_vol.Tick(ch.release, 64);
|
ch.volE = ch.env_vol.Tick(ch.release, 64);
|
||||||
ch.panE = ch.env_pan.Tick(ch.release, 32);
|
ch.panE = ch.env_pan.Tick(ch.release, 32);
|
||||||
|
UpdateChannelPeriod(ch, ch.period + ch.periodoffset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -272,6 +271,10 @@ function next_tick() {
|
||||||
// already to avoid clicks and pops when samples end.
|
// already to avoid clicks and pops when samples end.
|
||||||
function MixSilenceIntoBuf(ch, start, end, dataL, dataR) {
|
function MixSilenceIntoBuf(ch, start, end, dataL, dataR) {
|
||||||
var s = ch.filterstate[1];
|
var s = ch.filterstate[1];
|
||||||
|
if (isNaN(s)) {
|
||||||
|
console.log("NaN filterstate?", ch.filterstate, ch.filter);
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (var i = start; i < end; i++) {
|
for (var i = start; i < end; i++) {
|
||||||
if (Math.abs(s) < 1.526e-5) // == 1/65536.0
|
if (Math.abs(s) < 1.526e-5) // == 1/65536.0
|
||||||
break;
|
break;
|
||||||
|
|
@ -281,6 +284,10 @@ function MixSilenceIntoBuf(ch, start, end, dataL, dataR) {
|
||||||
}
|
}
|
||||||
ch.filterstate[1] = s;
|
ch.filterstate[1] = s;
|
||||||
ch.filterstate[2] = s;
|
ch.filterstate[2] = s;
|
||||||
|
if (isNaN(s)) {
|
||||||
|
console.log("NaN filterstate after adding silence?", ch.filterstate, ch.filter, i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -291,7 +298,7 @@ function MixChannelIntoBuf(ch, start, end, dataL, dataR) {
|
||||||
var looplen = 0;
|
var looplen = 0;
|
||||||
|
|
||||||
// nothing on this channel, just filter the last dc offset back down to zero
|
// nothing on this channel, just filter the last dc offset back down to zero
|
||||||
if (inst === undefined || ch.mute) {
|
if (inst == undefined || ch.mute) {
|
||||||
return MixSilenceIntoBuf(ch, start, end, dataL, dataR);
|
return MixSilenceIntoBuf(ch, start, end, dataL, dataR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -312,13 +319,31 @@ function MixChannelIntoBuf(ch, start, end, dataL, dataR) {
|
||||||
if (volR < 0) volR = 0;
|
if (volR < 0) volR = 0;
|
||||||
if (volR == 0 && volL == 0)
|
if (volR == 0 && volL == 0)
|
||||||
return;
|
return;
|
||||||
|
if (isNaN(volR) || isNaN(volL)) {
|
||||||
|
console.log("NaN volume!?", volL, volR, colE, panE, ch.vol);
|
||||||
|
return;
|
||||||
|
}
|
||||||
var k = ch.off;
|
var k = ch.off;
|
||||||
var dk = ch.doff;
|
var dk = ch.doff;
|
||||||
var Vrms = 0;
|
var Vrms = 0;
|
||||||
for (var i = start; i < end; i++) {
|
for (var i = start; i < end; i++) {
|
||||||
var s = 0;
|
if (k >= sample_end) { // TODO: implement pingpong looping
|
||||||
// if ((k|0) != (k-dk)|0)
|
if (loop) {
|
||||||
s = samp[k|0];
|
k %= looplen;
|
||||||
|
} else {
|
||||||
|
// kill sample
|
||||||
|
ch.inst = undefined;
|
||||||
|
// fill rest of buf with filtered dc offset using loop above
|
||||||
|
return Vrms + MixSilenceIntoBuf(ch, i+1, end, dataL, dataR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var s = samp[k|0];
|
||||||
|
// TODO: robustify, then remove these NaN checks from the inner loops
|
||||||
|
if (isNaN(s)) {
|
||||||
|
console.log("NaN sample idx", samp.length, k|0);
|
||||||
|
tempo = 10000;
|
||||||
|
break;
|
||||||
|
}
|
||||||
// we low-pass filter here since we are resampling some arbitrary
|
// we low-pass filter here since we are resampling some arbitrary
|
||||||
// frequency to f_smp; this is an anti-aliasing filter and is
|
// frequency to f_smp; this is an anti-aliasing filter and is
|
||||||
// implemented as an IIR butterworth filter (usually we'd use an FIR
|
// implemented as an IIR butterworth filter (usually we'd use an FIR
|
||||||
|
|
@ -326,6 +351,10 @@ function MixChannelIntoBuf(ch, start, end, dataL, dataR) {
|
||||||
// sounds fine)
|
// sounds fine)
|
||||||
var si = ch.filter[0] * (s + ch.filterstate[0]) +
|
var si = ch.filter[0] * (s + ch.filterstate[0]) +
|
||||||
ch.filter[1]*ch.filterstate[1] + ch.filter[2]*ch.filterstate[2];
|
ch.filter[1]*ch.filterstate[1] + ch.filter[2]*ch.filterstate[2];
|
||||||
|
if (isNaN(si)) {
|
||||||
|
console.log("NaN after filter sample idx", samp, k|0, ch.filter, ch.filterstate, s);
|
||||||
|
break;
|
||||||
|
}
|
||||||
ch.filterstate[2] = ch.filterstate[1];
|
ch.filterstate[2] = ch.filterstate[1];
|
||||||
ch.filterstate[1] = si; ch.filterstate[0] = s;
|
ch.filterstate[1] = si; ch.filterstate[0] = s;
|
||||||
// we also low-pass filter volume changes with a simple one-zero,
|
// we also low-pass filter volume changes with a simple one-zero,
|
||||||
|
|
@ -338,16 +367,6 @@ function MixChannelIntoBuf(ch, start, end, dataL, dataR) {
|
||||||
dataR[i] += ch.vR * si;
|
dataR[i] += ch.vR * si;
|
||||||
Vrms += (ch.vL + ch.vR) * si * si;
|
Vrms += (ch.vL + ch.vR) * si * si;
|
||||||
k += dk;
|
k += dk;
|
||||||
if (k >= sample_end) { // TODO: implement pingpong looping
|
|
||||||
if (loop) {
|
|
||||||
k -= looplen;
|
|
||||||
} else {
|
|
||||||
// kill sample
|
|
||||||
ch.inst = undefined;
|
|
||||||
// fill rest of buf with filtered dc offset using loop above
|
|
||||||
return Vrms + MixSilenceIntoBuf(ch, i+1, end, dataL, dataR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ch.off = k;
|
ch.off = k;
|
||||||
ch.doff = dk;
|
ch.doff = dk;
|
||||||
|
|
@ -455,10 +474,8 @@ function eff_t0_f(ch, data) { // set tempo
|
||||||
return;
|
return;
|
||||||
} else if(data < 0x20) {
|
} else if(data < 0x20) {
|
||||||
tempo = data;
|
tempo = data;
|
||||||
console.log("tempo", tempo);
|
|
||||||
} else {
|
} else {
|
||||||
bpm = data;
|
bpm = data;
|
||||||
console.log("bpm", bpm);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -471,9 +488,9 @@ var effects_t0 = [ // effect functions on tick 0
|
||||||
eff_t0_1,
|
eff_t0_1,
|
||||||
eff_t0_2,
|
eff_t0_2,
|
||||||
eff_t0_3,
|
eff_t0_3,
|
||||||
eff_t0_4,
|
eff_t0_4, // 4
|
||||||
eff_unimplemented_t0, // 5
|
eff_unimplemented_t0, // 5
|
||||||
eff_unimplemented_t0, // 6
|
eff_t0_a, // 6, same as A on first tick
|
||||||
eff_unimplemented_t0, // 7
|
eff_unimplemented_t0, // 7
|
||||||
eff_unimplemented_t0, // 8
|
eff_unimplemented_t0, // 8
|
||||||
eff_unimplemented_t0, // 9
|
eff_unimplemented_t0, // 9
|
||||||
|
|
@ -489,21 +506,21 @@ function eff_t1_0(ch) { // arpeggio
|
||||||
if (ch.effectdata != 0) {
|
if (ch.effectdata != 0) {
|
||||||
var arpeggio = [0, ch.effectdata>>4, ch.effectdata&15];
|
var arpeggio = [0, ch.effectdata>>4, ch.effectdata&15];
|
||||||
var note = ch.note + arpeggio[cur_tick % 3];
|
var note = ch.note + arpeggio[cur_tick % 3];
|
||||||
UpdateChannelNote(ch, note);
|
ch.period = PeriodForNote(ch, note);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function eff_t1_1(ch) { // pitch slide up
|
function eff_t1_1(ch) { // pitch slide up
|
||||||
if (ch.slideupspeed !== undefined) {
|
if (ch.slideupspeed !== undefined) {
|
||||||
|
// is this limited? it appears not
|
||||||
ch.period -= ch.slideupspeed;
|
ch.period -= ch.slideupspeed;
|
||||||
UpdateChannelPeriod(ch, ch.period);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function eff_t1_2(ch) { // pitch slide down
|
function eff_t1_2(ch) { // pitch slide down
|
||||||
if (ch.slidedownspeed !== undefined) {
|
if (ch.slidedownspeed !== undefined) {
|
||||||
ch.period += ch.slidedownspeed;
|
// 1728 is the period for C-1
|
||||||
UpdateChannelPeriod(ch, ch.period);
|
ch.period = Math.min(1728, ch.period + ch.slidedownspeed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -514,17 +531,20 @@ function eff_t1_3(ch) { // portamento
|
||||||
} else {
|
} else {
|
||||||
ch.period = Math.min(ch.periodtarget, ch.period + ch.portaspeed);
|
ch.period = Math.min(ch.periodtarget, ch.period + ch.portaspeed);
|
||||||
}
|
}
|
||||||
UpdateChannelPeriod(ch, ch.period);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function eff_t1_4(ch) { // vibrato
|
function eff_t1_4(ch) { // vibrato
|
||||||
ch.period += Math.sin(ch.vibratopos * Math.PI / 32) * ch.vibratodepth;
|
ch.periodoffset = Math.sin(ch.vibratopos * Math.PI / 32) * ch.vibratodepth;
|
||||||
UpdateChannelPeriod(ch, ch.period);
|
|
||||||
ch.vibratopos += ch.vibratospeed;
|
ch.vibratopos += ch.vibratospeed;
|
||||||
ch.vibratopos &= 63;
|
ch.vibratopos &= 63;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function eff_t1_6(ch) { // vibrato + volume slide
|
||||||
|
eff_t1_a(ch);
|
||||||
|
eff_t1_4(ch);
|
||||||
|
}
|
||||||
|
|
||||||
function eff_t1_a(ch) { // volume slide
|
function eff_t1_a(ch) { // volume slide
|
||||||
if (ch.volumeslide !== undefined) {
|
if (ch.volumeslide !== undefined) {
|
||||||
ch.vol = Math.max(0, Math.min(64, ch.vol + ch.volumeslide));
|
ch.vol = Math.max(0, Math.min(64, ch.vol + ch.volumeslide));
|
||||||
|
|
@ -539,7 +559,7 @@ var effects_t1 = [ // effect functions on tick 1+
|
||||||
eff_t1_3,
|
eff_t1_3,
|
||||||
eff_t1_4,
|
eff_t1_4,
|
||||||
eff_unimplemented, // 5
|
eff_unimplemented, // 5
|
||||||
eff_unimplemented, // 6
|
eff_t1_6,
|
||||||
eff_unimplemented, // 7
|
eff_unimplemented, // 7
|
||||||
eff_unimplemented, // 8
|
eff_unimplemented, // 8
|
||||||
eff_unimplemented, // 9
|
eff_unimplemented, // 9
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue