循环样品
当前,玩家只能播放“一次性”样本,这意味着他们一次播放一次然后停下来。这意味着循环样品无法正常工作。在仪器数据中,有两个称为repeatOffset
和repeatLength
的字段。这些用于定义样品的循环点。对于没有循环的样本,repeatOffset
设置为0,并且repeatLength
设置为0或2,具体取决于使用哪种版本的MOD格式。我在Instrument
类中添加了一个新的成员变量,以检查样品是否被循环:isLooped
,并在Channel
类中使用该样本来确定样品是否应循环。
class Instrument {
constructor(modfile, index, sampleStart) {
const data = new Uint8Array(modfile, 20 + index * 30, 30);
const nameBytes = data.slice(0, 21).filter(a => !!a);
this.name = String.fromCodePoint(...nameBytes).trim();
this.length = 2 * (data[22] * 256 + data[23]);
this.finetune = data[24]; // Signed 4 bit integer
if (this.finetune > 7) this.finetune -= 16;
this.volume = data[25];
this.repeatOffset = 2 * (data[26] * 256 + data[27]);
this.repeatLength = 2 * (data[28] * 256 + data[29]);
this.bytes = new Int8Array(modfile, sampleStart, this.length);
this.isLooped = this.repeatOffset != 0 || this.repeatLength > 2;
}
}
nextOutput() {
if (!this.instrument || !this.period) return 0.0;
const sample = this.instrument.bytes[this.sampleIndex | 0];
this.sampleIndex += this.sampleSpeed;
if (this.instrument.isLooped) {
if (this.sampleIndex >= this.instrument.repeatOffset + this.instrument.repeatLength) {
this.sampleIndex = this.instrument.repeatOffset;
}
}
else if (this.sampleIndex >= this.instrument.length) {
return 0.0;
}
return sample / 256.0;
}
找到最重要的效果
mod格式定义了很多效果,但是其中一些效果并不经常在演示歌曲中使用。我本文系列的目标是能够播放三首歌曲:
- Musiklinjen:《现象中的谜》的歌
- Livin' Instanity:Arte by Sanity的第一首歌
- Elekfunk:Arte的第二首歌
找出这三首歌曲使用的效果,我重构Channel
类的effect
方法来记录每个未完成效果的效果ID。在阅读了有关不同效果ID的更多信息之后,我还发现0x0E
效应被用作扩展效果的前缀,因此也添加了。
// Declared at the top of the file, outside of the class definition
const unimplementedEffects = new Set();
const EXTENDED = 0x0e;
const SET_SPEED = 0x0f;
// Updated effect method of the Channel class
effect(raw) {
let id = raw >> 8;
let data = raw & 0xff;
if (id == EXTENDED) {
// Effect ID can be E0..EF
id = (id << 4) | (data >> 4);
data = data & 0x0f;
}
if (id == SET_SPEED) {
if (data >= 1 && data <= 31) {
this.worklet.setTicksPerRow(data);
}
else {
this.worklet.setBpm(data);
}
}
else {
if (!unimplementedEffects.has(id)) {
unimplementedEffects.add(id);
console.log('Unimplemented effect ' + id.toString(16));
}
}
}
使用三首歌曲运行播放器后,我将获得以下效果列表,并且可以将其定义添加到player-worklet.js文件:
- 效果0•Arpeggio
- 效果1 - 滑动
- 效果2 - 滑动
- 效果3 - 音调
- 效果4颤音
- 效果5 - 音调 +音量幻灯片
- 效果6颤音 +音量幻灯片
- 效果9样品偏移
- 效果A量幻灯片
- 效果câset卷
- 效果D模式断裂
- 效果e9
- 效果ED延迟注释
-
效果f - 设置速度(完成)
const ARPEGGIO = 0x00;
const SLIDE_UP = 0x01;
const SLIDE_DOWN = 0x02;
const TONE_PORTAMENTO = 0x03;
const VIBRATO = 0x04;
const TONE_PORTAMENTO_WITH_VOLUME_SLIDE = 0x05;
const VIBRATO_WITH_VOLUME_SLIDE = 0x06;
const SAMPLE_OFFSET = 0x09;
const VOLUME_SLIDE = 0x0A;
const SET_VOLUME = 0x0C;
const PATTERN_BREAK = 0x0D;
const EXTENDED = 0x0e;
const SET_SPEED = 0x0f;
const RETRIGGER_NOTE = 0xe9;
const DELAY_NOTE = 0xed;
在我发现的文档中,效果以非常技术性的方式描述,但缺少许多细节。同样,如果不听取行动,就不可能确切知道效果听起来像什么。幸运的是,我发现了Demoscene音乐家Alex Löfgren (Wasp)以YouTube playlist的形式找到了一个很大的信息来源,其中详细解释了许多效果,并在作用中显示。在整个效果的实施过程中,我正在聆听播放列表中的示例,以确保我正确地完成它们,并在Protracker中制作最少的测试歌曲以尝试效果。
在本文中,我实施了四个效果,其余的剩下的文章都留下了:
:- 效果câset卷
- 效果9样品偏移
- 效果D模式断裂
- 效果A量幻灯片
效果c set卷
到目前为止,所有样本都在全部播放,这是不正确的。首先,每个仪器都有一个volume
属性,该属性是0到64 之间的值(Amiga Paula芯片中音频的最大音量)。其次,集体积效应可用于覆盖仪器的体积。可以在触发音符时设置它,或在音符期间的任何时间更改。要实现此目的,我将volume
属性添加到Channel
类中,更新nextOutput
方法以使用频道卷代替仪器卷,然后在遇到效果时更新effect
方法以设置卷。
constructor(worklet) {
this.worklet = worklet;
this.instrument = null;
this.playing = false;
this.period = 0;
this.sampleSpeed = 0.0;
this.sampleIndex = 0;
this.volume = 64;
}
nextOutput() {
if (!this.instrument || !this.period) return 0.0;
const sample = this.instrument.bytes[this.sampleIndex | 0];
this.sampleIndex += this.sampleSpeed;
if (this.instrument.isLooped) {
if (this.sampleIndex >= this.instrument.repeatOffset + this.instrument.repeatLength) {
this.sampleIndex = this.instrument.repeatOffset;
}
}
else if (this.sampleIndex >= this.instrument.length) {
return 0.0;
}
return sample / 256.0 * this.volume / 64;
}
switch (id) {
case SET_VOLUME:
this.volume = data;
break;
case SET_SPEED:
if (data >= 1 && data <= 31) {
this.worklet.setTicksPerRow(data);
}
else {
this.worklet.setBpm(data);
}
break;
default:
if (!unimplementedEffects.has(id)) {
unimplementedEffects.add(id);
console.log('Unimplemented effect ' + id.toString(16));
}
break;
}
效果9样品偏移
这种效果使玩家将样本中的位置设置为特定值。该值是样品偏移的上部八位,因此必须将其乘以256才能获得实际偏移。该效果可用于在样本播放时向前和向后跳,或者在触发音符时从选定的位置开始。实现这一点非常简单,我只需要更新遇到效果时设置样本索引的effect
方法。
case SAMPLE_OFFSET:
this.sampleIndex = data * 256;
break;
效果D图案中断
这种效果使玩家在下一个模式下跳到特定行。跳跃发生在当前行完成之后,因此不会立即触发效果。效果的值是以二进制编码十进制表示法开始播放的行号。第一个十六进制数字是行号的“数十个”,第二个十六进制数字是“一个”。为了制作这项工作,我添加了我在构造函数中初始化为false
的PlayerWorklet
类的patternBreak
成员变量,然后添加了一个setPatternBreak
方法,当遇到效果时,我从effect
方法中调用了setPatternBreak
方法。
constructor() {
super();
this.port.onmessage = this.onmessage.bind(this);
this.mod = null;
this.channels = [ new Channel(this), new Channel(this), new Channel(this), new Channel(this) ];
this.patternBreak = false;
}
setPatternBreak(row) {
this.patternBreak = row;
}
nextRow() {
++this.rowIndex;
if (this.patternBreak !== false) {
this.rowIndex = this.patternBreak;
++this.position;
this.patternBreak = false;
}
else if (this.rowIndex == 64) {
this.rowIndex = 0;
++this.position;
}
// The rest of the nextRow method is unchanged
case PATTERN_BREAK:
const row = (data >> 4) * 10 + (data & 0x0f);
this.worklet.setPatternBreak(row);
break;
效果卷幻灯片
通过反复更改通道的音量,每行的数量多次。由于次数的数量与设定速度效果设置的ticksPerRow
值相同,因此在播放代码中已经有一个“ tick”的想法。不幸的是,播放器不会跟踪当前刻度,因此我需要在PlayerWorklet
类中添加tick
成员变量和nextTick
方法。与其跟踪何时移至下一行,我会跟踪何时移至下一个tick,并在tick到达ticksPerRow
值时致电nextRow
。完成所有操作后,我可以实现卷幻灯片效应,以及其他使用tick数的效果。
// Only the changed methods are shown here
onmessage(e) {
if (e.data.type === 'play') {
this.mod = e.data.mod;
this.sampleRate = e.data.sampleRate;
this.setBpm(125);
this.setTicksPerRow(6);
// Start at the last tick of the pattern "before the first pattern"
this.position = -1;
this.rowIndex = 63;
this.tick = 5;
this.ticksPerRow = 6;
// Immediately move to the first row of the first pattern
this.outputsUntilNextTick = 0;
}
}
setTicksPerRow(ticksPerRow) {
this.ticksPerRow = ticksPerRow;
}
setBpm(bpm) {
this.bpm = bpm;
this.outputsPerTick = this.sampleRate * 60 / this.bpm / 4 / 6;
}
nextTick() {
++this.tick;
if (this.tick == this.ticksPerRow) {
this.tick = 0;
this.nextRow();
}
for (let i = 0; i < 4; ++i) {
this.channels[i].performTick();
}
}
nextOutput() {
if (!this.mod) return 0.0;
if (this.outputsUntilNextTick <= 0) {
this.nextTick();
this.outputsUntilNextTick += this.outputsPerTick;
}
this.outputsUntilNextTick--;
const rawOutput = this.channels.reduce((acc, channel) => acc + channel.nextOutput(), 0.0);
return Math.tanh(rawOutput);
}
// Only the changed methods are shown here
nextOutput() {
if (!this.instrument || !this.period) return 0.0;
const sample = this.instrument.bytes[this.sampleIndex | 0];
this.sampleIndex += this.sampleSpeed;
if (this.instrument.isLooped) {
if (this.sampleIndex >= this.instrument.repeatOffset + this.instrument.repeatLength) {
this.sampleIndex = this.instrument.repeatOffset;
}
}
else if (this.sampleIndex >= this.instrument.length) {
return 0.0;
}
return sample / 256.0 * this.currentVolume / 64;
}
performTick() {
if (this.volumeSlide) {
this.currentVolume += this.volumeSlide;
if (this.currentVolume < 0) this.currentVolume = 0;
if (this.currentVolume > 64) this.currentVolume = 64;
}
}
effect(raw) {
this.volumeSlide = 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 VOLUME_SLIDE:
if (data & 0xf0) {
this.volumeSlide = data >> 4;
}
else if (data & 0x0f) {
this.volumeSlide = -(data & 0x0f);
}
break;
// The rest of the effect method is unchanged
结论
效果是mod文件音乐体验的非常重要的一部分,仅在这四个效果之后,参考mod听起来好多了。在接下来的几篇文章中,我将实现剩余的效果,然后我将继续实施更多的事情,这些内容将帮助我在演示中使用mod文件。
您可以在atornblad.github.io/js-mod-player中尝试此解决方案。
该代码的最新版本始终在the GitHub repository中可用。