From 779a8d8810845dc30e5bb9a78a581107ca6ef362 Mon Sep 17 00:00:00 2001 From: cam900 Date: Fri, 29 Aug 2025 16:55:53 +0900 Subject: [PATCH 01/28] Add loop end hints for NDS in ADPCM, 8bit PCM --- src/gui/sampleEdit.cpp | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/gui/sampleEdit.cpp b/src/gui/sampleEdit.cpp index 240d0c8bd..e71f7f6b1 100644 --- a/src/gui/sampleEdit.cpp +++ b/src/gui/sampleEdit.cpp @@ -2318,6 +2318,32 @@ void FurnaceGUI::drawSampleEdit() { } } } + if (displayLoopHintsNDSA) { + if (sampleZoom<0.5) { + for (int i=0; i<(int)(sampleZoom*avail.x); i++) { + if (((i+samplePos)&7)==0) { + ImVec2 p1=ImVec2(rectMin.x+((float)i/sampleZoom),rectMin.y); + ImVec2 p2=p1; + p2.y=rectMax.y; + + dl->AddLine(p1,p2,ImGui::GetColorU32(uiColors[GUI_COLOR_SAMPLE_LOOP_HINT])); + } + } + } + } + if (displayLoopHintsNDS8) { + if (sampleZoom<0.375) { + for (int i=0; i<(int)(sampleZoom*avail.x); i++) { + if (((i+samplePos)&3)==0) { + ImVec2 p1=ImVec2(rectMin.x+((float)i/sampleZoom),rectMin.y); + ImVec2 p2=p1; + p2.y=rectMax.y; + + dl->AddLine(p1,p2,ImGui::GetColorU32(uiColors[GUI_COLOR_SAMPLE_LOOP_HINT])); + } + } + } + } if (displayLoopHintsAmiga) { if (sampleZoom<0.25) { for (int i=0; i<(int)(sampleZoom*avail.x); i++) { From 91965bca8c1965ff13084072afaf6dc15a32af11 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Wed, 3 Sep 2025 03:58:45 -0500 Subject: [PATCH 02/28] fix crash when converting to BRR with invalid loop end issue #2671 --- src/engine/brrUtils.c | 2 +- src/engine/sample.cpp | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/engine/brrUtils.c b/src/engine/brrUtils.c index 38073fb4b..ff62e5641 100644 --- a/src/engine/brrUtils.c +++ b/src/engine/brrUtils.c @@ -256,7 +256,7 @@ long brrEncode(short* buf, unsigned char* out, long len, long loopStart, unsigne total+=9; } // encode loop block - if (loopStart>=0) { + if (loopStart>=0 && loopStart=len) { diff --git a/src/engine/sample.cpp b/src/engine/sample.cpp index 2f7d50617..054d2bc4e 100644 --- a/src/engine/sample.cpp +++ b/src/engine/sample.cpp @@ -1479,7 +1479,8 @@ void DivSample::render(unsigned int formatMask) { } } if (NOT_IN_FORMAT(DIV_SAMPLE_DEPTH_BRR)) { // BRR - int sampleCount=loop?loopEnd:samples; + int sampleCount=isLoopable()?loopEnd:samples; + if (sampleCount>(int)samples) sampleCount=samples; if (!initInternal(DIV_SAMPLE_DEPTH_BRR,sampleCount)) return; brrEncode(data16,dataBRR,sampleCount,loop?loopStart:-1,brrEmphasis,brrNoFilter); } From dcf5f3f0c7b1e400e94f31b02562cd380613886b Mon Sep 17 00:00:00 2001 From: tildearrow Date: Wed, 3 Sep 2025 04:01:47 -0500 Subject: [PATCH 03/28] fix stage 20 not being purple tanks --- src/gui/tutorial.cpp | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/src/gui/tutorial.cpp b/src/gui/tutorial.cpp index 9b238be85..f019d0d47 100644 --- a/src/gui/tutorial.cpp +++ b/src/gui/tutorial.cpp @@ -1313,7 +1313,7 @@ void FurnaceCV::buildStage(int which) { curStage=NULL; } - if (which>19 || which==4 || which==7 || which==9 || which==11 || which==13 || which==16 || which==17) { + if (which>18 || which==4 || which==7 || which==9 || which==11 || which==13 || which==16 || which==17) { stageWidth=80; stageHeight=56; } else { @@ -1346,26 +1346,7 @@ void FurnaceCV::buildStage(int which) { memset(busy,0,28*40*sizeof(bool)); // special stages - if ((which%10)==9) { - // vortex - for (int i=0; i<20+(which>>2); i++) { - int tries=0; - while (tries<20) { - int x=rand()%(stageWidth>>1); - int y=rand()%(stageHeight>>1); - int finalX=x<<4; - int finalY=y<<4; - if (busy[y][x]) { - tries++; - continue; - } - createObject(finalX,finalY); - createObject(finalX-4,finalY-4); - busy[y][x]=true; - break; - } - } - } else if ((which%10)==19) { + if ((which%10)==19) { for (int i=0; i<20+(which>>2); i++) { int tries=0; while (tries<20) { @@ -1387,6 +1368,25 @@ void FurnaceCV::buildStage(int which) { break; } } + } else if ((which%10)==9) { + // vortex + for (int i=0; i<20+(which>>2); i++) { + int tries=0; + while (tries<20) { + int x=rand()%(stageWidth>>1); + int y=rand()%(stageHeight>>1); + int finalX=x<<4; + int finalY=y<<4; + if (busy[y][x]) { + tries++; + continue; + } + createObject(finalX,finalY); + createObject(finalX-4,finalY-4); + busy[y][x]=true; + break; + } + } } else { // large if (which>=2) for (int i=0; i<(rand()%3)+which-2; i++) { @@ -1675,6 +1675,7 @@ void FurnaceCV::render(unsigned char joyIn) { lives+=lifeBank; respawnTime=1; lifeBank=0; + score=0; gameOver=false; } From 24e7338dc56ddbf83397cd1a5dffb48e97e110ce Mon Sep 17 00:00:00 2001 From: tildearrow Date: Thu, 4 Sep 2025 05:01:48 -0500 Subject: [PATCH 04/28] GUI: add visualizer to cmd stream player --- src/engine/cmdStream.cpp | 5 +++ src/engine/cmdStream.h | 3 +- src/gui/csPlayer.cpp | 71 ++++++++++++++++++++++++++++++++++++++++ src/gui/gui.h | 2 ++ 4 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/engine/cmdStream.cpp b/src/engine/cmdStream.cpp index f1605426e..21ed84b5c 100644 --- a/src/engine/cmdStream.cpp +++ b/src/engine/cmdStream.cpp @@ -682,6 +682,11 @@ bool DivCSPlayer::init() { chan[i].readPos=chan[i].startPos; } } + + // read stack sizes + for (unsigned int i=0; igetTotalChannelCount(); i++) { diff --git a/src/engine/cmdStream.h b/src/engine/cmdStream.h index 326134dbd..459a49d62 100644 --- a/src/engine/cmdStream.h +++ b/src/engine/cmdStream.h @@ -41,7 +41,7 @@ struct DivCSChannelState { unsigned char arp, arpStage, arpTicks; unsigned int callStack[DIV_MAX_CSSTACK]; - unsigned char callStackPos; + unsigned char callStackPos, callStackSize; unsigned int trace[DIV_MAX_CSTRACE]; unsigned char tracePos; @@ -67,6 +67,7 @@ struct DivCSChannelState { arpStage(0), arpTicks(0), callStackPos(0), + callStackSize(0), tracePos(0) { for (int i=0; igetDataLen()); + + ImGui::PushFont(patFont); + if (ImGui::BeginTable("CSHexPos",chans,ImGuiTableFlags_SizingStretchSame)) { + ImGui::TableNextRow(); + for (int i=0; igetChanState(i); + ImGui::TableNextColumn(); + ImGui::Text("$%.4x",state->readPos); + } + ImGui::EndTable(); + } + ImGui::PopFont(); + + if (csTex==NULL || !rend->isTextureValid(csTex)) { + logD("recreating command stream data texture."); + csTex=rend->createTexture(true,256,256,false,GUI_TEXFORMAT_ABGR32); + if (csTex==NULL) { + logE("error while creating command stream data texture! %s",SDL_GetError()); + } + } + if (csTex!=NULL) { + unsigned int* dataT=NULL; + int pitch=0; + if (!rend->lockTexture(csTex,(void**)&dataT,&pitch)) { + logE("error while locking command stream data texture! %s",SDL_GetError()); + } else { + unsigned short* accessTS=cs->getDataAccess(); + unsigned int csTick=cs->getCurTick(); + const float fadeTime=64.0f; + size_t bufSize=cs->getDataLen(); + if (bufSize>65536) bufSize=65536; + + for (size_t i=0; i0.0f) { + dataT[i]=ImGui::GetColorU32(ImGuiCol_HeaderActive,cellAlpha); + } else { + dataT[i]=0; + } + } + for (size_t i=bufSize; i<65536; i++) { + dataT[i]=0; + } + + for (int i=0; igetTotalChannelCount(); i++) { + unsigned int pos=cs->getChanState(i)->readPos; + if (pos<65536) { + ImVec4 col=ImVec4(1.0f,1.0f,1.0f,1.0f); + ImGui::ColorConvertHSVtoRGB((float)i/(float)e->getTotalChannelCount(),0.8f,1.0f,col.x,col.y,col.z); + dataT[pos]=ImGui::GetColorU32(col); + } + } + rend->unlockTexture(csTex); + } + + ImGui::Image(rend->getTextureID(csTex),ImVec2(768.0*dpiScale,768.0*dpiScale)); + } + ImGui::EndTabItem(); + } if (ImGui::BeginTabItem(_("Stream Info"))) { ImGui::Text("%d bytes",(int)cs->getDataLen()); ImGui::Text("%u channels",cs->getFileChans()); @@ -538,6 +604,11 @@ void FurnaceGUI::drawCSPlayer() { ImGui::SameLine(); ImGui::Text("%d",cs->getFastCmds()[i]); } + ImGui::Text("stack sizes:"); + for (unsigned int i=0; igetFileChans(); i++) { + ImGui::SameLine(); + ImGui::Text("%d",cs->getChanState(i)->callStackSize); + } ImGui::Text("ticks: %u",cs->getCurTick()); ImGui::EndTabItem(); } diff --git a/src/gui/gui.h b/src/gui/gui.h index cdea368d9..9473fa5bd 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -1666,6 +1666,8 @@ class FurnaceGUI { int sampleTexW, sampleTexH; bool updateSampleTex; + FurnaceGUITexture* csTex; + String workingDir, fileName, clipboard, warnString, errorString, lastError, curFileName, nextFile, sysSearchQuery, newSongQuery, paletteQuery, sampleBankSearchQuery; String workingDirSong, workingDirIns, workingDirWave, workingDirSample, workingDirAudioExport; String workingDirVGMExport, workingDirROMExport; From 15d47cfe03b3f31cd8a5bd44ed4cd672e31869c4 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Thu, 4 Sep 2025 16:00:33 -0500 Subject: [PATCH 05/28] actually fix stage 20 comparison always was false --- src/gui/tutorial.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/tutorial.cpp b/src/gui/tutorial.cpp index f019d0d47..e06c52f6d 100644 --- a/src/gui/tutorial.cpp +++ b/src/gui/tutorial.cpp @@ -1346,7 +1346,7 @@ void FurnaceCV::buildStage(int which) { memset(busy,0,28*40*sizeof(bool)); // special stages - if ((which%10)==19) { + if ((which%20)==19) { for (int i=0; i<20+(which>>2); i++) { int tries=0; while (tries<20) { From d895a5724a2ad5209ad95c12ae56ef39429b107a Mon Sep 17 00:00:00 2001 From: freq-mod <32672779+freq-mod@users.noreply.github.com> Date: Thu, 4 Sep 2025 23:12:10 +0200 Subject: [PATCH 06/28] Clarify modulation description for two-tone mode --- doc/7-systems/pokey.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/7-systems/pokey.md b/doc/7-systems/pokey.md index 8bbb04b78..257b85d53 100644 --- a/doc/7-systems/pokey.md +++ b/doc/7-systems/pokey.md @@ -33,7 +33,7 @@ a sound and input chip developed by Atari for their 8-bit computers (Atari 400, - use for PWM effects (not automatic!). - bit 0: 15KHz mode. - `12xx`: **toggle two-tone mode.** - - when enabled, channel 2 modulates channel 1. I don't know how, but it does. + - when enabled, channel 2 modulates channel 1 in an oscillator sync-like manner. - only on ASAP core. ## info From 67c7afd4cdbaa9d9a63087ce6b4098a502138a83 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 5 Sep 2025 01:01:27 -0500 Subject: [PATCH 07/28] MMC5: fix env mode not set after reset/forceIns issue #2675 --- src/engine/platform/mmc5.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/engine/platform/mmc5.cpp b/src/engine/platform/mmc5.cpp index 672891bb4..bf8164012 100644 --- a/src/engine/platform/mmc5.cpp +++ b/src/engine/platform/mmc5.cpp @@ -377,6 +377,10 @@ void DivPlatformMMC5::forceIns() { for (int i=0; i<3; i++) { chan[i].insChanged=true; chan[i].prevFreq=65535; + if (i<2) { + // TODO: implement noise mode + rWrite(0x5000+i*4,(0x30)|(chan[i].active?chan[i].outVol:0)|((chan[i].duty&3)<<6)); + } } } @@ -429,6 +433,10 @@ void DivPlatformMMC5::reset() { rWrite(0x5015,0x03); rWrite(0x5010,0x00); + + for (int i=0; i<2; i++) { + rWrite(0x5000+i*4,(0x30)|0|((chan[i].duty&3)<<6)); + } } bool DivPlatformMMC5::keyOffAffectsArp(int ch) { From aa67f78d36f8705892d4071e8ceaec38dbaf3a95 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 5 Sep 2025 02:10:58 -0500 Subject: [PATCH 08/28] MMC5: fix typo in comment --- src/engine/platform/mmc5.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/platform/mmc5.cpp b/src/engine/platform/mmc5.cpp index bf8164012..fcb8dbdc6 100644 --- a/src/engine/platform/mmc5.cpp +++ b/src/engine/platform/mmc5.cpp @@ -378,7 +378,7 @@ void DivPlatformMMC5::forceIns() { chan[i].insChanged=true; chan[i].prevFreq=65535; if (i<2) { - // TODO: implement noise mode + // TODO: implement envelope mode rWrite(0x5000+i*4,(0x30)|(chan[i].active?chan[i].outVol:0)|((chan[i].duty&3)<<6)); } } From 0da42f18a6d503d1ec8d2b3b3cf499dfb0f94ffe Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 5 Sep 2025 13:26:52 -0500 Subject: [PATCH 09/28] T6W28: fix noise reset on all duty macro steps issue #2665 --- src/engine/platform/t6w28.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/platform/t6w28.cpp b/src/engine/platform/t6w28.cpp index c54ab9c9d..6ea2accc4 100644 --- a/src/engine/platform/t6w28.cpp +++ b/src/engine/platform/t6w28.cpp @@ -129,7 +129,7 @@ void DivPlatformT6W28::tick(bool sysTick) { chan[i].freqChanged=true; } if (i==3 && chan[i].std.duty.had) { - if (chan[i].duty!=chan[i].std.duty.val) { + if (chan[i].duty!=(((chan[i].std.duty.val==1)?4:0)|3)) { chan[i].duty=((chan[i].std.duty.val==1)?4:0)|3; rWrite(1,0xe0+chan[i].duty); } From 1e7bbf2045fe65fcbce253cb83a31fd003b445e3 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 5 Sep 2025 14:01:10 -0500 Subject: [PATCH 10/28] PFD debug for issue #2670 --- extern/pfd-fixed/portable-file-dialogs.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/extern/pfd-fixed/portable-file-dialogs.h b/extern/pfd-fixed/portable-file-dialogs.h index 8e5ca5079..134cc6cf7 100644 --- a/extern/pfd-fixed/portable-file-dialogs.h +++ b/extern/pfd-fixed/portable-file-dialogs.h @@ -652,6 +652,11 @@ inline void internal::executor::start(int exit_code) #else inline void internal::executor::start_process(std::vector const &command) { + printf("---- WILL EXECUTE:\n"); + for (std::string& i: command) { + printf("- %s\n",i.c_str()); + } + printf("END\n"); stop(); m_stdout.clear(); m_exit_code = -1; From 10e15790894fc17d2f29c35ed36b715ee4738f4b Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 5 Sep 2025 14:12:00 -0500 Subject: [PATCH 11/28] .......... --- extern/pfd-fixed/portable-file-dialogs.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/pfd-fixed/portable-file-dialogs.h b/extern/pfd-fixed/portable-file-dialogs.h index 134cc6cf7..6d152670e 100644 --- a/extern/pfd-fixed/portable-file-dialogs.h +++ b/extern/pfd-fixed/portable-file-dialogs.h @@ -653,7 +653,7 @@ inline void internal::executor::start(int exit_code) inline void internal::executor::start_process(std::vector const &command) { printf("---- WILL EXECUTE:\n"); - for (std::string& i: command) { + for (const std::string& i: command) { printf("- %s\n",i.c_str()); } printf("END\n"); From 710545e70629539547c3dc4662d65a48a306378e Mon Sep 17 00:00:00 2001 From: KungFuFurby Date: Fri, 5 Sep 2025 14:42:46 -0400 Subject: [PATCH 12/28] Add documentation for effect 16xy for FDS Automatic modulation speed, using effect 16xy, was not documented for the Famicom Disk System. This commit adds documentation. It was adapted from pre-existing documentation for envelope speeds that says roughly the same thing. --- doc/7-systems/fds.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/7-systems/fds.md b/doc/7-systems/fds.md index 42c0168dd..04cfdca9d 100644 --- a/doc/7-systems/fds.md +++ b/doc/7-systems/fds.md @@ -24,6 +24,11 @@ it also offers an additional 6-bit, 64-byte wavetable sound channel with (somewh - 6: -2 - 7: -1 - **do not use this effect.** it only exists for compatibility reasons +- `16xy`: **enable automatic modulation speed mode.** + - in this mode the modulation speed is set to the channel's notes, multiplied by a fraction. + - `x` is the numerator. + - `y` is the denominator. + - if `x` or `y` are 0 this will disable automatic modulation speed mode. ## info From 7709640aa0a61ea8c2a0f46b351bf01a2c340ec3 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 5 Sep 2025 18:50:28 -0500 Subject: [PATCH 13/28] GUI: possibly fix crash on audio export issue #2672 --- src/gui/gui.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/gui.cpp b/src/gui/gui.cpp index 8c3929022..3434a8490 100644 --- a/src/gui/gui.cpp +++ b/src/gui/gui.cpp @@ -2634,7 +2634,7 @@ void FurnaceGUI::exportAudio(String path, DivAudioExportModes mode) { e->findSongLength(loopOrder,loopRow,audioExportOptions.fadeOut,songFadeoutSectionLength,songHasSongEndCommand,songOrdersLengths,songLength); // for progress estimation songLoopedSectionLength=songLength; - for (int i=0; i Date: Sat, 6 Sep 2025 17:46:16 -0500 Subject: [PATCH 14/28] IT import: comments, channel names and pat names the latter two are MPT extensions this may not work on songs with MIDI macro setup info --- src/engine/fileOps/it.cpp | 97 +++++++++++++++++++++++++++++++++++++++ src/engine/safeReader.cpp | 26 +++++++++++ src/engine/safeReader.h | 3 ++ 3 files changed, 126 insertions(+) diff --git a/src/engine/fileOps/it.cpp b/src/engine/fileOps/it.cpp index ba6b6d743..0975ed698 100644 --- a/src/engine/fileOps/it.cpp +++ b/src/engine/fileOps/it.cpp @@ -363,6 +363,103 @@ bool DivEngine::loadIT(unsigned char* file, size_t len) { patPtr[i]=reader.readI(); } + // skip edit history if present + if (special&2) { + logD("skipping edit history..."); + unsigned short editHistSize=reader.readS(); + if (editHistSize>0) { + if (!reader.seek(editHistSize*8,SEEK_CUR)) { + logV("what? I wasn't expecting that from you."); + } + } + } + + // read extension blocks, if any + logD("looking for extensions..."); + bool hasExtensions=true; + while (hasExtensions) { + char extType[4]; + unsigned int extSize=0; + memset(extType,0,4); + + reader.read(extType,4); + extSize=reader.readI(); + + if (memcmp(extType,"PNAM",4)==0) { + logV("found MPT extension: pattern names"); + // check whether this block is valid + if (extSize>patCount*32) { + logV("block may not be valid"); + break; + } + // read pattern names + logV("reading pattern names..."); + for (unsigned int i=0; i<(extSize>>5); i++) { + DivPattern* p=ds.subsong[0]->pat[0].getPattern(i,true); + p->name=reader.readStringLatin1(32); + } + } else if (memcmp(extType,"CNAM",4)==0) { + logV("found MPT extension: channel names"); + // check whether this block is valid + if (extSize>DIV_MAX_CHANS*20) { + logV("block may not be valid"); + break; + } + // read channel names + logV("reading channel names..."); + for (unsigned int i=0; i<(extSize>>5); i++) { + String chanName=reader.readStringLatin1(20); + for (DivSubSong* j: ds.subsong) { + j->chanName[i]=chanName; + } + } + } else if (memcmp(extType,"CHFX",4)==0) { + logV("found MPT extension: channel effects"); + // skip (stop if we cannot seek) + if (!reader.seek(extSize,SEEK_CUR)) { + break; + } + } else if ( + extType[0]=='F' && + (extType[1]=='X' || (extType[1]>='0' && extType[1]<='9')) && + (extType[2]>='0' && extType[2]<='9') && + (extType[3]>='0' && extType[3]<='9') + ) { // effect slot + logV("found MPT extension: effect slot"); + // skip (stop if we cannot seek) + if (!reader.seek(extSize,SEEK_CUR)) { + break; + } + } else { + logV("no further extensions found... %.2x%.2x%.2x%.2x",extType[0],extType[1],extType[2],extType[3]); + hasExtensions=false; + } + } + + // read song comment + logD("reading song comment..."); + if (reader.seek(commentPtr,SEEK_SET)) { + try { + String comment=reader.readStringLatin1Special(commentLen); + + ds.notes=""; + ds.notes.reserve(comment.size()); + + for (char& i: comment) { + if (i=='\r') { + ds.notes+='\n'; + } else { + ds.notes+=i; + } + } + } catch (EndOfFileException& e) { + logW("couldn't read song comment due to premature end of file."); + } + } else { + logW("couldn't seek to comment!"); + } + + // read instruments for (int i=0; i=0xa0) { + ret.push_back(0xc0|(c>>6)); + ret.push_back(0x80|(c&63)); + } + } else { + ret.push_back(c); + } } else { ret.push_back(c); } @@ -297,6 +306,15 @@ String SafeReader::readStringWithEncoding(DivStringEncoding encoding) { ret.push_back(c); } } + } else if (encoding==DIV_ENCODING_LATIN1_SPECIAL) { + if (c&0x80) { + if (c>=0xa0) { + ret.push_back(0xc0|(c>>6)); + ret.push_back(0x80|(c&63)); + } + } else { + ret.push_back(c); + } } else { ret.push_back(c); } @@ -320,6 +338,14 @@ String SafeReader::readStringLatin1(size_t stlen) { return readStringWithEncoding(DIV_ENCODING_LATIN1,stlen); } +String SafeReader::readStringLatin1Special() { + return readStringWithEncoding(DIV_ENCODING_LATIN1_SPECIAL); +} + +String SafeReader::readStringLatin1Special(size_t stlen) { + return readStringWithEncoding(DIV_ENCODING_LATIN1_SPECIAL,stlen); +} + String SafeReader::readStringLine() { String ret; unsigned char c; diff --git a/src/engine/safeReader.h b/src/engine/safeReader.h index 04385c3b6..a3b0579d8 100644 --- a/src/engine/safeReader.h +++ b/src/engine/safeReader.h @@ -33,6 +33,7 @@ enum DivStringEncoding { DIV_ENCODING_NONE=0, DIV_ENCODING_UTF8, DIV_ENCODING_LATIN1, + DIV_ENCODING_LATIN1_SPECIAL, DIV_ENCODING_SHIFT_JIS }; @@ -77,6 +78,8 @@ class SafeReader { String readString(size_t len); String readStringLatin1(); String readStringLatin1(size_t len); + String readStringLatin1Special(); + String readStringLatin1Special(size_t len); String readStringLine(); String readStringToken(unsigned char delim, bool stripContiguous); String readStringToken(); From 3a66e0ceeeddd264187d47a1bccce116e09f5133 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 7 Sep 2025 04:34:59 -0500 Subject: [PATCH 15/28] T6W28: actually fix the phase reset issue issue #2665 --- src/engine/platform/t6w28.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/engine/platform/t6w28.cpp b/src/engine/platform/t6w28.cpp index 6ea2accc4..05d09f888 100644 --- a/src/engine/platform/t6w28.cpp +++ b/src/engine/platform/t6w28.cpp @@ -153,7 +153,9 @@ void DivPlatformT6W28::tick(bool sysTick) { chan[i].freqChanged=true; } if (chan[i].std.phaseReset.had) { - rWrite(1,0xe0+chan[i].duty); + if (chan[i].std.phaseReset.val==1) { + rWrite(1,0xe0+chan[i].duty); + } } if (chan[i].freqChanged || chan[i].keyOn || chan[i].keyOff) { chan[i].freq=snCalcFreq(i); From 9734c2ebaa6e970748457588514d144312e82449 Mon Sep 17 00:00:00 2001 From: freq-mod <32672779+freq-mod@users.noreply.github.com> Date: Sun, 7 Sep 2025 14:37:01 +0200 Subject: [PATCH 16/28] Correct comparison of Game.com to Nintendo Game Boy --- doc/7-systems/sm8521.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/7-systems/sm8521.md b/doc/7-systems/sm8521.md index e49852b3b..c243f1929 100644 --- a/doc/7-systems/sm8521.md +++ b/doc/7-systems/sm8521.md @@ -1,6 +1,6 @@ # Sharp SM8521 -the SM8521 is the CPU and sound chip of the Game.com, a handheld console released in 1997 as a competitor to the infamous Nintendo Virtual Boy. +the SM8521 is the CPU and sound chip of the Game.com, a handheld console released in 1997 as a competitor to the Nintendo Game Boy. sadly, the Game.com ended up being a failure as well, mostly due to poor quality games. the Game.com only lasted 3 years before being discontinued. From c75f36345336a9cbfd0af91779cd41df6574cea4 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 7 Sep 2025 17:14:24 -0500 Subject: [PATCH 17/28] XM import: actually consider pattern data size issue #2681 --- src/engine/fileOps/xm.cpp | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/engine/fileOps/xm.cpp b/src/engine/fileOps/xm.cpp index 0a798d090..6627d0e73 100644 --- a/src/engine/fileOps/xm.cpp +++ b/src/engine/fileOps/xm.cpp @@ -325,7 +325,9 @@ bool DivEngine::loadXM(unsigned char* file, size_t len) { for (unsigned short i=0; i=packedSeek) { + logV("end of data - stopping here..."); + break; + } for (int k=0; k=packedSeek) { + logV("end of data - stopping here..."); + break; + } for (int k=0; kpat[k].getPattern(i,true); From fb35e6f99399ab0edcb9ae938ce2321250823e61 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 7 Sep 2025 18:13:57 -0500 Subject: [PATCH 18/28] GUI: add a compressed font cache reduce memory usage by not loading the same font twice --- src/gui/fontzlib.cpp | 19 ++++++++++++++++--- src/gui/gui.h | 13 +++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/gui/fontzlib.cpp b/src/gui/fontzlib.cpp index c5f5fcf05..e2b9bcc48 100644 --- a/src/gui/fontzlib.cpp +++ b/src/gui/fontzlib.cpp @@ -38,9 +38,21 @@ struct InflateBlock { }; ImFont* FurnaceGUI::addFontZlib(const void* data, size_t len, float size_pixels, const ImFontConfig* font_cfg, const ImWchar* glyph_ranges) { + // find font in cache + logV("addFontZlib..."); + for (FurnaceGUIUncompFont& i: fontCache) { + if (i.origPtr==data && i.origLen==len) { + logV("found in cache"); + ImFontConfig fontConfig=(font_cfg==NULL)?ImFontConfig():(*font_cfg); + fontConfig.FontDataOwnedByAtlas=false; + + return ImGui::GetIO().Fonts->AddFontFromMemoryTTF(i.data,i.len,size_pixels,&fontConfig,glyph_ranges); + } + } + z_stream zl; memset(&zl,0,sizeof(z_stream)); - logV("addFontZlib..."); + logV("not found in cache - decompressing..."); zl.avail_in=len; zl.next_in=(Bytef*)data; @@ -116,10 +128,11 @@ ImFont* FurnaceGUI::addFontZlib(const void* data, size_t len, float size_pixels, delete i; } blocks.clear(); - len=finalSize; + + fontCache.push_back(FurnaceGUIUncompFont(data,len,finalData,finalSize)); ImFontConfig fontConfig=(font_cfg==NULL)?ImFontConfig():(*font_cfg); - fontConfig.FontDataOwnedByAtlas=true; + fontConfig.FontDataOwnedByAtlas=false; return ImGui::GetIO().Fonts->AddFontFromMemoryTTF(finalData,finalSize,size_pixels,&fontConfig,glyph_ranges); } diff --git a/src/gui/gui.h b/src/gui/gui.h index 9473fa5bd..a037090cb 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -1546,6 +1546,18 @@ struct FurnaceGUIPerfMetric { elapsed(0) {} }; +struct FurnaceGUIUncompFont { + const void* origPtr; + size_t origLen; + void* data; + size_t len; + FurnaceGUIUncompFont(const void* ptr, size_t len, void* d, size_t l): + origPtr(ptr), + origLen(len), + data(d), + len(l) {} +}; + struct FurnaceGUIBackupEntry { String name; uint64_t size; @@ -1778,6 +1790,7 @@ class FurnaceGUI { MIDIMap midiMap; int learning; + std::vector fontCache; ImFont* mainFont; ImFont* iconFont; ImFont* furIconFont; From 7cf66b56eed33f193fc81003aa421167036fe268 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Sun, 7 Sep 2025 18:22:14 -0500 Subject: [PATCH 19/28] GUI: load fallback fonts in header font issue #2400 --- src/gui/settings.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/gui/settings.cpp b/src/gui/settings.cpp index 173eb958b..c05cd678b 100644 --- a/src/gui/settings.cpp +++ b/src/gui/settings.cpp @@ -6761,7 +6761,7 @@ void FurnaceGUI::applyUISettings(bool updateFonts) { logW("could not load header font! reverting to default font"); settings.headFont=0; if ((headFont=addFontZlib(builtinFont[settings.headFont],builtinFontLen[settings.headFont],MAX(1,e->getConfInt("headFontSize",27)*dpiScale),&fontConfH))==NULL) { - logE("could not load header font! falling back to IBM Plex Sans."); + logE("could not load header font! falling back to fallback. wahahaha, get it? fallback."); headFont=ImGui::GetIO().Fonts->AddFontDefault(); } } @@ -6775,6 +6775,14 @@ void FurnaceGUI::applyUISettings(bool updateFonts) { } } + // four fallback fonts + if (settings.loadFallback) { + headFont=addFontZlib(font_plexSans_compressed_data,font_plexSans_compressed_size,MAX(1,e->getConfInt("headFontSize",27)*dpiScale),&fc1); + headFont=addFontZlib(font_plexSansJP_compressed_data,font_plexSansJP_compressed_size,MAX(1,e->getConfInt("headFontSize",27)*dpiScale),&fc1); + headFont=addFontZlib(font_plexSansKR_compressed_data,font_plexSansKR_compressed_size,MAX(1,e->getConfInt("headFontSize",27)*dpiScale),&fc1); + headFont=addFontZlib(font_unifont_compressed_data,font_unifont_compressed_size,MAX(1,e->getConfInt("headFontSize",27)*dpiScale),&fc1); + } + mainFont->FallbackChar='?'; mainFont->EllipsisChar='.'; //mainFont->EllipsisCharCount=3; From 17420d5d230b42a55d874743e6114a3cda8fa7c9 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Mon, 8 Sep 2025 02:32:18 -0500 Subject: [PATCH 20/28] QSound: fix loop end being used as sample end even if loop is disabled --- src/engine/platform/qsound.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/engine/platform/qsound.cpp b/src/engine/platform/qsound.cpp index 9b5da44dd..6b8b4089a 100644 --- a/src/engine/platform/qsound.cpp +++ b/src/engine/platform/qsound.cpp @@ -322,7 +322,7 @@ void DivPlatformQSound::tick(bool sysTick) { } int loopStart=s->loopStart; - int length=s->loopEnd; + int length=s->isLoopable()?s->loopEnd:s->samples; if (i<16) { if (length>65536-16) { length=65536-16; From 7a7d381889053f680a24b9d1045a364843c57fbb Mon Sep 17 00:00:00 2001 From: tildearrow Date: Mon, 8 Sep 2025 18:13:33 -0500 Subject: [PATCH 21/28] restore scroll text debug --- src/gui/debugWindow.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/gui/debugWindow.cpp b/src/gui/debugWindow.cpp index 1144a0d87..4153a5044 100644 --- a/src/gui/debugWindow.cpp +++ b/src/gui/debugWindow.cpp @@ -365,12 +365,10 @@ void FurnaceGUI::drawDebug() { ImGui::TreePop(); } if (ImGui::TreeNode("Scroll Text Test")) { - /* - ImGui::ScrollText(ImGui::GetID("scrolltest1"),"Lorem ipsum, quia dolor sit, amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt, ut labore et dolore magnam aliquam quaerat voluptatem. ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur?"); - ImGui::ScrollText(ImGui::GetID("scrolltest2"),"quis autem vel eum iure reprehenderit"); - ImGui::ScrollText(ImGui::GetID("scrolltest3"),"qui in ea voluptate velit esse",ImVec2(100.0f*dpiScale,0),true); - ImGui::ScrollText(ImGui::GetID("scrolltest4"),"quam nihil molestiae consequatur, vel illum, qui dolorem eum fugiat, quo voluptas nulla pariatur?",ImVec2(0,0),true); - */ + ImGui::ScrollText(ImGui::GetID("scrolltest1"),"Lorem ipsum, quia dolor sit, amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt, ut labore et dolore magnam aliquam quaerat voluptatem. ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur?",ImGui::GetCursorPos()); + ImGui::ScrollText(ImGui::GetID("scrolltest2"),"quis autem vel eum iure reprehenderit",ImGui::GetCursorPos()); + ImGui::ScrollText(ImGui::GetID("scrolltest3"),"qui in ea voluptate velit esse",ImGui::GetCursorPos(),ImVec2(100.0f*dpiScale,0),true); + ImGui::ScrollText(ImGui::GetID("scrolltest4"),"quam nihil molestiae consequatur, vel illum, qui dolorem eum fugiat, quo voluptas nulla pariatur?",ImGui::GetCursorPos(),ImVec2(0,0),true); ImGui::TreePop(); } if (ImGui::TreeNode("Pitch Table Calculator")) { From 3df67fb1b3c1a614842aaeb69b71dcc79ff9dad3 Mon Sep 17 00:00:00 2001 From: KungFuFurby Date: Tue, 9 Sep 2025 18:36:02 -0400 Subject: [PATCH 22/28] Add some commentary for special series of effects in S3M, XM and IT --- src/engine/fileOps/it.cpp | 6 +++--- src/engine/fileOps/s3m.cpp | 6 +++--- src/engine/fileOps/xm.cpp | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/engine/fileOps/it.cpp b/src/engine/fileOps/it.cpp index 0975ed698..facbecaf9 100644 --- a/src/engine/fileOps/it.cpp +++ b/src/engine/fileOps/it.cpp @@ -1516,15 +1516,15 @@ bool DivEngine::loadIT(unsigned char* file, size_t len) { break; case 'S': // special... switch (effectVal[chan]>>4) { - case 0x8: + case 0x8: // panning p->data[readRow][effectCol[chan]++]=0x80; p->data[readRow][effectCol[chan]++]=(effectVal[chan]&15)<<4; break; - case 0xc: + case 0xc: // note cut p->data[readRow][effectCol[chan]++]=0xec; p->data[readRow][effectCol[chan]++]=effectVal[chan]&15; break; - case 0xd: + case 0xd: // note delay p->data[readRow][effectCol[chan]++]=0xed; p->data[readRow][effectCol[chan]++]=effectVal[chan]&15; break; diff --git a/src/engine/fileOps/s3m.cpp b/src/engine/fileOps/s3m.cpp index f7d4ea22b..aacfc4fdf 100644 --- a/src/engine/fileOps/s3m.cpp +++ b/src/engine/fileOps/s3m.cpp @@ -1101,15 +1101,15 @@ bool DivEngine::loadS3M(unsigned char* file, size_t len) { break; case 'S': // special... switch (effectVal>>4) { - case 0x8: + case 0x8: // panning p->data[readRow][effectCol[chan]++]=0x80; p->data[readRow][effectCol[chan]++]=(effectVal&15)<<4; break; - case 0xc: + case 0xc: // note cut p->data[readRow][effectCol[chan]++]=0xec; p->data[readRow][effectCol[chan]++]=effectVal&15; break; - case 0xd: + case 0xd: // note delay p->data[readRow][effectCol[chan]++]=0xed; p->data[readRow][effectCol[chan]++]=effectVal&15; break; diff --git a/src/engine/fileOps/xm.cpp b/src/engine/fileOps/xm.cpp index 6627d0e73..f78973ee8 100644 --- a/src/engine/fileOps/xm.cpp +++ b/src/engine/fileOps/xm.cpp @@ -1147,11 +1147,11 @@ bool DivEngine::loadXM(unsigned char* file, size_t len) { case 0xe: // special... // TODO: implement the rest switch (effectVal>>4) { - case 0x5: + case 0x5: // fine tune p->data[j][effectCol[k]++]=0xe5; p->data[j][effectCol[k]++]=(effectVal&15)<<4; break; - case 0x9: + case 0x9: // retrigger p->data[j][effectCol[k]++]=0x0c; p->data[j][effectCol[k]++]=(effectVal&15); break; @@ -1171,11 +1171,11 @@ bool DivEngine::loadXM(unsigned char* file, size_t len) { } volSliding[k]=true; break; - case 0xc: + case 0xc: // note cut p->data[j][effectCol[k]++]=0xdc; p->data[j][effectCol[k]++]=MAX(1,effectVal&15); break; - case 0xd: + case 0xd: // note delay p->data[j][effectCol[k]++]=0xed; p->data[j][effectCol[k]++]=MAX(1,effectVal&15); break; From 0660e25f06a412aeae530c0c7b436182362f1e84 Mon Sep 17 00:00:00 2001 From: KungFuFurby Date: Tue, 9 Sep 2025 20:31:22 -0400 Subject: [PATCH 23/28] Add vibrato waveform import support for S3M, XM, MOD and IT Fine print: - OpenMPT hacks are not implemented here for MOD and XM for random waveform - Retrigger/Continuous setting is not handled for MOD, XM and S3M -- For S3M, I can't confirm if the setting even existed: I'm pretty sure it got cut out of Impulse Tracker. - Your waveform may sound different here than in the original player If tremolo and panbrello waveforms are implemented in Furnace, then their corresponding conversions can be implemented. For now, though, they can't make the cut since there is no corresponding effect to change those waveforms from the default setting. --- src/engine/fileOps/it.cpp | 20 ++++++++++++++++++++ src/engine/fileOps/mod.cpp | 14 ++++++++++++++ src/engine/fileOps/s3m.cpp | 20 ++++++++++++++++++++ src/engine/fileOps/xm.cpp | 17 +++++++++++++++++ 4 files changed, 71 insertions(+) diff --git a/src/engine/fileOps/it.cpp b/src/engine/fileOps/it.cpp index facbecaf9..5677dc0cc 100644 --- a/src/engine/fileOps/it.cpp +++ b/src/engine/fileOps/it.cpp @@ -1516,6 +1516,26 @@ bool DivEngine::loadIT(unsigned char* file, size_t len) { break; case 'S': // special... switch (effectVal[chan]>>4) { + case 0x3: // vibrato waveform + switch (effectVal[chan]&3) { + case 0x0: // sine + p->data[readRow][effectCol[chan]++]=0xe3; + p->data[readRow][effectCol[chan]++]=0x00; + break; + case 0x1: // ramp down + p->data[readRow][effectCol[chan]++]=0xe3; + p->data[readRow][effectCol[chan]++]=0x05; + break; + case 0x2: // square + p->data[readRow][effectCol[chan]++]=0xe3; + p->data[readRow][effectCol[chan]++]=0x06; + break; + case 0x3: // random + p->data[readRow][effectCol[chan]++]=0xe3; + p->data[readRow][effectCol[chan]++]=0x07; + break; + } + break; case 0x8: // panning p->data[readRow][effectCol[chan]++]=0x80; p->data[readRow][effectCol[chan]++]=(effectVal[chan]&15)<<4; diff --git a/src/engine/fileOps/mod.cpp b/src/engine/fileOps/mod.cpp index 353060bff..44df0a35e 100644 --- a/src/engine/fileOps/mod.cpp +++ b/src/engine/fileOps/mod.cpp @@ -356,6 +356,20 @@ bool DivEngine::loadMod(unsigned char* file, size_t len) { case 2: // single note slide down writeFxCol(fxTyp-1+0xf1,fxVal); break; + case 0x3: // vibrato waveform + switch (fxVal&3) { + case 0x0: // sine + writeFxCol(0xe3,0x00); + break; + case 0x1: // ramp down + writeFxCol(0xe3,0x05); + break; + case 0x2: // square + case 0x3: + writeFxCol(0xe3,0x06); + break; + } + break; case 9: // retrigger writeFxCol(0x0c,fxVal); break; diff --git a/src/engine/fileOps/s3m.cpp b/src/engine/fileOps/s3m.cpp index aacfc4fdf..2aaf6f757 100644 --- a/src/engine/fileOps/s3m.cpp +++ b/src/engine/fileOps/s3m.cpp @@ -1101,6 +1101,26 @@ bool DivEngine::loadS3M(unsigned char* file, size_t len) { break; case 'S': // special... switch (effectVal>>4) { + case 0x3: // vibrato waveform + switch (effectVal&3) { + case 0x0: // sine + p->data[readRow][effectCol[chan]++]=0xe3; + p->data[readRow][effectCol[chan]++]=0x00; + break; + case 0x1: // ramp down + p->data[readRow][effectCol[chan]++]=0xe3; + p->data[readRow][effectCol[chan]++]=0x05; + break; + case 0x2: // square + p->data[readRow][effectCol[chan]++]=0xe3; + p->data[readRow][effectCol[chan]++]=0x06; + break; + case 0x3: // random + p->data[readRow][effectCol[chan]++]=0xe3; + p->data[readRow][effectCol[chan]++]=0x07; + break; + } + break; case 0x8: // panning p->data[readRow][effectCol[chan]++]=0x80; p->data[readRow][effectCol[chan]++]=(effectVal&15)<<4; diff --git a/src/engine/fileOps/xm.cpp b/src/engine/fileOps/xm.cpp index f78973ee8..3e533eb11 100644 --- a/src/engine/fileOps/xm.cpp +++ b/src/engine/fileOps/xm.cpp @@ -1147,6 +1147,23 @@ bool DivEngine::loadXM(unsigned char* file, size_t len) { case 0xe: // special... // TODO: implement the rest switch (effectVal>>4) { + case 0x3: // vibrato waveform + switch (effectVal&3) { + case 0x0: // sine + p->data[j][effectCol[k]++]=0xe3; + p->data[j][effectCol[k]++]=0x00; + break; + case 0x1: // ramp down + p->data[j][effectCol[k]++]=0xe3; + p->data[j][effectCol[k]++]=0x05; + break; + case 0x2: // square + case 0x3: + p->data[j][effectCol[k]++]=0xe3; + p->data[j][effectCol[k]++]=0x06; + break; + } + break; case 0x5: // fine tune p->data[j][effectCol[k]++]=0xe5; p->data[j][effectCol[k]++]=(effectVal&15)<<4; From 6252843ade0a83e946f47896b4c3f1664d78963a Mon Sep 17 00:00:00 2001 From: KungFuFurby Date: Tue, 9 Sep 2025 21:56:34 -0400 Subject: [PATCH 24/28] Add envelope enable/disable and high nibble sample offset for IT Fine print: - Enabling and disabling panning envelopes take up two effect columns instead of one due to the macro being split into left and right sides. --- src/engine/fileOps/it.cpp | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/engine/fileOps/it.cpp b/src/engine/fileOps/it.cpp index 5677dc0cc..e31d78180 100644 --- a/src/engine/fileOps/it.cpp +++ b/src/engine/fileOps/it.cpp @@ -1536,10 +1536,46 @@ bool DivEngine::loadIT(unsigned char* file, size_t len) { break; } break; + case 0x7: + switch (effectVal[chan]&15) { + case 0x7: // volume envelope off + p->data[readRow][effectCol[chan]++]=0xf5; + p->data[readRow][effectCol[chan]++]=0x00; + break; + case 0x8: // volume envelope on + p->data[readRow][effectCol[chan]++]=0xf6; + p->data[readRow][effectCol[chan]++]=0x00; + break; + case 0x9: // panning envelope off + p->data[readRow][effectCol[chan]++]=0xf5; + p->data[readRow][effectCol[chan]++]=0x0c; + p->data[readRow][effectCol[chan]++]=0xf5; + p->data[readRow][effectCol[chan]++]=0x0d; + break; + case 0xa: // panning envelope on + p->data[readRow][effectCol[chan]++]=0xf6; + p->data[readRow][effectCol[chan]++]=0x0c; + p->data[readRow][effectCol[chan]++]=0xf6; + p->data[readRow][effectCol[chan]++]=0x0d; + break; + case 0xb: // pitch envelope off + p->data[readRow][effectCol[chan]++]=0xf5; + p->data[readRow][effectCol[chan]++]=0x04; + break; + case 0xc: //pitch envelope on + p->data[readRow][effectCol[chan]++]=0xf6; + p->data[readRow][effectCol[chan]++]=0x04; + break; + } + break; case 0x8: // panning p->data[readRow][effectCol[chan]++]=0x80; p->data[readRow][effectCol[chan]++]=(effectVal[chan]&15)<<4; break; + case 0xa: // offset (high nibble) + p->data[readRow][effectCol[chan]++]=0x92; + p->data[readRow][effectCol[chan]++]=effectVal[chan]&15; + break; case 0xc: // note cut p->data[readRow][effectCol[chan]++]=0xec; p->data[readRow][effectCol[chan]++]=effectVal[chan]&15; From 0549acc1d987207b6b4e76b672e830ab6e2674b0 Mon Sep 17 00:00:00 2001 From: KungFuFurby Date: Wed, 10 Sep 2025 01:12:07 -0400 Subject: [PATCH 25/28] Correct a typo where the wrong effect ID was used for vibrato waveform E4x is the correct ID, not E3x, for MOD and XM. Additionally, updated the coding style for MOD to be more consistent with the pre-existing code, namely by not using hex for the effect IDs being checked. --- src/engine/fileOps/mod.cpp | 10 +++++----- src/engine/fileOps/xm.cpp | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/engine/fileOps/mod.cpp b/src/engine/fileOps/mod.cpp index 44df0a35e..8cdd4a61f 100644 --- a/src/engine/fileOps/mod.cpp +++ b/src/engine/fileOps/mod.cpp @@ -356,16 +356,16 @@ bool DivEngine::loadMod(unsigned char* file, size_t len) { case 2: // single note slide down writeFxCol(fxTyp-1+0xf1,fxVal); break; - case 0x3: // vibrato waveform + case 4: // vibrato waveform switch (fxVal&3) { - case 0x0: // sine + case 0: // sine writeFxCol(0xe3,0x00); break; - case 0x1: // ramp down + case 1: // ramp down writeFxCol(0xe3,0x05); break; - case 0x2: // square - case 0x3: + case 2: // square + case 3: writeFxCol(0xe3,0x06); break; } diff --git a/src/engine/fileOps/xm.cpp b/src/engine/fileOps/xm.cpp index 3e533eb11..edc1cf14c 100644 --- a/src/engine/fileOps/xm.cpp +++ b/src/engine/fileOps/xm.cpp @@ -1147,7 +1147,7 @@ bool DivEngine::loadXM(unsigned char* file, size_t len) { case 0xe: // special... // TODO: implement the rest switch (effectVal>>4) { - case 0x3: // vibrato waveform + case 0x4: // vibrato waveform switch (effectVal&3) { case 0x0: // sine p->data[j][effectCol[k]++]=0xe3; From 825039ec5a9f694d585691eb569f00203d0c8432 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Wed, 10 Sep 2025 23:51:08 -0500 Subject: [PATCH 26/28] Lynx: write duty on forceIns() issue #2674 --- src/engine/platform/lynx.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/engine/platform/lynx.cpp b/src/engine/platform/lynx.cpp index 7a2db6025..6a78c0f46 100644 --- a/src/engine/platform/lynx.cpp +++ b/src/engine/platform/lynx.cpp @@ -492,6 +492,9 @@ void DivPlatformLynx::forceIns() { if (chan[i].active) { chan[i].insChanged=true; chan[i].freqChanged=true; + if (!chan[i].pcm) { + WRITE_FEEDBACK(i,chan[i].duty.feedback); + } } WRITE_ATTEN(i,chan[i].pan); } From d16bad802d98514d9de55d2f3e89d0d92723e663 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 12 Sep 2025 05:10:17 -0500 Subject: [PATCH 27/28] NDS: fix volume it was twice as loud and global volume would clip --- CMakeLists.txt | 10 +- src/engine/platform/nds.cpp | 56 ++- src/engine/platform/nds.h | 9 +- src/engine/platform/sound/nds.cpp | 23 +- src/engine/platform/sound/nds.hpp | 4 + src/engine/platform/sound/nds_unopt.cpp | 634 ++++++++++++++++++++++++ src/engine/platform/sound/nds_unopt.hpp | 415 ++++++++++++++++ 7 files changed, 1137 insertions(+), 14 deletions(-) create mode 100644 src/engine/platform/sound/nds_unopt.cpp create mode 100644 src/engine/platform/sound/nds_unopt.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index afc9689e2..542417135 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -140,6 +140,7 @@ option(CONSOLE_SUBSYSTEM "Build Furnace with Console subsystem on Windows" OFF) option(FORCE_CODEVIEW "Force -gcodeview on MinGW GCC" OFF) option(FLATPAK_WORKAROUNDS "Enable Flatpak-specific workaround for system file picker" OFF) option(NO_INTRO "Disable intro animation entirely" OFF) +option(ORIG_NDS_CORE "Use original NDS emulation core (no acquireDirect)" OFF) if (APPLE) option(FORCE_APPLE_BIN "Force enable binary installation to /bin" OFF) option(MAKE_BUNDLE "Make a bundle" OFF) @@ -674,8 +675,6 @@ src/engine/platform/sound/c140_c219.c src/engine/platform/sound/dave/dave.cpp -src/engine/platform/sound/nds.cpp - src/engine/platform/sound/sid2/envelope.cc src/engine/platform/sound/sid2/filter.cc src/engine/platform/sound/sid2/sid.cc @@ -830,6 +829,13 @@ src/engine/effect/abstract.cpp src/engine/effect/dummy.cpp ) +if (ORIG_NDS_CORE) + list(APPEND ENGINE_SOURCES src/engine/platform/sound/nds_unopt.cpp) + list(APPEND DEPENDENCIES_DEFINES ORIG_NDS_CORE) +else() + list(APPEND ENGINE_SOURCES src/engine/platform/sound/nds.cpp) +endif() + if (USE_SNDFILE) list(APPEND ENGINE_SOURCES src/engine/sfWrapper.cpp) endif() diff --git a/src/engine/platform/nds.cpp b/src/engine/platform/nds.cpp index 72be6f0bb..7fe0a3cd1 100644 --- a/src/engine/platform/nds.cpp +++ b/src/engine/platform/nds.cpp @@ -23,6 +23,7 @@ #include #define CHIP_DIVIDER 32 +#define NDS_CORE_QUALITY 64 #define rRead8(a) (nds.read8(a)) #define rWrite8(a,v) {if(!skipRegisterWrites){writes.push_back(QueuedWrite((a),1,(v)));regPool[(a)]=(v);if(dumpWrites)addWrite((a),(v));}} @@ -68,6 +69,45 @@ const char** DivPlatformNDS::getRegisterSheet() { return regCheatSheetNDS; } +#ifdef ORIG_NDS_CORE +void DivPlatformNDS::acquire(short** buf, size_t len) { + for (int i=0; i<16; i++) { + oscBuf[i]->begin(len); + } + + while (!writes.empty()) { + QueuedWrite w=writes.front(); + if (w.size==4) { + nds.write32(w.addr>>2,w.val); + } else if (w.size==2) { + nds.write16(w.addr>>1,w.val); + } else { + nds.write8(w.addr,w.val); + } + writes.pop(); + } + + for (size_t h=0; h32767) lout=32767; + if (lout<-32768) lout=-32768; + if (rout>32767) rout=32767; + if (rout<-32768) rout=-32768; + buf[0][h]=lout; + buf[1][h]=rout; + + for (int i=0; i<16; i++) { + oscBuf[i]->putSample(h,(nds.chan_lout(i)+nds.chan_rout(i))>>1); + } + } + + for (int i=0; i<16; i++) { + oscBuf[i]->end(len); + } +} +#else void DivPlatformNDS::acquireDirect(blip_buffer_t** bb, size_t len) { for (int i=0; i<16; i++) { oscBuf[i]->begin(len); @@ -95,13 +135,7 @@ void DivPlatformNDS::acquireDirect(blip_buffer_t** bb, size_t len) { oscBuf[i]->end(len); } } - -void DivPlatformNDS::postProcess(short* buf, int outIndex, size_t len, int sampleRate) { - // this is where we handle global volume. it is faster than doing it on each blip... - for (size_t i=0; i>7); - } -} +#endif u8 DivPlatformNDS::read_byte(u32 addr) { if (addrsetRate(rate); } diff --git a/src/engine/platform/nds.h b/src/engine/platform/nds.h index 2186529bb..71b2fa5c6 100644 --- a/src/engine/platform/nds.h +++ b/src/engine/platform/nds.h @@ -21,7 +21,11 @@ #define _NDS_H #include "../dispatch.h" +#ifdef ORIG_NDS_CORE +#include "sound/nds_unopt.hpp" +#else #include "sound/nds.hpp" +#endif using namespace nds_sound_emu; @@ -73,8 +77,11 @@ class DivPlatformNDS: public DivDispatch, public nds_sound_intf { virtual u8 read_byte(u32 addr) override; virtual void write_byte(u32 addr, u8 data) override; +#ifdef ORIG_NDS_CORE + virtual void acquire(short** buf, size_t len) override; +#else virtual void acquireDirect(blip_buffer_t** bb, size_t len) override; - virtual void postProcess(short* buf, int outIndex, size_t len, int sampleRate) override; +#endif virtual int dispatch(DivCommand c) override; virtual void* getChanState(int chan) override; virtual DivMacroInt* getChanMacroInt(int ch) override; diff --git a/src/engine/platform/sound/nds.cpp b/src/engine/platform/sound/nds.cpp index bc3f3076f..140182cf2 100644 --- a/src/engine/platform/sound/nds.cpp +++ b/src/engine/platform/sound/nds.cpp @@ -225,6 +225,9 @@ namespace nds_sound_emu { case 0x00: m_control = (m_control & ~mask) | (data & mask); + for (u8 i = 0; i < 16; i++) { + m_channel[i].setMasterVol(mvol()); + } break; case 0x04: mask &= 0x3ff; @@ -302,6 +305,7 @@ namespace nds_sound_emu m_ctl_repeat = bitfield(m_control, 27, 2); m_ctl_format = bitfield(m_control, 29, 2); m_ctl_busy = bitfield(m_control, 31); + computeVol(); if (bitfield(old ^ m_control, 31)) { @@ -393,14 +397,14 @@ namespace nds_sound_emu advance(); m_counter += 0x10000 - m_freq; } - m_output = (m_sample * m_ctl_volume) >> (7 + m_ctl_voldiv); - const s32 loutput = (m_output * lvol()) >> 7; - const s32 routput = (m_output * rvol()) >> 7; + m_output = (m_sample * m_final_volume) >> (14 + m_ctl_voldiv); + const s32 loutput = (m_output * lvol()) >> 8; + const s32 routput = (m_output * rvol()) >> 8; i+=cycle-1; if (m_loutput!=loutput || m_routput!=routput) { - m_oscBuf->putSample(i,(loutput+routput)>>1); + m_oscBuf->putSample(i,(loutput+routput)); } if (m_loutput!=loutput) { blip_add_delta(m_bb[0],i,loutput-m_loutput); @@ -559,6 +563,17 @@ namespace nds_sound_emu } } + void nds_sound_t::channel_t::computeVol() + { + m_final_volume = m_ctl_volume * m_master_volume; + } + + void nds_sound_t::channel_t::setMasterVol(s32 masterVol) + { + m_master_volume = masterVol; + computeVol(); + } + // capture void nds_sound_t::capture_t::reset() { diff --git a/src/engine/platform/sound/nds.hpp b/src/engine/platform/sound/nds.hpp index e410a82b8..70f303eb5 100644 --- a/src/engine/platform/sound/nds.hpp +++ b/src/engine/platform/sound/nds.hpp @@ -232,6 +232,7 @@ namespace nds_sound_emu void write(u32 offset, u32 data, u32 mask = ~0); void update(s32 cycle); + void setMasterVol(s32 masterVol); void set_bb(blip_buffer_t* bbLeft, blip_buffer_t* bbRight) { m_bb[0] = bbLeft; m_bb[1] = bbRight; } void set_oscbuf(DivDispatchOscBuffer* oscBuf) { m_oscBuf = oscBuf; } void resetTS(u32 what) { m_lastts = what; } @@ -272,6 +273,7 @@ namespace nds_sound_emu void keyoff(); void fetch(); void advance(); + void computeVol(); // interfaces nds_sound_t &m_host; // host device @@ -303,6 +305,8 @@ namespace nds_sound_emu // internal states bool m_playing = false; // playing flag + s32 m_final_volume = 0; // calculated volume + s32 m_master_volume = 0; // master volume cache s32 m_adpcm_out = 0; // current ADPCM sample value s32 m_adpcm_index = 0; // current ADPCM step s32 m_prev_adpcm_out = 0; // previous ADPCM sample value diff --git a/src/engine/platform/sound/nds_unopt.cpp b/src/engine/platform/sound/nds_unopt.cpp new file mode 100644 index 000000000..ca4368e5f --- /dev/null +++ b/src/engine/platform/sound/nds_unopt.cpp @@ -0,0 +1,634 @@ +/* + +============================================================================ + +NDS sound emulator +by cam900 + +This file is licensed under zlib license. + +============================================================================ + +zlib License + +(C) 2024-present cam900 and contributors + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + +============================================================================ +TODO: +- needs to further verifications from real hardware + +Tech info: https://problemkaputt.de/gbatek.htm + +*/ + +#include "nds_unopt.hpp" + +namespace nds_sound_emu +{ + void nds_sound_t::reset() + { + for (channel_t &elem : m_channel) + elem.reset(); + for (capture_t &elem : m_capture) + elem.reset(); + + m_control = 0; + m_bias = 0; + m_loutput = 0; + m_routput = 0; + } + + void nds_sound_t::tick(s32 cycle) + { + m_loutput = m_routput = (m_bias & 0x3ff); + if (!enable()) + return; + + // mix outputs + s32 lmix = 0, rmix = 0; + for (u8 i = 0; i < 16; i++) + { + channel_t &channel = m_channel[i]; + channel.update(cycle); + // bypass mixer + if (((i == 1) && (mix_ch1())) || ((i == 3) && (mix_ch3()))) + continue; + + lmix += channel.loutput(); + rmix += channel.routput(); + } + + // send mixer output to capture + m_capture[0].update(lmix, cycle); + m_capture[1].update(rmix, cycle); + + // select left/right output source + switch (lout_from()) + { + case 0: // left mixer + break; + case 1: // channel 1 + lmix = m_channel[1].loutput(); + break; + case 2: // channel 3 + lmix = m_channel[3].loutput(); + break; + case 3: // channel 1 + 3 + lmix = m_channel[1].loutput() + m_channel[3].loutput(); + break; + } + + switch (rout_from()) + { + case 0: // right mixer + break; + case 1: // channel 1 + rmix = m_channel[1].routput(); + break; + case 2: // channel 3 + rmix = m_channel[3].routput(); + break; + case 3: // channel 1 + 3 + rmix = m_channel[1].routput() + m_channel[3].routput(); + break; + } + + // adjust master volume + lmix = (lmix * mvol()) >> 13; + rmix = (rmix * mvol()) >> 13; + + // add bias and clip output + m_loutput = clamp((lmix + (m_bias & 0x3ff)), 0, 0x3ff); + m_routput = clamp((rmix + (m_bias & 0x3ff)), 0, 0x3ff); + } + + u8 nds_sound_t::read8(u32 addr) + { + return bitfield(read32(addr >> 2), bitfield(addr, 0, 2) << 3, 8); + } + + u16 nds_sound_t::read16(u32 addr) + { + return bitfield(read32(addr >> 1), bitfield(addr, 0) << 4, 16); + } + + u32 nds_sound_t::read32(u32 addr) + { + addr <<= 2; // word address + + switch (addr & 0x100) + { + case 0x000: + if ((addr & 0xc) == 0) + return m_channel[bitfield(addr, 4, 4)].control(); + break; + case 0x100: + switch (addr & 0xff) + { + case 0x00: + return m_control; + case 0x04: + return m_bias; + case 0x08: + return m_capture[0].control() | (m_capture[1].control() << 8); + case 0x10: + case 0x18: + return m_capture[bitfield(addr, 3)].dstaddr(); + default: + break; + } + break; + } + return 0; + } + + void nds_sound_t::write8(u32 addr, u8 data) + { + const u8 bit = bitfield(addr, 0, 2); + const u32 in = u32(data) << (bit << 3); + const u32 in_mask = 0xff << (bit << 3); + write32(addr >> 2, in, in_mask); + } + + void nds_sound_t::write16(u32 addr, u16 data, u16 mask) + { + const u8 bit = bitfield(addr, 0); + const u32 in = u32(data) << (bit << 4); + const u32 in_mask = u32(mask) << (bit << 4); + write32(addr >> 1, in, in_mask); + } + + void nds_sound_t::write32(u32 addr, u32 data, u32 mask) + { + addr <<= 2; // word address + + switch (addr & 0x100) + { + case 0x000: + m_channel[bitfield(addr, 4, 4)].write(bitfield(addr, 2, 2), data, mask); + break; + case 0x100: + switch (addr & 0xff) + { + case 0x00: + m_control = (m_control & ~mask) | (data & mask); + break; + case 0x04: + mask &= 0x3ff; + m_bias = (m_bias & ~mask) | (data & mask); + break; + case 0x08: + if (bitfield(mask, 0, 8)) + m_capture[0].control_w(data & 0xff); + if (bitfield(mask, 8, 8)) + m_capture[1].control_w((data >> 8) & 0xff); + break; + case 0x10: + case 0x14: + case 0x18: + case 0x1c: + m_capture[bitfield(addr, 3)].addrlen_w(bitfield(addr, 2), data, mask); + break; + default: + break; + } + break; + } + } + + // channels + void nds_sound_t::channel_t::reset() + { + m_control = 0; + m_sourceaddr = 0; + m_freq = 0; + m_loopstart = 0; + m_length = 0; + + m_playing = false; + m_adpcm_out = 0; + m_adpcm_index = 0; + m_prev_adpcm_out = 0; + m_prev_adpcm_index = 0; + m_cur_addr = 0; + m_cur_state = 0; + m_cur_bitaddr = 0; + m_delay = 0; + m_sample = 0; + m_lfsr = 0x7fff; + m_lfsr_out = 0x7fff; + m_counter = 0x10000; + m_output = 0; + m_loutput = 0; + m_routput = 0; + } + + void nds_sound_t::channel_t::write(u32 offset, u32 data, u32 mask) + { + const u32 old = m_control; + switch (offset & 3) + { + case 0: // Control/Status + m_control = (m_control & ~mask) | (data & mask); + if (bitfield(old ^ m_control, 31)) + { + if (busy()) + keyon(); + else if (!busy()) + keyoff(); + } + // reset hold flag + if (!m_playing && !hold()) + { + m_sample = m_lfsr_out = 0; + m_output = m_loutput = m_routput = 0; + } + break; + case 1: // Source address + mask &= 0x7ffffff; + m_sourceaddr = (m_sourceaddr & ~mask) | (data & mask); + break; + case 2: // Frequency, Loopstart + if (bitfield(mask, 0, 16)) + m_freq = (m_freq & bitfield(~mask, 0, 16)) | (bitfield(data & mask, 0, 16)); + if (bitfield(mask, 16, 16)) + m_loopstart = (m_loopstart & bitfield(~mask, 16, 16)) | (bitfield(data & mask, 16, 16)); + break; + case 3: // Length + mask &= 0x3fffff; + m_length = (m_length & ~mask) | (data & mask); + break; + } + } + + void nds_sound_t::channel_t::keyon() + { + if (!m_playing) + { + m_playing = true; + m_delay = format() == 2 ? 11 : 3; // 3 (11 for ADPCM) delay for playing sample + m_cur_bitaddr = m_cur_addr = 0; + m_cur_state = (format() == 2) ? STATE_ADPCM_LOAD : ((m_loopstart == 0) ? STATE_POST_LOOP : STATE_PRE_LOOP); + m_counter = 0x10000; + m_sample = 0; + m_lfsr_out = 0x7fff; + m_lfsr = 0x7fff; + } + } + + + void nds_sound_t::channel_t::keyoff() + { + if (m_playing) + { + if (busy()) + m_control &= ~(1 << 31); + if (!hold()) + { + m_sample = m_lfsr_out = 0; + m_output = m_loutput = m_routput = 0; + } + + m_playing = false; + } + } + + void nds_sound_t::channel_t::update(s32 cycle) + { + if (m_playing) + { + // get output + fetch(); + m_counter -= cycle; + while (m_counter <= m_freq) + { + // advance + advance(); + m_counter += 0x10000 - m_freq; + } + m_output = (m_sample * volume()) >> (7 + voldiv()); + m_loutput = (m_output * lvol()) >> 7; + m_routput = (m_output * rvol()) >> 7; + } + } + + void nds_sound_t::channel_t::fetch() + { + if (m_playing) + { + // fetch samples + switch (format()) + { + case 0: // PCM8 + m_sample = s16(m_host.m_intf.read_byte(addr()) << 8); + break; + case 1: // PCM16 + m_sample = m_host.m_intf.read_word(addr()); + break; + case 2: // ADPCM + m_sample = m_cur_state == STATE_ADPCM_LOAD ? 0 : m_adpcm_out; + break; + case 3: // PSG or Noise + m_sample = 0; + if (m_psg) // psg + m_sample = (duty() == 7) ? -0x7fff : ((m_cur_bitaddr < s32(u32(7) - duty())) ? -0x7fff : 0x7fff); + else if (m_noise) // noise + m_sample = m_lfsr_out; + break; + } + } + + // apply delay + if (format() != 3 && m_delay > 0) + m_sample = 0; + } + + void nds_sound_t::channel_t::advance() + { + if (m_playing) + { + // advance bit address + switch (format()) + { + case 0: // PCM8 + m_cur_bitaddr += 8; + break; + case 1: // PCM16 + m_cur_bitaddr += 16; + break; + case 2: // ADPCM + if (m_cur_state == STATE_ADPCM_LOAD) // load ADPCM data + { + if (m_cur_bitaddr == 0) + m_prev_adpcm_out = m_adpcm_out = m_host.m_intf.read_word(addr()); + if (m_cur_bitaddr == 16) + m_prev_adpcm_index = m_adpcm_index = clamp(m_host.m_intf.read_byte(addr()) & 0x7f, 0, 88); + } + else // decode ADPCM + { + const u8 input = bitfield(m_host.m_intf.read_byte(addr()), m_cur_bitaddr & 4, 4); + s32 diff = ((bitfield(input, 0, 3) * 2 + 1) * m_host.adpcm_diff_table[m_adpcm_index] / 8); + if (bitfield(input, 3)) diff = -diff; + m_adpcm_out = clamp(m_adpcm_out + diff, -0x8000, 0x7fff); + m_adpcm_index = clamp(m_adpcm_index + m_host.adpcm_index_table[bitfield(input, 0, 3)], 0, 88); + } + m_cur_bitaddr += 4; + break; + case 3: // PSG or Noise + if (m_psg) // psg + m_cur_bitaddr = (m_cur_bitaddr + 1) & 7; + else if (m_noise) // noise + { + if (bitfield(m_lfsr, 0)) + { + m_lfsr = (m_lfsr >> 1) ^ 0x6000; + m_lfsr_out = -0x7fff; + } + else + { + m_lfsr >>= 1; + m_lfsr_out = 0x7fff; + } + } + break; + } + + // address update + if (format() != 3) + { + // adjust delay + m_delay--; + + // update address, loop + while (m_cur_bitaddr >= 32) + { + // already loaded? + if (format() == 2 && m_cur_state == STATE_ADPCM_LOAD) + { + m_cur_state = m_loopstart == 0 ? STATE_POST_LOOP : STATE_PRE_LOOP; + } + m_cur_addr++; + if (m_cur_state == STATE_PRE_LOOP && m_cur_addr >= m_loopstart) + { + m_cur_state = STATE_POST_LOOP; + m_cur_addr = 0; + if (format() == 2) + { + m_prev_adpcm_out = m_adpcm_out; + m_prev_adpcm_index = m_adpcm_index; + } + } + else if (m_cur_state == STATE_POST_LOOP && m_cur_addr >= m_length) + { + switch (repeat()) + { + case 0: // manual; not correct? + case 2: // one-shot + case 3: // prohibited + keyoff(); + break; + case 1: // loop infinitely + if (format() == 2) + { + if (m_loopstart == 0) // reload ADPCM + { + m_cur_state = STATE_ADPCM_LOAD; + } + else // restore + { + m_adpcm_out = m_prev_adpcm_out; + m_adpcm_index = m_prev_adpcm_index; + } + } + m_cur_addr = 0; + break; + } + } + m_cur_bitaddr -= 32; + } + } + } + } + + // capture + void nds_sound_t::capture_t::reset() + { + m_control = 0; + m_dstaddr = 0; + m_length = 0; + + m_counter = 0x10000; + m_cur_addr = 0; + m_cur_waddr = 0; + m_cur_bitaddr = 0; + m_enable = false; + } + + void nds_sound_t::capture_t::control_w(u8 data) + { + const u8 old = m_control; + m_control = data; + if (bitfield(old ^ m_control, 7)) + { + if (busy()) + capture_on(); + else if (!busy()) + capture_off(); + } + } + + void nds_sound_t::capture_t::addrlen_w(u32 offset, u32 data, u32 mask) + { + switch (offset & 1) + { + case 0: // Destination Address + mask &= 0x7ffffff; + m_dstaddr = (m_dstaddr & ~mask) | (data & mask); + break; + case 1: // Buffer Length + mask &= 0xffff; + m_length = (m_length & ~mask) | (data & mask); + break; + } + } + + void nds_sound_t::capture_t::update(s32 mix, s32 cycle) + { + if (m_enable) + { + s32 inval = 0; + // get inputs + // TODO: hardware bugs aren't emulated, add mode behavior not verified + if (addmode()) + inval = get_source() ? m_input.output() + m_output.output() : mix; + else + inval = get_source() ? m_input.output() : mix; + + // clip output + inval = clamp(inval, -0x8000, 0x7fff); + + // update counter + m_counter -= cycle; + while (m_counter <= m_output.freq()) + { + // write to memory; TODO: verify write behavior + if (format()) // 8 bit output + { + m_fifo[m_fifo_head & 7].write_byte(m_cur_bitaddr & 0x18, (inval >> 8) & 0xff); + m_cur_bitaddr += 8; + } + else + { + m_fifo[m_fifo_head & 7].write_word(m_cur_bitaddr & 0x10, inval & 0xffff); + m_cur_bitaddr += 16; + } + + // update address + while (m_cur_bitaddr >= 32) + { + // clear FIFO empty flag + m_fifo_empty = false; + + // advance FIFO head position + m_fifo_head = (m_fifo_head + 1) & 7; + if ((m_fifo_head & fifo_mask()) == (m_fifo_tail & fifo_mask())) + m_fifo_full = true; + + // update loop + if (++m_cur_addr >= m_length) + { + if (repeat()) + m_cur_addr = 0; + else + capture_off(); + } + + if (m_fifo_full) + { + // execute FIFO + fifo_write(); + + // check repeat + if (m_cur_waddr >= m_length && repeat()) + m_cur_waddr = 0; + } + + m_cur_bitaddr -= 32; + } + m_counter += 0x10000 - m_output.freq(); + } + } + } + + bool nds_sound_t::capture_t::fifo_write() + { + if (m_fifo_empty) + return true; + + // clear FIFO full flag + m_fifo_full = false; + + // write FIFO data to memory + m_host.m_intf.write_dword(waddr(), m_fifo[m_fifo_tail].data()); + m_cur_waddr++; + + // advance FIFO tail position + m_fifo_tail = (m_fifo_tail + 1) & 7; + if ((m_fifo_head & fifo_mask()) == (m_fifo_tail & fifo_mask())) + m_fifo_empty = true; + + return m_fifo_empty; + } + + void nds_sound_t::capture_t::capture_on() + { + if (!m_enable) + { + m_enable = true; + + // reset address + m_cur_bitaddr = 0; + m_cur_addr = m_cur_waddr = 0; + m_counter = 0x10000; + + // reset FIFO + m_fifo_head = m_fifo_tail = 0; + m_fifo_empty = true; + m_fifo_full = false; + } + } + + void nds_sound_t::capture_t::capture_off() + { + if (m_enable) + { + // flush FIFO + while (m_cur_waddr < m_length) + { + if (fifo_write()) + break; + } + + m_enable = false; + if (busy()) + m_control &= ~(1 << 7); + } + } + +}; // namespace nds_sound_emu diff --git a/src/engine/platform/sound/nds_unopt.hpp b/src/engine/platform/sound/nds_unopt.hpp new file mode 100644 index 000000000..ef780784e --- /dev/null +++ b/src/engine/platform/sound/nds_unopt.hpp @@ -0,0 +1,415 @@ +/* + +============================================================================ + +NDS sound emulator +by cam900 + +This file is licensed under zlib license. + +============================================================================ + +zlib License + +(C) 2024-present cam900 and contributors + +This software is provided 'as-is', without any express or implied +warranty. In no event will the authors be held liable for any damages +arising from the use of this software. + +Permission is granted to anyone to use this software for any purpose, +including commercial applications, and to alter it and redistribute it +freely, subject to the following restrictions: + +1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. +3. This notice may not be removed or altered from any source distribution. + +============================================================================ +TODO: +- needs to further verifications from real hardware + +Tech info: https://problemkaputt.de/gbatek.htm + +*/ + +#ifndef NDS_SOUND_EMU_H +#define NDS_SOUND_EMU_H + +namespace nds_sound_emu +{ + using u8 = unsigned char; + using u16 = unsigned short; + using u32 = unsigned int; + using u64 = unsigned long long; + using s8 = signed char; + using s16 = signed short; + using s32 = signed int; + using s64 = signed long long; + + template + static const inline T bitfield(const T in, const u8 pos) + { + return (in >> pos) & 1; + } // bitfield + + template + static const inline T bitfield(const T in, const u8 pos, const u8 len) + { + return (in >> pos) & ((1 << len) - 1); + } // bitfield + + template + static const inline T clamp(const T in, const T min, const T max) + { + return (in < min) ? min : ((in > max) ? max : in); + } // clamp + + class nds_sound_intf + { + public: + nds_sound_intf() + { + } + + virtual u8 read_byte(u32 addr) { return 0; } + inline u16 read_word(u32 addr) { return read_byte(addr) | (u16(read_byte(addr + 1)) << 8); } + inline u32 read_dword(u32 addr) { return read_word(addr) | (u16(read_word(addr + 2)) << 16); } + + virtual void write_byte(u32 addr, u8 data) {} + inline void write_word(u32 addr, u16 data) + { + write_byte(addr, data & 0xff); + write_byte(addr + 1, data >> 8); + } + inline void write_dword(u32 addr, u32 data) + { + write_word(addr, data & 0xffff); + write_word(addr + 2, data >> 16); + } + }; + + class nds_sound_t + { + public: + nds_sound_t(nds_sound_intf &intf) + : m_intf(intf) + , m_channel{ + channel_t(*this, false, false), channel_t(*this, false, false), + channel_t(*this, false, false), channel_t(*this, false, false), + channel_t(*this, false, false), channel_t(*this, false, false), + channel_t(*this, false, false), channel_t(*this, false, false), + channel_t(*this, true, false), channel_t(*this, true, false), + channel_t(*this, true, false), channel_t(*this, true, false), + channel_t(*this, true, false), channel_t(*this, true, false), + channel_t(*this, false, true), channel_t(*this, false, true) + } + , m_capture{ + capture_t(*this, m_channel[0], m_channel[1]), + capture_t(*this, m_channel[2], m_channel[3]) + } + , m_control(0) + , m_bias(0) + , m_loutput(0) + , m_routput(0) + { + } + + void reset(); + void tick(s32 cycle); + + // host accesses + u32 read32(u32 addr); + void write32(u32 addr, u32 data, u32 mask = ~0); + + u16 read16(u32 addr); + void write16(u32 addr, u16 data, u16 mask = ~0); + + u8 read8(u32 addr); + void write8(u32 addr, u8 data); + + s32 loutput() { return m_loutput; } + s32 routput() { return m_routput; } + + // for debug + s32 chan_lout(u8 ch) { return m_channel[ch].loutput(); } + s32 chan_rout(u8 ch) { return m_channel[ch].routput(); } + + private: + // ADPCM tables + s8 adpcm_index_table[8] = + { + -1, -1, -1, -1, 2, 4, 6, 8 + }; + + u16 adpcm_diff_table[89] = + { + 0x0007, 0x0008, 0x0009, 0x000a, 0x000b, 0x000c, 0x000d, 0x000e, 0x0010, + 0x0011, 0x0013, 0x0015, 0x0017, 0x0019, 0x001c, 0x001f, 0x0022, 0x0025, + 0x0029, 0x002d, 0x0032, 0x0037, 0x003c, 0x0042, 0x0049, 0x0050, 0x0058, + 0x0061, 0x006b, 0x0076, 0x0082, 0x008f, 0x009d, 0x00ad, 0x00be, 0x00d1, + 0x00e6, 0x00fd, 0x0117, 0x0133, 0x0151, 0x0173, 0x0198, 0x01c1, 0x01ee, + 0x0220, 0x0256, 0x0292, 0x02d4, 0x031c, 0x036c, 0x03c3, 0x0424, 0x048e, + 0x0502, 0x0583, 0x0610, 0x06ab, 0x0756, 0x0812, 0x08e0, 0x09c3, 0x0abd, + 0x0bd0, 0x0cff, 0x0e4c, 0x0fba, 0x114c, 0x1307, 0x14ee, 0x1706, 0x1954, + 0x1bdc, 0x1ea5, 0x21b6, 0x2515, 0x28ca, 0x2cdf, 0x315b, 0x364b, 0x3bb9, + 0x41b2, 0x4844, 0x4f7e, 0x5771, 0x602f, 0x69ce, 0x7462, 0x7fff + }; + + // structs + enum + { + STATE_ADPCM_LOAD = 0, + STATE_PRE_LOOP, + STATE_POST_LOOP + }; + + class channel_t + { + public: + channel_t(nds_sound_t &host, bool psg, bool noise) + : m_host(host) + + , m_psg(psg) + , m_noise(noise) + + , m_control(0) + , m_sourceaddr(0) + , m_freq(0) + , m_loopstart(0) + , m_length(0) + , m_playing(false) + , m_adpcm_out(0) + , m_adpcm_index(0) + , m_prev_adpcm_out(0) + , m_prev_adpcm_index(0) + , m_cur_addr(0) + , m_cur_state(0) + , m_cur_bitaddr(0) + , m_delay(0) + , m_sample(0) + , m_lfsr(0x7fff) + , m_lfsr_out(0x7fff) + , m_counter(0x10000) + , m_output(0) + , m_loutput(0) + , m_routput(0) + { + } + + void reset(); + void write(u32 offset, u32 data, u32 mask = ~0); + + void update(s32 cycle); + + // getters + // control word + u32 control() const { return m_control; } + u32 freq() const { return m_freq; } + + // outputs + s32 output() const { return m_output; } + s32 loutput() const { return m_loutput; } + s32 routput() const { return m_routput; } + + private: + // inline constants + const u8 m_voldiv_shift[4] = {0, 1, 2, 4}; + + // control bits + s32 volume() const { return bitfield(m_control, 0, 7); } // global volume + u32 voldiv() const { return m_voldiv_shift[bitfield(m_control, 8, 2)]; } // volume shift + bool hold() const { return bitfield(m_control, 15); } // hold bit + u32 pan() const { return bitfield(m_control, 16, 7); } // panning (0...127, 0 = left, 127 = right, 64 = half) + u32 duty() const { return bitfield(m_control, 24, 3); } // PSG duty + u32 repeat() const { return bitfield(m_control, 27, 2); } // Repeat mode (Manual, Loop infinitely, One-shot) + u32 format() const { return bitfield(m_control, 29, 2); } // Sound Format (PCM8, PCM16, ADPCM, PSG/Noise when exists) + bool busy() const { return bitfield(m_control, 31); } // Busy flag + + // calculated values + s32 lvol() const { return (pan() == 0x7f) ? 0 : 128 - pan(); } // calculated left volume + s32 rvol() const { return (pan() == 0x7f) ? 128 : pan(); } // calculated right volume + + // calculated address + u32 addr() const { return (m_sourceaddr & ~3) + (m_cur_bitaddr >> 3) + (m_cur_state == STATE_POST_LOOP ? ((m_loopstart + m_cur_addr) << 2) : (m_cur_addr << 2)); } + + void keyon(); + void keyoff(); + void fetch(); + void advance(); + + // interfaces + nds_sound_t &m_host; // host device + + // configuration + bool m_psg = false; // PSG Enable + bool m_noise = false; // Noise Enable + + // registers + u32 m_control = 0; // Control + u32 m_sourceaddr = 0; // Source Address + u16 m_freq = 0; // Frequency + u16 m_loopstart = 0; // Loop Start + u32 m_length = 0; // Length + + // internal states + bool m_playing = false; // playing flag + s32 m_adpcm_out = 0; // current ADPCM sample value + s32 m_adpcm_index = 0; // current ADPCM step + s32 m_prev_adpcm_out = 0; // previous ADPCM sample value + s32 m_prev_adpcm_index = 0; // previous ADPCM step + u32 m_cur_addr = 0; // current address + s32 m_cur_state = 0; // current state + s32 m_cur_bitaddr = 0; // bit address + s32 m_delay = 0; // delay + s16 m_sample = 0; // current sample + u32 m_lfsr = 0x7fff; // noise LFSR + s16 m_lfsr_out = 0x7fff; // LFSR output + s32 m_counter = 0x10000; // clock counter + s32 m_output = 0; // current output + s32 m_loutput = 0; // current left output + s32 m_routput = 0; // current right output + }; + + class capture_t + { + public: + capture_t(nds_sound_t &host, channel_t &input, channel_t &output) + : m_host(host) + , m_input(input) + , m_output(output) + + , m_control(0) + , m_dstaddr(0) + , m_length(0) + + , m_counter(0x10000) + , m_cur_addr(0) + , m_cur_waddr(0) + , m_cur_bitaddr(0) + , m_enable(0) + + , m_fifo{ + fifo_data_t(), fifo_data_t(), fifo_data_t(), fifo_data_t(), + fifo_data_t(), fifo_data_t(), fifo_data_t(), fifo_data_t() + } + , m_fifo_head(0) + , m_fifo_tail(0) + , m_fifo_empty(true) + , m_fifo_full(false) + { + } + + void reset(); + void update(s32 mix, s32 cycle); + + void control_w(u8 data); + void addrlen_w(u32 offset, u32 data, u32 mask = ~0); + + // getters + u32 control() const { return m_control; } + u32 dstaddr() const { return m_dstaddr; } + + private: + // inline constants + // control bits + bool addmode() const { return bitfield(m_control, 0); } // Add mode (add channel 1/3 output with channel 0/2) + bool get_source() const { return bitfield(m_control, 1); } // Select source (left or right mixer, channel 0/2) + bool repeat() const { return bitfield(m_control, 2); } // repeat flag + bool format() const { return bitfield(m_control, 3); } // store format (PCM16, PCM8) + bool busy() const { return bitfield(m_control, 7); } // busy flag + + // FIFO offset mask + u32 fifo_mask() const { return format() ? 7 : 3; } + + // calculated address + u32 waddr() const { return (m_dstaddr & ~3) + (m_cur_waddr << 2); } + + void capture_on(); + void capture_off(); + bool fifo_write(); + + // interfaces + nds_sound_t &m_host; // host device + channel_t &m_input; // Input channel + channel_t &m_output; // Output channel + + // registers + u8 m_control = 0; // Control + u32 m_dstaddr = 0; // Destination Address + u32 m_length = 0; // Buffer Length + + // internal states + u32 m_counter = 0x10000; // clock counter + u32 m_cur_addr = 0; // current address + u32 m_cur_waddr = 0; // current write address + s32 m_cur_bitaddr = 0; // bit address + bool m_enable = false; // capture enable + + // FIFO + class fifo_data_t + { + public: + fifo_data_t() + : m_data(0) + { + } + + void reset() + { + m_data = 0; + } + + // accessors + void write_byte(const u8 bit, const u8 data) + { + u32 input = u32(data) << bit; + u32 mask = (0xff << bit); + m_data = (m_data & ~mask) | (input & mask); + } + + void write_word(const u8 bit, const u16 data) + { + u32 input = u32(data) << bit; + u32 mask = (0xffff << bit); + m_data = (m_data & ~mask) | (input & mask); + } + + // getters + u32 data() const { return m_data; } + + private: + u32 m_data = 0; + }; + + fifo_data_t m_fifo[8]; // FIFO (8 word, for 16 sample delay) + u32 m_fifo_head = 0; // FIFO head + u32 m_fifo_tail = 0; // FIFO tail + bool m_fifo_empty = true; // FIFO empty flag + bool m_fifo_full = false; // FIFO full flag + }; + + nds_sound_intf &m_intf; // memory interface + + channel_t m_channel[16]; // 16 channels + capture_t m_capture[2]; // 2 capture channels + + inline u8 mvol() const { return bitfield(m_control, 0, 7); } // master volume + inline u8 lout_from() const { return bitfield(m_control, 8, 2); } // left output source (mixer, channel 1, channel 3, channel 1+3) + inline u8 rout_from() const { return bitfield(m_control, 10, 2); } // right output source (mixer, channel 1, channel 3, channel 1+3) + inline bool mix_ch1() const { return bitfield(m_control, 12); } // mix/bypass channel 1 + inline bool mix_ch3() const { return bitfield(m_control, 13); } // mix/bypass channel 3 + inline bool enable() const { return bitfield(m_control, 15); } // global enable + + u32 m_control = 0; // global control + u32 m_bias = 0; // output bias + s32 m_loutput = 0; // left output + s32 m_routput = 0; // right output + }; +}; // namespace nds_sound_emu + +#endif // NDS_SOUND_EMU_H From ef61241c399bab901c4523a650b740c248686b6c Mon Sep 17 00:00:00 2001 From: tildearrow Date: Fri, 12 Sep 2025 05:11:48 -0500 Subject: [PATCH 28/28] replace Wandering.fur with original version now that volume is fixed --- demos/blank/Wandering.fur | Bin 1970 -> 1973 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/demos/blank/Wandering.fur b/demos/blank/Wandering.fur index 47bb3315744063d06c8827ac53a9aeb048733f8f..1448b56fe572161506b7920ea088b95510b80b57 100644 GIT binary patch literal 1973 zcmV;m2TJ&Oob6dnY+P3rKIgsrX8fBW1GM~TV49Gyq2kA-D=asSDJp`*wPPoWq6(9- zAc;NWc{3i*#7#WUGm~Hhp-2@Hkd_Ta2vOL0(FI5>5JIAeE`SAPF@hCEED)QN1;cmG z%=@|V_%Dv*V)1;x`|dgSobR4{-hJ2a&3p8z1vfi=aV9;N%Ph>!Jo*L^r71j~KK0b; z=M@^Z&A)vu^JJBJNWG)ocMkVcE*r_1QMh@&3-a=dS*U zAL%puhplW2w~*zp61#KkctMW8`s@h#D^3^XFFs3)^ez0gk5iYj#hGj-mmZyF0SmAJ1vo$qhyy)9FVF|{0|USya0o~ML{0cN;opRR z6aG#3H{suee-r*q_&4F-gntwMP53wA--N&R^@Fiv=0UGws=owY2TqJWJ+3;I8@0zy zoH-Tu>HgCSMK(pt6m3&frsxq8GFv9IWinePvt=?{CbMNS+a|MZ zGTSDzZ8F;?vu!dflUbR}%4Ak1voe{L$?TZSj>+tp%#O+In9PpJ95b0?CUeYWj+x9c zlR0KG$4%zA$s9MC<0f<5WR3^S!&=0COv5APqe-R4tBF-EH@JY}aar0W6u%wS=TU^@ zq>%7VNorw1S3VMtwYzxQB=$X!cwAZKN`ypdlBIxT$#-0mb_K=btE+sq!3!vUTN6ey z<7kpfKmxBKN_(sZFQRNCGvCovz4}Te@~LF3+G?m?rSEwspYqhZ)gjeO&TML>twqa2 zM>m^XV3XU>W;;BW1Dne!iSU+0$omsv&4BjnMAgol^)}Ai3lgEd{}DQFD7w+?>AN6N zG-E4nG;T4~&BhIET-X#sn?h)l4Q#R-+C<}ztU7&*C0sK?H}skdyyotnW?(ZP*v#*K z)qPG!h38sIO}a_d;5CmY>%7Ke4IV=?R7^B@!ceTUIN#v&!ufgOiljvJ)V-rR3uPbkJLGz^jyesE>ys2%Hy-5II9)o4IVcX7aDva5-8SAYRc!F z(gx2o_?cjItvJ)*GuzE*kC5X3EU!fd>ECUJ&1}4>Nx?}sh7&7ClXiVXO*-|Xs8&$% z8aY*8ke1dgYvG!%mO7eia;~+_w(u6sb%SC@{^saaX;$@r+`8w~hrHoLHIr0*Ef{7H zSjKafaIOI5jLi5iGGjhCGd|X{mT8g7pm+6RVhQ;Vl~1pa}f$b*_{iu#%6cvQa; z=39%n+z|Z@(buI>m$oN1p5I<3z9#LcJ?hnNzAJ%?%;&7k2iq)P_xL#()$_RK>ALEP z($mUzK-%D&T2m&jkgiL_i(Gzz3&cgzb)MMe!q++*>=mKc8*Y774~-aBJWbY~V>|LC z($CuZ!M~W&QR( zpr6o(@{>uf|5P1u<|a`Ub(Pmud2+ojALz<{N2v{P$Gk}SQcYk@ec#pOWU5xuLc zoF-Td2(D=HdLZU@LBA)_YY*qkM)XceS62+3tx5jBe!rJ&w`A_`n(f9XVPti+5QvraI?36X z5|=m>LD?BcsQ-=-lC4;B0%!_-Tvu1>Dk6$0JW zpku*IZq~HhoKK7|Z<+r{UiHiNkn4xM^(Bd1x=PRyrgYw1eLLbSm|34SbpJbhtbB19 zl%HYq*hl(W*7xxsRoiokHjy6m=#q$pB!Uk_BD{N)x}>GOKCAJGk~v;vkC2Zl8)j@TB)VIAd+iZfeQO-jKMAJ3yWH0z z^$B%Y(dym_GU_E6mGAec-wuVa6`7{>fT!6W)!yS#Z+9J4=}}*b+HF&KamPaBk~(_S z+Z#l;nXvOqsuwLKU)OzR&HZ*XkjzURm3$!uoIxsd%9jTI*yr!q2?O@A(p z`k@EW`={e%7_97=eGlfpAz$wS{|35kqK^Uh0}laT0Zswu0S9n_r-7dVzXE;-yaxOg zco%pd=!y~D3mgT$1PlXXz&C+BPz8PfJO{i4{1$i>_zUn3a0`gXiS7Z801pDkfl*)* zm<1Mr8^E)`FM)poU+zMD!0&XWLYv$pDK$7U#hlvh;1mgm41Gj+kJw$KZOLX$%M72*~{+}coNFjz#VXmJhI+w<^ zxR2<`Lqzk3iT(jR@fo5&0$)2q^fK^;qeNc?CV&gTQ%BY1Figwd@!8bR@!s~i>$d)g zAE`6@hiRDxZXv^8C1(5B&YBz|f5oX{`HRm`BK-(|t>eUnY;h`^$)$(K=#lBk?4|Va z95jVuyT3Z*6CU!Ht}wjWWWWGSzyfR_2E>6bpd07`dVxNmA2~j-5IY z_v!xA3`N#OLl;e5v~aT2b!Jm%wsdAoXSQ@^OJ}xpW=m(bb!J;&$VTIj%Fu1Li>`Vn3$Aq4MFRWsO!7%Ut%jfZ}pl+9edf9o6Sigygu8 z@K#A`VL?|u5|6R7cv>XpHb`8qEORA7Vkwe^fMmgUT#$AJ#pTP(eA(kU6u+$qBbl)k zNhKhGR}rN(mdEobo5;*h6jisnQi*&j8LKuutJ_ldyq!-aRyWzxsil?{Ef*b~ zY;u83ZcUrb@LUXRE~X^H8xkS!O@uWK%C8+&D{saJIB(BLgy!ByXgi_kM6;*vf<#e| zt+>{>#Z)I7C$Mo~QwVJep-nch$*yS=jX$#L)Gd~9^a!2MYbNlTxqX^}&1_&ZyYp4| zbrlt!YbiDEBrT6uTpq9U3Xga^f@Y`~^LR{Ctg<-k@mb;ghH&!sIcs4-|A|PT#x4Yk z1)Vy(6Hb>Wq%jc*6xSnl#wR@!a-0bjaGG%Wv?xw1#i+-ln&OD$t|A2|T^md+9Zs6{A#2>OA4au=idV?4 z`hv8SW>E=ORJG97T#<9FWwwF0Xs&7$Tk_XOuS&D3{^QnNyFTC!CaRgF)zgGw27zT< zdjaPPP|nDV-;x>g{+aQSu9ZxatPF}PHHysimdq5lWLJ+jm?+`7LFQUkOrcJi%7 zT&{`!n&|7&s7u=w8`p0y5?_&a)E@P6C*P&OMdou_=7VjPue$t{jOw~vb5&h+Md>PK zDhmtJ4sVjjvt;EGZ+!DQ`s+=O2 z4+t(P@oFIEc0j)?(JK%8+gkK?NmZ9Lo#g+dMtt{T z+^o1(T3gH7`2VurqhxC*L8`Xv60IZM?a?I>2}uO+i$u8FlscrPwLXk}k(M2;kAJBL?|yTCCc;@u zimUQbIm$K0n+Os}F@=suRIi?p_hqM}_C-dzulI`km|rU8R1}|AkWh7v=|6 Empa(mO#lD@