/* * Load_dbm.cpp * ------------ * Purpose: DigiBooster Pro module Loader (DBM) * Notes : (currently none) * Authors: OpenMPT Devs * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. */ #include "stdafx.h" #include "Loaders.h" #include "../common/mptStringBuffer.h" #ifndef NO_PLUGINS #include "plugins/DigiBoosterEcho.h" #endif // NO_PLUGINS #ifdef LIBOPENMPT_BUILD #define MPT_DBM_USE_REAL_SUBSONGS #endif OPENMPT_NAMESPACE_BEGIN struct DBMFileHeader { char dbm0[4]; uint8 trkVerHi; uint8 trkVerLo; char reserved[2]; }; MPT_BINARY_STRUCT(DBMFileHeader, 8) // IFF-style Chunk struct DBMChunk { // 32-Bit chunk identifiers enum ChunkIdentifiers { idNAME = MagicBE("NAME"), idINFO = MagicBE("INFO"), idSONG = MagicBE("SONG"), idINST = MagicBE("INST"), idVENV = MagicBE("VENV"), idPENV = MagicBE("PENV"), idPATT = MagicBE("PATT"), idPNAM = MagicBE("PNAM"), idSMPL = MagicBE("SMPL"), idDSPE = MagicBE("DSPE"), idMPEG = MagicBE("MPEG"), }; uint32be id; uint32be length; size_t GetLength() const { return length; } ChunkIdentifiers GetID() const { return static_cast(id.get()); } }; MPT_BINARY_STRUCT(DBMChunk, 8) struct DBMInfoChunk { uint16be instruments; uint16be samples; uint16be songs; uint16be patterns; uint16be channels; }; MPT_BINARY_STRUCT(DBMInfoChunk, 10) // Instrument header struct DBMInstrument { enum DBMInstrFlags { smpLoop = 0x01, smpPingPongLoop = 0x02, }; char name[30]; uint16be sample; // Sample reference uint16be volume; // 0...64 uint32be sampleRate; uint32be loopStart; uint32be loopLength; int16be panning; // -128...128 uint16be flags; // See DBMInstrFlags }; MPT_BINARY_STRUCT(DBMInstrument, 50) // Volume or panning envelope struct DBMEnvelope { enum DBMEnvelopeFlags { envEnabled = 0x01, envSustain = 0x02, envLoop = 0x04, }; uint16be instrument; uint8be flags; // See DBMEnvelopeFlags uint8be numSegments; // Number of envelope points - 1 uint8be sustain1; uint8be loopBegin; uint8be loopEnd; uint8be sustain2; // Second sustain point uint16be data[2 * 32]; }; MPT_BINARY_STRUCT(DBMEnvelope, 136) // Note: Unlike in MOD, 1Fx, 2Fx, 5Fx / 5xF, 6Fx / 6xF and AFx / AxF are fine slides. static constexpr ModCommand::COMMAND dbmEffects[] = { CMD_ARPEGGIO, CMD_PORTAMENTOUP, CMD_PORTAMENTODOWN, CMD_TONEPORTAMENTO, CMD_VIBRATO, CMD_TONEPORTAVOL, CMD_VIBRATOVOL, CMD_TREMOLO, CMD_PANNING8, CMD_OFFSET, CMD_VOLUMESLIDE, CMD_POSITIONJUMP, CMD_VOLUME, CMD_PATTERNBREAK, CMD_MODCMDEX, CMD_TEMPO, CMD_GLOBALVOLUME, CMD_GLOBALVOLSLIDE, CMD_NONE, CMD_NONE, CMD_KEYOFF, CMD_SETENVPOSITION, CMD_NONE, CMD_NONE, CMD_NONE, CMD_PANNINGSLIDE, CMD_NONE, CMD_NONE, CMD_NONE, CMD_NONE, CMD_NONE, #ifndef NO_PLUGINS CMD_DBMECHO, // Toggle DSP CMD_MIDI, // Wxx Echo Delay CMD_MIDI, // Xxx Echo Feedback CMD_MIDI, // Yxx Echo Mix CMD_MIDI, // Zxx Echo Cross #endif // NO_PLUGINS }; static void ConvertDBMEffect(uint8 &command, uint8 ¶m) { uint8 oldCmd = command; if(command < std::size(dbmEffects)) command = dbmEffects[command]; else command = CMD_NONE; switch(command) { case CMD_ARPEGGIO: if(param == 0) command = CMD_NONE; break; case CMD_PATTERNBREAK: param = ((param >> 4) * 10) + (param & 0x0F); break; #ifdef MODPLUG_TRACKER case CMD_VIBRATO: if(param & 0x0F) { // DBM vibrato is half as deep as most other trackers. Convert it to IT fine vibrato range if possible. uint8 depth = (param & 0x0F) * 2u; param &= 0xF0; if(depth < 16) command = CMD_FINEVIBRATO; else depth = (depth + 2u) / 4u; param |= depth; } break; #endif // Volume slide nibble priority - first nibble (slide up) has precedence. case CMD_VOLUMESLIDE: case CMD_TONEPORTAVOL: case CMD_VIBRATOVOL: if((param & 0xF0) != 0x00 && (param & 0xF0) != 0xF0 && (param & 0x0F) != 0x0F) param &= 0xF0; break; case CMD_GLOBALVOLUME: if(param <= 64) param *= 2; else param = 128; break; case CMD_MODCMDEX: switch(param & 0xF0) { case 0x30: // Play backwards command = CMD_S3MCMDEX; param = 0x9F; break; case 0x40: // Turn off sound in channel (volume / portamento commands after this can't pick up the note anymore) command = CMD_S3MCMDEX; param = 0xC0; break; case 0x50: // Turn on/off channel // TODO: Apparently this should also kill the playing note. if((param & 0x0F) <= 0x01) { command = CMD_CHANNELVOLUME; param = (param == 0x50) ? 0x00 : 0x40; } break; case 0x70: // Coarse offset command = CMD_S3MCMDEX; param = 0xA0 | (param & 0x0F); break; default: // Rest will be converted later from CMD_MODCMDEX to CMD_S3MCMDEX. break; } break; case CMD_TEMPO: if(param <= 0x1F) command = CMD_SPEED; break; case CMD_KEYOFF: if (param == 0) { // TODO key off at tick 0 } break; case CMD_MIDI: // Encode echo parameters into fixed MIDI macros param = 128 + (oldCmd - 32) * 32 + param / 8; } } // Read a chunk of volume or panning envelopes static void ReadDBMEnvelopeChunk(FileReader chunk, EnvelopeType envType, CSoundFile &sndFile, bool scaleEnv) { uint16 numEnvs = chunk.ReadUint16BE(); for(uint16 env = 0; env < numEnvs; env++) { DBMEnvelope dbmEnv; chunk.ReadStruct(dbmEnv); uint16 dbmIns = dbmEnv.instrument; if(dbmIns > 0 && dbmIns <= sndFile.GetNumInstruments() && (sndFile.Instruments[dbmIns] != nullptr)) { ModInstrument *mptIns = sndFile.Instruments[dbmIns]; InstrumentEnvelope &mptEnv = mptIns->GetEnvelope(envType); if(dbmEnv.numSegments) { if(dbmEnv.flags & DBMEnvelope::envEnabled) mptEnv.dwFlags.set(ENV_ENABLED); if(dbmEnv.flags & DBMEnvelope::envSustain) mptEnv.dwFlags.set(ENV_SUSTAIN); if(dbmEnv.flags & DBMEnvelope::envLoop) mptEnv.dwFlags.set(ENV_LOOP); } uint8 numPoints = std::min(dbmEnv.numSegments.get(), uint8(31)) + 1; mptEnv.resize(numPoints); mptEnv.nLoopStart = dbmEnv.loopBegin; mptEnv.nLoopEnd = dbmEnv.loopEnd; mptEnv.nSustainStart = mptEnv.nSustainEnd = dbmEnv.sustain1; for(uint8 i = 0; i < numPoints; i++) { mptEnv[i].tick = dbmEnv.data[i * 2]; uint16 val = dbmEnv.data[i * 2 + 1]; if(scaleEnv) { // Panning envelopes are -128...128 in DigiBooster Pro 3.x val = (val + 128) / 4; } LimitMax(val, uint16(64)); mptEnv[i].value = static_cast(val); } } } } static bool ValidateHeader(const DBMFileHeader &fileHeader) { if(std::memcmp(fileHeader.dbm0, "DBM0", 4) || fileHeader.trkVerHi > 3) { return false; } return true; } CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderDBM(MemoryFileReader file, const uint64 *pfilesize) { DBMFileHeader fileHeader; if(!file.ReadStruct(fileHeader)) { return ProbeWantMoreData; } if(!ValidateHeader(fileHeader)) { return ProbeFailure; } MPT_UNREFERENCED_PARAMETER(pfilesize); return ProbeSuccess; } bool CSoundFile::ReadDBM(FileReader &file, ModLoadingFlags loadFlags) { file.Rewind(); DBMFileHeader fileHeader; if(!file.ReadStruct(fileHeader)) { return false; } if(!ValidateHeader(fileHeader)) { return false; } if(loadFlags == onlyVerifyHeader) { return true; } ChunkReader chunkFile(file); auto chunks = chunkFile.ReadChunks(1); // Globals DBMInfoChunk infoData; if(!chunks.GetChunk(DBMChunk::idINFO).ReadStruct(infoData)) { return false; } InitializeGlobals(MOD_TYPE_DBM); InitializeChannels(); m_SongFlags = SONG_ITCOMPATGXX | SONG_ITOLDEFFECTS; m_nChannels = Clamp(infoData.channels, 1, MAX_BASECHANNELS); // note: MAX_BASECHANNELS is currently 127, but DBPro 2 supports up to 128 channels, DBPro 3 apparently up to 254. m_nInstruments = std::min(static_cast(infoData.instruments), static_cast(MAX_INSTRUMENTS - 1)); m_nSamples = std::min(static_cast(infoData.samples), static_cast(MAX_SAMPLES - 1)); m_playBehaviour.set(kSlidesAtSpeed1); m_playBehaviour.reset(kITVibratoTremoloPanbrello); m_playBehaviour.reset(kITArpeggio); m_modFormat.formatName = U_("DigiBooster Pro"); m_modFormat.type = U_("dbm"); m_modFormat.madeWithTracker = MPT_UFORMAT("DigiBooster Pro {}.{}")(mpt::ufmt::hex(fileHeader.trkVerHi), mpt::ufmt::hex(fileHeader.trkVerLo)); m_modFormat.charset = mpt::Charset::Amiga_no_C1; // Name chunk FileReader nameChunk = chunks.GetChunk(DBMChunk::idNAME); nameChunk.ReadString(m_songName, nameChunk.GetLength()); // Song chunk FileReader songChunk = chunks.GetChunk(DBMChunk::idSONG); Order().clear(); uint16 numSongs = infoData.songs; for(uint16 i = 0; i < numSongs && songChunk.CanRead(46); i++) { char name[44]; songChunk.ReadString(name, 44); if(m_songName.empty()) { m_songName = name; } uint16 numOrders = songChunk.ReadUint16BE(); #ifdef MPT_DBM_USE_REAL_SUBSONGS if(!Order().empty()) { // Add a new sequence for this song if(Order.AddSequence() == SEQUENCEINDEX_INVALID) break; } Order().SetName(mpt::ToUnicode(mpt::Charset::Amiga_no_C1, name)); ReadOrderFromFile(Order(), songChunk, numOrders); #else const ORDERINDEX startIndex = Order().GetLength(); if(startIndex < MAX_ORDERS && songChunk.CanRead(numOrders * 2u)) { LimitMax(numOrders, static_cast(MAX_ORDERS - startIndex - 1)); Order().resize(startIndex + numOrders + 1); for(uint16 ord = 0; ord < numOrders; ord++) { Order()[startIndex + ord] = static_cast(songChunk.ReadUint16BE()); } } #endif // MPT_DBM_USE_REAL_SUBSONGS } #ifdef MPT_DBM_USE_REAL_SUBSONGS Order.SetSequence(0); #endif // MPT_DBM_USE_REAL_SUBSONGS // Read instruments if(FileReader instChunk = chunks.GetChunk(DBMChunk::idINST)) { for(INSTRUMENTINDEX i = 1; i <= GetNumInstruments(); i++) { DBMInstrument instrHeader; instChunk.ReadStruct(instrHeader); ModInstrument *mptIns = AllocateInstrument(i, instrHeader.sample); if(mptIns == nullptr || instrHeader.sample >= MAX_SAMPLES) { continue; } mptIns->name = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, instrHeader.name); m_szNames[instrHeader.sample] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, instrHeader.name); mptIns->nFadeOut = 0; mptIns->nPan = static_cast(instrHeader.panning + 128); LimitMax(mptIns->nPan, uint32(256)); mptIns->dwFlags.set(INS_SETPANNING); // Sample Info ModSample &mptSmp = Samples[instrHeader.sample]; mptSmp.Initialize(); mptSmp.nVolume = std::min(static_cast(instrHeader.volume), uint16(64)) * 4u; mptSmp.nC5Speed = Util::muldivr(instrHeader.sampleRate, 8303, 8363); if(instrHeader.loopLength && (instrHeader.flags & (DBMInstrument::smpLoop | DBMInstrument::smpPingPongLoop))) { mptSmp.nLoopStart = instrHeader.loopStart; mptSmp.nLoopEnd = mptSmp.nLoopStart + instrHeader.loopLength; mptSmp.uFlags.set(CHN_LOOP); if(instrHeader.flags & DBMInstrument::smpPingPongLoop) mptSmp.uFlags.set(CHN_PINGPONGLOOP); } } // Read envelopes ReadDBMEnvelopeChunk(chunks.GetChunk(DBMChunk::idVENV), ENV_VOLUME, *this, false); ReadDBMEnvelopeChunk(chunks.GetChunk(DBMChunk::idPENV), ENV_PANNING, *this, fileHeader.trkVerHi > 2); // Note-Off cuts samples if there's no envelope. for(INSTRUMENTINDEX i = 1; i <= GetNumInstruments(); i++) { if(Instruments[i] != nullptr && !Instruments[i]->VolEnv.dwFlags[ENV_ENABLED]) { Instruments[i]->nFadeOut = 32767; } } } // Patterns FileReader patternChunk = chunks.GetChunk(DBMChunk::idPATT); #ifndef NO_PLUGINS bool hasEchoEnable = false, hasEchoParams = false; #endif // NO_PLUGINS if(patternChunk.IsValid() && (loadFlags & loadPatternData)) { FileReader patternNameChunk = chunks.GetChunk(DBMChunk::idPNAM); patternNameChunk.Skip(1); // Encoding, should be UTF-8 or ASCII Patterns.ResizeArray(infoData.patterns); std::vector> lostGlobalCommands; for(PATTERNINDEX pat = 0; pat < infoData.patterns; pat++) { uint16 numRows = patternChunk.ReadUint16BE(); uint32 packedSize = patternChunk.ReadUint32BE(); FileReader chunk = patternChunk.ReadChunk(packedSize); if(!Patterns.Insert(pat, numRows)) continue; std::string patName; patternNameChunk.ReadSizedString(patName); Patterns[pat].SetName(patName); PatternRow patRow = Patterns[pat].GetRow(0); ROWINDEX row = 0; lostGlobalCommands.clear(); while(chunk.CanRead(1)) { const uint8 ch = chunk.ReadUint8(); if(!ch) { // End Of Row for(const auto &cmd : lostGlobalCommands) { Patterns[pat].WriteEffect(EffectWriter(cmd.first, cmd.second).Row(row)); } lostGlobalCommands.clear(); if(++row >= numRows) break; patRow = Patterns[pat].GetRow(row); continue; } ModCommand dummy = ModCommand::Empty(); ModCommand &m = ch <= GetNumChannels() ? patRow[ch - 1] : dummy; const uint8 b = chunk.ReadUint8(); if(b & 0x01) { uint8 note = chunk.ReadUint8(); if(note == 0x1F) m.note = NOTE_KEYOFF; else if(note > 0 && note < 0xFE) m.note = ((note >> 4) * 12) + (note & 0x0F) + 13; } if(b & 0x02) { m.instr = chunk.ReadUint8(); } if(b & 0x3C) { uint8 cmd1 = 0, cmd2 = 0, param1 = 0, param2 = 0; if(b & 0x04) cmd2 = chunk.ReadUint8(); if(b & 0x08) param2 = chunk.ReadUint8(); if(b & 0x10) cmd1 = chunk.ReadUint8(); if(b & 0x20) param1 = chunk.ReadUint8(); ConvertDBMEffect(cmd1, param1); ConvertDBMEffect(cmd2, param2); if (cmd2 == CMD_VOLUME || (cmd2 == CMD_NONE && cmd1 != CMD_VOLUME)) { std::swap(cmd1, cmd2); std::swap(param1, param2); } const auto lostCommand = ModCommand::TwoRegularCommandsToMPT(cmd1, param1, cmd2, param2); if(ModCommand::IsGlobalCommand(lostCommand.first, lostCommand.second)) lostGlobalCommands.insert(lostGlobalCommands.begin(), lostCommand); // Insert at front so that the last command of same type "wins" m.volcmd = cmd1; m.vol = param1; m.command = cmd2; m.param = param2; #ifdef MODPLUG_TRACKER m.ExtendedMODtoS3MEffect(); #endif // MODPLUG_TRACKER #ifndef NO_PLUGINS if(m.command == CMD_DBMECHO) hasEchoEnable = true; else if(m.command == CMD_MIDI) hasEchoParams = true; #endif // NO_PLUGINS } } } } #ifndef NO_PLUGINS // Echo DSP if(loadFlags & loadPluginData) { if(hasEchoEnable) { // If there are any Vxx effects to dynamically enable / disable echo, use the CHN_NOFX flag. for(CHANNELINDEX i = 0; i < m_nChannels; i++) { ChnSettings[i].nMixPlugin = 1; ChnSettings[i].dwFlags.set(CHN_NOFX); } } bool anyEnabled = hasEchoEnable; // DBP 3 Documentation says that the defaults are 64/128/128/255, but they appear to be 80/150/80/255 in DBP 2.21 uint8 settings[8] = { 0, 80, 0, 150, 0, 80, 0, 255 }; if(FileReader dspChunk = chunks.GetChunk(DBMChunk::idDSPE)) { uint16 maskLen = dspChunk.ReadUint16BE(); for(uint16 i = 0; i < maskLen; i++) { bool enabled = (dspChunk.ReadUint8() == 0); if(i < m_nChannels) { if(hasEchoEnable) { // If there are any Vxx effects to dynamically enable / disable echo, use the CHN_NOFX flag. ChnSettings[i].dwFlags.set(CHN_NOFX, !enabled); } else if(enabled) { ChnSettings[i].nMixPlugin = 1; anyEnabled = true; } } } dspChunk.ReadArray(settings); } if(anyEnabled) { // Note: DigiBooster Pro 3 has a more versatile per-channel echo effect. // In this case, we'd have to create one plugin per channel. SNDMIXPLUGIN &plugin = m_MixPlugins[0]; plugin.Destroy(); memcpy(&plugin.Info.dwPluginId1, "DBM0", 4); memcpy(&plugin.Info.dwPluginId2, "Echo", 4); plugin.Info.routingFlags = SNDMIXPLUGININFO::irAutoSuspend; plugin.Info.mixMode = 0; plugin.Info.gain = 10; plugin.Info.reserved = 0; plugin.Info.dwOutputRouting = 0; std::fill(plugin.Info.dwReserved, plugin.Info.dwReserved + std::size(plugin.Info.dwReserved), 0); plugin.Info.szName = "Echo"; plugin.Info.szLibraryName = "DigiBooster Pro Echo"; plugin.pluginData.resize(sizeof(DigiBoosterEcho::PluginChunk)); DigiBoosterEcho::PluginChunk chunk = DigiBoosterEcho::PluginChunk::Create(settings[1], settings[3], settings[5], settings[7]); new (plugin.pluginData.data()) DigiBoosterEcho::PluginChunk(chunk); } } // Encode echo parameters into fixed MIDI macros if(hasEchoParams) { for(uint32 i = 0; i < 32; i++) { uint32 param = (i * 127u) / 32u; m_MidiCfg.Zxx[i ] = MPT_AFORMAT("F0F080{}")(mpt::afmt::HEX0<2>(param)); m_MidiCfg.Zxx[i + 32] = MPT_AFORMAT("F0F081{}")(mpt::afmt::HEX0<2>(param)); m_MidiCfg.Zxx[i + 64] = MPT_AFORMAT("F0F082{}")(mpt::afmt::HEX0<2>(param)); m_MidiCfg.Zxx[i + 96] = MPT_AFORMAT("F0F083{}")(mpt::afmt::HEX0<2>(param)); } } #endif // NO_PLUGINS // Samples FileReader sampleChunk = chunks.GetChunk(DBMChunk::idSMPL); if(sampleChunk.IsValid() && (loadFlags & loadSampleData)) { for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) { uint32 sampleFlags = sampleChunk.ReadUint32BE(); uint32 sampleLength = sampleChunk.ReadUint32BE(); if(sampleFlags & 7) { ModSample &sample = Samples[smp]; sample.nLength = sampleLength; SampleIO( (sampleFlags & 4) ? SampleIO::_32bit : ((sampleFlags & 2) ? SampleIO::_16bit : SampleIO::_8bit), SampleIO::mono, SampleIO::bigEndian, SampleIO::signedPCM) .ReadSample(sample, sampleChunk); } } } #if defined(MPT_ENABLE_MP3_SAMPLES) && 0 // Compressed samples - this does not quite work yet... FileReader mpegChunk = chunks.GetChunk(DBMChunk::idMPEG); if(mpegChunk.IsValid() && (loadFlags & loadSampleData)) { for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) { Samples[smp].nLength = mpegChunk.ReadUint32BE(); } mpegChunk.Skip(2); // 0x00 0x40 // Read whole MPEG stream into one sample and then split it up. FileReader chunk = mpegChunk.GetChunk(mpegChunk.BytesLeft()); if(ReadMP3Sample(0, chunk, true)) { ModSample &srcSample = Samples[0]; const std::byte *smpData = srcSample.sampleb(); SmpLength predelay = Util::muldiv_unsigned(20116, srcSample.nC5Speed, 100000); LimitMax(predelay, srcSample.nLength); smpData += predelay * srcSample.GetBytesPerSample(); srcSample.nLength -= predelay; for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) { ModSample &sample = Samples[smp]; sample.uFlags.set(srcSample.uFlags); LimitMax(sample.nLength, srcSample.nLength); if(sample.nLength) { sample.AllocateSample(); memcpy(sample.sampleb(), smpData, sample.GetSampleSizeInBytes()); smpData += sample.GetSampleSizeInBytes(); srcSample.nLength -= sample.nLength; SmpLength gap = Util::muldiv_unsigned(454, srcSample.nC5Speed, 10000); LimitMax(gap, srcSample.nLength); smpData += gap * srcSample.GetBytesPerSample(); srcSample.nLength -= gap; } } srcSample.FreeSample(); } } #endif // MPT_ENABLE_MP3_SAMPLES return true; } OPENMPT_NAMESPACE_END