/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ /* Adapted from ArcTracker https://github.com/richardjwild/arctracker/ */ #include "common/array.h" #include "common/memstream.h" #include "common/stream.h" #include "common/util.h" #include "audio/audiostream.h" namespace Audio { namespace Modules { static uint32 alignToWord(uint32 x) { return (x + 1) & ~1U; } static uint32 alignToDword(uint32 x) { return (x + 3) & ~3U; } static uint32 readLE32FromBuf(const byte *p) { return (uint32)p[0] | ((uint32)p[1] << 8) | ((uint32)p[2] << 16) | ((uint32)p[3] << 24); } static int8 vidcToSigned8(byte enc) { static bool sInit = false; static int8 sTable[256]; if (!sInit) { const int BIAS = 0x84; const int SIGN_BIT = 0x80; const int QUANT_MASK = 0x0F; const int SEG_MASK = 0x70; const double EXPANDED_MAX = 32124.0; for (int i = 0; i <= 127; ++i) { const int mu = 127 - i; const int normal = ~mu; const int biased = ((normal & QUANT_MASK) << 3) + BIAS; const unsigned int seg = ((unsigned int)normal & (unsigned int)SEG_MASK) >> 4; const int linearPos = (normal & SIGN_BIT) ? (BIAS - (biased << (int)seg)) : ((biased << (int)seg) - BIAS); const double f = (double)linearPos / EXPANDED_MAX; const double p = f * 127.0; const double n = -f * 127.0; const int vpos = CLIP((int)(p >= 0.0 ? (p + 0.5) : (p - 0.5)), -127, 127); const int vneg = CLIP((int)(n >= 0.0 ? (n + 0.5) : (n - 0.5)), -127, 127); sTable[i * 2] = (int8)vpos; sTable[(i * 2) + 1] = (int8)vneg; } sInit = true; } return sTable[enc]; } static uint16 periodForNote(uint32 n) { static const uint16 kPeriods[62] = { 0x06B0, 0x0650, 0x05F5, 0x05A0, 0x054D, 0x0501, 0x04B9, 0x0475, 0x0435, 0x03F9, 0x03C1, 0x038B, 0x0358, 0x0328, 0x02FA, 0x02D0, 0x02A6, 0x0280, 0x025C, 0x023A, 0x021A, 0x01FC, 0x01E0, 0x01C5, 0x01AC, 0x0194, 0x017D, 0x0168, 0x0153, 0x0140, 0x012E, 0x011D, 0x010D, 0x00FE, 0x00F0, 0x00E2, 0x00D6, 0x00CA, 0x00BE, 0x00B4, 0x00AA, 0x00A0, 0x0097, 0x008F, 0x0087, 0x007F, 0x0078, 0x0071, 0x006B, 0x0065, 0x005F, 0x005A, 0x0055, 0x0050, 0x004C, 0x0047, 0x0043, 0x0040, 0x003C, 0x0039, 0x0035, 0x0032 }; n = CLIP(n, 0, 61); return kPeriods[n]; } struct DttHeader { char name[64]; char author[64]; uint32 flags; uint32 numChannels; uint32 tuneLength; byte initialStereo[8]; uint32 initialSpeed; uint32 restart; uint32 numPatterns; uint32 numSamples; }; struct DttSample { char name[32]; int transpose; uint8 defaultGain; uint32 repeatOffset; uint32 repeatLength; uint32 sampleLength; uint32 sampleDataOffset; Common::Array pcm; }; struct DttEffect { uint8 cmd; uint8 param; }; struct DttEvent { uint8 sample; uint8 note; DttEffect effects[4]; uint8 numEffects; }; struct VoiceState { int sampleIdx; uint8 vol; uint16 period; uint32 pos16; uint32 step16; }; static uint8 mask6(uint32 x, int shift) { return (uint8)((x >> shift) & 0x3F); } static uint8 mask5(uint32 x, int shift) { return (uint8)((x >> shift) & 0x1F); } static uint8 mask8(uint32 x, int shift) { return (uint8)((x >> shift) & 0xFF); } static bool isMultipleEffect(uint32 raw0) { return (raw0 & (0x1FU << 17)) != 0; } static void decodeEvent(const byte *p, DttEvent &out, uint32 &bytesUsed) { const uint32 raw0 = readLE32FromBuf(p); out.sample = mask6(raw0, 0); out.note = mask6(raw0, 6); if (isMultipleEffect(raw0)) { const uint32 raw1 = readLE32FromBuf(p + 4); out.numEffects = 4; out.effects[0] = DttEffect{ mask5(raw0, 12), mask8(raw1, 0) }; out.effects[1] = DttEffect{ mask5(raw0, 17), mask8(raw1, 8) }; out.effects[2] = DttEffect{ mask5(raw0, 22), mask8(raw1, 16) }; out.effects[3] = DttEffect{ mask5(raw0, 27), mask8(raw1, 24) }; bytesUsed = 8; } else { out.numEffects = 1; out.effects[0] = DttEffect{ mask5(raw0, 12), mask8(raw0, 24) }; out.effects[1] = DttEffect{ 0, 0 }; out.effects[2] = DttEffect{ 0, 0 }; out.effects[3] = DttEffect{ 0, 0 }; bytesUsed = 4; } } class DesktopTrackerStream final : public AudioStream { public: DesktopTrackerStream(Common::SeekableReadStream *stream, int offs, int rate, bool stereo) : _rate(rate), _stereo(false), _songPos(0), _row(0), _speed50ths(6), _samplesUntilNextRow(0), _ended(false), _sawAnyNote(false) { if (!stream) error("DesktopTrackerStream: null input stream"); stream->seek(0, SEEK_END); const int32 sz = (int32)stream->pos(); stream->seek(0, SEEK_SET); if (sz <= 0) error("DesktopTrackerStream: empty input stream"); _module.resize((uint32)sz); if (stream->read(_module.begin(), (uint32)sz) != (uint32)sz) error("DesktopTrackerStream: short read"); delete stream; stream = nullptr; parseDskT(offs); for (uint32 c = 0; c < 8; ++c) { _voices[c].sampleIdx = -1; _voices[c].vol = 0; _voices[c].period = 0; _voices[c].pos16 = 0; _voices[c].step16 = 0; } const double secondsPerRow = (double)MAX(_speed50ths, 1) / 50.0; _samplesUntilNextRow = (int32)MAX(1, (int)(secondsPerRow * (double)_rate + 0.5)); } bool isStereo() const override { return _stereo; } int getRate() const override { return _rate; } bool endOfData() const override { return _ended; } int readBuffer(int16 *buffer, const int numSamples) override { if (!buffer || numSamples <= 0) return 0; for (int i = 0; i < numSamples; ++i) buffer[i] = 0; int framesLeft = numSamples; while (framesLeft > 0 && !_ended) { const int chunk = MIN(framesLeft, MAX(1, _samplesUntilNextRow)); mixMono(buffer, chunk); buffer += chunk; framesLeft -= chunk; _samplesUntilNextRow -= chunk; while (_samplesUntilNextRow <= 0 && !_ended) { advanceRow(); const double secondsPerRow = (double)MAX(_speed50ths, 1) / 50.0; _samplesUntilNextRow += (int32)MAX(1, (int)(secondsPerRow * (double)_rate + 0.5)); } } return numSamples; } private: void parseDskT(int offs) { if ((uint32)offs >= _module.size()) error("DesktopTrackerStream: offs out of range"); const byte *fileBase = _module.begin(); const uint32 fileSize = (uint32)_module.size(); const byte *base = fileBase + offs; const uint32 size = fileSize - (uint32)offs; if (size < 4 + 64 + 64) error("DesktopTrackerStream: module too small"); if (base[0] != 'D' || base[1] != 's' || base[2] != 'k' || base[3] != 'T') error("DesktopTrackerStream: not a DskT module"); memset(&_hdr, 0, sizeof(_hdr)); const byte *p = base + 4; memcpy(_hdr.name, p, 64); _hdr.name[63] = 0; p += 64; memcpy(_hdr.author, p, 64); _hdr.author[63] = 0; p += 64; _hdr.flags = readLE32FromBuf(p); p += 4; _hdr.numChannels = readLE32FromBuf(p); p += 4; _hdr.tuneLength = readLE32FromBuf(p); p += 4; memcpy(_hdr.initialStereo, p, 8); p += 8; _hdr.initialSpeed = readLE32FromBuf(p); p += 4; _hdr.restart = readLE32FromBuf(p); p += 4; _hdr.numPatterns = readLE32FromBuf(p); p += 4; _hdr.numSamples = readLE32FromBuf(p); p += 4; if (_hdr.numChannels == 0 || _hdr.numChannels > 8) error("DesktopTrackerStream: unsupported channels=%u", (uint32)_hdr.numChannels); if (_hdr.tuneLength == 0 || _hdr.tuneLength > 256) error("DesktopTrackerStream: bad tune length=%u", (uint32)_hdr.tuneLength); if (_hdr.numPatterns == 0 || _hdr.numPatterns > 1024) error("DesktopTrackerStream: bad numPatterns=%u", (uint32)_hdr.numPatterns); if (_hdr.numSamples == 0 || _hdr.numSamples > 256) error("DesktopTrackerStream: bad numSamples=%u", (uint32)_hdr.numSamples); _speed50ths = (uint32)_hdr.initialSpeed != 0 ? (uint8)_hdr.initialSpeed : (uint8)6; const uint32 hdrSize = 4 + 64 + 64 + 4 + 4 + 4 + 8 + 4 + 4 + 4 + 4; const uint32 positionsOff = hdrSize; const uint32 positionsSize = (uint32)_hdr.tuneLength; if (positionsOff + positionsSize > size) error("DesktopTrackerStream: positions out of range"); _sequence.resize((uint32)_hdr.tuneLength); for (uint32 i = 0; i < (uint32)_hdr.tuneLength; ++i) _sequence[i] = base[positionsOff + i]; const uint32 patternOffsetsOffWord = positionsOff + alignToWord(positionsSize); const uint32 patternOffsetsOffDword = positionsOff + alignToDword(positionsSize); auto scorePatternOffsets = [&](uint32 candOff) -> uint32 { const uint32 need = (uint32)_hdr.numPatterns * 4U + (uint32)_hdr.numPatterns; if (candOff + need > size) return 0; uint32 validOffs = 0; for (uint32 i = 0; i < (uint32)_hdr.numPatterns; ++i) { const uint32 po = readLE32FromBuf(base + candOff + (i * 4)); if (po != 0 && po < fileSize) validOffs++; } uint32 validLens = 0; const uint32 lensOff = candOff + (uint32)_hdr.numPatterns * 4U; for (uint32 i = 0; i < (uint32)_hdr.numPatterns; ++i) { const uint8 rows = base[lensOff + i]; if (rows != 0 && rows <= 128) validLens++; } if (validOffs == 0) return 0; return (validOffs * 2U) + validLens; }; const uint32 scoreWord = scorePatternOffsets(patternOffsetsOffWord); const uint32 scoreDword = scorePatternOffsets(patternOffsetsOffDword); const uint32 patternOffsetsOff = (scoreDword > scoreWord) ? patternOffsetsOffDword : patternOffsetsOffWord; const uint32 patternOffsetsSize = (uint32)_hdr.numPatterns * 4U; if (patternOffsetsOff + patternOffsetsSize > size) error("DesktopTrackerStream: patternOffsets out of range"); _patternOffsets.resize((uint32)_hdr.numPatterns); for (uint32 i = 0; i < (uint32)_hdr.numPatterns; ++i) _patternOffsets[i] = readLE32FromBuf(base + patternOffsetsOff + (i * 4)); const uint32 patternLengthsOff = patternOffsetsOff + patternOffsetsSize; const uint32 patternLengthsSize = (uint32)_hdr.numPatterns; if (patternLengthsOff + patternLengthsSize > size) error("DesktopTrackerStream: patternLengths out of range"); _patternLengths.resize((uint32)_hdr.numPatterns); for (uint32 i = 0; i < (uint32)_hdr.numPatterns; ++i) _patternLengths[i] = base[patternLengthsOff + i]; const uint32 samplesOff = patternLengthsOff + alignToDword(patternLengthsSize); const uint32 sampleStructSize = 64; const uint32 samplesSize = (uint32)_hdr.numSamples * sampleStructSize; if (samplesOff + samplesSize > size) error("DesktopTrackerStream: samples out of range"); _samples.resize((uint32)_hdr.numSamples); for (uint32 i = 0; i < (uint32)_hdr.numSamples; ++i) { const byte *sp = base + samplesOff + i * sampleStructSize; DttSample &smp = _samples[i]; memset(smp.name, 0, sizeof(smp.name)); const uint8 note = sp[0]; const uint8 vol = sp[1]; const uint32 repeatOffset = readLE32FromBuf(sp + 16); const uint32 repeatLength = readLE32FromBuf(sp + 20); const uint32 sampleLength = readLE32FromBuf(sp + 24); memcpy(smp.name, sp + 28, 32); smp.name[31] = 0; const uint32 sampleDataOffset = readLE32FromBuf(sp + 60); smp.transpose = 26 - (int)note; smp.defaultGain = (uint8)(vol & 0x7F); smp.repeatOffset = repeatOffset; smp.repeatLength = repeatLength; smp.sampleLength = sampleLength; smp.sampleDataOffset = sampleDataOffset; smp.pcm.clear(); if (sampleDataOffset != 0 && sampleLength != 0) { if (sampleDataOffset + sampleLength > fileSize) { smp.sampleLength = 0; smp.pcm.clear(); } else { smp.pcm.resize(sampleLength + 1); for (uint32 k = 0; k < sampleLength; ++k) smp.pcm[k] = vidcToSigned8(fileBase[sampleDataOffset + k]); smp.pcm[sampleLength] = (sampleLength > 0) ? smp.pcm[sampleLength - 1] : 0; } } } } void advanceRow() { if (_ended) return; if (_songPos >= (uint32)_sequence.size()) { _ended = true; return; } const uint32 patIdx = (uint32)_sequence[_songPos]; if (patIdx >= (uint32)_patternOffsets.size()) { _ended = true; return; } applyRow(patIdx, _row); const uint32 rows = (uint32)_patternLengths[patIdx]; _row++; if (_row >= rows) { _row = 0; _songPos++; if (_songPos >= (uint32)_sequence.size()) _songPos = 0; } } void applyRow(uint32 patIdx, uint32 row) { const uint32 poff = _patternOffsets[patIdx]; if (poff == 0 || poff >= _module.size()) return; const byte *p = _module.begin() + poff; for (uint32 r = 0; r < row; ++r) { for (uint32 c = 0; c < (uint32)_hdr.numChannels; ++c) { DttEvent ev; uint32 used = 0; decodeEvent(p, ev, used); p += used; } } for (uint32 c = 0; c < (uint32)_hdr.numChannels; ++c) { DttEvent ev; uint32 used = 0; decodeEvent(p, ev, used); p += used; if (ev.sample != 0 && ev.sample <= _samples.size()) { const int si = (int)ev.sample - 1; _voices[c].sampleIdx = si; _voices[c].vol = _samples[si].defaultGain; _voices[c].pos16 = 0; } for (uint32 i = 0; i < (uint32)ev.numEffects; ++i) { const uint8 cmd = ev.effects[i].cmd; const uint8 param = ev.effects[i].param; if (cmd == 0x0C) { _voices[c].vol = (uint8)MIN(param & 0x7F, 127); } else if (cmd == 0x0F) { if (param != 0) _speed50ths = param; } } if (ev.note != 0 && _voices[c].sampleIdx >= 0) { const DttSample &smp = _samples[(uint32)_voices[c].sampleIdx]; if (!_sawAnyNote) { _sawAnyNote = true; } if (!smp.pcm.empty()) { const int note = (int)ev.note + smp.transpose; const uint32 nn = (uint32)CLIP(note, 0, 61); _voices[c].period = periodForNote(nn); const double conv = 3273808.59375; const double step = conv / ((double)_voices[c].period * (double)_rate); const uint32 step16 = (uint32)MAX(1, (uint32)(step * 65536.0 + 0.5)); _voices[c].step16 = step16; _voices[c].pos16 = 0; } } } } void mixMono(int16 *dst, int frames) { for (int i = 0; i < frames; ++i) { int mix = 0; for (uint32 c = 0; c < (uint32)_hdr.numChannels; ++c) { VoiceState &vs = _voices[c]; if (vs.sampleIdx < 0 || vs.step16 == 0) continue; const DttSample &smp = _samples[(uint32)vs.sampleIdx]; if (smp.pcm.empty() || smp.sampleLength < 2) continue; uint32 pos = vs.pos16; uint32 idx = pos >> 16; if (idx >= smp.sampleLength) { if (smp.repeatLength > 2) { const uint32 loopEnd = smp.repeatOffset + smp.repeatLength; if (loopEnd > smp.repeatOffset) { if (idx >= loopEnd) idx = smp.repeatOffset + ((idx - smp.repeatOffset) % smp.repeatLength); } else { idx = smp.repeatOffset; } pos = (idx << 16) | (pos & 0xFFFF); } else { continue; } } const uint32 i0 = (pos >> 16); const uint32 frac = pos & 0xFFFF; const int s0 = (int)smp.pcm[MIN(i0, (uint32)smp.pcm.size() - 1)]; const int s1 = (int)smp.pcm[MIN(i0 + 1, (uint32)smp.pcm.size() - 1)]; const int s8 = (int)((((int64)s0 * (int64)(65536 - (int)frac)) + ((int64)s1 * (int64)frac)) >> 16); const int sample16 = s8 << 8; mix += (sample16 * (int)vs.vol); vs.pos16 = pos + vs.step16; } const int denom = MAX(1, (int)_hdr.numChannels * 128); mix = mix / denom; mix = CLIP(mix, -32768, 32767); dst[i] = (int16)mix; } } private: int _rate; bool _stereo; Common::Array _module; DttHeader _hdr; Common::Array _sequence; Common::Array _patternOffsets; Common::Array _patternLengths; Common::Array _samples; uint32 _songPos; uint32 _row; uint8 _speed50ths; int32 _samplesUntilNextRow; VoiceState _voices[8]; bool _ended; bool _sawAnyNote; }; } // namespace Modules AudioStream *makeDesktopTrackerStream(Common::SeekableReadStream *stream, int offs, int rate, bool stereo) { if (!stream) return nullptr; return new Modules::DesktopTrackerStream(stream, offs, rate, stereo); } AudioStream *makeDesktopTrackerStream(Common::SeekableReadStream *stream, DisposeAfterUse::Flag disposeAfterUse) { if (!stream) return nullptr; const int offs = 0; const int rate = 44100; const bool stereo = false; Modules::DesktopTrackerStream *dt = new Modules::DesktopTrackerStream(stream, offs, rate, stereo); (void)disposeAfterUse; return dt; } } // namespace Audio