香草JavaScript mod播放器的进步很好。现在是时候进行某种与音高相关的效果 - 颤音,Portamento和Arpeggio。我还将添加这些效果的变体,使您在音调变化的同时,使您上下滑动音量。这些是我要实现的效果:
- 效果0 - arpeggio
- 效果1 - 滑动
- 效果2 - 滑动
- 效果3 - 音portamento
- 效果4颤音
- 效果5 - 音调 +音量幻灯片
- 效果6 vibrato +音量幻灯片
-
效果9样品偏移 of> (完成) em> -
效果a卷幻灯片(完成) -
效果câset卷(完成) -
效果D图案断裂(完成) - 效果e9 recrigger Note (在下一篇文章中)
- 效果eD delay note (在下一篇文章中)
-
效果f - 设置速度(完成)
效果1 slide
这种效果通过减少音符的周期来向上滑动音高。每个刻度,效果参数中指定的值都会缩短周期。为了管理这一点,我需要在Channel
类中添加currentPeriod
变量和一个periodDelta
变量,并在performTick
方法中更新该期间。计算样品速度的代码从play
方法移动到performTick
方法,以便更新速度每个刻度。该周期永远不会小于113 (b-3,mod格式中的最高音符)_ or超过856 _(c-1,最低音符)范围:
// Showing only the changed methods of the Channel class
play(note) {
if (note.instrument) {
this.instrument = this.worklet.mod.instruments[note.instrument - 1];
this.sampleIndex = 0;
this.volume = this.instrument.volume;
this.currentVolume = this.volume;
}
if (note.period) {
this.period = note.period - this.instrument.finetune;
this.currentPeriod = this.period;
}
this.effect(note.effect);
}
performTick() {
if (this.volumeSlide) {
this.currentVolume += this.volumeSlide;
if (this.currentVolume < 0) this.currentVolume = 0;
if (this.currentVolume > 64) this.currentVolume = 64;
}
if (this.periodDelta) {
this.currentPeriod += this.periodDelta;
if (this.currentPeriod < 113) this.currentPeriod = 113;
if (this.currentPeriod > 856) this.currentPeriod = 856;
}
const sampleRate = PAULA_FREQUENCY / this.currentPeriod;
this.sampleSpeed = sampleRate / this.worklet.sampleRate;
}
effect(raw) {
this.volumeSlide = 0;
this.periodDelta = 0;
if (!raw) return;
let id = raw >> 8;
let data = raw & 0xff;
if (id == EXTENDED) {
id = (id << 4) | (data >> 4);
data = data & 0x0f;
}
switch (id) {
case SET_VOLUME:
this.volume = data;
this.currentVolume = this.volume;
break;
case SLIDE_UP:
this.periodDelta = -data;
break;
// The rest of the effect method is unchanged
效果2滑动
既然滑动效应正在起作用,则易于实现滑动下降效果。它归结为仅更改periodDelta
变量的符号,其余的已经完成:
case SLIDE_DOWN:
this.periodDelta = data;
break;
效果3轴承轴承
音调portamento效应是幻灯片向上和滑动效应的组合,它利用播放的音符周期来确定幻灯片的方向。当音高达到目标音高时,效果就会停止。 Tone Portamento也是第一个实施“记住”其在行之间的参数的效果。这意味着您可以使用Note并设置慢速的Pertamento效果,例如301
,然后任何连续的Portamento效果(零值)都将使用相同的速度。为了实现这一点,我需要确保发挥带有Pertamento效果的音符不会立即设置currentPeriod
。相反,该周期仅由performTick
方法设置。要注意的一件事是,使用此命令的音符不会从一开始就不会启动样本,而是从启动Pertamento效应的位置继续。要实现此目的,我需要延迟设置sampleIndex
变量直到调用effect
方法。
constructor(worklet) {
// Skipping unchanged lines
this.portamentoSpeed = 0;
}
performTick() {
// Skipping unchanged volume handling
if (this.periodDelta) {
if (this.portamento) {
if (this.currentPeriod != this.period) {
// Which direction to slide?
const sign = Math.sign(this.period - this.currentPeriod);
// Don't slide past the target period
const distance = Math.abs(this.currentPeriod - this.period);
const diff = Math.min(distance, this.periodDelta);
this.currentPeriod += sign * diff;
}
}
else {
this.currentPeriod += this.periodDelta;
if (this.currentPeriod < 113) this.currentPeriod = 113;
if (this.currentPeriod > 856) this.currentPeriod = 856;
}
}
const sampleRate = PAULA_FREQUENCY / this.currentPeriod;
this.sampleSpeed = sampleRate / this.worklet.sampleRate;
}
play(note) {
// Keep track of whether the sample should start from the beginning
this.setSampleIndex = false;
if (note.instrument) {
this.instrument = this.worklet.mod.instruments[note.instrument - 1];
this.setSampleIndex = 0;
this.volume = this.instrument.volume;
this.currentVolume = this.volume;
}
// Keep track of whether period should change immediately
this.setCurrentPeriod = false;
if (note.period) {
this.period = note.period - this.instrument.finetune;
this.setCurrentPeriod = true;
}
// setSampleIndex and setCurrentPeriod could change back to false in the effect method
this.effect(note.effect);
if (this.setCurrentPeriod) {
this.currentPeriod = this.period;
}
if (this.setSampleIndex !== false) {
this.sampleIndex = this.setSampleIndex;
}
}
effect(raw) {
this.volumeSlide = 0;
this.periodDelta = 0;
this.portamento = false;
if (!raw) return;
let id = raw >> 8;
let data = raw & 0xff;
if (id == EXTENDED) {
id = (id << 4) | (data >> 4);
data = data & 0x0f;
}
switch (id) {
case SET_VOLUME:
this.volume = data;
this.currentVolume = this.volume;
break;
case SLIDE_UP:
this.periodDelta = -data;
break;
case SLIDE_DOWN:
this.periodDelta = data;
break;
case TONE_PORTAMENTO:
this.portamento = true;
if (data) this.portamentoSpeed = data;
this.periodDelta = this.portamentoSpeed;
this.setCurrentPeriod = false;
this.setSampleIndex = false;
break;
效果4颤音
颤音会影响每个刻度的音符周期。通过在当前时期添加颤音值来计算生效期。参数的上部4位是颤音速度,下部4位是颤音深度。实际上,找到有关此效果正确实现的任何好的文档实际上非常棘手,但是我找到了一个解释,可以让我开始使用非常旧的多媒体Wiki。它似乎使用64步正弦波来改变周期,而速度是每刻度正弦波中向前的步骤数。颤音效应的深度和速度在每个通道的音符之间单独记住。
constructor(worklet) {
// Added three variables
this.vibratoDepth = 0;
this.vibratoSpeed = 0;
this.vibratoIndex = 0;
}
performTick() {
if (this.volumeSlide) {
this.currentVolume += this.volumeSlide;
if (this.currentVolume < 0) this.currentVolume = 0;
if (this.currentVolume > 64) this.currentVolume = 64;
}
if (this.vibrato) {
this.vibratoIndex = (this.vibratoIndex + this.vibratoSpeed) % 64;
this.currentPeriod = this.period + Math.sin(this.vibratoIndex / 64 * Math.PI * 2) * this.vibratoDepth;
}
else if (this.periodDelta) {
if (this.portamento) {
if (this.currentPeriod != this.period) {
const sign = Math.sign(this.period - this.currentPeriod);
const distance = Math.abs(this.currentPeriod - this.period);
const diff = Math.min(distance, this.periodDelta);
this.currentPeriod += sign * diff;
}
}
else {
this.currentPeriod += this.periodDelta;
}
}
if (this.currentPeriod < 113) this.currentPeriod = 113;
if (this.currentPeriod > 856) this.currentPeriod = 856;
const sampleRate = PAULA_FREQUENCY / this.currentPeriod;
this.sampleSpeed = sampleRate / this.worklet.sampleRate;
}
play(note) {
this.setSampleIndex = false;
if (note.instrument) {
this.instrument = this.worklet.mod.instruments[note.instrument - 1];
this.setSampleIndex = 0;
this.volume = this.instrument.volume;
this.currentVolume = this.volume;
}
this.setCurrentPeriod = false;
if (note.period) {
this.period = note.period - this.instrument.finetune;
this.setCurrentPeriod = true;
}
this.effect(note.effect);
if (this.setCurrentPeriod) {
this.currentPeriod = this.period;
}
if (this.setSampleIndex !== false) {
this.sampleIndex = this.setSampleIndex;
}
}
effect(raw) {
this.volumeSlide = 0;
this.periodDelta = 0;
this.portamento = false;
this.vibrato = false;
if (!raw) return;
let id = raw >> 8;
let data = raw & 0xff;
if (id == EXTENDED) {
id = (id << 4) | (data >> 4);
data = data & 0x0f;
}
switch (id) {
// Added one case
case VIBRATO:
const speed = data >> 4;
const depth = data & 0x0f;
if (speed) this.vibratoSpeed = speed;
if (depth) this.vibratoDepth = depth;
this.vibrato = true;
break;
效果5 - 音portamento +音量幻灯片
这种效果是效果3和a的组合,已经实现了。 effect
方法所需的唯一更改是将新案例添加到Switch语句中,并结合两个现有效果:
case TONE_PORTAMENTO_WITH_VOLUME_SLIDE:
this.portamento = true;
this.setCurrentPeriod = false;
this.setSampleIndex = false;
this.periodDelta = this.portamentoSpeed;
if (data & 0xf0) {
this.volumeSlide = data >> 4;
}
else if (data & 0x0f) {
this.volumeSlide = -(data & 0x0f);
}
break;
效果6 vibrato +音量滑梯
就像效果5一样,效果3和a,效果6的组合是效果4和A的组合。这意味着一切已经到位,可以实现这种效果。唯一需要做的是在频道类的效果方法中添加一个案例。
case VIBRATO_WITH_VOLUME_SLIDE:
this.vibrato = true;
if (data & 0xf0) {
this.volumeSlide = data >> 4;
}
else if (data & 0x0f) {
this.volumeSlide = -(data & 0x0f);
}
break;
效果0
这种效果用于快速连续演奏三个音符来演奏和弦。第一个注释是播放的音符,第二个和第三个音符是通过将arpeggio
值添加到注释中来播放的。 arpeggio
值由上4位和参数的下部4位设置。每个tick,该期间都更改为和弦中的下一个音符。
constructor(worklet) {
// Adding one line:
this.arpeggio = false;
}
performTick() {
if (this.volumeSlide && this.worklet.tick > 0) {
// Unchanged
}
if (this.vibrato) {
// Unchanged
}
else if (this.periodDelta) {
// Unchanged
}
else if (this.arpeggio) {
const index = this.worklet.tick % this.arpeggio.length;
const halfNotes = this.arpeggio[index];
this.currentPeriod = this.period / Math.pow(2, halfNotes / 12);
}
if (this.currentPeriod < 113) this.currentPeriod = 113;
if (this.currentPeriod > 856) this.currentPeriod = 856;
const sampleRate = PAULA_FREQUENCY / this.currentPeriod;
this.sampleSpeed = sampleRate / this.worklet.sampleRate;
}
effect(raw) {
// Add one line at the beginning
this.arpeggio = false;
// In the switch statement:
switch (id) {
case ARPEGGIO:
this.arpeggio = [0, data >> 4, data & 0x0f];
break;
一些错误修复
现在,我选择的三首歌几乎应该完美地播放。但是,在听它们时,我意识到需要修复一些错误。第一个是,即使将仪器设置为零,便会在样本开始时从样本的开头重新启动。我通过听现象的歌曲来发现这一点。在图案30 中(歌曲位置45)频道3听起来几乎是沉默的。修复程序是将设置样本索引的代码移至另一个位置:
play(note) {
if (note.instrument) {
this.instrument = this.worklet.mod.instruments[note.instrument - 1];
this.volume = this.instrument.volume;
this.currentVolume = this.volume;
}
this.setSampleIndex = false;
this.setCurrentPeriod = false;
if (note.period) {
this.period = note.period - this.instrument.finetune;
this.setCurrentPeriod = true;
this.setSampleIndex = 0;
}
this.effect(note.effect);
if (this.setCurrentPeriod) {
this.currentPeriod = this.period;
}
if (this.setSampleIndex !== false) {
this.sampleIndex = this.setSampleIndex;
}
}
另一个错误是,音量幻灯片效果的运行太快了。在聆听Livin的精神错乱的开放时,在模式2和4 中(歌曲位置0和1) 降低了零。经过一番谷歌搜索后,我发现不应在每一行的第一滴度上运行音量幻灯片效果。因此,对于每排6个刻度的正常速度设置,音量幻灯片效果不应在tick 0上运行,而应在1、2、3、4和5上运行。
performTick() {
if (this.volumeSlide && this.worklet.tick > 0) {
this.currentVolume += this.volumeSlide;
if (this.currentVolume < 0) this.currentVolume = 0;
if (this.currentVolume > 64) this.currentVolume = 64;
}
结论
目前达到目标的仅有的两个效果是Retigger Note和Delay Note,我将在下一篇文章中实现这些效果。这些效果在《谜》中没有使用,因此现在实际上扮演了正确的效果。到目前为止,这是一个有趣的挑战,我对Mod格式的内部运作有了很多了解。
您可以在atornblad.github.io/js-mod-player尝试此解决方案。
该代码的最新版本始终在the GitHub repository中可用。