12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744574557465747574857495750575157525753575457555756575757585759576057615762576357645765576657675768576957705771577257735774577557765777577857795780578157825783578457855786578757885789579057915792579357945795579657975798579958005801580258035804580558065807580858095810581158125813581458155816581758185819582058215822582358245825582658275828582958305831583258335834583558365837583858395840584158425843584458455846584758485849585058515852585358545855585658575858585958605861586258635864586558665867586858695870587158725873587458755876587758785879588058815882588358845885588658875888588958905891589258935894589558965897589858995900590159025903590459055906590759085909591059115912591359145915591659175918591959205921592259235924592559265927592859295930593159325933593459355936593759385939594059415942594359445945594659475948594959505951595259535954595559565957595859595960596159625963596459655966596759685969597059715972597359745975597659775978597959805981598259835984598559865987598859895990599159925993599459955996599759985999600060016002600360046005600660076008600960106011601260136014601560166017601860196020602160226023602460256026602760286029603060316032603360346035603660376038603960406041604260436044604560466047604860496050605160526053605460556056605760586059606060616062606360646065606660676068606960706071607260736074607560766077607860796080608160826083608460856086608760886089609060916092609360946095609660976098609961006101610261036104610561066107610861096110611161126113611461156116611761186119612061216122612361246125612661276128612961306131613261336134613561366137613861396140614161426143614461456146614761486149615061516152615361546155615661576158615961606161616261636164616561666167616861696170617161726173617461756176617761786179618061816182618361846185618661876188618961906191619261936194619561966197619861996200620162026203620462056206620762086209621062116212621362146215621662176218621962206221622262236224622562266227622862296230623162326233623462356236623762386239624062416242624362446245624662476248624962506251625262536254625562566257625862596260626162626263626462656266626762686269627062716272627362746275627662776278627962806281628262836284628562866287628862896290629162926293629462956296629762986299630063016302630363046305630663076308630963106311631263136314631563166317631863196320632163226323632463256326632763286329633063316332633363346335633663376338633963406341634263436344 |
- /*
- * Snd_fx.cpp
- * -----------
- * Purpose: Processing of pattern commands, song length calculation...
- * Notes : This needs some heavy refactoring.
- * I thought of actually adding an effect interface class. Every pattern effect
- * could then be moved into its own class that inherits from the effect interface.
- * If effect handling differs severly between module formats, every format would have
- * its own class for that effect. Then, a call chain of effect classes could be set up
- * for each format, since effects cannot be processed in the same order in all formats.
- * Authors: Olivier Lapicque
- * OpenMPT Devs
- * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
- */
- #include "stdafx.h"
- #include "Sndfile.h"
- #include "mod_specifications.h"
- #ifdef MODPLUG_TRACKER
- #include "../mptrack/Moddoc.h"
- #endif // MODPLUG_TRACKER
- #include "tuning.h"
- #include "Tables.h"
- #include "modsmp_ctrl.h" // For updating the loop wraparound data with the invert loop effect
- #include "plugins/PlugInterface.h"
- #include "OPL.h"
- #include "MIDIEvents.h"
- OPENMPT_NAMESPACE_BEGIN
- // Formats which have 7-bit (0...128) instead of 6-bit (0...64) global volume commands, or which are imported to this range (mostly formats which are converted to IT internally)
- #ifdef MODPLUG_TRACKER
- static constexpr auto GLOBALVOL_7BIT_FORMATS_EXT = MOD_TYPE_MT2;
- #else
- static constexpr auto GLOBALVOL_7BIT_FORMATS_EXT = MOD_TYPE_NONE;
- #endif // MODPLUG_TRACKER
- static constexpr auto GLOBALVOL_7BIT_FORMATS = MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_IMF | MOD_TYPE_J2B | MOD_TYPE_MID | MOD_TYPE_AMS | MOD_TYPE_DBM | MOD_TYPE_PTM | MOD_TYPE_MDL | MOD_TYPE_DTM | GLOBALVOL_7BIT_FORMATS_EXT;
- // Compensate frequency slide LUTs depending on whether we are handling periods or frequency - "up" and "down" in function name are seen from frequency perspective.
- static uint32 GetLinearSlideDownTable (const CSoundFile *sndFile, uint32 i) { MPT_ASSERT(i < std::size(LinearSlideDownTable)); return sndFile->m_playBehaviour[kPeriodsAreHertz] ? LinearSlideDownTable[i] : LinearSlideUpTable[i]; }
- static uint32 GetLinearSlideUpTable (const CSoundFile *sndFile, uint32 i) { MPT_ASSERT(i < std::size(LinearSlideDownTable)); return sndFile->m_playBehaviour[kPeriodsAreHertz] ? LinearSlideUpTable[i] : LinearSlideDownTable[i]; }
- static uint32 GetFineLinearSlideDownTable(const CSoundFile *sndFile, uint32 i) { MPT_ASSERT(i < std::size(FineLinearSlideDownTable)); return sndFile->m_playBehaviour[kPeriodsAreHertz] ? FineLinearSlideDownTable[i] : FineLinearSlideUpTable[i]; }
- static uint32 GetFineLinearSlideUpTable (const CSoundFile *sndFile, uint32 i) { MPT_ASSERT(i < std::size(FineLinearSlideDownTable)); return sndFile->m_playBehaviour[kPeriodsAreHertz] ? FineLinearSlideUpTable[i] : FineLinearSlideDownTable[i]; }
- ////////////////////////////////////////////////////////////
- // Length
- // Memory class for GetLength() code
- class GetLengthMemory
- {
- protected:
- const CSoundFile &sndFile;
- public:
- std::unique_ptr<CSoundFile::PlayState> state;
- struct ChnSettings
- {
- uint32 ticksToRender = 0; // When using sample sync, we still need to render this many ticks
- bool incChanged = false; // When using sample sync, note frequency has changed
- uint8 vol = 0xFF;
- };
- std::vector<ChnSettings> chnSettings;
- double elapsedTime;
- static constexpr uint32 IGNORE_CHANNEL = uint32_max;
- GetLengthMemory(const CSoundFile &sf)
- : sndFile(sf)
- , state(std::make_unique<CSoundFile::PlayState>(sf.m_PlayState))
- {
- Reset();
- }
- void Reset()
- {
- if(state->m_midiMacroEvaluationResults)
- state->m_midiMacroEvaluationResults.emplace();
- elapsedTime = 0.0;
- state->m_lTotalSampleCount = 0;
- state->m_nMusicSpeed = sndFile.m_nDefaultSpeed;
- state->m_nMusicTempo = sndFile.m_nDefaultTempo;
- state->m_nGlobalVolume = sndFile.m_nDefaultGlobalVolume;
- chnSettings.assign(sndFile.GetNumChannels(), ChnSettings());
- const auto muteFlag = CSoundFile::GetChannelMuteFlag();
- for(CHANNELINDEX chn = 0; chn < sndFile.GetNumChannels(); chn++)
- {
- state->Chn[chn].Reset(ModChannel::resetTotal, sndFile, chn, muteFlag);
- state->Chn[chn].nOldGlobalVolSlide = 0;
- state->Chn[chn].nOldChnVolSlide = 0;
- state->Chn[chn].nNote = state->Chn[chn].nNewNote = state->Chn[chn].nLastNote = NOTE_NONE;
- }
- }
- // Increment playback position of sample and envelopes on a channel
- void RenderChannel(CHANNELINDEX channel, uint32 tickDuration, uint32 portaStart = uint32_max)
- {
- ModChannel &chn = state->Chn[channel];
- uint32 numTicks = chnSettings[channel].ticksToRender;
- if(numTicks == IGNORE_CHANNEL || numTicks == 0 || (!chn.IsSamplePlaying() && !chnSettings[channel].incChanged) || chn.pModSample == nullptr)
- {
- return;
- }
- const SamplePosition loopStart(chn.dwFlags[CHN_LOOP] ? chn.nLoopStart : 0u, 0);
- const SamplePosition sampleEnd(chn.dwFlags[CHN_LOOP] ? chn.nLoopEnd : chn.nLength, 0);
- const SmpLength loopLength = chn.nLoopEnd - chn.nLoopStart;
- const bool itEnvMode = sndFile.m_playBehaviour[kITEnvelopePositionHandling];
- const bool updatePitchEnv = (chn.PitchEnv.flags & (ENV_ENABLED | ENV_FILTER)) == ENV_ENABLED;
- bool stopNote = false;
- SamplePosition inc = chn.increment * tickDuration;
- if(chn.dwFlags[CHN_PINGPONGFLAG]) inc.Negate();
- for(uint32 i = 0; i < numTicks; i++)
- {
- bool updateInc = (chn.PitchEnv.flags & (ENV_ENABLED | ENV_FILTER)) == ENV_ENABLED;
- if(i >= portaStart)
- {
- chn.isFirstTick = false;
- const ModCommand &m = *sndFile.Patterns[state->m_nPattern].GetpModCommand(state->m_nRow, channel);
- auto command = m.command;
- if(m.volcmd == VOLCMD_TONEPORTAMENTO)
- {
- const auto [porta, clearEffectCommand] = sndFile.GetVolCmdTonePorta(m, 0);
- sndFile.TonePortamento(chn, porta);
- if(clearEffectCommand)
- command = CMD_NONE;
- }
- if(command == CMD_TONEPORTAMENTO)
- sndFile.TonePortamento(chn, m.param);
- else if(command == CMD_TONEPORTAVOL)
- sndFile.TonePortamento(chn, 0);
- updateInc = true;
- }
- int32 period = chn.nPeriod;
- if(itEnvMode) sndFile.IncrementEnvelopePositions(chn);
- if(updatePitchEnv)
- {
- sndFile.ProcessPitchFilterEnvelope(chn, period);
- updateInc = true;
- }
- if(!itEnvMode) sndFile.IncrementEnvelopePositions(chn);
- int vol = 0;
- sndFile.ProcessInstrumentFade(chn, vol);
- if(chn.dwFlags[CHN_ADLIB])
- continue;
- if(updateInc || chnSettings[channel].incChanged)
- {
- if(chn.m_CalculateFreq || chn.m_ReCalculateFreqOnFirstTick)
- {
- chn.RecalcTuningFreq(1, 0, sndFile);
- if(!chn.m_CalculateFreq)
- chn.m_ReCalculateFreqOnFirstTick = false;
- else
- chn.m_CalculateFreq = false;
- }
- chn.increment = sndFile.GetChannelIncrement(chn, period, 0).first;
- chnSettings[channel].incChanged = false;
- inc = chn.increment * tickDuration;
- if(chn.dwFlags[CHN_PINGPONGFLAG]) inc.Negate();
- }
- chn.position += inc;
- if(chn.position >= sampleEnd || (chn.position < loopStart && inc.IsNegative()))
- {
- if(!chn.dwFlags[CHN_LOOP])
- {
- // Past sample end.
- stopNote = true;
- break;
- }
- // We exceeded the sample loop, go back to loop start.
- if(chn.dwFlags[CHN_PINGPONGLOOP])
- {
- if(chn.position < loopStart)
- {
- chn.position = SamplePosition(chn.nLoopStart + chn.nLoopStart, 0) - chn.position;
- chn.dwFlags.flip(CHN_PINGPONGFLAG);
- inc.Negate();
- }
- SmpLength posInt = chn.position.GetUInt() - chn.nLoopStart;
- SmpLength pingpongLength = loopLength * 2;
- if(sndFile.m_playBehaviour[kITPingPongMode]) pingpongLength--;
- posInt %= pingpongLength;
- bool forward = (posInt < loopLength);
- if(forward)
- chn.position.SetInt(chn.nLoopStart + posInt);
- else
- chn.position.SetInt(chn.nLoopEnd - (posInt - loopLength));
- if(forward == chn.dwFlags[CHN_PINGPONGFLAG])
- {
- chn.dwFlags.flip(CHN_PINGPONGFLAG);
- inc.Negate();
- }
- } else
- {
- SmpLength posInt = chn.position.GetUInt();
- if(posInt >= chn.nLoopEnd + loopLength)
- {
- const SmpLength overshoot = posInt - chn.nLoopEnd;
- posInt -= (overshoot / loopLength) * loopLength;
- }
- while(posInt >= chn.nLoopEnd)
- {
- posInt -= loopLength;
- }
- chn.position.SetInt(posInt);
- }
- }
- }
- if(stopNote)
- {
- chn.Stop();
- chn.nPortamentoDest = 0;
- }
- chnSettings[channel].ticksToRender = 0;
- }
- };
- // Get mod length in various cases. Parameters:
- // [in] adjustMode: See enmGetLengthResetMode for possible adjust modes.
- // [in] target: Time or position target which should be reached, or no target to get length of the first sub song. Use GetLengthTarget::StartPos to also specify a position from where the seeking should begin.
- // [out] See definition of type GetLengthType for the returned values.
- std::vector<GetLengthType> CSoundFile::GetLength(enmGetLengthResetMode adjustMode, GetLengthTarget target)
- {
- std::vector<GetLengthType> results;
- GetLengthType retval;
- // Are we trying to reach a certain pattern position?
- const bool hasSearchTarget = target.mode != GetLengthTarget::NoTarget && target.mode != GetLengthTarget::GetAllSubsongs;
- const bool adjustSamplePos = (adjustMode & eAdjustSamplePositions) == eAdjustSamplePositions;
- SEQUENCEINDEX sequence = target.sequence;
- if(sequence >= Order.GetNumSequences()) sequence = Order.GetCurrentSequenceIndex();
- const ModSequence &orderList = Order(sequence);
- GetLengthMemory memory(*this);
- CSoundFile::PlayState &playState = *memory.state;
- // Temporary visited rows vector (so that GetLength() won't interfere with the player code if the module is playing at the same time)
- RowVisitor visitedRows(*this, sequence);
- ROWINDEX allowedPatternLoopComplexity = 32768;
- // If sequence starts with some non-existent patterns, find a better start
- while(target.startOrder < orderList.size() && !orderList.IsValidPat(target.startOrder))
- {
- target.startOrder++;
- target.startRow = 0;
- }
- retval.startRow = playState.m_nNextRow = playState.m_nRow = target.startRow;
- retval.startOrder = playState.m_nNextOrder = playState.m_nCurrentOrder = target.startOrder;
- // Fast LUTs for commands that are too weird / complicated / whatever to emulate in sample position adjust mode.
- std::bitset<MAX_EFFECTS> forbiddenCommands;
- std::bitset<MAX_VOLCMDS> forbiddenVolCommands;
- if(adjustSamplePos)
- {
- forbiddenCommands.set(CMD_ARPEGGIO); forbiddenCommands.set(CMD_PORTAMENTOUP);
- forbiddenCommands.set(CMD_PORTAMENTODOWN); forbiddenCommands.set(CMD_XFINEPORTAUPDOWN);
- forbiddenCommands.set(CMD_NOTESLIDEUP); forbiddenCommands.set(CMD_NOTESLIDEUPRETRIG);
- forbiddenCommands.set(CMD_NOTESLIDEDOWN); forbiddenCommands.set(CMD_NOTESLIDEDOWNRETRIG);
- forbiddenVolCommands.set(VOLCMD_PORTAUP); forbiddenVolCommands.set(VOLCMD_PORTADOWN);
- if(target.mode == GetLengthTarget::SeekPosition && target.pos.order < orderList.size())
- {
- // If we know where to seek, we can directly rule out any channels on which a new note would be triggered right at the start.
- const PATTERNINDEX seekPat = orderList[target.pos.order];
- if(Patterns.IsValidPat(seekPat) && Patterns[seekPat].IsValidRow(target.pos.row))
- {
- const ModCommand *m = Patterns[seekPat].GetpModCommand(target.pos.row, 0);
- for(CHANNELINDEX i = 0; i < GetNumChannels(); i++, m++)
- {
- if(m->note == NOTE_NOTECUT || m->note == NOTE_KEYOFF || (m->note == NOTE_FADE && GetNumInstruments())
- || (m->IsNote() && !m->IsPortamento()))
- {
- memory.chnSettings[i].ticksToRender = GetLengthMemory::IGNORE_CHANNEL;
- }
- }
- }
- }
- }
- if(adjustMode & eAdjust)
- playState.m_midiMacroEvaluationResults.emplace();
- // If samples are being synced, force them to resync if tick duration changes
- uint32 oldTickDuration = 0;
- bool breakToRow = false;
- for (;;)
- {
- const bool ignoreRow = NextRow(playState, breakToRow).first;
- // Time target reached.
- if(target.mode == GetLengthTarget::SeekSeconds && memory.elapsedTime >= target.time)
- {
- retval.targetReached = true;
- break;
- }
- // Check if pattern is valid
- playState.m_nPattern = playState.m_nCurrentOrder < orderList.size() ? orderList[playState.m_nCurrentOrder] : orderList.GetInvalidPatIndex();
- if(!Patterns.IsValidPat(playState.m_nPattern) && playState.m_nPattern != orderList.GetInvalidPatIndex() && target.mode == GetLengthTarget::SeekPosition && playState.m_nCurrentOrder == target.pos.order)
- {
- // Early test: Target is inside +++ or non-existing pattern
- retval.targetReached = true;
- break;
- }
- while(playState.m_nPattern >= Patterns.Size())
- {
- // End of song?
- if((playState.m_nPattern == orderList.GetInvalidPatIndex()) || (playState.m_nCurrentOrder >= orderList.size()))
- {
- if(playState.m_nCurrentOrder == orderList.GetRestartPos())
- break;
- else
- playState.m_nCurrentOrder = orderList.GetRestartPos();
- } else
- {
- playState.m_nCurrentOrder++;
- }
- playState.m_nPattern = (playState.m_nCurrentOrder < orderList.size()) ? orderList[playState.m_nCurrentOrder] : orderList.GetInvalidPatIndex();
- playState.m_nNextOrder = playState.m_nCurrentOrder;
- if((!Patterns.IsValidPat(playState.m_nPattern)) && visitedRows.Visit(playState.m_nCurrentOrder, 0, playState.Chn, ignoreRow))
- {
- if(!hasSearchTarget)
- {
- retval.lastOrder = playState.m_nCurrentOrder;
- retval.lastRow = 0;
- }
- if(target.mode == GetLengthTarget::NoTarget || !visitedRows.GetFirstUnvisitedRow(playState.m_nNextOrder, playState.m_nRow, true))
- {
- // We aren't searching for a specific row, or we couldn't find any more unvisited rows.
- break;
- } else
- {
- // We haven't found the target row yet, but we found some other unplayed row... continue searching from here.
- retval.duration = memory.elapsedTime;
- results.push_back(retval);
- retval.startRow = playState.m_nRow;
- retval.startOrder = playState.m_nNextOrder;
- memory.Reset();
- playState.m_nCurrentOrder = playState.m_nNextOrder;
- playState.m_nPattern = orderList[playState.m_nCurrentOrder];
- playState.m_nNextRow = playState.m_nRow;
- break;
- }
- }
- }
- if(playState.m_nNextOrder == ORDERINDEX_INVALID)
- {
- // GetFirstUnvisitedRow failed, so there is nothing more to play
- break;
- }
- // Skip non-existing patterns
- if(!Patterns.IsValidPat(playState.m_nPattern))
- {
- // If there isn't even a tune, we should probably stop here.
- if(playState.m_nCurrentOrder == orderList.GetRestartPos())
- {
- if(target.mode == GetLengthTarget::NoTarget || !visitedRows.GetFirstUnvisitedRow(playState.m_nNextOrder, playState.m_nRow, true))
- {
- // We aren't searching for a specific row, or we couldn't find any more unvisited rows.
- break;
- } else
- {
- // We haven't found the target row yet, but we found some other unplayed row... continue searching from here.
- retval.duration = memory.elapsedTime;
- results.push_back(retval);
- retval.startRow = playState.m_nRow;
- retval.startOrder = playState.m_nNextOrder;
- memory.Reset();
- playState.m_nNextRow = playState.m_nRow;
- continue;
- }
- }
- playState.m_nNextOrder = playState.m_nCurrentOrder + 1;
- continue;
- }
- // Should never happen
- if(playState.m_nRow >= Patterns[playState.m_nPattern].GetNumRows())
- playState.m_nRow = 0;
- // Check whether target was reached.
- if(target.mode == GetLengthTarget::SeekPosition && playState.m_nCurrentOrder == target.pos.order && playState.m_nRow == target.pos.row)
- {
- retval.targetReached = true;
- break;
- }
- // If pattern loops are nested too deeply, they can cause an effectively infinite amount of loop evalations to be generated.
- // As we don't want the user to wait forever, we bail out if the pattern loops are too complex.
- const bool moduleTooComplex = target.mode != GetLengthTarget::SeekSeconds && visitedRows.ModuleTooComplex(allowedPatternLoopComplexity);
- if(moduleTooComplex)
- {
- memory.elapsedTime = std::numeric_limits<decltype(memory.elapsedTime)>::infinity();
- // Decrease allowed complexity with each subsong, as this seems to be a malicious module
- if(allowedPatternLoopComplexity > 256)
- allowedPatternLoopComplexity /= 2;
- visitedRows.ResetComplexity();
- }
- if(visitedRows.Visit(playState.m_nCurrentOrder, playState.m_nRow, playState.Chn, ignoreRow) || moduleTooComplex)
- {
- if(!hasSearchTarget)
- {
- retval.lastOrder = playState.m_nCurrentOrder;
- retval.lastRow = playState.m_nRow;
- }
- if(target.mode == GetLengthTarget::NoTarget || !visitedRows.GetFirstUnvisitedRow(playState.m_nNextOrder, playState.m_nRow, true))
- {
- // We aren't searching for a specific row, or we couldn't find any more unvisited rows.
- break;
- } else
- {
- // We haven't found the target row yet, but we found some other unplayed row... continue searching from here.
- retval.duration = memory.elapsedTime;
- results.push_back(retval);
- retval.startRow = playState.m_nRow;
- retval.startOrder = playState.m_nNextOrder;
- memory.Reset();
- playState.m_nNextRow = playState.m_nRow;
- continue;
- }
- }
- retval.endOrder = playState.m_nCurrentOrder;
- retval.endRow = playState.m_nRow;
- // Update next position
- SetupNextRow(playState, false);
- // Jumped to invalid pattern row?
- if(playState.m_nRow >= Patterns[playState.m_nPattern].GetNumRows())
- {
- playState.m_nRow = 0;
- }
- if(ignoreRow)
- continue;
- // For various effects, we need to know first how many ticks there are in this row.
- const ModCommand *p = Patterns[playState.m_nPattern].GetpModCommand(playState.m_nRow, 0);
- const bool ignoreMutedChn = m_playBehaviour[kST3NoMutedChannels];
- for(CHANNELINDEX nChn = 0; nChn < GetNumChannels(); nChn++, p++)
- {
- ModChannel &chn = playState.Chn[nChn];
- if(p->IsEmpty() || (ignoreMutedChn && ChnSettings[nChn].dwFlags[CHN_MUTE])) // not even effects are processed on muted S3M channels
- {
- chn.rowCommand.Clear();
- continue;
- }
- if(p->IsPcNote())
- {
- #ifndef NO_PLUGINS
- if(playState.m_midiMacroEvaluationResults && p->instr > 0 && p->instr <= MAX_MIXPLUGINS)
- {
- playState.m_midiMacroEvaluationResults->pluginParameter[{static_cast<PLUGINDEX>(p->instr - 1), p->GetValueVolCol()}] = p->GetValueEffectCol() / PlugParamValue(ModCommand::maxColumnValue);
- }
- #endif // NO_PLUGINS
- chn.rowCommand.Clear();
- continue;
- }
- chn.rowCommand = *p;
- switch(p->command)
- {
- case CMD_SPEED:
- SetSpeed(playState, p->param);
- break;
- case CMD_TEMPO:
- if(m_playBehaviour[kMODVBlankTiming])
- {
- // ProTracker MODs with VBlank timing: All Fxx parameters set the tick count.
- if(p->param != 0) SetSpeed(playState, p->param);
- }
- break;
- case CMD_S3MCMDEX:
- if(!chn.rowCommand.param && (GetType() & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT)))
- chn.rowCommand.param = chn.nOldCmdEx;
- else
- chn.nOldCmdEx = static_cast<ModCommand::PARAM>(chn.rowCommand.param);
- if((p->param & 0xF0) == 0x60)
- {
- // Fine Pattern Delay
- playState.m_nFrameDelay += (p->param & 0x0F);
- } else if((p->param & 0xF0) == 0xE0 && !playState.m_nPatternDelay)
- {
- // Pattern Delay
- if(!(GetType() & MOD_TYPE_S3M) || (p->param & 0x0F) != 0)
- {
- // While Impulse Tracker *does* count S60 as a valid row delay (and thus ignores any other row delay commands on the right),
- // Scream Tracker 3 simply ignores such commands.
- playState.m_nPatternDelay = 1 + (p->param & 0x0F);
- }
- }
- break;
- case CMD_MODCMDEX:
- if((p->param & 0xF0) == 0xE0)
- {
- // Pattern Delay
- playState.m_nPatternDelay = 1 + (p->param & 0x0F);
- }
- break;
- }
- }
- const uint32 numTicks = playState.TicksOnRow();
- const uint32 nonRowTicks = numTicks - std::max(playState.m_nPatternDelay, uint32(1));
- playState.m_patLoopRow = ROWINDEX_INVALID;
- playState.m_breakRow = ROWINDEX_INVALID;
- playState.m_posJump = ORDERINDEX_INVALID;
- for(CHANNELINDEX nChn = 0; nChn < GetNumChannels(); nChn++)
- {
- ModChannel &chn = playState.Chn[nChn];
- if(chn.rowCommand.IsEmpty())
- continue;
- ModCommand::COMMAND command = chn.rowCommand.command;
- ModCommand::PARAM param = chn.rowCommand.param;
- ModCommand::NOTE note = chn.rowCommand.note;
- if(adjustMode & eAdjust)
- {
- if(chn.rowCommand.instr)
- {
- chn.nNewIns = chn.rowCommand.instr;
- chn.nLastNote = NOTE_NONE;
- memory.chnSettings[nChn].vol = 0xFF;
- }
- if(chn.rowCommand.IsNote())
- {
- chn.nLastNote = note;
- chn.RestorePanAndFilter();
- }
- // Update channel panning
- if(chn.rowCommand.IsNote() || chn.rowCommand.instr)
- {
- ModInstrument *pIns;
- if(chn.nNewIns > 0 && chn.nNewIns <= GetNumInstruments() && (pIns = Instruments[chn.nNewIns]) != nullptr)
- {
- if(pIns->dwFlags[INS_SETPANNING])
- chn.SetInstrumentPan(pIns->nPan, *this);
- }
- const SAMPLEINDEX smp = GetSampleIndex(note, chn.nNewIns);
- if(smp > 0)
- {
- if(Samples[smp].uFlags[CHN_PANNING])
- chn.SetInstrumentPan(Samples[smp].nPan, *this);
- }
- }
- switch(chn.rowCommand.volcmd)
- {
- case VOLCMD_VOLUME:
- memory.chnSettings[nChn].vol = chn.rowCommand.vol;
- break;
- case VOLCMD_VOLSLIDEUP:
- case VOLCMD_VOLSLIDEDOWN:
- if(chn.rowCommand.vol != 0)
- chn.nOldVolParam = chn.rowCommand.vol;
- break;
- case VOLCMD_TONEPORTAMENTO:
- if(chn.rowCommand.vol)
- {
- const auto [porta, clearEffectCommand] = GetVolCmdTonePorta(chn.rowCommand, 0);
- chn.portamentoSlide = porta;
- if(clearEffectCommand)
- command = CMD_NONE;
- }
- break;
- }
- }
- switch(command)
- {
- // Position Jump
- case CMD_POSITIONJUMP:
- PositionJump(playState, nChn);
- break;
- // Pattern Break
- case CMD_PATTERNBREAK:
- if(ROWINDEX row = PatternBreak(playState, nChn, param); row != ROWINDEX_INVALID)
- playState.m_breakRow = row;
- break;
- // Set Tempo
- case CMD_TEMPO:
- if(!m_playBehaviour[kMODVBlankTiming])
- {
- TEMPO tempo(CalculateXParam(playState.m_nPattern, playState.m_nRow, nChn), 0);
- if ((adjustMode & eAdjust) && (GetType() & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT)))
- {
- if (tempo.GetInt()) chn.nOldTempo = static_cast<uint8>(tempo.GetInt()); else tempo.Set(chn.nOldTempo);
- }
- if (tempo.GetInt() >= 0x20) playState.m_nMusicTempo = tempo;
- else
- {
- // Tempo Slide
- TEMPO tempoDiff((tempo.GetInt() & 0x0F) * nonRowTicks, 0);
- if ((tempo.GetInt() & 0xF0) == 0x10)
- {
- playState.m_nMusicTempo += tempoDiff;
- } else
- {
- if(tempoDiff < playState.m_nMusicTempo)
- playState.m_nMusicTempo -= tempoDiff;
- else
- playState.m_nMusicTempo.Set(0);
- }
- }
- TEMPO tempoMin = GetModSpecifications().GetTempoMin(), tempoMax = GetModSpecifications().GetTempoMax();
- if(m_playBehaviour[kTempoClamp]) // clamp tempo correctly in compatible mode
- {
- tempoMax.Set(255);
- }
- Limit(playState.m_nMusicTempo, tempoMin, tempoMax);
- }
- break;
- case CMD_S3MCMDEX:
- switch(param & 0xF0)
- {
- case 0x90:
- if(param <= 0x91)
- chn.dwFlags.set(CHN_SURROUND, param == 0x91);
- break;
- case 0xA0: // High sample offset
- chn.nOldHiOffset = param & 0x0F;
- break;
- case 0xB0: // Pattern Loop
- PatternLoop(playState, chn, param & 0x0F);
- break;
-
- case 0xF0: // Active macro
- chn.nActiveMacro = param & 0x0F;
- break;
- }
- break;
- case CMD_MODCMDEX:
- switch(param & 0xF0)
- {
- case 0x60: // Pattern Loop
- PatternLoop(playState, chn, param & 0x0F);
- break;
- case 0xF0: // Active macro
- chn.nActiveMacro = param & 0x0F;
- break;
- }
- break;
- case CMD_XFINEPORTAUPDOWN:
- // ignore high offset in compatible mode
- if(((param & 0xF0) == 0xA0) && !m_playBehaviour[kFT2RestrictXCommand])
- chn.nOldHiOffset = param & 0x0F;
- break;
- }
- // The following calculations are not interesting if we just want to get the song length.
- if(!(adjustMode & eAdjust))
- continue;
- switch(command)
- {
- // Portamento Up/Down
- case CMD_PORTAMENTOUP:
- if(param)
- {
- // FT2 compatibility: Separate effect memory for all portamento commands
- // Test case: Porta-LinkMem.xm
- if(!m_playBehaviour[kFT2PortaUpDownMemory])
- chn.nOldPortaDown = param;
- chn.nOldPortaUp = param;
- }
- break;
- case CMD_PORTAMENTODOWN:
- if(param)
- {
- // FT2 compatibility: Separate effect memory for all portamento commands
- // Test case: Porta-LinkMem.xm
- if(!m_playBehaviour[kFT2PortaUpDownMemory])
- chn.nOldPortaUp = param;
- chn.nOldPortaDown = param;
- }
- break;
- // Tone-Portamento
- case CMD_TONEPORTAMENTO:
- if (param) chn.portamentoSlide = param;
- break;
- // Offset
- case CMD_OFFSET:
- if(param)
- chn.oldOffset = param << 8;
- break;
- // Volume Slide
- case CMD_VOLUMESLIDE:
- case CMD_TONEPORTAVOL:
- if (param) chn.nOldVolumeSlide = param;
- break;
- // Set Volume
- case CMD_VOLUME:
- memory.chnSettings[nChn].vol = param;
- break;
- // Global Volume
- case CMD_GLOBALVOLUME:
- if(!(GetType() & GLOBALVOL_7BIT_FORMATS) && param < 128) param *= 2;
- // IT compatibility 16. ST3 and IT ignore out-of-range values
- if(param <= 128)
- {
- playState.m_nGlobalVolume = param * 2;
- } else if(!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_S3M)))
- {
- playState.m_nGlobalVolume = 256;
- }
- break;
- // Global Volume Slide
- case CMD_GLOBALVOLSLIDE:
- if(m_playBehaviour[kPerChannelGlobalVolSlide])
- {
- // IT compatibility 16. Global volume slide params are stored per channel (FT2/IT)
- if (param) chn.nOldGlobalVolSlide = param; else param = chn.nOldGlobalVolSlide;
- } else
- {
- if (param) playState.Chn[0].nOldGlobalVolSlide = param; else param = playState.Chn[0].nOldGlobalVolSlide;
- }
- if (((param & 0x0F) == 0x0F) && (param & 0xF0))
- {
- param >>= 4;
- if (!(GetType() & GLOBALVOL_7BIT_FORMATS)) param <<= 1;
- playState.m_nGlobalVolume += param << 1;
- } else if (((param & 0xF0) == 0xF0) && (param & 0x0F))
- {
- param = (param & 0x0F) << 1;
- if (!(GetType() & GLOBALVOL_7BIT_FORMATS)) param <<= 1;
- playState.m_nGlobalVolume -= param;
- } else if (param & 0xF0)
- {
- param >>= 4;
- param <<= 1;
- if (!(GetType() & GLOBALVOL_7BIT_FORMATS)) param <<= 1;
- playState.m_nGlobalVolume += param * nonRowTicks;
- } else
- {
- param = (param & 0x0F) << 1;
- if (!(GetType() & GLOBALVOL_7BIT_FORMATS)) param <<= 1;
- playState.m_nGlobalVolume -= param * nonRowTicks;
- }
- Limit(playState.m_nGlobalVolume, 0, 256);
- break;
- case CMD_CHANNELVOLUME:
- if (param <= 64) chn.nGlobalVol = param;
- break;
- case CMD_CHANNELVOLSLIDE:
- {
- if (param) chn.nOldChnVolSlide = param; else param = chn.nOldChnVolSlide;
- int32 volume = chn.nGlobalVol;
- if((param & 0x0F) == 0x0F && (param & 0xF0))
- volume += (param >> 4); // Fine Up
- else if((param & 0xF0) == 0xF0 && (param & 0x0F))
- volume -= (param & 0x0F); // Fine Down
- else if(param & 0x0F) // Down
- volume -= (param & 0x0F) * nonRowTicks;
- else // Up
- volume += ((param & 0xF0) >> 4) * nonRowTicks;
- Limit(volume, 0, 64);
- chn.nGlobalVol = volume;
- }
- break;
- case CMD_PANNING8:
- Panning(chn, param, Pan8bit);
- break;
- case CMD_MODCMDEX:
- if(param < 0x10)
- {
- // LED filter
- for(CHANNELINDEX channel = 0; channel < GetNumChannels(); channel++)
- {
- playState.Chn[channel].dwFlags.set(CHN_AMIGAFILTER, !(param & 1));
- }
- }
- [[fallthrough]];
- case CMD_S3MCMDEX:
- if((param & 0xF0) == 0x80)
- {
- Panning(chn, (param & 0x0F), Pan4bit);
- }
- break;
- case CMD_VIBRATOVOL:
- if (param) chn.nOldVolumeSlide = param;
- param = 0;
- [[fallthrough]];
- case CMD_VIBRATO:
- Vibrato(chn, param);
- break;
- case CMD_FINEVIBRATO:
- FineVibrato(chn, param);
- break;
- case CMD_TREMOLO:
- Tremolo(chn, param);
- break;
- case CMD_PANBRELLO:
- Panbrello(chn, param);
- break;
- case CMD_MIDI:
- case CMD_SMOOTHMIDI:
- if(param < 0x80)
- ProcessMIDIMacro(playState, nChn, false, m_MidiCfg.SFx[chn.nActiveMacro], chn.rowCommand.param, 0);
- else
- ProcessMIDIMacro(playState, nChn, false, m_MidiCfg.Zxx[param & 0x7F], chn.rowCommand.param, 0);
- break;
- default:
- break;
- }
- switch(chn.rowCommand.volcmd)
- {
- case VOLCMD_PANNING:
- Panning(chn, chn.rowCommand.vol, Pan6bit);
- break;
- case VOLCMD_VIBRATOSPEED:
- // FT2 does not automatically enable vibrato with the "set vibrato speed" command
- if(m_playBehaviour[kFT2VolColVibrato])
- chn.nVibratoSpeed = chn.rowCommand.vol & 0x0F;
- else
- Vibrato(chn, chn.rowCommand.vol << 4);
- break;
- case VOLCMD_VIBRATODEPTH:
- Vibrato(chn, chn.rowCommand.vol);
- break;
- }
- // Process vibrato / tremolo / panbrello
- switch(chn.rowCommand.command)
- {
- case CMD_VIBRATO:
- case CMD_FINEVIBRATO:
- case CMD_VIBRATOVOL:
- if(adjustMode & eAdjust)
- {
- uint32 vibTicks = ((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) && !m_SongFlags[SONG_ITOLDEFFECTS]) ? numTicks : nonRowTicks;
- uint32 inc = chn.nVibratoSpeed * vibTicks;
- if(m_playBehaviour[kITVibratoTremoloPanbrello])
- inc *= 4;
- chn.nVibratoPos += static_cast<uint8>(inc);
- }
- break;
- case CMD_TREMOLO:
- if(adjustMode & eAdjust)
- {
- uint32 tremTicks = ((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) && !m_SongFlags[SONG_ITOLDEFFECTS]) ? numTicks : nonRowTicks;
- uint32 inc = chn.nTremoloSpeed * tremTicks;
- if(m_playBehaviour[kITVibratoTremoloPanbrello])
- inc *= 4;
- chn.nTremoloPos += static_cast<uint8>(inc);
- }
- break;
- case CMD_PANBRELLO:
- if(adjustMode & eAdjust)
- {
- // Panbrello effect is permanent in compatible mode, so actually apply panbrello for the last tick of this row
- chn.nPanbrelloPos += static_cast<uint8>(chn.nPanbrelloSpeed * (numTicks - 1));
- ProcessPanbrello(chn);
- }
- break;
- }
-
- if(m_playBehaviour[kST3EffectMemory] && param != 0)
- {
- UpdateS3MEffectMemory(chn, param);
- }
- }
- // Interpret F00 effect in XM files as "stop song"
- if(GetType() == MOD_TYPE_XM && playState.m_nMusicSpeed == uint16_max)
- {
- break;
- }
- playState.m_nCurrentRowsPerBeat = m_nDefaultRowsPerBeat;
- if(Patterns[playState.m_nPattern].GetOverrideSignature())
- {
- playState.m_nCurrentRowsPerBeat = Patterns[playState.m_nPattern].GetRowsPerBeat();
- }
- const uint32 tickDuration = GetTickDuration(playState);
- const uint32 rowDuration = tickDuration * numTicks;
- memory.elapsedTime += static_cast<double>(rowDuration) / static_cast<double>(m_MixerSettings.gdwMixingFreq);
- playState.m_lTotalSampleCount += rowDuration;
- if(adjustSamplePos)
- {
- // Super experimental and dirty sample seeking
- for(CHANNELINDEX nChn = 0; nChn < GetNumChannels(); nChn++)
- {
- if(memory.chnSettings[nChn].ticksToRender == GetLengthMemory::IGNORE_CHANNEL)
- continue;
- ModChannel &chn = playState.Chn[nChn];
- const ModCommand &m = chn.rowCommand;
- if(!chn.nPeriod && m.IsEmpty())
- continue;
- uint32 paramHi = m.param >> 4, paramLo = m.param & 0x0F;
- uint32 startTick = 0;
- bool porta = m.command == CMD_TONEPORTAMENTO || m.command == CMD_TONEPORTAVOL || m.volcmd == VOLCMD_TONEPORTAMENTO;
- bool stopNote = false;
- if(m.instr) chn.prevNoteOffset = 0;
- if(m.IsNote())
- {
- if(porta && memory.chnSettings[nChn].incChanged)
- {
- // If there's a portamento, the current channel increment mustn't be 0 in NoteChange()
- chn.increment = GetChannelIncrement(chn, chn.nPeriod, 0).first;
- }
- int32 setPan = chn.nPan;
- chn.nNewNote = chn.nLastNote;
- if(chn.nNewIns != 0) InstrumentChange(chn, chn.nNewIns, porta);
- NoteChange(chn, m.note, porta);
- HandleDigiSamplePlayDirection(playState, nChn);
- memory.chnSettings[nChn].incChanged = true;
- if((m.command == CMD_MODCMDEX || m.command == CMD_S3MCMDEX) && (m.param & 0xF0) == 0xD0 && paramLo < numTicks)
- {
- startTick = paramLo;
- } else if(m.command == CMD_DELAYCUT && paramHi < numTicks)
- {
- startTick = paramHi;
- }
- if(playState.m_nPatternDelay > 1 && startTick != 0 && (GetType() & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT)))
- {
- startTick += (playState.m_nMusicSpeed + playState.m_nFrameDelay) * (playState.m_nPatternDelay - 1);
- }
- if(!porta) memory.chnSettings[nChn].ticksToRender = 0;
- // Panning commands have to be re-applied after a note change with potential pan change.
- if(m.command == CMD_PANNING8
- || ((m.command == CMD_MODCMDEX || m.command == CMD_S3MCMDEX) && paramHi == 0x8)
- || m.volcmd == VOLCMD_PANNING)
- {
- chn.nPan = setPan;
- }
- }
- if(m.IsNote() || m_playBehaviour[kApplyOffsetWithoutNote])
- {
- if(m.command == CMD_OFFSET)
- {
- ProcessSampleOffset(chn, nChn, playState);
- } else if(m.command == CMD_OFFSETPERCENTAGE)
- {
- SampleOffset(chn, Util::muldiv_unsigned(chn.nLength, m.param, 256));
- } else if(m.command == CMD_REVERSEOFFSET && chn.pModSample != nullptr)
- {
- memory.RenderChannel(nChn, oldTickDuration); // Re-sync what we've got so far
- ReverseSampleOffset(chn, m.param);
- startTick = playState.m_nMusicSpeed - 1;
- } else if(m.volcmd == VOLCMD_OFFSET)
- {
- if(chn.pModSample != nullptr && m.vol <= std::size(chn.pModSample->cues))
- {
- SmpLength offset;
- if(m.vol == 0)
- offset = chn.oldOffset;
- else
- offset = chn.oldOffset = chn.pModSample->cues[m.vol - 1];
- SampleOffset(chn, offset);
- }
- }
- }
- if(m.note == NOTE_KEYOFF || m.note == NOTE_NOTECUT || (m.note == NOTE_FADE && GetNumInstruments())
- || ((m.command == CMD_MODCMDEX || m.command == CMD_S3MCMDEX) && (m.param & 0xF0) == 0xC0 && paramLo < numTicks)
- || (m.command == CMD_DELAYCUT && paramLo != 0 && startTick + paramLo < numTicks)
- || m.command == CMD_KEYOFF)
- {
- stopNote = true;
- }
- if(m.command == CMD_VOLUME)
- {
- chn.nVolume = m.param * 4;
- } else if(m.volcmd == VOLCMD_VOLUME)
- {
- chn.nVolume = m.vol * 4;
- }
-
- if(chn.pModSample && !stopNote)
- {
- // Check if we don't want to emulate some effect and thus stop processing.
- if(m.command < MAX_EFFECTS)
- {
- if(forbiddenCommands[m.command])
- {
- stopNote = true;
- } else if(m.command == CMD_MODCMDEX)
- {
- // Special case: Slides using extended commands
- switch(m.param & 0xF0)
- {
- case 0x10:
- case 0x20:
- stopNote = true;
- }
- }
- }
- if(m.volcmd < forbiddenVolCommands.size() && forbiddenVolCommands[m.volcmd])
- {
- stopNote = true;
- }
- }
- if(stopNote)
- {
- chn.Stop();
- memory.chnSettings[nChn].ticksToRender = 0;
- } else
- {
- if(oldTickDuration != tickDuration && oldTickDuration != 0)
- {
- memory.RenderChannel(nChn, oldTickDuration); // Re-sync what we've got so far
- }
- switch(m.command)
- {
- case CMD_TONEPORTAVOL:
- case CMD_VOLUMESLIDE:
- case CMD_VIBRATOVOL:
- if(m.param || (GetType() != MOD_TYPE_MOD))
- {
- for(uint32 i = 0; i < numTicks; i++)
- {
- chn.isFirstTick = (i == 0);
- VolumeSlide(chn, m.param);
- }
- }
- break;
- case CMD_MODCMDEX:
- if((m.param & 0x0F) || (GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2)))
- {
- chn.isFirstTick = true;
- switch(m.param & 0xF0)
- {
- case 0xA0: FineVolumeUp(chn, m.param & 0x0F, false); break;
- case 0xB0: FineVolumeDown(chn, m.param & 0x0F, false); break;
- }
- }
- break;
- case CMD_S3MCMDEX:
- if(m.param == 0x9E)
- {
- // Play forward
- memory.RenderChannel(nChn, oldTickDuration); // Re-sync what we've got so far
- chn.dwFlags.reset(CHN_PINGPONGFLAG);
- } else if(m.param == 0x9F)
- {
- // Reverse
- memory.RenderChannel(nChn, oldTickDuration); // Re-sync what we've got so far
- chn.dwFlags.set(CHN_PINGPONGFLAG);
- if(!chn.position.GetInt() && chn.nLength && (m.IsNote() || !chn.dwFlags[CHN_LOOP]))
- {
- chn.position.Set(chn.nLength - 1, SamplePosition::fractMax);
- }
- } else if((m.param & 0xF0) == 0x70)
- {
- if(m.param >= 0x73)
- chn.InstrumentControl(m.param, *this);
- }
- break;
- case CMD_DIGIREVERSESAMPLE:
- DigiBoosterSampleReverse(chn, m.param);
- break;
- case CMD_FINETUNE:
- case CMD_FINETUNE_SMOOTH:
- memory.RenderChannel(nChn, oldTickDuration); // Re-sync what we've got so far
- SetFinetune(nChn, playState, false); // TODO should render each tick individually for CMD_FINETUNE_SMOOTH for higher sync accuracy
- break;
- }
- chn.isFirstTick = true;
- switch(m.volcmd)
- {
- case VOLCMD_FINEVOLUP: FineVolumeUp(chn, m.vol, m_playBehaviour[kITVolColMemory]); break;
- case VOLCMD_FINEVOLDOWN: FineVolumeDown(chn, m.vol, m_playBehaviour[kITVolColMemory]); break;
- case VOLCMD_VOLSLIDEUP:
- case VOLCMD_VOLSLIDEDOWN:
- {
- // IT Compatibility: Volume column volume slides have their own memory
- // Test case: VolColMemory.it
- ModCommand::VOL vol = m.vol;
- if(vol == 0 && m_playBehaviour[kITVolColMemory])
- {
- vol = chn.nOldVolParam;
- if(vol == 0)
- break;
- }
- if(m.volcmd == VOLCMD_VOLSLIDEUP)
- vol <<= 4;
- for(uint32 i = 0; i < numTicks; i++)
- {
- chn.isFirstTick = (i == 0);
- VolumeSlide(chn, vol);
- }
- }
- break;
- case VOLCMD_PLAYCONTROL:
- if(m.vol <= 1)
- chn.isPaused = (m.vol == 0);
- break;
- }
- if(chn.isPaused)
- continue;
- if(porta)
- {
- // Portamento needs immediate syncing, as the pitch changes on each tick
- uint32 portaTick = memory.chnSettings[nChn].ticksToRender + startTick + 1;
- memory.chnSettings[nChn].ticksToRender += numTicks;
- memory.RenderChannel(nChn, tickDuration, portaTick);
- } else
- {
- memory.chnSettings[nChn].ticksToRender += (numTicks - startTick);
- }
- }
- }
- }
- oldTickDuration = tickDuration;
- breakToRow = HandleNextRow(playState, orderList, false);
- }
- // Now advance the sample positions for sample seeking on channels that are still playing
- if(adjustSamplePos)
- {
- for(CHANNELINDEX nChn = 0; nChn < GetNumChannels(); nChn++)
- {
- if(memory.chnSettings[nChn].ticksToRender != GetLengthMemory::IGNORE_CHANNEL)
- {
- memory.RenderChannel(nChn, oldTickDuration);
- }
- }
- }
- if(retval.targetReached)
- {
- retval.lastOrder = playState.m_nCurrentOrder;
- retval.lastRow = playState.m_nRow;
- }
- retval.duration = memory.elapsedTime;
- results.push_back(retval);
- // Store final variables
- if(adjustMode & eAdjust)
- {
- if(retval.targetReached || target.mode == GetLengthTarget::NoTarget)
- {
- const auto midiMacroEvaluationResults = std::move(playState.m_midiMacroEvaluationResults);
- playState.m_midiMacroEvaluationResults.reset();
- // Target found, or there is no target (i.e. play whole song)...
- m_PlayState = std::move(playState);
- m_PlayState.ResetGlobalVolumeRamping();
- m_PlayState.m_nNextRow = m_PlayState.m_nRow;
- m_PlayState.m_nFrameDelay = m_PlayState.m_nPatternDelay = 0;
- m_PlayState.m_nTickCount = TICKS_ROW_FINISHED;
- m_PlayState.m_bPositionChanged = true;
- if(m_opl != nullptr)
- m_opl->Reset();
- for(CHANNELINDEX n = 0; n < GetNumChannels(); n++)
- {
- auto &chn = m_PlayState.Chn[n];
- if(chn.nLastNote != NOTE_NONE)
- {
- chn.nNewNote = chn.nLastNote;
- }
- if(memory.chnSettings[n].vol != 0xFF && !adjustSamplePos)
- {
- chn.nVolume = std::min(memory.chnSettings[n].vol, uint8(64)) * 4;
- }
- if(chn.pModSample != nullptr && chn.pModSample->uFlags[CHN_ADLIB] && m_opl)
- {
- m_opl->Patch(n, chn.pModSample->adlib);
- m_opl->NoteCut(n);
- }
- chn.pCurrentSample = nullptr;
- }
- #ifndef NO_PLUGINS
- // If there were any PC events or MIDI macros updating plugin parameters, update plugin parameters to their latest value.
- std::bitset<MAX_MIXPLUGINS> plugSetProgram;
- for(const auto &[plugParam, value] : midiMacroEvaluationResults->pluginParameter)
- {
- PLUGINDEX plug = plugParam.first;
- IMixPlugin *plugin = m_MixPlugins[plug].pMixPlugin;
- if(plugin != nullptr)
- {
- if(!plugSetProgram[plug])
- {
- // Used for bridged plugins to avoid sending out individual messages for each parameter.
- plugSetProgram.set(plug);
- plugin->BeginSetProgram();
- }
- plugin->SetParameter(plugParam.second, value);
- }
- }
- if(plugSetProgram.any())
- {
- for(PLUGINDEX i = 0; i < MAX_MIXPLUGINS; i++)
- {
- if(plugSetProgram[i])
- {
- m_MixPlugins[i].pMixPlugin->EndSetProgram();
- }
- }
- }
- // Do the same for dry/wet ratios
- for(const auto &[plug, dryWetRatio] : midiMacroEvaluationResults->pluginDryWetRatio)
- {
- m_MixPlugins[plug].fDryRatio = dryWetRatio;
- }
- #endif // NO_PLUGINS
- } else if(adjustMode != eAdjustOnSuccess)
- {
- // Target not found (e.g. when jumping to a hidden sub song), reset global variables...
- m_PlayState.m_nMusicSpeed = m_nDefaultSpeed;
- m_PlayState.m_nMusicTempo = m_nDefaultTempo;
- m_PlayState.m_nGlobalVolume = m_nDefaultGlobalVolume;
- }
- // When adjusting the playback status, we will also want to update the visited rows vector according to the current position.
- if(sequence != Order.GetCurrentSequenceIndex())
- {
- Order.SetSequence(sequence);
- }
- }
- if(adjustMode & (eAdjust | eAdjustOnlyVisitedRows))
- m_visitedRows.MoveVisitedRowsFrom(visitedRows);
- return results;
- }
- //////////////////////////////////////////////////////////////////////////////////////////////////
- // Effects
- // Change sample or instrument number.
- void CSoundFile::InstrumentChange(ModChannel &chn, uint32 instr, bool bPorta, bool bUpdVol, bool bResetEnv) const
- {
- const ModInstrument *pIns = instr <= GetNumInstruments() ? Instruments[instr] : nullptr;
- const ModSample *pSmp = &Samples[instr];
- const auto oldInsVol = chn.nInsVol;
- ModCommand::NOTE note = chn.nNewNote;
- if(note == NOTE_NONE && m_playBehaviour[kITInstrWithoutNote]) return;
- if(pIns != nullptr && ModCommand::IsNote(note))
- {
- // Impulse Tracker ignores empty slots.
- // We won't ignore them if a plugin is assigned to this slot, so that VSTis still work as intended.
- // Test case: emptyslot.it, PortaInsNum.it, gxsmp.it, gxsmp2.it
- if(pIns->Keyboard[note - NOTE_MIN] == 0 && m_playBehaviour[kITEmptyNoteMapSlot] && !pIns->HasValidMIDIChannel())
- {
- chn.pModInstrument = pIns;
- return;
- }
- if(pIns->NoteMap[note - NOTE_MIN] > NOTE_MAX) return;
- uint32 n = pIns->Keyboard[note - NOTE_MIN];
- pSmp = ((n) && (n < MAX_SAMPLES)) ? &Samples[n] : nullptr;
- } else if(GetNumInstruments())
- {
- // No valid instrument, or not a valid note.
- if (note >= NOTE_MIN_SPECIAL) return;
- if(m_playBehaviour[kITEmptyNoteMapSlot] && (pIns == nullptr || !pIns->HasValidMIDIChannel()))
- {
- // Impulse Tracker ignores empty slots.
- // We won't ignore them if a plugin is assigned to this slot, so that VSTis still work as intended.
- // Test case: emptyslot.it, PortaInsNum.it, gxsmp.it, gxsmp2.it
- chn.pModInstrument = nullptr;
- chn.nNewIns = 0;
- return;
- }
- pSmp = nullptr;
- }
- bool returnAfterVolumeAdjust = false;
- // instrumentChanged is used for IT carry-on env option
- bool instrumentChanged = (pIns != chn.pModInstrument);
- const bool sampleChanged = (chn.pModSample != nullptr) && (pSmp != chn.pModSample);
- const bool newTuning = (GetType() == MOD_TYPE_MPT && pIns && pIns->pTuning);
- if(!bPorta || instrumentChanged || sampleChanged)
- chn.microTuning = 0;
- // Playback behavior change for MPT: With portamento don't change sample if it is in
- // the same instrument as previous sample.
- if(bPorta && newTuning && pIns == chn.pModInstrument && sampleChanged)
- return;
- if(sampleChanged && bPorta)
- {
- // IT compatibility: No sample change (also within multi-sample instruments) during portamento when using Compatible Gxx.
- // Test case: PortaInsNumCompat.it, PortaSampleCompat.it, PortaCutCompat.it
- if(m_playBehaviour[kITPortamentoInstrument] && m_SongFlags[SONG_ITCOMPATGXX] && !chn.increment.IsZero())
- {
- pSmp = chn.pModSample;
- }
- // Special XM hack (also applies to MOD / S3M, except when playing IT-style S3Ms, such as k_vision.s3m)
- // Test case: PortaSmpChange.mod, PortaSmpChange.s3m, PortaSwap.s3m
- if((!instrumentChanged && (GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2)) && pIns)
- || (GetType() == MOD_TYPE_PLM)
- || (GetType() == MOD_TYPE_MOD && chn.IsSamplePlaying())
- || (m_playBehaviour[kST3PortaSampleChange] && chn.IsSamplePlaying()))
- {
- // FT2 doesn't change the sample in this case,
- // but still uses the sample info from the old one (bug?)
- returnAfterVolumeAdjust = true;
- }
- }
- // IT compatibility: A lone instrument number should only reset sample properties to those of the corresponding sample in instrument mode.
- // C#5 01 ... <-- sample 1
- // C-5 .. g02 <-- sample 2
- // ... 01 ... <-- still sample 1, but with properties of sample 2
- // In the above example, no sample change happens on the second row. In the third row, sample 1 keeps playing but with the
- // volume and panning properties of sample 2.
- // Test case: InstrAfterMultisamplePorta.it
- if(m_nInstruments && !instrumentChanged && sampleChanged && chn.pCurrentSample != nullptr && m_playBehaviour[kITMultiSampleInstrumentNumber] && !chn.rowCommand.IsNote())
- {
- returnAfterVolumeAdjust = true;
- }
- // IT Compatibility: Envelope pickup after SCx cut (but don't do this when working with plugins, or else envelope carry stops working)
- // Test case: cut-carry.it
- if(!chn.IsSamplePlaying() && (GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) && (!pIns || !pIns->HasValidMIDIChannel()))
- {
- instrumentChanged = true;
- }
- // FT2 compatibility: new instrument + portamento = ignore new instrument number, but reload old instrument settings (the world of XM is upside down...)
- // And this does *not* happen if volume column portamento is used together with note delay... (handled in ProcessEffects(), where all the other note delay stuff is.)
- // Test case: porta-delay.xm, SamplePortaInInstrument.xm
- if((instrumentChanged || sampleChanged) && bPorta && m_playBehaviour[kFT2PortaIgnoreInstr] && (chn.pModInstrument != nullptr || chn.pModSample != nullptr))
- {
- pIns = chn.pModInstrument;
- pSmp = chn.pModSample;
- instrumentChanged = false;
- } else
- {
- chn.pModInstrument = pIns;
- }
- // Update Volume
- if (bUpdVol && (!(GetType() & (MOD_TYPE_MOD | MOD_TYPE_S3M)) || ((pSmp != nullptr && pSmp->HasSampleData()) || chn.HasMIDIOutput())))
- {
- if(pSmp)
- {
- if(!pSmp->uFlags[SMP_NODEFAULTVOLUME])
- chn.nVolume = pSmp->nVolume;
- } else if(pIns && pIns->nMixPlug)
- {
- chn.nVolume = chn.GetVSTVolume();
- } else
- {
- chn.nVolume = 0;
- }
- }
- if(returnAfterVolumeAdjust && sampleChanged && pSmp != nullptr)
- {
- // ProTracker applies new instrument's finetune but keeps the old sample playing.
- // Test case: PortaSwapPT.mod
- if(m_playBehaviour[kMODSampleSwap])
- chn.nFineTune = pSmp->nFineTune;
- // ST3 does it similarly for middle-C speed.
- // Test case: PortaSwap.s3m, SampleSwap.s3m
- if(GetType() == MOD_TYPE_S3M && pSmp->HasSampleData())
- chn.nC5Speed = pSmp->nC5Speed;
- }
- if(returnAfterVolumeAdjust) return;
- // Instrument adjust
- chn.nNewIns = 0;
- // IT Compatiblity: NNA is reset on every note change, not every instrument change (fixes s7xinsnum.it).
- if (pIns && ((!m_playBehaviour[kITNNAReset] && pSmp) || pIns->nMixPlug || instrumentChanged))
- chn.nNNA = pIns->nNNA;
- // Update volume
- chn.UpdateInstrumentVolume(pSmp, pIns);
- // Update panning
- // FT2 compatibility: Only reset panning on instrument numbers, not notes (bUpdVol condition)
- // Test case: PanMemory.xm
- // IT compatibility: Sample and instrument panning is only applied on note change, not instrument change
- // Test case: PanReset.it
- if((bUpdVol || !(GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2))) && !m_playBehaviour[kITPanningReset])
- {
- ApplyInstrumentPanning(chn, pIns, pSmp);
- }
- // Reset envelopes
- if(bResetEnv)
- {
- // Blurb by Storlek (from the SchismTracker code):
- // Conditions experimentally determined to cause envelope reset in Impulse Tracker:
- // - no note currently playing (of course)
- // - note given, no portamento
- // - instrument number given, portamento, compat gxx enabled
- // - instrument number given, no portamento, after keyoff, old effects enabled
- // If someone can enlighten me to what the logic really is here, I'd appreciate it.
- // Seems like it's just a total mess though, probably to get XMs to play right.
- bool reset, resetAlways;
- // IT Compatibility: Envelope reset
- // Test case: EnvReset.it
- if(m_playBehaviour[kITEnvelopeReset])
- {
- const bool insNumber = (instr != 0);
- reset = (!chn.nLength
- || (insNumber && bPorta && m_SongFlags[SONG_ITCOMPATGXX])
- || (insNumber && !bPorta && chn.dwFlags[CHN_NOTEFADE | CHN_KEYOFF] && m_SongFlags[SONG_ITOLDEFFECTS]));
- // NOTE: IT2.14 with SB/GUS/etc. output is different. We are going after IT's WAV writer here.
- // For SB/GUS/etc. emulation, envelope carry should only apply when the NNA isn't set to "Note Cut".
- // Test case: CarryNNA.it
- resetAlways = (!chn.nFadeOutVol || instrumentChanged || chn.dwFlags[CHN_KEYOFF]);
- } else
- {
- reset = (!bPorta || !(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_DBM)) || m_SongFlags[SONG_ITCOMPATGXX]
- || !chn.nLength || (chn.dwFlags[CHN_NOTEFADE] && !chn.nFadeOutVol));
- resetAlways = !(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_DBM)) || instrumentChanged || pIns == nullptr || chn.dwFlags[CHN_KEYOFF | CHN_NOTEFADE];
- }
- if(reset)
- {
- chn.dwFlags.set(CHN_FASTVOLRAMP);
- if(pIns != nullptr)
- {
- if(resetAlways)
- {
- chn.ResetEnvelopes();
- } else
- {
- if(!pIns->VolEnv.dwFlags[ENV_CARRY]) chn.VolEnv.Reset();
- if(!pIns->PanEnv.dwFlags[ENV_CARRY]) chn.PanEnv.Reset();
- if(!pIns->PitchEnv.dwFlags[ENV_CARRY]) chn.PitchEnv.Reset();
- }
- }
- // IT Compatibility: Autovibrato reset
- if(!m_playBehaviour[kITVibratoTremoloPanbrello])
- {
- chn.nAutoVibDepth = 0;
- chn.nAutoVibPos = 0;
- }
- } else if(pIns != nullptr && !pIns->VolEnv.dwFlags[ENV_ENABLED])
- {
- if(m_playBehaviour[kITPortamentoInstrument])
- {
- chn.VolEnv.Reset();
- } else
- {
- chn.ResetEnvelopes();
- }
- }
- }
- // Invalid sample ?
- if(pSmp == nullptr && (pIns == nullptr || !pIns->HasValidMIDIChannel()))
- {
- chn.pModSample = nullptr;
- chn.nInsVol = 0;
- return;
- }
- // Tone-Portamento doesn't reset the pingpong direction flag
- if(bPorta && pSmp == chn.pModSample && pSmp != nullptr)
- {
- // If channel length is 0, we cut a previous sample using SCx. In that case, we have to update sample length, loop points, etc...
- if(GetType() & (MOD_TYPE_S3M|MOD_TYPE_IT|MOD_TYPE_MPT) && chn.nLength != 0)
- return;
- // FT2 compatibility: Do not reset key-off status on portamento without instrument number
- // Test case: Off-Porta.xm
- if(GetType() != MOD_TYPE_XM || !m_playBehaviour[kITFT2DontResetNoteOffOnPorta] || chn.rowCommand.instr != 0)
- chn.dwFlags.reset(CHN_KEYOFF | CHN_NOTEFADE);
- chn.dwFlags = (chn.dwFlags & (CHN_CHANNELFLAGS | CHN_PINGPONGFLAG));
- } else //if(!instrumentChanged || chn.rowCommand.instr != 0 || !IsCompatibleMode(TRK_FASTTRACKER2)) // SampleChange.xm?
- {
- chn.dwFlags.reset(CHN_KEYOFF | CHN_NOTEFADE);
- // IT compatibility: Don't change bidi loop direction when no sample nor instrument is changed.
- if((m_playBehaviour[kITPingPongNoReset] || !(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT))) && pSmp == chn.pModSample && !instrumentChanged)
- chn.dwFlags = (chn.dwFlags & (CHN_CHANNELFLAGS | CHN_PINGPONGFLAG));
- else
- chn.dwFlags = (chn.dwFlags & CHN_CHANNELFLAGS);
- if(pIns)
- {
- // Copy envelope flags (we actually only need the "enabled" and "pitch" flag)
- chn.VolEnv.flags = pIns->VolEnv.dwFlags;
- chn.PanEnv.flags = pIns->PanEnv.dwFlags;
- chn.PitchEnv.flags = pIns->PitchEnv.dwFlags;
- // A cutoff frequency of 0 should not be reset just because the filter envelope is enabled.
- // Test case: FilterEnvReset.it
- if((pIns->PitchEnv.dwFlags & (ENV_ENABLED | ENV_FILTER)) == (ENV_ENABLED | ENV_FILTER) && !m_playBehaviour[kITFilterBehaviour])
- {
- if(!chn.nCutOff) chn.nCutOff = 0x7F;
- }
- if(pIns->IsCutoffEnabled()) chn.nCutOff = pIns->GetCutoff();
- if(pIns->IsResonanceEnabled()) chn.nResonance = pIns->GetResonance();
- }
- }
- if(pSmp == nullptr)
- {
- chn.pModSample = nullptr;
- chn.nLength = 0;
- return;
- }
- if(bPorta && chn.nLength == 0 && (m_playBehaviour[kFT2PortaNoNote] || m_playBehaviour[kITPortaNoNote]))
- {
- // IT/FT2 compatibility: If the note just stopped on the previous tick, prevent it from restarting.
- // Test cases: PortaJustStoppedNote.xm, PortaJustStoppedNote.it
- chn.increment.Set(0);
- }
- // IT compatibility: Note-off with instrument number + Old Effects retriggers envelopes.
- // If the instrument changes, keep playing the previous sample, but load the new instrument's envelopes.
- // Test case: ResetEnvNoteOffOldFx.it
- if(chn.rowCommand.note == NOTE_KEYOFF && m_playBehaviour[kITInstrWithNoteOffOldEffects] && m_SongFlags[SONG_ITOLDEFFECTS] && sampleChanged)
- {
- if(chn.pModSample)
- {
- chn.dwFlags |= (chn.pModSample->uFlags & CHN_SAMPLEFLAGS);
- }
- chn.nInsVol = oldInsVol;
- chn.nVolume = pSmp->nVolume;
- if(pSmp->uFlags[CHN_PANNING]) chn.SetInstrumentPan(pSmp->nPan, *this);
- return;
- }
- chn.pModSample = pSmp;
- chn.nLength = pSmp->nLength;
- chn.nLoopStart = pSmp->nLoopStart;
- chn.nLoopEnd = pSmp->nLoopEnd;
- // ProTracker "oneshot" loops (if loop start is 0, play the whole sample once and then repeat until loop end)
- if(m_playBehaviour[kMODOneShotLoops] && chn.nLoopStart == 0) chn.nLoopEnd = pSmp->nLength;
- chn.dwFlags |= (pSmp->uFlags & CHN_SAMPLEFLAGS);
- // IT Compatibility: Autovibrato reset
- if(m_playBehaviour[kITVibratoTremoloPanbrello])
- {
- chn.nAutoVibDepth = 0;
- chn.nAutoVibPos = 0;
- }
- if(newTuning)
- {
- chn.nC5Speed = pSmp->nC5Speed;
- chn.m_CalculateFreq = true;
- chn.nFineTune = 0;
- } else if(!bPorta || sampleChanged || !(GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM)))
- {
- // Don't reset finetune changed by "set finetune" command.
- // Test case: finetune.xm, finetune.mod
- // But *do* change the finetune if we switch to a different sample, to fix
- // Miranda`s axe by Jamson (jam007.xm).
- chn.nC5Speed = pSmp->nC5Speed;
- chn.nFineTune = pSmp->nFineTune;
- }
- chn.nTranspose = UseFinetuneAndTranspose() ? pSmp->RelativeTone : 0;
- // FT2 compatibility: Don't reset portamento target with new instrument numbers.
- // Test case: Porta-Pickup.xm
- // ProTracker does the same.
- // Test case: PortaTarget.mod
- if(!m_playBehaviour[kFT2PortaTargetNoReset] && GetType() != MOD_TYPE_MOD)
- {
- chn.nPortamentoDest = 0;
- }
- chn.m_PortamentoFineSteps = 0;
- if(chn.dwFlags[CHN_SUSTAINLOOP])
- {
- chn.nLoopStart = pSmp->nSustainStart;
- chn.nLoopEnd = pSmp->nSustainEnd;
- if(chn.dwFlags[CHN_PINGPONGSUSTAIN]) chn.dwFlags.set(CHN_PINGPONGLOOP);
- chn.dwFlags.set(CHN_LOOP);
- }
- if(chn.dwFlags[CHN_LOOP] && chn.nLoopEnd < chn.nLength) chn.nLength = chn.nLoopEnd;
- // Fix sample position on instrument change. This is needed for IT "on the fly" sample change.
- // XXX is this actually called? In ProcessEffects(), a note-on effect is emulated if there's an on the fly sample change!
- if(chn.position.GetUInt() >= chn.nLength)
- {
- if((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)))
- {
- chn.position.Set(0);
- }
- }
- }
- void CSoundFile::NoteChange(ModChannel &chn, int note, bool bPorta, bool bResetEnv, bool bManual, CHANNELINDEX channelHint) const
- {
- if(note < NOTE_MIN)
- return;
- const int origNote = note;
- const ModSample *pSmp = chn.pModSample;
- const ModInstrument *pIns = chn.pModInstrument;
- const bool newTuning = (GetType() == MOD_TYPE_MPT && pIns != nullptr && pIns->pTuning);
- // save the note that's actually used, as it's necessary to properly calculate PPS and stuff
- const int realnote = note;
- if((pIns) && (note - NOTE_MIN < (int)std::size(pIns->Keyboard)))
- {
- uint32 n = pIns->Keyboard[note - NOTE_MIN];
- if((n) && (n < MAX_SAMPLES))
- {
- pSmp = &Samples[n];
- } else if(m_playBehaviour[kITEmptyNoteMapSlot] && !chn.HasMIDIOutput())
- {
- // Impulse Tracker ignores empty slots.
- // We won't ignore them if a plugin is assigned to this slot, so that VSTis still work as intended.
- // Test case: emptyslot.it, PortaInsNum.it, gxsmp.it, gxsmp2.it
- return;
- }
- note = pIns->NoteMap[note - NOTE_MIN];
- }
- // Key Off
- if(note > NOTE_MAX)
- {
- // Key Off (+ Invalid Note for XM - TODO is this correct?)
- if(note == NOTE_KEYOFF || !(GetType() & (MOD_TYPE_IT|MOD_TYPE_MPT)))
- {
- KeyOff(chn);
- // IT compatibility: Note-off + instrument releases sample sustain but does not release envelopes or fade the instrument
- // Test case: noteoff3.it, ResetEnvNoteOffOldFx2.it
- if(!bPorta && m_playBehaviour[kITInstrWithNoteOffOldEffects] && m_SongFlags[SONG_ITOLDEFFECTS] && chn.rowCommand.instr)
- chn.dwFlags.reset(CHN_NOTEFADE | CHN_KEYOFF);
- } else // Invalid Note -> Note Fade
- {
- if(/*note == NOTE_FADE && */ GetNumInstruments())
- chn.dwFlags.set(CHN_NOTEFADE);
- }
- // Note Cut
- if (note == NOTE_NOTECUT)
- {
- if(chn.dwFlags[CHN_ADLIB] && GetType() == MOD_TYPE_S3M)
- {
- // OPL voices are not cut but enter the release portion of their envelope
- // In S3M we can still modify the volume after note-off, in legacy MPTM mode we can't
- chn.dwFlags.set(CHN_KEYOFF);
- } else
- {
- chn.dwFlags.set(CHN_NOTEFADE | CHN_FASTVOLRAMP);
- // IT compatibility: Stopping sample playback by setting sample increment to 0 rather than volume
- // Test case: NoteOffInstr.it
- if ((!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT))) || (m_nInstruments != 0 && !m_playBehaviour[kITInstrWithNoteOff])) chn.nVolume = 0;
- if (m_playBehaviour[kITInstrWithNoteOff]) chn.increment.Set(0);
- chn.nFadeOutVol = 0;
- }
- }
- // IT compatibility tentative fix: Clear channel note memory (TRANCE_N.IT by A3F).
- if(m_playBehaviour[kITClearOldNoteAfterCut])
- {
- chn.nNote = chn.nNewNote = NOTE_NONE;
- }
- return;
- }
- if(newTuning)
- {
- if(!bPorta || chn.nNote == NOTE_NONE)
- chn.nPortamentoDest = 0;
- else
- {
- chn.nPortamentoDest = pIns->pTuning->GetStepDistance(chn.nNote, chn.m_PortamentoFineSteps, static_cast<Tuning::NOTEINDEXTYPE>(note), 0);
- //Here chn.nPortamentoDest means 'steps to slide'.
- chn.m_PortamentoFineSteps = -chn.nPortamentoDest;
- }
- }
- if(!bPorta && (GetType() & (MOD_TYPE_XM | MOD_TYPE_MED | MOD_TYPE_MT2)))
- {
- if(pSmp)
- {
- chn.nTranspose = pSmp->RelativeTone;
- chn.nFineTune = pSmp->nFineTune;
- }
- }
- // IT Compatibility: Update multisample instruments frequency even if instrument is not specified (fixes the guitars in spx-shuttledeparture.it)
- // Test case: freqreset-noins.it
- if(!bPorta && pSmp && m_playBehaviour[kITMultiSampleBehaviour])
- chn.nC5Speed = pSmp->nC5Speed;
- if(bPorta && !chn.IsSamplePlaying())
- {
- if(m_playBehaviour[kFT2PortaNoNote])
- {
- // FT2 Compatibility: Ignore notes with portamento if there was no note playing.
- // Test case: 3xx-no-old-samp.xm
- chn.nPeriod = 0;
- return;
- } else if(m_playBehaviour[kITPortaNoNote])
- {
- // IT Compatibility: Ignore portamento command if no note was playing (e.g. if a previous note has faded out).
- // Test case: Fade-Porta.it
- bPorta = false;
- }
- }
- if(UseFinetuneAndTranspose())
- {
- note += chn.nTranspose;
- // RealNote = PatternNote + RelativeTone; (0..118, 0 = C-0, 118 = A#9)
- Limit(note, NOTE_MIN + 11, NOTE_MIN + 130); // 119 possible notes
- } else
- {
- Limit(note, NOTE_MIN, NOTE_MAX);
- }
- if(m_playBehaviour[kITRealNoteMapping])
- {
- // need to memorize the original note for various effects (e.g. PPS)
- chn.nNote = static_cast<ModCommand::NOTE>(Clamp(realnote, NOTE_MIN, NOTE_MAX));
- } else
- {
- chn.nNote = static_cast<ModCommand::NOTE>(note);
- }
- chn.m_CalculateFreq = true;
- chn.isPaused = false;
- if ((!bPorta) || (GetType() & (MOD_TYPE_S3M|MOD_TYPE_IT|MOD_TYPE_MPT)))
- chn.nNewIns = 0;
- uint32 period = GetPeriodFromNote(note, chn.nFineTune, chn.nC5Speed);
- chn.nPanbrelloOffset = 0;
- // IT compatibility: Sample and instrument panning is only applied on note change, not instrument change
- // Test case: PanReset.it
- if(m_playBehaviour[kITPanningReset])
- ApplyInstrumentPanning(chn, pIns, pSmp);
- // IT compatibility: Pitch/Pan Separation can be overriden by panning commands, and shouldn't be affected by note-off commands
- // Test case: PitchPanReset.it
- if(m_playBehaviour[kITPitchPanSeparation] && pIns && pIns->nPPS)
- {
- if(!chn.nRestorePanOnNewNote)
- chn.nRestorePanOnNewNote = static_cast<uint16>(chn.nPan + 1);
- ProcessPitchPanSeparation(chn.nPan, origNote, *pIns);
- }
- if(bResetEnv && !bPorta)
- {
- chn.nVolSwing = chn.nPanSwing = 0;
- chn.nResSwing = chn.nCutSwing = 0;
- if(pIns)
- {
- // IT Compatiblity: NNA is reset on every note change, not every instrument change (fixes spx-farspacedance.it).
- if(m_playBehaviour[kITNNAReset]) chn.nNNA = pIns->nNNA;
- if(!pIns->VolEnv.dwFlags[ENV_CARRY]) chn.VolEnv.Reset();
- if(!pIns->PanEnv.dwFlags[ENV_CARRY]) chn.PanEnv.Reset();
- if(!pIns->PitchEnv.dwFlags[ENV_CARRY]) chn.PitchEnv.Reset();
- // Volume Swing
- if(pIns->nVolSwing)
- {
- chn.nVolSwing = static_cast<int16>(((mpt::random<int8>(AccessPRNG()) * pIns->nVolSwing) / 64 + 1) * (m_playBehaviour[kITSwingBehaviour] ? chn.nInsVol : ((chn.nVolume + 1) / 2)) / 199);
- }
- // Pan Swing
- if(pIns->nPanSwing)
- {
- chn.nPanSwing = static_cast<int16>(((mpt::random<int8>(AccessPRNG()) * pIns->nPanSwing * 4) / 128));
- if(!m_playBehaviour[kITSwingBehaviour] && chn.nRestorePanOnNewNote == 0)
- {
- chn.nRestorePanOnNewNote = static_cast<uint16>(chn.nPan + 1);
- }
- }
- // Cutoff Swing
- if(pIns->nCutSwing)
- {
- int32 d = ((int32)pIns->nCutSwing * (int32)(static_cast<int32>(mpt::random<int8>(AccessPRNG())) + 1)) / 128;
- chn.nCutSwing = static_cast<int16>((d * chn.nCutOff + 1) / 128);
- chn.nRestoreCutoffOnNewNote = chn.nCutOff + 1;
- }
- // Resonance Swing
- if(pIns->nResSwing)
- {
- int32 d = ((int32)pIns->nResSwing * (int32)(static_cast<int32>(mpt::random<int8>(AccessPRNG())) + 1)) / 128;
- chn.nResSwing = static_cast<int16>((d * chn.nResonance + 1) / 128);
- chn.nRestoreResonanceOnNewNote = chn.nResonance + 1;
- }
- }
- }
- if(!pSmp) return;
- if(period)
- {
- if((!bPorta) || (!chn.nPeriod)) chn.nPeriod = period;
- if(!newTuning)
- {
- // FT2 compatibility: Don't reset portamento target with new notes.
- // Test case: Porta-Pickup.xm
- // ProTracker does the same.
- // Test case: PortaTarget.mod
- // IT compatibility: Portamento target is completely cleared with new notes.
- // Test case: PortaReset.it
- if(bPorta || !(m_playBehaviour[kFT2PortaTargetNoReset] || m_playBehaviour[kITClearPortaTarget] || GetType() == MOD_TYPE_MOD))
- {
- chn.nPortamentoDest = period;
- chn.portaTargetReached = false;
- }
- }
- if(!bPorta || (!chn.nLength && !(GetType() & MOD_TYPE_S3M)))
- {
- chn.pModSample = pSmp;
- chn.nLength = pSmp->nLength;
- chn.nLoopEnd = pSmp->nLength;
- chn.nLoopStart = 0;
- chn.position.Set(0);
- if((m_SongFlags[SONG_PT_MODE] || m_playBehaviour[kST3OffsetWithoutInstrument]) && !chn.rowCommand.instr)
- {
- chn.position.SetInt(std::min(chn.prevNoteOffset, chn.nLength - SmpLength(1)));
- } else
- {
- chn.prevNoteOffset = 0;
- }
- chn.dwFlags = (chn.dwFlags & CHN_CHANNELFLAGS) | (pSmp->uFlags & CHN_SAMPLEFLAGS);
- chn.dwFlags.reset(CHN_PORTAMENTO);
- if(chn.dwFlags[CHN_SUSTAINLOOP])
- {
- chn.nLoopStart = pSmp->nSustainStart;
- chn.nLoopEnd = pSmp->nSustainEnd;
- chn.dwFlags.set(CHN_PINGPONGLOOP, chn.dwFlags[CHN_PINGPONGSUSTAIN]);
- chn.dwFlags.set(CHN_LOOP);
- if (chn.nLength > chn.nLoopEnd) chn.nLength = chn.nLoopEnd;
- } else if(chn.dwFlags[CHN_LOOP])
- {
- chn.nLoopStart = pSmp->nLoopStart;
- chn.nLoopEnd = pSmp->nLoopEnd;
- if (chn.nLength > chn.nLoopEnd) chn.nLength = chn.nLoopEnd;
- }
- // ProTracker "oneshot" loops (if loop start is 0, play the whole sample once and then repeat until loop end)
- if(m_playBehaviour[kMODOneShotLoops] && chn.nLoopStart == 0) chn.nLoopEnd = chn.nLength = pSmp->nLength;
- if(chn.dwFlags[CHN_REVERSE] && chn.nLength > 0)
- {
- chn.dwFlags.set(CHN_PINGPONGFLAG);
- chn.position.SetInt(chn.nLength - 1);
- }
- // Handle "retrigger" waveform type
- if(chn.nVibratoType < 4)
- {
- // IT Compatibilty: Slightly different waveform offsets (why does MPT have two different offsets here with IT old effects enabled and disabled?)
- if(!m_playBehaviour[kITVibratoTremoloPanbrello] && (GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) && !m_SongFlags[SONG_ITOLDEFFECTS])
- chn.nVibratoPos = 0x10;
- else if(GetType() == MOD_TYPE_MTM)
- chn.nVibratoPos = 0x20;
- else if(!(GetType() & (MOD_TYPE_DIGI | MOD_TYPE_DBM)))
- chn.nVibratoPos = 0;
- }
- // IT Compatibility: No "retrigger" waveform here
- if(!m_playBehaviour[kITVibratoTremoloPanbrello] && chn.nTremoloType < 4)
- {
- chn.nTremoloPos = 0;
- }
- }
- if(chn.position.GetUInt() >= chn.nLength) chn.position.SetInt(chn.nLoopStart);
- } else
- {
- bPorta = false;
- }
- if (!bPorta
- || (!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_DBM)))
- || (chn.dwFlags[CHN_NOTEFADE] && !chn.nFadeOutVol)
- || (m_SongFlags[SONG_ITCOMPATGXX] && chn.rowCommand.instr != 0))
- {
- if((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_DBM)) && chn.dwFlags[CHN_NOTEFADE] && !chn.nFadeOutVol)
- {
- chn.ResetEnvelopes();
- // IT Compatibility: Autovibrato reset
- if(!m_playBehaviour[kITVibratoTremoloPanbrello])
- {
- chn.nAutoVibDepth = 0;
- chn.nAutoVibPos = 0;
- }
- chn.dwFlags.reset(CHN_NOTEFADE);
- chn.nFadeOutVol = 65536;
- }
- if ((!bPorta) || (!m_SongFlags[SONG_ITCOMPATGXX]) || (chn.rowCommand.instr))
- {
- if ((!(GetType() & (MOD_TYPE_XM|MOD_TYPE_MT2))) || (chn.rowCommand.instr))
- {
- chn.dwFlags.reset(CHN_NOTEFADE);
- chn.nFadeOutVol = 65536;
- }
- }
- }
- // IT compatibility: Don't reset key-off flag on porta notes unless Compat Gxx is enabled.
- // Test case: Off-Porta.it, Off-Porta-CompatGxx.it, Off-Porta.xm
- if(m_playBehaviour[kITFT2DontResetNoteOffOnPorta] && bPorta && (!m_SongFlags[SONG_ITCOMPATGXX] || chn.rowCommand.instr == 0))
- chn.dwFlags.reset(CHN_EXTRALOUD);
- else
- chn.dwFlags.reset(CHN_EXTRALOUD | CHN_KEYOFF);
- // Enable Ramping
- if(!bPorta)
- {
- chn.nLeftVU = chn.nRightVU = 0xFF;
- chn.dwFlags.reset(CHN_FILTER);
- chn.dwFlags.set(CHN_FASTVOLRAMP);
- // IT compatibility 15. Retrigger is reset in RetrigNote (Tremor doesn't store anything here, so we just don't reset this as well)
- if(!m_playBehaviour[kITRetrigger] && !m_playBehaviour[kITTremor])
- {
- // FT2 compatibility: Retrigger is reset in RetrigNote, tremor in ProcessEffects
- if(!m_playBehaviour[kFT2Retrigger] && !m_playBehaviour[kFT2Tremor])
- {
- chn.nRetrigCount = 0;
- chn.nTremorCount = 0;
- }
- }
- if(bResetEnv)
- {
- chn.nAutoVibDepth = 0;
- chn.nAutoVibPos = 0;
- }
- chn.rightVol = chn.leftVol = 0;
- bool useFilter = !m_SongFlags[SONG_MPTFILTERMODE];
- // Setup Initial Filter for this note
- if(pIns)
- {
- if(pIns->IsResonanceEnabled())
- {
- chn.nResonance = pIns->GetResonance();
- useFilter = true;
- }
- if(pIns->IsCutoffEnabled())
- {
- chn.nCutOff = pIns->GetCutoff();
- useFilter = true;
- }
- if(useFilter && (pIns->filterMode != FilterMode::Unchanged))
- {
- chn.nFilterMode = pIns->filterMode;
- }
- } else
- {
- chn.nVolSwing = chn.nPanSwing = 0;
- chn.nCutSwing = chn.nResSwing = 0;
- }
- if((chn.nCutOff < 0x7F || m_playBehaviour[kITFilterBehaviour]) && useFilter)
- {
- int cutoff = SetupChannelFilter(chn, true);
- if(cutoff >= 0 && chn.dwFlags[CHN_ADLIB] && m_opl && channelHint != CHANNELINDEX_INVALID)
- m_opl->Volume(channelHint, chn.nCutOff / 2u, true);
- }
- if(chn.dwFlags[CHN_ADLIB] && m_opl && channelHint != CHANNELINDEX_INVALID)
- {
- // Test case: AdlibZeroVolumeNote.s3m
- if(m_playBehaviour[kOPLNoteOffOnNoteChange])
- m_opl->NoteOff(channelHint);
- else if(m_playBehaviour[kOPLNoteStopWith0Hz])
- m_opl->Frequency(channelHint, 0, true, false);
- }
- }
- // Special case for MPT
- if (bManual) chn.dwFlags.reset(CHN_MUTE);
- if((chn.dwFlags[CHN_MUTE] && (m_MixerSettings.MixerFlags & SNDMIX_MUTECHNMODE))
- || (chn.pModSample != nullptr && chn.pModSample->uFlags[CHN_MUTE] && !bManual)
- || (chn.pModInstrument != nullptr && chn.pModInstrument->dwFlags[INS_MUTE] && !bManual))
- {
- if (!bManual) chn.nPeriod = 0;
- }
- // Reset the Amiga resampler for this channel
- if(!bPorta)
- {
- chn.paulaState.Reset();
- }
- }
- // Apply sample or instrument panning
- void CSoundFile::ApplyInstrumentPanning(ModChannel &chn, const ModInstrument *instr, const ModSample *smp) const
- {
- int32 newPan = int32_min;
- // Default instrument panning
- if(instr != nullptr && instr->dwFlags[INS_SETPANNING])
- newPan = instr->nPan;
- // Default sample panning
- if(smp != nullptr && smp->uFlags[CHN_PANNING])
- newPan = smp->nPan;
- if(newPan != int32_min)
- {
- chn.SetInstrumentPan(newPan, *this);
- // IT compatibility: Sample and instrument panning overrides channel surround status.
- // Test case: SmpInsPanSurround.it
- if(m_playBehaviour[kPanOverride] && !m_SongFlags[SONG_SURROUNDPAN])
- {
- chn.dwFlags.reset(CHN_SURROUND);
- }
- }
- }
- CHANNELINDEX CSoundFile::GetNNAChannel(CHANNELINDEX nChn) const
- {
- // Check for empty channel
- for(CHANNELINDEX i = m_nChannels; i < MAX_CHANNELS; i++)
- {
- const ModChannel &c = m_PlayState.Chn[i];
- // No sample and no plugin playing
- if(!c.nLength && !c.HasMIDIOutput())
- return i;
- // Plugin channel with already released note
- if(!c.nLength && c.dwFlags[CHN_KEYOFF | CHN_NOTEFADE])
- return i;
- // Stopped OPL channel
- if(c.dwFlags[CHN_ADLIB] && (!m_opl || !m_opl->IsActive(i)))
- return i;
- }
- uint32 vol = 0x800000;
- if(nChn < MAX_CHANNELS)
- {
- const ModChannel &srcChn = m_PlayState.Chn[nChn];
- if(!srcChn.nFadeOutVol && srcChn.nLength)
- return CHANNELINDEX_INVALID;
- vol = (srcChn.nRealVolume << 9) | srcChn.nVolume;
- }
- // All channels are used: check for lowest volume
- CHANNELINDEX result = CHANNELINDEX_INVALID;
- uint32 envpos = 0;
- for(CHANNELINDEX i = m_nChannels; i < MAX_CHANNELS; i++)
- {
- const ModChannel &c = m_PlayState.Chn[i];
- if(c.nLength && !c.nFadeOutVol)
- return i;
- // Use a combination of real volume [14 bit] (which includes volume envelopes, but also potentially global volume) and note volume [9 bit].
- // Rationale: We need volume envelopes in case e.g. all NNA channels are playing at full volume but are looping on a 0-volume envelope node.
- // But if global volume is not applied to master and the global volume temporarily drops to 0, we would kill arbitrary channels. Hence, add the note volume as well.
- uint32 v = (c.nRealVolume << 9) | c.nVolume;
- if(c.dwFlags[CHN_LOOP])
- v /= 2;
- if((v < vol) || ((v == vol) && (c.VolEnv.nEnvPosition > envpos)))
- {
- envpos = c.VolEnv.nEnvPosition;
- vol = v;
- result = i;
- }
- }
- return result;
- }
- CHANNELINDEX CSoundFile::CheckNNA(CHANNELINDEX nChn, uint32 instr, int note, bool forceCut)
- {
- ModChannel &srcChn = m_PlayState.Chn[nChn];
- const ModInstrument *pIns = nullptr;
- if(!ModCommand::IsNote(static_cast<ModCommand::NOTE>(note)))
- return CHANNELINDEX_INVALID;
- // Always NNA cut - using
- if((!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_MT2)) || !m_nInstruments || forceCut) && !srcChn.HasMIDIOutput())
- {
- if(!srcChn.nLength || srcChn.dwFlags[CHN_MUTE] || !(srcChn.rightVol | srcChn.leftVol))
- return CHANNELINDEX_INVALID;
- if(srcChn.dwFlags[CHN_ADLIB] && m_opl)
- {
- m_opl->NoteCut(nChn, false);
- return CHANNELINDEX_INVALID;
- }
- const CHANNELINDEX nnaChn = GetNNAChannel(nChn);
- if(nnaChn == CHANNELINDEX_INVALID)
- return CHANNELINDEX_INVALID;
- ModChannel &chn = m_PlayState.Chn[nnaChn];
- // Copy Channel
- chn = srcChn;
- chn.dwFlags.reset(CHN_VIBRATO | CHN_TREMOLO | CHN_MUTE | CHN_PORTAMENTO);
- chn.nPanbrelloOffset = 0;
- chn.nMasterChn = nChn + 1;
- chn.nCommand = CMD_NONE;
- chn.rowCommand.Clear();
- // Cut the note
- chn.nFadeOutVol = 0;
- chn.dwFlags.set(CHN_NOTEFADE | CHN_FASTVOLRAMP);
- // Stop this channel
- srcChn.nLength = 0;
- srcChn.position.Set(0);
- srcChn.nROfs = srcChn.nLOfs = 0;
- srcChn.rightVol = srcChn.leftVol = 0;
- return nnaChn;
- }
- if(instr > GetNumInstruments())
- instr = 0;
- const ModSample *pSample = srcChn.pModSample;
- // If no instrument is given, assume previous instrument to still be valid.
- // Test case: DNA-NoInstr.it
- pIns = instr > 0 ? Instruments[instr] : srcChn.pModInstrument;
- auto dnaNote = note;
- if(pIns != nullptr)
- {
- auto smp = pIns->Keyboard[note - NOTE_MIN];
- // IT compatibility: DCT = note uses pattern notes for comparison
- // Note: This is not applied in case kITRealNoteMapping is not set to keep playback of legacy modules simple (chn.nNote is translated note in that case)
- // Test case: dct_smp_note_test.it
- if(!m_playBehaviour[kITDCTBehaviour] || !m_playBehaviour[kITRealNoteMapping])
- dnaNote = pIns->NoteMap[note - NOTE_MIN];
- if(smp > 0 && smp < MAX_SAMPLES)
- {
- pSample = &Samples[smp];
- } else if(m_playBehaviour[kITEmptyNoteMapSlot] && !pIns->HasValidMIDIChannel())
- {
- // Impulse Tracker ignores empty slots.
- // We won't ignore them if a plugin is assigned to this slot, so that VSTis still work as intended.
- // Test case: emptyslot.it, PortaInsNum.it, gxsmp.it, gxsmp2.it
- return CHANNELINDEX_INVALID;
- }
- }
- if(srcChn.dwFlags[CHN_MUTE])
- return CHANNELINDEX_INVALID;
- for(CHANNELINDEX i = nChn; i < MAX_CHANNELS; i++)
- {
- // Only apply to background channels, or the same pattern channel
- if(i < m_nChannels && i != nChn)
- continue;
- ModChannel &chn = m_PlayState.Chn[i];
- bool applyDNAtoPlug = false;
- if((chn.nMasterChn == nChn + 1 || i == nChn) && chn.pModInstrument != nullptr)
- {
- bool applyDNA = false;
- // Duplicate Check Type
- switch(chn.pModInstrument->nDCT)
- {
- case DuplicateCheckType::None:
- break;
- // Note
- case DuplicateCheckType::Note:
- if(dnaNote != NOTE_NONE && chn.nNote == dnaNote && pIns == chn.pModInstrument)
- applyDNA = true;
- if(pIns && pIns->nMixPlug)
- applyDNAtoPlug = true;
- break;
- // Sample
- case DuplicateCheckType::Sample:
- // IT compatibility: DCT = sample only applies to same instrument
- // Test case: dct_smp_note_test.it
- if(pSample != nullptr && pSample == chn.pModSample && (pIns == chn.pModInstrument || !m_playBehaviour[kITDCTBehaviour]))
- applyDNA = true;
- break;
- // Instrument
- case DuplicateCheckType::Instrument:
- if(pIns == chn.pModInstrument)
- applyDNA = true;
- if(pIns && pIns->nMixPlug)
- applyDNAtoPlug = true;
- break;
- // Plugin
- case DuplicateCheckType::Plugin:
- if(pIns && (pIns->nMixPlug) && (pIns->nMixPlug == chn.pModInstrument->nMixPlug))
- {
- applyDNAtoPlug = true;
- applyDNA = true;
- }
- break;
- }
-
- // Duplicate Note Action
- if(applyDNA)
- {
- #ifndef NO_PLUGINS
- if(applyDNAtoPlug && chn.nNote != NOTE_NONE)
- {
- switch(chn.pModInstrument->nDNA)
- {
- case DuplicateNoteAction::NoteCut:
- case DuplicateNoteAction::NoteOff:
- case DuplicateNoteAction::NoteFade:
- // Switch off duplicated note played on this plugin
- if(const auto oldNote = chn.GetPluginNote(m_playBehaviour[kITRealNoteMapping]); oldNote != NOTE_NONE)
- {
- SendMIDINote(i, oldNote + NOTE_MAX_SPECIAL, 0);
- chn.nArpeggioLastNote = NOTE_NONE;
- chn.nNote = NOTE_NONE;
- }
- break;
- }
- }
- #endif // NO_PLUGINS
- switch(chn.pModInstrument->nDNA)
- {
- // Cut
- case DuplicateNoteAction::NoteCut:
- KeyOff(chn);
- chn.nVolume = 0;
- if(chn.dwFlags[CHN_ADLIB] && m_opl)
- m_opl->NoteCut(i);
- break;
- // Note Off
- case DuplicateNoteAction::NoteOff:
- KeyOff(chn);
- if(chn.dwFlags[CHN_ADLIB] && m_opl)
- m_opl->NoteOff(i);
- break;
- // Note Fade
- case DuplicateNoteAction::NoteFade:
- chn.dwFlags.set(CHN_NOTEFADE);
- if(chn.dwFlags[CHN_ADLIB] && m_opl && !m_playBehaviour[kOPLwithNNA])
- m_opl->NoteOff(i);
- break;
- }
- if(!chn.nVolume)
- {
- chn.nFadeOutVol = 0;
- chn.dwFlags.set(CHN_NOTEFADE | CHN_FASTVOLRAMP);
- }
- }
- }
- }
- // Do we need to apply New/Duplicate Note Action to a VSTi?
- bool applyNNAtoPlug = false;
- #ifndef NO_PLUGINS
- IMixPlugin *pPlugin = nullptr;
- if(srcChn.HasMIDIOutput() && ModCommand::IsNote(srcChn.nNote)) // instro sends to a midi chan
- {
- PLUGINDEX plugin = GetBestPlugin(m_PlayState, nChn, PrioritiseInstrument, RespectMutes);
- if(plugin > 0 && plugin <= MAX_MIXPLUGINS)
- {
- pPlugin = m_MixPlugins[plugin - 1].pMixPlugin;
- if(pPlugin)
- {
- // apply NNA to this plugin iff it is currently playing a note on this tracker channel
- // (and if it is playing a note, we know that would be the last note played on this chan).
- const auto oldNote = srcChn.GetPluginNote(m_playBehaviour[kITRealNoteMapping]);
- applyNNAtoPlug = (oldNote != NOTE_NONE) && pPlugin->IsNotePlaying(oldNote, nChn);
- }
- }
- }
- #endif // NO_PLUGINS
- // New Note Action
- if(!srcChn.IsSamplePlaying() && !applyNNAtoPlug)
- return CHANNELINDEX_INVALID;
- #ifndef NO_PLUGINS
- if(applyNNAtoPlug && pPlugin)
- {
- switch(srcChn.nNNA)
- {
- case NewNoteAction::NoteOff:
- case NewNoteAction::NoteCut:
- case NewNoteAction::NoteFade:
- // Switch off note played on this plugin, on this tracker channel and midi channel
- SendMIDINote(nChn, NOTE_KEYOFF, 0);
- srcChn.nArpeggioLastNote = NOTE_NONE;
- break;
- case NewNoteAction::Continue:
- break;
- }
- }
- #endif // NO_PLUGINS
- CHANNELINDEX nnaChn = GetNNAChannel(nChn);
- if(nnaChn == CHANNELINDEX_INVALID)
- return CHANNELINDEX_INVALID;
- ModChannel &chn = m_PlayState.Chn[nnaChn];
- if(chn.dwFlags[CHN_ADLIB] && m_opl)
- m_opl->NoteCut(nnaChn);
- // Copy Channel
- chn = srcChn;
- chn.dwFlags.reset(CHN_VIBRATO | CHN_TREMOLO | CHN_PORTAMENTO);
- chn.nPanbrelloOffset = 0;
- chn.nMasterChn = nChn < GetNumChannels() ? nChn + 1 : 0;
- chn.nCommand = CMD_NONE;
- // Key Off the note
- switch(srcChn.nNNA)
- {
- case NewNoteAction::NoteOff:
- KeyOff(chn);
- if(chn.dwFlags[CHN_ADLIB] && m_opl)
- {
- m_opl->NoteOff(nChn);
- if(m_playBehaviour[kOPLwithNNA])
- m_opl->MoveChannel(nChn, nnaChn);
- }
- break;
- case NewNoteAction::NoteCut:
- chn.nFadeOutVol = 0;
- chn.dwFlags.set(CHN_NOTEFADE);
- if(chn.dwFlags[CHN_ADLIB] && m_opl)
- m_opl->NoteCut(nChn);
- break;
- case NewNoteAction::NoteFade:
- chn.dwFlags.set(CHN_NOTEFADE);
- if(chn.dwFlags[CHN_ADLIB] && m_opl)
- {
- if(m_playBehaviour[kOPLwithNNA])
- m_opl->MoveChannel(nChn, nnaChn);
- else
- m_opl->NoteOff(nChn);
- }
- break;
- case NewNoteAction::Continue:
- if(chn.dwFlags[CHN_ADLIB] && m_opl)
- m_opl->MoveChannel(nChn, nnaChn);
- break;
- }
- if(!chn.nVolume)
- {
- chn.nFadeOutVol = 0;
- chn.dwFlags.set(CHN_NOTEFADE | CHN_FASTVOLRAMP);
- }
- // Stop this channel
- srcChn.nLength = 0;
- srcChn.position.Set(0);
- srcChn.nROfs = srcChn.nLOfs = 0;
-
- return nnaChn;
- }
- bool CSoundFile::ProcessEffects()
- {
- m_PlayState.m_breakRow = ROWINDEX_INVALID; // Is changed if a break to row command is encountered
- m_PlayState.m_patLoopRow = ROWINDEX_INVALID; // Is changed if a pattern loop jump-back is executed
- m_PlayState.m_posJump = ORDERINDEX_INVALID;
- for(CHANNELINDEX nChn = 0; nChn < GetNumChannels(); nChn++)
- {
- ModChannel &chn = m_PlayState.Chn[nChn];
- const uint32 tickCount = m_PlayState.m_nTickCount % (m_PlayState.m_nMusicSpeed + m_PlayState.m_nFrameDelay);
- uint32 instr = chn.rowCommand.instr;
- ModCommand::VOLCMD volcmd = chn.rowCommand.volcmd;
- uint32 vol = chn.rowCommand.vol;
- ModCommand::COMMAND cmd = chn.rowCommand.command;
- uint32 param = chn.rowCommand.param;
- bool bPorta = chn.rowCommand.IsPortamento();
- uint32 nStartTick = 0;
- chn.isFirstTick = m_SongFlags[SONG_FIRSTTICK];
- // Process parameter control note.
- if(chn.rowCommand.note == NOTE_PC)
- {
- #ifndef NO_PLUGINS
- const PLUGINDEX plug = chn.rowCommand.instr;
- const PlugParamIndex plugparam = chn.rowCommand.GetValueVolCol();
- const PlugParamValue value = chn.rowCommand.GetValueEffectCol() / PlugParamValue(ModCommand::maxColumnValue);
- if(plug > 0 && plug <= MAX_MIXPLUGINS && m_MixPlugins[plug - 1].pMixPlugin)
- m_MixPlugins[plug-1].pMixPlugin->SetParameter(plugparam, value);
- #endif // NO_PLUGINS
- }
- // Process continuous parameter control note.
- // Row data is cleared after first tick so on following
- // ticks using channels m_nPlugParamValueStep to identify
- // the need for parameter control. The condition cmd == 0
- // is to make sure that m_nPlugParamValueStep != 0 because
- // of NOTE_PCS, not because of macro.
- if(chn.rowCommand.note == NOTE_PCS || (cmd == CMD_NONE && chn.m_plugParamValueStep != 0))
- {
- #ifndef NO_PLUGINS
- const bool isFirstTick = m_SongFlags[SONG_FIRSTTICK];
- if(isFirstTick)
- chn.m_RowPlug = chn.rowCommand.instr;
- const PLUGINDEX plugin = chn.m_RowPlug;
- const bool hasValidPlug = (plugin > 0 && plugin <= MAX_MIXPLUGINS && m_MixPlugins[plugin - 1].pMixPlugin);
- if(hasValidPlug)
- {
- if(isFirstTick)
- chn.m_RowPlugParam = ModCommand::GetValueVolCol(chn.rowCommand.volcmd, chn.rowCommand.vol);
- const PlugParamIndex plugparam = chn.m_RowPlugParam;
- if(isFirstTick)
- {
- PlugParamValue targetvalue = ModCommand::GetValueEffectCol(chn.rowCommand.command, chn.rowCommand.param) / PlugParamValue(ModCommand::maxColumnValue);
- chn.m_plugParamTargetValue = targetvalue;
- chn.m_plugParamValueStep = (targetvalue - m_MixPlugins[plugin - 1].pMixPlugin->GetParameter(plugparam)) / PlugParamValue(m_PlayState.TicksOnRow());
- }
- if(m_PlayState.m_nTickCount + 1 == m_PlayState.TicksOnRow())
- { // On last tick, set parameter exactly to target value.
- m_MixPlugins[plugin - 1].pMixPlugin->SetParameter(plugparam, chn.m_plugParamTargetValue);
- }
- else
- m_MixPlugins[plugin - 1].pMixPlugin->ModifyParameter(plugparam, chn.m_plugParamValueStep);
- }
- #endif // NO_PLUGINS
- }
- // Apart from changing parameters, parameter control notes are intended to be 'invisible'.
- // To achieve this, clearing the note data so that rest of the process sees the row as empty row.
- if(ModCommand::IsPcNote(chn.rowCommand.note))
- {
- chn.ClearRowCmd();
- instr = 0;
- volcmd = VOLCMD_NONE;
- vol = 0;
- cmd = CMD_NONE;
- param = 0;
- bPorta = false;
- }
- // Process Invert Loop (MOD Effect, called every row if it's active)
- if(!m_SongFlags[SONG_FIRSTTICK])
- {
- InvertLoop(m_PlayState.Chn[nChn]);
- } else
- {
- if(instr) m_PlayState.Chn[nChn].nEFxOffset = 0;
- }
- // Process special effects (note delay, pattern delay, pattern loop)
- if (cmd == CMD_DELAYCUT)
- {
- //:xy --> note delay until tick x, note cut at tick x+y
- nStartTick = (param & 0xF0) >> 4;
- const uint32 cutAtTick = nStartTick + (param & 0x0F);
- NoteCut(nChn, cutAtTick, m_playBehaviour[kITSCxStopsSample]);
- } else if ((cmd == CMD_MODCMDEX) || (cmd == CMD_S3MCMDEX))
- {
- if ((!param) && (GetType() & (MOD_TYPE_S3M|MOD_TYPE_IT|MOD_TYPE_MPT)))
- param = chn.nOldCmdEx;
- else
- chn.nOldCmdEx = static_cast<ModCommand::PARAM>(param);
- // Note Delay ?
- if ((param & 0xF0) == 0xD0)
- {
- nStartTick = param & 0x0F;
- if(nStartTick == 0)
- {
- //IT compatibility 22. SD0 == SD1
- if(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT))
- nStartTick = 1;
- //ST3 ignores notes with SD0 completely
- else if(GetType() == MOD_TYPE_S3M)
- continue;
- } else if(nStartTick >= (m_PlayState.m_nMusicSpeed + m_PlayState.m_nFrameDelay) && m_playBehaviour[kITOutOfRangeDelay])
- {
- // IT compatibility 08. Handling of out-of-range delay command.
- // Additional test case: tickdelay.it
- if(instr)
- {
- chn.nNewIns = static_cast<ModCommand::INSTR>(instr);
- }
- continue;
- }
- } else if(m_SongFlags[SONG_FIRSTTICK])
- {
- // Pattern Loop ?
- if((param & 0xF0) == 0xE0)
- {
- // Pattern Delay
- // In Scream Tracker 3 / Impulse Tracker, only the first delay command on this row is considered.
- // Test cases: PatternDelays.it, PatternDelays.s3m, PatternDelays.xm
- // XXX In Scream Tracker 3, the "left" channels are evaluated before the "right" channels, which is not emulated here!
- if(!(GetType() & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT)) || !m_PlayState.m_nPatternDelay)
- {
- if(!(GetType() & (MOD_TYPE_S3M)) || (param & 0x0F) != 0)
- {
- // While Impulse Tracker *does* count S60 as a valid row delay (and thus ignores any other row delay commands on the right),
- // Scream Tracker 3 simply ignores such commands.
- m_PlayState.m_nPatternDelay = 1 + (param & 0x0F);
- }
- }
- }
- }
- }
- if(GetType() == MOD_TYPE_MTM && cmd == CMD_MODCMDEX && (param & 0xF0) == 0xD0)
- {
- // Apparently, retrigger and note delay have the same behaviour in MultiTracker:
- // They both restart the note at tick x, and if there is a note on the same row,
- // this note is started on the first tick.
- nStartTick = 0;
- param = 0x90 | (param & 0x0F);
- }
- if(nStartTick != 0 && chn.rowCommand.note == NOTE_KEYOFF && chn.rowCommand.volcmd == VOLCMD_PANNING && m_playBehaviour[kFT2PanWithDelayedNoteOff])
- {
- // FT2 compatibility: If there's a delayed note off, panning commands are ignored. WTF!
- // Test case: PanOff.xm
- chn.rowCommand.volcmd = VOLCMD_NONE;
- }
- bool triggerNote = (m_PlayState.m_nTickCount == nStartTick); // Can be delayed by a note delay effect
- if(m_playBehaviour[kFT2OutOfRangeDelay] && nStartTick >= m_PlayState.m_nMusicSpeed)
- {
- // FT2 compatibility: Note delays greater than the song speed should be ignored.
- // However, EEx pattern delay is *not* considered at all.
- // Test case: DelayCombination.xm, PortaDelay.xm
- triggerNote = false;
- } else if(m_playBehaviour[kRowDelayWithNoteDelay] && nStartTick > 0 && tickCount == nStartTick)
- {
- // IT compatibility: Delayed notes (using SDx) that are on the same row as a Row Delay effect are retriggered.
- // ProTracker / Scream Tracker 3 / FastTracker 2 do the same.
- // Test case: PatternDelay-NoteDelay.it, PatternDelay-NoteDelay.xm, PatternDelaysRetrig.mod
- triggerNote = true;
- }
- // IT compatibility: Tick-0 vs non-tick-0 effect distinction is always based on tick delay.
- // Test case: SlideDelay.it
- if(m_playBehaviour[kITFirstTickHandling])
- {
- chn.isFirstTick = tickCount == nStartTick;
- }
- chn.triggerNote = triggerNote;
- // FT2 compatibility: Note + portamento + note delay = no portamento
- // Test case: PortaDelay.xm
- if(m_playBehaviour[kFT2PortaDelay] && nStartTick != 0)
- {
- bPorta = false;
- }
- if(m_SongFlags[SONG_PT_MODE] && instr && !m_PlayState.m_nTickCount)
- {
- // Instrument number resets the stacked ProTracker offset.
- // Test case: ptoffset.mod
- chn.prevNoteOffset = 0;
- // ProTracker compatibility: Sample properties are always loaded on the first tick, even when there is a note delay.
- // Test case: InstrDelay.mod
- if(!triggerNote && chn.IsSamplePlaying())
- {
- chn.nNewIns = static_cast<ModCommand::INSTR>(instr);
- if(instr <= GetNumSamples())
- {
- chn.nVolume = Samples[instr].nVolume;
- chn.nFineTune = Samples[instr].nFineTune;
- }
- }
- }
- // Handles note/instrument/volume changes
- if(triggerNote)
- {
- ModCommand::NOTE note = chn.rowCommand.note;
- if(instr) chn.nNewIns = static_cast<ModCommand::INSTR>(instr);
- if(ModCommand::IsNote(note) && m_playBehaviour[kFT2Transpose])
- {
- // Notes that exceed FT2's limit are completely ignored.
- // Test case: NoteLimit.xm
- int transpose = chn.nTranspose;
- if(instr && !bPorta)
- {
- // Refresh transpose
- // Test case: NoteLimit2.xm
- const SAMPLEINDEX sample = GetSampleIndex(note, instr);
- if(sample > 0)
- transpose = GetSample(sample).RelativeTone;
- }
- const int computedNote = note + transpose;
- if((computedNote < NOTE_MIN + 11 || computedNote > NOTE_MIN + 130))
- {
- note = NOTE_NONE;
- }
- } else if((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_J2B)) && GetNumInstruments() != 0 && ModCommand::IsNoteOrEmpty(static_cast<ModCommand::NOTE>(note)))
- {
- // IT compatibility: Invalid instrument numbers do nothing, but they are remembered for upcoming notes and do not trigger a note in that case.
- // Test case: InstrumentNumberChange.it
- INSTRUMENTINDEX instrToCheck = static_cast<INSTRUMENTINDEX>((instr != 0) ? instr : chn.nOldIns);
- if(instrToCheck != 0 && (instrToCheck > GetNumInstruments() || Instruments[instrToCheck] == nullptr))
- {
- note = NOTE_NONE;
- instr = 0;
- }
- }
- // XM: FT2 ignores a note next to a K00 effect, and a fade-out seems to be done when no volume envelope is present (not exactly the Kxx behaviour)
- if(cmd == CMD_KEYOFF && param == 0 && m_playBehaviour[kFT2KeyOff])
- {
- note = NOTE_NONE;
- instr = 0;
- }
- bool retrigEnv = note == NOTE_NONE && instr != 0;
- // Apparently, any note number in a pattern causes instruments to recall their original volume settings - no matter if there's a Note Off next to it or whatever.
- // Test cases: keyoff+instr.xm, delay.xm
- bool reloadSampleSettings = (m_playBehaviour[kFT2ReloadSampleSettings] && instr != 0);
- // ProTracker Compatibility: If a sample was stopped before, lone instrument numbers can retrigger it
- // Test case: PTSwapEmpty.mod, PTInstrVolume.mod, SampleSwap.s3m
- bool keepInstr = (GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT))
- || m_playBehaviour[kST3SampleSwap]
- || (m_playBehaviour[kMODSampleSwap] && !chn.IsSamplePlaying() && (chn.pModSample == nullptr || !chn.pModSample->HasSampleData()));
- // Now it's time for some FT2 crap...
- if (GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2))
- {
- // XM: Key-Off + Sample == Note Cut (BUT: Only if no instr number or volume effect is present!)
- // Test case: NoteOffVolume.xm
- if(note == NOTE_KEYOFF
- && ((!instr && volcmd != VOLCMD_VOLUME && cmd != CMD_VOLUME) || !m_playBehaviour[kFT2KeyOff])
- && (chn.pModInstrument == nullptr || !chn.pModInstrument->VolEnv.dwFlags[ENV_ENABLED]))
- {
- chn.dwFlags.set(CHN_FASTVOLRAMP);
- chn.nVolume = 0;
- note = NOTE_NONE;
- instr = 0;
- retrigEnv = false;
- // FT2 Compatbility: Start fading the note for notes with no delay. Only relevant when a volume command is encountered after the note-off.
- // Test case: NoteOffFadeNoEnv.xm
- if(m_SongFlags[SONG_FIRSTTICK] && m_playBehaviour[kFT2NoteOffFlags])
- chn.dwFlags.set(CHN_NOTEFADE);
- } else if(m_playBehaviour[kFT2RetrigWithNoteDelay] && !m_SongFlags[SONG_FIRSTTICK])
- {
- // FT2 Compatibility: Some special hacks for rogue note delays... (EDx with x > 0)
- // Apparently anything that is next to a note delay behaves totally unpredictable in FT2. Swedish tracker logic. :)
- retrigEnv = true;
- // Portamento + Note Delay = No Portamento
- // Test case: porta-delay.xm
- bPorta = false;
- if(note == NOTE_NONE)
- {
- // If there's a note delay but no real note, retrig the last note.
- // Test case: delay2.xm, delay3.xm
- note = static_cast<ModCommand::NOTE>(chn.nNote - chn.nTranspose);
- } else if(note >= NOTE_MIN_SPECIAL)
- {
- // Gah! Even Note Off + Note Delay will cause envelopes to *retrigger*! How stupid is that?
- // ... Well, and that is actually all it does if there's an envelope. No fade out, no nothing. *sigh*
- // Test case: OffDelay.xm
- note = NOTE_NONE;
- keepInstr = false;
- reloadSampleSettings = true;
- } else if(instr || !m_playBehaviour[kFT2NoteDelayWithoutInstr])
- {
- // Normal note (only if there is an instrument, test case: DelayVolume.xm)
- keepInstr = true;
- reloadSampleSettings = true;
- }
- }
- }
- if((retrigEnv && !m_playBehaviour[kFT2ReloadSampleSettings]) || reloadSampleSettings)
- {
- const ModSample *oldSample = nullptr;
- // Reset default volume when retriggering envelopes
- if(GetNumInstruments())
- {
- oldSample = chn.pModSample;
- } else if (instr <= GetNumSamples())
- {
- // Case: Only samples are used; no instruments.
- oldSample = &Samples[instr];
- }
- if(oldSample != nullptr)
- {
- if(!oldSample->uFlags[SMP_NODEFAULTVOLUME] && (GetType() != MOD_TYPE_S3M || oldSample->HasSampleData()))
- chn.nVolume = oldSample->nVolume;
- if(reloadSampleSettings)
- {
- // Also reload panning
- chn.SetInstrumentPan(oldSample->nPan, *this);
- }
- }
- }
- // FT2 compatibility: Instrument number disables tremor effect
- // Test case: TremorInstr.xm, TremoRecover.xm
- if(m_playBehaviour[kFT2Tremor] && instr != 0)
- {
- chn.nTremorCount = 0x20;
- }
- // IT compatibility: Envelope retriggering with instrument number based on Old Effects and Compatible Gxx flags:
- // OldFX CompatGxx Env Behaviour
- // ----- --------- -------------
- // off off never reset
- // on off reset on instrument without portamento
- // off on reset on instrument with portamento
- // on on always reset
- // Test case: ins-xx.it, ins-ox.it, ins-oc.it, ins-xc.it, ResetEnvNoteOffOldFx.it, ResetEnvNoteOffOldFx2.it, noteoff3.it
- if(GetNumInstruments() && m_playBehaviour[kITInstrWithNoteOffOldEffects]
- && instr && !ModCommand::IsNote(note))
- {
- if((bPorta && m_SongFlags[SONG_ITCOMPATGXX])
- || (!bPorta && m_SongFlags[SONG_ITOLDEFFECTS]))
- {
- chn.ResetEnvelopes();
- chn.dwFlags.set(CHN_FASTVOLRAMP);
- chn.nFadeOutVol = 65536;
- }
- }
- if(retrigEnv) //Case: instrument with no note data.
- {
- //IT compatibility: Instrument with no note.
- if(m_playBehaviour[kITInstrWithoutNote] || GetType() == MOD_TYPE_PLM)
- {
- // IT compatibility: Completely retrigger note after sample end to also reset portamento.
- // Test case: PortaResetAfterRetrigger.it
- bool triggerAfterSmpEnd = m_playBehaviour[kITMultiSampleInstrumentNumber] && !chn.IsSamplePlaying();
- if(GetNumInstruments())
- {
- // Instrument mode
- if(instr <= GetNumInstruments() && (chn.pModInstrument != Instruments[instr] || triggerAfterSmpEnd))
- note = chn.nNote;
- } else
- {
- // Sample mode
- if(instr < MAX_SAMPLES && (chn.pModSample != &Samples[instr] || triggerAfterSmpEnd))
- note = chn.nNote;
- }
- }
- if(GetNumInstruments() && (GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2 | MOD_TYPE_MED)))
- {
- chn.ResetEnvelopes();
- chn.dwFlags.set(CHN_FASTVOLRAMP);
- chn.dwFlags.reset(CHN_NOTEFADE);
- chn.nAutoVibDepth = 0;
- chn.nAutoVibPos = 0;
- chn.nFadeOutVol = 65536;
- // FT2 Compatbility: Reset key-off status with instrument number
- // Test case: NoteOffInstrChange.xm
- if(m_playBehaviour[kFT2NoteOffFlags])
- chn.dwFlags.reset(CHN_KEYOFF);
- }
- if (!keepInstr) instr = 0;
- }
- // Note Cut/Off/Fade => ignore instrument
- if (note >= NOTE_MIN_SPECIAL)
- {
- // IT compatibility: Default volume of sample is recalled if instrument number is next to a note-off.
- // Test case: NoteOffInstr.it, noteoff2.it
- if(m_playBehaviour[kITInstrWithNoteOff] && instr)
- {
- const SAMPLEINDEX smp = GetSampleIndex(chn.nLastNote, instr);
- if(smp > 0 && !Samples[smp].uFlags[SMP_NODEFAULTVOLUME])
- chn.nVolume = Samples[smp].nVolume;
- }
- // IT compatibility: Note-off with instrument number + Old Effects retriggers envelopes.
- // Test case: ResetEnvNoteOffOldFx.it
- if(!m_playBehaviour[kITInstrWithNoteOffOldEffects] || !m_SongFlags[SONG_ITOLDEFFECTS])
- instr = 0;
- }
- if(ModCommand::IsNote(note))
- {
- chn.nNewNote = chn.nLastNote = note;
- // New Note Action ?
- if(!bPorta)
- {
- CheckNNA(nChn, instr, note, false);
- }
- chn.RestorePanAndFilter();
- }
- // Instrument Change ?
- if(instr)
- {
- const ModSample *oldSample = chn.pModSample;
- //const ModInstrument *oldInstrument = chn.pModInstrument;
- InstrumentChange(chn, instr, bPorta, true);
- if(chn.pModSample != nullptr && chn.pModSample->uFlags[CHN_ADLIB] && m_opl)
- {
- m_opl->Patch(nChn, chn.pModSample->adlib);
- }
- // IT compatibility: Keep new instrument number for next instrument-less note even if sample playback is stopped
- // Test case: StoppedInstrSwap.it
- if(GetType() == MOD_TYPE_MOD)
- {
- // Test case: PortaSwapPT.mod
- if(!bPorta || !m_playBehaviour[kMODSampleSwap]) chn.nNewIns = 0;
- } else
- {
- if(!m_playBehaviour[kITInstrWithNoteOff] || ModCommand::IsNote(note)) chn.nNewIns = 0;
- }
- if(m_playBehaviour[kITPortamentoSwapResetsPos])
- {
- // Test cases: PortaInsNum.it, PortaSample.it
- if(ModCommand::IsNote(note) && oldSample != chn.pModSample)
- {
- //const bool newInstrument = oldInstrument != chn.pModInstrument && chn.pModInstrument->Keyboard[chn.nNewNote - NOTE_MIN] != 0;
- chn.position.Set(0);
- }
- } else if((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) && oldSample != chn.pModSample && ModCommand::IsNote(note))
- {
- // Special IT case: portamento+note causes sample change -> ignore portamento
- bPorta = false;
- } else if(m_playBehaviour[kST3SampleSwap] && oldSample != chn.pModSample && (bPorta || !ModCommand::IsNote(note)) && chn.position.GetUInt() > chn.nLength)
- {
- // ST3 with SoundBlaster does sample swapping and continues playing the new sample where the old sample was stopped.
- // If the new sample is shorter than that, it is stopped, even if it could be looped.
- // This also applies to portamento between different samples.
- // Test case: SampleSwap.s3m
- chn.nLength = 0;
- } else if(m_playBehaviour[kMODSampleSwap] && !chn.IsSamplePlaying())
- {
- // If channel was paused and is resurrected by a lone instrument number, reset the sample position.
- // Test case: PTSwapEmpty.mod
- chn.position.Set(0);
- }
- }
- // New Note ?
- if (note != NOTE_NONE)
- {
- const bool instrChange = (!instr) && (chn.nNewIns) && ModCommand::IsNote(note);
- if(instrChange)
- {
- InstrumentChange(chn, chn.nNewIns, bPorta, chn.pModSample == nullptr && chn.pModInstrument == nullptr, !(GetType() & (MOD_TYPE_XM|MOD_TYPE_MT2)));
- chn.nNewIns = 0;
- }
- if(chn.pModSample != nullptr && chn.pModSample->uFlags[CHN_ADLIB] && m_opl && (instrChange || !m_opl->IsActive(nChn)))
- {
- m_opl->Patch(nChn, chn.pModSample->adlib);
- }
- NoteChange(chn, note, bPorta, !(GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2)), false, nChn);
- HandleDigiSamplePlayDirection(m_PlayState, nChn);
- if ((bPorta) && (GetType() & (MOD_TYPE_XM|MOD_TYPE_MT2)) && (instr))
- {
- chn.dwFlags.set(CHN_FASTVOLRAMP);
- chn.ResetEnvelopes();
- chn.nAutoVibDepth = 0;
- chn.nAutoVibPos = 0;
- }
- if(chn.dwFlags[CHN_ADLIB] && m_opl
- && ((note == NOTE_NOTECUT || note == NOTE_KEYOFF) || (note == NOTE_FADE && !m_playBehaviour[kOPLFlexibleNoteOff])))
- {
- if(m_playBehaviour[kOPLNoteStopWith0Hz])
- m_opl->Frequency(nChn, 0, true, false);
- m_opl->NoteOff(nChn);
- }
- }
- // Tick-0 only volume commands
- if (volcmd == VOLCMD_VOLUME)
- {
- if (vol > 64) vol = 64;
- chn.nVolume = vol << 2;
- chn.dwFlags.set(CHN_FASTVOLRAMP);
- } else
- if (volcmd == VOLCMD_PANNING)
- {
- Panning(chn, vol, Pan6bit);
- }
- #ifndef NO_PLUGINS
- if (m_nInstruments) ProcessMidiOut(nChn);
- #endif // NO_PLUGINS
- }
- if(m_playBehaviour[kST3NoMutedChannels] && ChnSettings[nChn].dwFlags[CHN_MUTE]) // not even effects are processed on muted S3M channels
- continue;
- // Volume Column Effect (except volume & panning)
- /* A few notes, paraphrased from ITTECH.TXT by Storlek (creator of schismtracker):
- Ex/Fx/Gx are shared with Exx/Fxx/Gxx; Ex/Fx are 4x the 'normal' slide value
- Gx is linked with Ex/Fx if Compat Gxx is off, just like Gxx is with Exx/Fxx
- Gx values: 1, 4, 8, 16, 32, 64, 96, 128, 255
- Ax/Bx/Cx/Dx values are used directly (i.e. D9 == D09), and are NOT shared with Dxx
- (value is stored into nOldVolParam and used by A0/B0/C0/D0)
- Hx uses the same value as Hxx and Uxx, and affects the *depth*
- so... hxx = (hx | (oldhxx & 0xf0)) ???
- TODO is this done correctly?
- */
- bool doVolumeColumn = m_PlayState.m_nTickCount >= nStartTick;
- // FT2 compatibility: If there's a note delay, volume column effects are NOT executed
- // on the first tick and, if there's an instrument number, on the delayed tick.
- // Test case: VolColDelay.xm, PortaDelay.xm
- if(m_playBehaviour[kFT2VolColDelay] && nStartTick != 0)
- {
- doVolumeColumn = m_PlayState.m_nTickCount != 0 && (m_PlayState.m_nTickCount != nStartTick || (chn.rowCommand.instr == 0 && volcmd != VOLCMD_TONEPORTAMENTO));
- }
- if(volcmd > VOLCMD_PANNING && doVolumeColumn)
- {
- if(volcmd == VOLCMD_TONEPORTAMENTO)
- {
- const auto [porta, clearEffectCommand] = GetVolCmdTonePorta(chn.rowCommand, nStartTick);
- if(clearEffectCommand)
- cmd = CMD_NONE;
- TonePortamento(chn, porta);
- } else
- {
- // FT2 Compatibility: FT2 ignores some volume commands with parameter = 0.
- if(m_playBehaviour[kFT2VolColMemory] && vol == 0)
- {
- switch(volcmd)
- {
- case VOLCMD_VOLUME:
- case VOLCMD_PANNING:
- case VOLCMD_VIBRATODEPTH:
- break;
- case VOLCMD_PANSLIDELEFT:
- // FT2 Compatibility: Pan slide left with zero parameter causes panning to be set to full left on every non-row tick.
- // Test case: PanSlideZero.xm
- if(!m_SongFlags[SONG_FIRSTTICK])
- {
- chn.nPan = 0;
- }
- [[fallthrough]];
- default:
- // no memory here.
- volcmd = VOLCMD_NONE;
- }
- } else if(!m_playBehaviour[kITVolColMemory])
- {
- // IT Compatibility: Effects in the volume column don't have an unified memory.
- // Test case: VolColMemory.it
- if(vol) chn.nOldVolParam = static_cast<ModCommand::PARAM>(vol); else vol = chn.nOldVolParam;
- }
- switch(volcmd)
- {
- case VOLCMD_VOLSLIDEUP:
- case VOLCMD_VOLSLIDEDOWN:
- // IT Compatibility: Volume column volume slides have their own memory
- // Test case: VolColMemory.it
- if(vol == 0 && m_playBehaviour[kITVolColMemory])
- {
- vol = chn.nOldVolParam;
- if(vol == 0)
- break;
- } else
- {
- chn.nOldVolParam = static_cast<ModCommand::PARAM>(vol);
- }
- VolumeSlide(chn, static_cast<ModCommand::PARAM>(volcmd == VOLCMD_VOLSLIDEUP ? (vol << 4) : vol));
- break;
- case VOLCMD_FINEVOLUP:
- // IT Compatibility: Fine volume slides in the volume column are only executed on the first tick, not on multiples of the first tick in case of pattern delay
- // Test case: FineVolColSlide.it
- if(m_PlayState.m_nTickCount == nStartTick || !m_playBehaviour[kITVolColMemory])
- {
- // IT Compatibility: Volume column volume slides have their own memory
- // Test case: VolColMemory.it
- FineVolumeUp(chn, static_cast<ModCommand::PARAM>(vol), m_playBehaviour[kITVolColMemory]);
- }
- break;
- case VOLCMD_FINEVOLDOWN:
- // IT Compatibility: Fine volume slides in the volume column are only executed on the first tick, not on multiples of the first tick in case of pattern delay
- // Test case: FineVolColSlide.it
- if(m_PlayState.m_nTickCount == nStartTick || !m_playBehaviour[kITVolColMemory])
- {
- // IT Compatibility: Volume column volume slides have their own memory
- // Test case: VolColMemory.it
- FineVolumeDown(chn, static_cast<ModCommand::PARAM>(vol), m_playBehaviour[kITVolColMemory]);
- }
- break;
- case VOLCMD_VIBRATOSPEED:
- // FT2 does not automatically enable vibrato with the "set vibrato speed" command
- if(m_playBehaviour[kFT2VolColVibrato])
- chn.nVibratoSpeed = vol & 0x0F;
- else
- Vibrato(chn, vol << 4);
- break;
- case VOLCMD_VIBRATODEPTH:
- Vibrato(chn, vol);
- break;
- case VOLCMD_PANSLIDELEFT:
- PanningSlide(chn, static_cast<ModCommand::PARAM>(vol), !m_playBehaviour[kFT2VolColMemory]);
- break;
- case VOLCMD_PANSLIDERIGHT:
- PanningSlide(chn, static_cast<ModCommand::PARAM>(vol << 4), !m_playBehaviour[kFT2VolColMemory]);
- break;
- case VOLCMD_PORTAUP:
- // IT compatibility (one of the first testcases - link effect memory)
- PortamentoUp(nChn, static_cast<ModCommand::PARAM>(vol << 2), m_playBehaviour[kITVolColFinePortamento]);
- break;
- case VOLCMD_PORTADOWN:
- // IT compatibility (one of the first testcases - link effect memory)
- PortamentoDown(nChn, static_cast<ModCommand::PARAM>(vol << 2), m_playBehaviour[kITVolColFinePortamento]);
- break;
- case VOLCMD_OFFSET:
- if(triggerNote && chn.pModSample && vol <= std::size(chn.pModSample->cues))
- {
- SmpLength offset;
- if(vol == 0)
- offset = chn.oldOffset;
- else
- offset = chn.oldOffset = chn.pModSample->cues[vol - 1];
- SampleOffset(chn, offset);
- }
- break;
- case VOLCMD_PLAYCONTROL:
- if(vol <= 1)
- chn.isPaused = (vol == 0);
- break;
- }
- }
- }
- // Effects
- if(cmd != CMD_NONE) switch (cmd)
- {
- // Set Volume
- case CMD_VOLUME:
- if(m_SongFlags[SONG_FIRSTTICK])
- {
- chn.nVolume = (param < 64) ? param * 4 : 256;
- chn.dwFlags.set(CHN_FASTVOLRAMP);
- }
- break;
- // Portamento Up
- case CMD_PORTAMENTOUP:
- if ((!param) && (GetType() & MOD_TYPE_MOD)) break;
- PortamentoUp(nChn, static_cast<ModCommand::PARAM>(param));
- break;
- // Portamento Down
- case CMD_PORTAMENTODOWN:
- if ((!param) && (GetType() & MOD_TYPE_MOD)) break;
- PortamentoDown(nChn, static_cast<ModCommand::PARAM>(param));
- break;
- // Volume Slide
- case CMD_VOLUMESLIDE:
- if (param || (GetType() != MOD_TYPE_MOD)) VolumeSlide(chn, static_cast<ModCommand::PARAM>(param));
- break;
- // Tone-Portamento
- case CMD_TONEPORTAMENTO:
- TonePortamento(chn, static_cast<uint16>(param));
- break;
- // Tone-Portamento + Volume Slide
- case CMD_TONEPORTAVOL:
- if ((param) || (GetType() != MOD_TYPE_MOD)) VolumeSlide(chn, static_cast<ModCommand::PARAM>(param));
- TonePortamento(chn, 0);
- break;
- // Vibrato
- case CMD_VIBRATO:
- Vibrato(chn, param);
- break;
- // Vibrato + Volume Slide
- case CMD_VIBRATOVOL:
- if ((param) || (GetType() != MOD_TYPE_MOD)) VolumeSlide(chn, static_cast<ModCommand::PARAM>(param));
- Vibrato(chn, 0);
- break;
- // Set Speed
- case CMD_SPEED:
- if(m_SongFlags[SONG_FIRSTTICK])
- SetSpeed(m_PlayState, param);
- break;
- // Set Tempo
- case CMD_TEMPO:
- if(m_playBehaviour[kMODVBlankTiming])
- {
- // ProTracker MODs with VBlank timing: All Fxx parameters set the tick count.
- if(m_SongFlags[SONG_FIRSTTICK] && param != 0) SetSpeed(m_PlayState, param);
- break;
- }
- {
- param = CalculateXParam(m_PlayState.m_nPattern, m_PlayState.m_nRow, nChn);
- if (GetType() & (MOD_TYPE_S3M|MOD_TYPE_IT|MOD_TYPE_MPT))
- {
- if (param) chn.nOldTempo = static_cast<ModCommand::PARAM>(param); else param = chn.nOldTempo;
- }
- TEMPO t(param, 0);
- LimitMax(t, GetModSpecifications().GetTempoMax());
- SetTempo(t);
- }
- break;
- // Set Offset
- case CMD_OFFSET:
- if(triggerNote)
- {
- // FT2 compatibility: Portamento + Offset = Ignore offset
- // Test case: porta-offset.xm
- if(bPorta && GetType() == MOD_TYPE_XM)
- break;
- ProcessSampleOffset(chn, nChn, m_PlayState);
- }
- break;
- // Disorder Tracker 2 percentage offset
- case CMD_OFFSETPERCENTAGE:
- if(triggerNote)
- {
- SampleOffset(chn, Util::muldiv_unsigned(chn.nLength, param, 256));
- }
- break;
- // Arpeggio
- case CMD_ARPEGGIO:
- // IT compatibility 01. Don't ignore Arpeggio if no note is playing (also valid for ST3)
- if(m_PlayState.m_nTickCount) break;
- if((!chn.nPeriod || !chn.nNote)
- && (chn.pModInstrument == nullptr || !chn.pModInstrument->HasValidMIDIChannel()) // Plugin arpeggio
- && !m_playBehaviour[kITArpeggio] && (GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT))) break;
- if (!param && (GetType() & (MOD_TYPE_XM | MOD_TYPE_MOD))) break; // Only important when editing MOD/XM files (000 effects are removed when loading files where this means "no effect")
- chn.nCommand = CMD_ARPEGGIO;
- if (param) chn.nArpeggio = static_cast<ModCommand::PARAM>(param);
- break;
- // Retrig
- case CMD_RETRIG:
- if (GetType() & (MOD_TYPE_XM|MOD_TYPE_MT2))
- {
- if (!(param & 0xF0)) param |= chn.nRetrigParam & 0xF0;
- if (!(param & 0x0F)) param |= chn.nRetrigParam & 0x0F;
- param |= 0x100; // increment retrig count on first row
- }
- // IT compatibility 15. Retrigger
- if(m_playBehaviour[kITRetrigger])
- {
- if (param) chn.nRetrigParam = static_cast<uint8>(param & 0xFF);
- RetrigNote(nChn, chn.nRetrigParam, (volcmd == VOLCMD_OFFSET) ? vol + 1 : 0);
- } else
- {
- // XM Retrig
- if (param) chn.nRetrigParam = static_cast<uint8>(param & 0xFF); else param = chn.nRetrigParam;
- RetrigNote(nChn, param, (volcmd == VOLCMD_OFFSET) ? vol + 1 : 0);
- }
- break;
- // Tremor
- case CMD_TREMOR:
- if(!m_SongFlags[SONG_FIRSTTICK])
- {
- break;
- }
- // IT compatibility 12. / 13. Tremor (using modified DUMB's Tremor logic here because of old effects - http://dumb.sf.net/)
- if(m_playBehaviour[kITTremor])
- {
- if(param && !m_SongFlags[SONG_ITOLDEFFECTS])
- {
- // Old effects have different length interpretation (+1 for both on and off)
- if(param & 0xF0)
- param -= 0x10;
- if(param & 0x0F)
- param -= 0x01;
- chn.nTremorParam = static_cast<ModCommand::PARAM>(param);
- }
- chn.nTremorCount |= 0x80; // set on/off flag
- } else if(m_playBehaviour[kFT2Tremor])
- {
- // XM Tremor. Logic is being processed in sndmix.cpp
- chn.nTremorCount |= 0x80; // set on/off flag
- }
- chn.nCommand = CMD_TREMOR;
- if(param)
- chn.nTremorParam = static_cast<ModCommand::PARAM>(param);
- break;
- // Set Global Volume
- case CMD_GLOBALVOLUME:
- // IT compatibility: Only apply global volume on first tick (and multiples)
- // Test case: GlobalVolFirstTick.it
- if(!m_SongFlags[SONG_FIRSTTICK])
- break;
- // ST3 applies global volume on tick 1 and does other weird things, but we won't emulate this for now.
- // if(((GetType() & MOD_TYPE_S3M) && m_nTickCount != 1)
- // || (!(GetType() & MOD_TYPE_S3M) && !m_SongFlags[SONG_FIRSTTICK]))
- // {
- // break;
- // }
- // FT2 compatibility: On channels that are "left" of the global volume command, the new global volume is not applied
- // until the second tick of the row. Since we apply global volume on the mix buffer rather than note volumes, this
- // cannot be fixed for now.
- // Test case: GlobalVolume.xm
- // if(IsCompatibleMode(TRK_FASTTRACKER2) && m_SongFlags[SONG_FIRSTTICK] && m_nMusicSpeed > 1)
- // {
- // break;
- // }
- if (!(GetType() & GLOBALVOL_7BIT_FORMATS)) param *= 2;
- // IT compatibility 16. ST3 and IT ignore out-of-range values.
- // Test case: globalvol-invalid.it
- if(param <= 128)
- {
- m_PlayState.m_nGlobalVolume = param * 2;
- } else if(!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_S3M)))
- {
- m_PlayState.m_nGlobalVolume = 256;
- }
- break;
- // Global Volume Slide
- case CMD_GLOBALVOLSLIDE:
- //IT compatibility 16. Saving last global volume slide param per channel (FT2/IT)
- if(m_playBehaviour[kPerChannelGlobalVolSlide])
- GlobalVolSlide(static_cast<ModCommand::PARAM>(param), chn.nOldGlobalVolSlide);
- else
- GlobalVolSlide(static_cast<ModCommand::PARAM>(param), m_PlayState.Chn[0].nOldGlobalVolSlide);
- break;
- // Set 8-bit Panning
- case CMD_PANNING8:
- if(m_SongFlags[SONG_FIRSTTICK])
- {
- Panning(chn, param, Pan8bit);
- }
- break;
- // Panning Slide
- case CMD_PANNINGSLIDE:
- PanningSlide(chn, static_cast<ModCommand::PARAM>(param));
- break;
- // Tremolo
- case CMD_TREMOLO:
- Tremolo(chn, param);
- break;
- // Fine Vibrato
- case CMD_FINEVIBRATO:
- FineVibrato(chn, param);
- break;
- // MOD/XM Exx Extended Commands
- case CMD_MODCMDEX:
- ExtendedMODCommands(nChn, static_cast<ModCommand::PARAM>(param));
- break;
- // S3M/IT Sxx Extended Commands
- case CMD_S3MCMDEX:
- ExtendedS3MCommands(nChn, static_cast<ModCommand::PARAM>(param));
- break;
- // Key Off
- case CMD_KEYOFF:
- // This is how Key Off is supposed to sound... (in FT2 at least)
- if(m_playBehaviour[kFT2KeyOff])
- {
- if (m_PlayState.m_nTickCount == param)
- {
- // XM: Key-Off + Sample == Note Cut
- if(chn.pModInstrument == nullptr || !chn.pModInstrument->VolEnv.dwFlags[ENV_ENABLED])
- {
- if(param == 0 && (chn.rowCommand.instr || chn.rowCommand.volcmd != VOLCMD_NONE)) // FT2 is weird....
- {
- chn.dwFlags.set(CHN_NOTEFADE);
- }
- else
- {
- chn.dwFlags.set(CHN_FASTVOLRAMP);
- chn.nVolume = 0;
- }
- }
- KeyOff(chn);
- }
- }
- // This is how it's NOT supposed to sound...
- else
- {
- if(m_SongFlags[SONG_FIRSTTICK])
- KeyOff(chn);
- }
- break;
- // Extra-fine porta up/down
- case CMD_XFINEPORTAUPDOWN:
- switch(param & 0xF0)
- {
- case 0x10: ExtraFinePortamentoUp(chn, param & 0x0F); break;
- case 0x20: ExtraFinePortamentoDown(chn, param & 0x0F); break;
- // ModPlug XM Extensions (ignore in compatible mode)
- case 0x50:
- case 0x60:
- case 0x70:
- case 0x90:
- case 0xA0:
- if(!m_playBehaviour[kFT2RestrictXCommand]) ExtendedS3MCommands(nChn, static_cast<ModCommand::PARAM>(param));
- break;
- }
- break;
- case CMD_FINETUNE:
- case CMD_FINETUNE_SMOOTH:
- if(m_SongFlags[SONG_FIRSTTICK] || cmd == CMD_FINETUNE_SMOOTH)
- {
- SetFinetune(nChn, m_PlayState, cmd == CMD_FINETUNE_SMOOTH);
- #ifndef NO_PLUGINS
- if(IMixPlugin *plugin = GetChannelInstrumentPlugin(m_PlayState.Chn[nChn]); plugin != nullptr)
- plugin->MidiPitchBendRaw(chn.GetMIDIPitchBend(), nChn);
- #endif // NO_PLUGINS
- }
- break;
- // Set Channel Global Volume
- case CMD_CHANNELVOLUME:
- if(!m_SongFlags[SONG_FIRSTTICK]) break;
- if (param <= 64)
- {
- chn.nGlobalVol = param;
- chn.dwFlags.set(CHN_FASTVOLRAMP);
- }
- break;
- // Channel volume slide
- case CMD_CHANNELVOLSLIDE:
- ChannelVolSlide(chn, static_cast<ModCommand::PARAM>(param));
- break;
- // Panbrello (IT)
- case CMD_PANBRELLO:
- Panbrello(chn, param);
- break;
- // Set Envelope Position
- case CMD_SETENVPOSITION:
- if(m_SongFlags[SONG_FIRSTTICK])
- {
- chn.VolEnv.nEnvPosition = param;
- // FT2 compatibility: FT2 only sets the position of the panning envelope if the volume envelope's sustain flag is set
- // Test case: SetEnvPos.xm
- if(!m_playBehaviour[kFT2SetPanEnvPos] || chn.VolEnv.flags[ENV_SUSTAIN])
- {
- chn.PanEnv.nEnvPosition = param;
- chn.PitchEnv.nEnvPosition = param;
- }
- }
- break;
- // Position Jump
- case CMD_POSITIONJUMP:
- PositionJump(m_PlayState, nChn);
- break;
- // Pattern Break
- case CMD_PATTERNBREAK:
- if(ROWINDEX row = PatternBreak(m_PlayState, nChn, static_cast<ModCommand::PARAM>(param)); row != ROWINDEX_INVALID)
- {
- m_PlayState.m_breakRow = row;
- if(m_SongFlags[SONG_PATTERNLOOP])
- {
- //If song is set to loop and a pattern break occurs we should stay on the same pattern.
- //Use nPosJump to force playback to "jump to this pattern" rather than move to next, as by default.
- m_PlayState.m_posJump = m_PlayState.m_nCurrentOrder;
- }
- }
- break;
- // IMF / PTM Note Slides
- case CMD_NOTESLIDEUP:
- case CMD_NOTESLIDEDOWN:
- case CMD_NOTESLIDEUPRETRIG:
- case CMD_NOTESLIDEDOWNRETRIG:
- // Note that this command seems to be a bit buggy in Polytracker... Luckily, no tune seems to seriously use this
- // (Vic uses it e.g. in Spaceman or Perfect Reason to slide effect samples, noone will notice the difference :)
- NoteSlide(chn, param, cmd == CMD_NOTESLIDEUP || cmd == CMD_NOTESLIDEUPRETRIG, cmd == CMD_NOTESLIDEUPRETRIG || cmd == CMD_NOTESLIDEDOWNRETRIG);
- break;
- // PTM Reverse sample + offset (executed on every tick)
- case CMD_REVERSEOFFSET:
- ReverseSampleOffset(chn, static_cast<ModCommand::PARAM>(param));
- break;
- #ifndef NO_PLUGINS
- // DBM: Toggle DSP Echo
- case CMD_DBMECHO:
- if(m_PlayState.m_nTickCount == 0)
- {
- uint32 echoType = (param >> 4), enable = (param & 0x0F);
- if(echoType > 2 || enable > 1)
- {
- break;
- }
- CHANNELINDEX firstChn = nChn, lastChn = nChn;
- if(echoType == 1)
- {
- firstChn = 0;
- lastChn = m_nChannels - 1;
- }
- for(CHANNELINDEX c = firstChn; c <= lastChn; c++)
- {
- ChnSettings[c].dwFlags.set(CHN_NOFX, enable == 1);
- m_PlayState.Chn[c].dwFlags.set(CHN_NOFX, enable == 1);
- }
- }
- break;
- #endif // NO_PLUGINS
- // Digi Booster sample reverse
- case CMD_DIGIREVERSESAMPLE:
- DigiBoosterSampleReverse(chn, static_cast<ModCommand::PARAM>(param));
- break;
- }
- if(m_playBehaviour[kST3EffectMemory] && param != 0)
- {
- UpdateS3MEffectMemory(chn, static_cast<ModCommand::PARAM>(param));
- }
- if(chn.rowCommand.instr)
- {
- // Not necessarily consistent with actually playing instrument for IT compatibility
- chn.nOldIns = chn.rowCommand.instr;
- }
- } // for(...) end
- // Navigation Effects
- if(m_SongFlags[SONG_FIRSTTICK])
- {
- if(HandleNextRow(m_PlayState, Order(), true))
- m_SongFlags.set(SONG_BREAKTOROW);
- }
- return true;
- }
- bool CSoundFile::HandleNextRow(PlayState &state, const ModSequence &order, bool honorPatternLoop) const
- {
- const bool doPatternLoop = (state.m_patLoopRow != ROWINDEX_INVALID);
- const bool doBreakRow = (state.m_breakRow != ROWINDEX_INVALID);
- const bool doPosJump = (state.m_posJump != ORDERINDEX_INVALID);
- bool breakToRow = false;
- // Pattern Break / Position Jump only if no loop running
- // Exception: FastTracker 2 in all cases, Impulse Tracker in case of position jump
- // Test case for FT2 exception: PatLoop-Jumps.xm, PatLoop-Various.xm
- // Test case for IT: exception: LoopBreak.it, sbx-priority.it
- if((doBreakRow || doPosJump)
- && (!doPatternLoop
- || m_playBehaviour[kFT2PatternLoopWithJumps]
- || (m_playBehaviour[kITPatternLoopWithJumps] && doPosJump)
- || (m_playBehaviour[kITPatternLoopWithJumpsOld] && doPosJump)))
- {
- if(!doPosJump)
- state.m_posJump = state.m_nCurrentOrder + 1;
- if(!doBreakRow)
- state.m_breakRow = 0;
- breakToRow = true;
- if(state.m_posJump >= order.size())
- state.m_posJump = order.GetRestartPos();
- // IT / FT2 compatibility: don't reset loop count on pattern break.
- // Test case: gm-trippy01.it, PatLoop-Break.xm, PatLoop-Weird.xm, PatLoop-Break.mod
- if(state.m_posJump != state.m_nCurrentOrder
- && !m_playBehaviour[kITPatternLoopBreak] && !m_playBehaviour[kFT2PatternLoopWithJumps] && GetType() != MOD_TYPE_MOD)
- {
- for(CHANNELINDEX i = 0; i < GetNumChannels(); i++)
- {
- state.Chn[i].nPatternLoopCount = 0;
- }
- }
- state.m_nNextRow = state.m_breakRow;
- if(!honorPatternLoop || !m_SongFlags[SONG_PATTERNLOOP])
- state.m_nNextOrder = state.m_posJump;
- } else if(doPatternLoop)
- {
- // Pattern Loop
- state.m_nNextOrder = state.m_nCurrentOrder;
- state.m_nNextRow = state.m_patLoopRow;
- // FT2 skips the first row of the pattern loop if there's a pattern delay, ProTracker sometimes does it too (didn't quite figure it out yet).
- // But IT and ST3 don't do this.
- // Test cases: PatLoopWithDelay.it, PatLoopWithDelay.s3m
- if(state.m_nPatternDelay
- && (GetType() != MOD_TYPE_IT || !m_playBehaviour[kITPatternLoopWithJumps])
- && GetType() != MOD_TYPE_S3M)
- {
- state.m_nNextRow++;
- }
- // IT Compatibility: If the restart row is past the end of the current pattern
- // (e.g. when continued from a previous pattern without explicit SB0 effect), continue the next pattern.
- // Test case: LoopStartAfterPatternEnd.it
- if(state.m_patLoopRow >= Patterns[state.m_nPattern].GetNumRows())
- {
- state.m_nNextOrder++;
- state.m_nNextRow = 0;
- }
- }
- return breakToRow;
- }
- ////////////////////////////////////////////////////////////
- // Channels effects
- // Update the effect memory of all S3M effects that use the last non-zero effect parameter as memory (Dxy, Exx, Fxx, Ixy, Jxy, Kxy, Lxy, Qxy, Rxy, Sxy)
- // Test case: ParamMemory.s3m
- void CSoundFile::UpdateS3MEffectMemory(ModChannel &chn, ModCommand::PARAM param) const
- {
- chn.nOldVolumeSlide = param; // Dxy / Kxy / Lxy
- chn.nOldPortaUp = param; // Exx / Fxx
- chn.nOldPortaDown = param; // Exx / Fxx
- chn.nTremorParam = param; // Ixy
- chn.nArpeggio = param; // Jxy
- chn.nRetrigParam = param; // Qxy
- chn.nTremoloDepth = (param & 0x0F) << 2; // Rxy
- chn.nTremoloSpeed = (param >> 4) & 0x0F; // Rxy
- chn.nOldCmdEx = param; // Sxy
- }
- // Calculate full parameter for effects that support parameter extension at the given pattern location.
- // maxCommands sets the maximum number of XParam commands to look at for this effect
- // extendedRows returns how many extended rows are used (i.e. a value of 0 means the command is not extended).
- uint32 CSoundFile::CalculateXParam(PATTERNINDEX pat, ROWINDEX row, CHANNELINDEX chn, uint32 *extendedRows) const
- {
- if(extendedRows != nullptr)
- *extendedRows = 0;
- if(!Patterns.IsValidPat(pat))
- {
- #ifdef MPT_BUILD_FUZZER
- // Ending up in this situation implies a logic error
- std::abort();
- #else
- return 0;
- #endif
- }
- ROWINDEX maxCommands = 4;
- const ModCommand *m = Patterns[pat].GetpModCommand(row, chn);
- const auto startCmd = m->command;
- uint32 val = m->param;
- switch(m->command)
- {
- case CMD_OFFSET:
- // 24 bit command
- maxCommands = 2;
- break;
- case CMD_TEMPO:
- case CMD_PATTERNBREAK:
- case CMD_POSITIONJUMP:
- case CMD_FINETUNE:
- case CMD_FINETUNE_SMOOTH:
- // 16 bit command
- maxCommands = 1;
- break;
- default:
- return val;
- }
- const bool xmTempoFix = m->command == CMD_TEMPO && GetType() == MOD_TYPE_XM;
- ROWINDEX numRows = std::min(Patterns[pat].GetNumRows() - row - 1, maxCommands);
- uint32 extRows = 0;
- while(numRows > 0)
- {
- m += Patterns[pat].GetNumChannels();
- if(m->command != CMD_XPARAM)
- break;
-
- if(xmTempoFix && val < 256)
- {
- // With XM, 0x20 is the lowest tempo. Anything below changes ticks per row.
- val -= 0x20;
- }
- val = (val << 8) | m->param;
- numRows--;
- extRows++;
- }
- // Always return a full-precision value for finetune
- if((startCmd == CMD_FINETUNE || startCmd == CMD_FINETUNE_SMOOTH) && !extRows)
- val <<= 8;
-
- if(extendedRows != nullptr)
- *extendedRows = extRows;
- return val;
- }
- void CSoundFile::PositionJump(PlayState &state, CHANNELINDEX chn) const
- {
- state.m_nextPatStartRow = 0; // FT2 E60 bug
- state.m_posJump = static_cast<ORDERINDEX>(CalculateXParam(state.m_nPattern, state.m_nRow, chn));
- // see https://forum.openmpt.org/index.php?topic=2769.0 - FastTracker resets Dxx if Bxx is called _after_ Dxx
- // Test case: PatternJump.mod
- if((GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM)) && state.m_breakRow != ROWINDEX_INVALID)
- {
- state.m_breakRow = 0;
- }
- }
- ROWINDEX CSoundFile::PatternBreak(PlayState &state, CHANNELINDEX chn, uint8 param) const
- {
- if(param >= 64 && (GetType() & MOD_TYPE_S3M))
- {
- // ST3 ignores invalid pattern breaks.
- return ROWINDEX_INVALID;
- }
- state.m_nextPatStartRow = 0; // FT2 E60 bug
- return static_cast<ROWINDEX>(CalculateXParam(state.m_nPattern, state.m_nRow, chn));
- }
- void CSoundFile::PortamentoUp(CHANNELINDEX nChn, ModCommand::PARAM param, const bool doFinePortamentoAsRegular)
- {
- ModChannel &chn = m_PlayState.Chn[nChn];
- if(param)
- {
- // FT2 compatibility: Separate effect memory for all portamento commands
- // Test case: Porta-LinkMem.xm
- if(!m_playBehaviour[kFT2PortaUpDownMemory])
- chn.nOldPortaDown = param;
- chn.nOldPortaUp = param;
- } else
- {
- param = chn.nOldPortaUp;
- }
- const bool doFineSlides = !doFinePortamentoAsRegular && !(GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM | MOD_TYPE_MT2 | MOD_TYPE_MED | MOD_TYPE_AMF0 | MOD_TYPE_DIGI | MOD_TYPE_STP | MOD_TYPE_DTM));
- // Process MIDI pitch bend for instrument plugins
- MidiPortamento(nChn, param, doFineSlides);
- if(GetType() == MOD_TYPE_MPT && chn.pModInstrument && chn.pModInstrument->pTuning)
- {
- // Portamento for instruments with custom tuning
- if(param >= 0xF0 && !doFinePortamentoAsRegular)
- PortamentoFineMPT(chn, param - 0xF0);
- else if(param >= 0xE0 && !doFinePortamentoAsRegular)
- PortamentoExtraFineMPT(chn, param - 0xE0);
- else
- PortamentoMPT(chn, param);
- return;
- } else if(GetType() == MOD_TYPE_PLM)
- {
- // A normal portamento up or down makes a follow-up tone portamento go the same direction.
- chn.nPortamentoDest = 1;
- }
- if (doFineSlides && param >= 0xE0)
- {
- if (param & 0x0F)
- {
- if ((param & 0xF0) == 0xF0)
- {
- FinePortamentoUp(chn, param & 0x0F);
- return;
- } else if ((param & 0xF0) == 0xE0 && GetType() != MOD_TYPE_DBM)
- {
- ExtraFinePortamentoUp(chn, param & 0x0F);
- return;
- }
- }
- if(GetType() != MOD_TYPE_DBM)
- {
- // DBM only has fine slides, no extra-fine slides.
- return;
- }
- }
- // Regular Slide
- if(!chn.isFirstTick
- || (m_PlayState.m_nMusicSpeed == 1 && m_playBehaviour[kSlidesAtSpeed1])
- || (GetType() & (MOD_TYPE_669 | MOD_TYPE_OKT))
- || (GetType() == MOD_TYPE_MED && m_SongFlags[SONG_FASTVOLSLIDES]))
- {
- DoFreqSlide(chn, chn.nPeriod, param * 4);
- }
- }
- void CSoundFile::PortamentoDown(CHANNELINDEX nChn, ModCommand::PARAM param, const bool doFinePortamentoAsRegular)
- {
- ModChannel &chn = m_PlayState.Chn[nChn];
- if(param)
- {
- // FT2 compatibility: Separate effect memory for all portamento commands
- // Test case: Porta-LinkMem.xm
- if(!m_playBehaviour[kFT2PortaUpDownMemory])
- chn.nOldPortaUp = param;
- chn.nOldPortaDown = param;
- } else
- {
- param = chn.nOldPortaDown;
- }
- const bool doFineSlides = !doFinePortamentoAsRegular && !(GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM | MOD_TYPE_MT2 | MOD_TYPE_MED | MOD_TYPE_AMF0 | MOD_TYPE_DIGI | MOD_TYPE_STP | MOD_TYPE_DTM));
- // Process MIDI pitch bend for instrument plugins
- MidiPortamento(nChn, -static_cast<int>(param), doFineSlides);
- if(GetType() == MOD_TYPE_MPT && chn.pModInstrument && chn.pModInstrument->pTuning)
- {
- // Portamento for instruments with custom tuning
- if(param >= 0xF0 && !doFinePortamentoAsRegular)
- PortamentoFineMPT(chn, -static_cast<int>(param - 0xF0));
- else if(param >= 0xE0 && !doFinePortamentoAsRegular)
- PortamentoExtraFineMPT(chn, -static_cast<int>(param - 0xE0));
- else
- PortamentoMPT(chn, -static_cast<int>(param));
- return;
- } else if(GetType() == MOD_TYPE_PLM)
- {
- // A normal portamento up or down makes a follow-up tone portamento go the same direction.
- chn.nPortamentoDest = 65535;
- }
- if(doFineSlides && param >= 0xE0)
- {
- if (param & 0x0F)
- {
- if ((param & 0xF0) == 0xF0)
- {
- FinePortamentoDown(chn, param & 0x0F);
- return;
- } else if ((param & 0xF0) == 0xE0 && GetType() != MOD_TYPE_DBM)
- {
- ExtraFinePortamentoDown(chn, param & 0x0F);
- return;
- }
- }
- if(GetType() != MOD_TYPE_DBM)
- {
- // DBM only has fine slides, no extra-fine slides.
- return;
- }
- }
- if(!chn.isFirstTick
- || (m_PlayState.m_nMusicSpeed == 1 && m_playBehaviour[kSlidesAtSpeed1])
- || (GetType() & (MOD_TYPE_669 | MOD_TYPE_OKT))
- || (GetType() == MOD_TYPE_MED && m_SongFlags[SONG_FASTVOLSLIDES]))
- {
- DoFreqSlide(chn, chn.nPeriod, param * -4);
- }
- }
- // Send portamento commands to plugins
- void CSoundFile::MidiPortamento(CHANNELINDEX nChn, int param, bool doFineSlides)
- {
- int actualParam = std::abs(param);
- int pitchBend = 0;
- // Old MIDI Pitch Bends:
- // - Applied on every tick
- // - No fine pitch slides (they are interpreted as normal slides)
- // New MIDI Pitch Bends:
- // - Behaviour identical to sample pitch bends if the instrument's PWD parameter corresponds to the actual VSTi setting.
- if(doFineSlides && actualParam >= 0xE0 && !m_playBehaviour[kOldMIDIPitchBends])
- {
- if(m_PlayState.Chn[nChn].isFirstTick)
- {
- // Extra fine slide...
- pitchBend = (actualParam & 0x0F) * mpt::signum(param);
- if(actualParam >= 0xF0)
- {
- // ... or just a fine slide!
- pitchBend *= 4;
- }
- }
- } else if(!m_PlayState.Chn[nChn].isFirstTick || m_playBehaviour[kOldMIDIPitchBends])
- {
- // Regular slide
- pitchBend = param * 4;
- }
- if(pitchBend)
- {
- #ifndef NO_PLUGINS
- IMixPlugin *plugin = GetChannelInstrumentPlugin(m_PlayState.Chn[nChn]);
- if(plugin != nullptr)
- {
- int8 pwd = 13; // Early OpenMPT legacy... Actually it's not *exactly* 13, but close enough...
- if(m_PlayState.Chn[nChn].pModInstrument != nullptr)
- {
- pwd = m_PlayState.Chn[nChn].pModInstrument->midiPWD;
- }
- plugin->MidiPitchBend(pitchBend, pwd, nChn);
- }
- #endif // NO_PLUGINS
- }
- }
- void CSoundFile::FinePortamentoUp(ModChannel &chn, ModCommand::PARAM param) const
- {
- MPT_ASSERT(!chn.HasCustomTuning());
- if(GetType() == MOD_TYPE_XM)
- {
- // FT2 compatibility: E1x / E2x / X1x / X2x memory is not linked
- // Test case: Porta-LinkMem.xm
- if(param) chn.nOldFinePortaUpDown = (chn.nOldFinePortaUpDown & 0x0F) | (param << 4); else param = (chn.nOldFinePortaUpDown >> 4);
- } else if(GetType() == MOD_TYPE_MT2)
- {
- if(param) chn.nOldFinePortaUpDown = param; else param = chn.nOldFinePortaUpDown;
- }
- if(chn.isFirstTick && chn.nPeriod && param)
- DoFreqSlide(chn, chn.nPeriod, param * 4);
- }
- void CSoundFile::FinePortamentoDown(ModChannel &chn, ModCommand::PARAM param) const
- {
- MPT_ASSERT(!chn.HasCustomTuning());
- if(GetType() == MOD_TYPE_XM)
- {
- // FT2 compatibility: E1x / E2x / X1x / X2x memory is not linked
- // Test case: Porta-LinkMem.xm
- if(param) chn.nOldFinePortaUpDown = (chn.nOldFinePortaUpDown & 0xF0) | (param & 0x0F); else param = (chn.nOldFinePortaUpDown & 0x0F);
- } else if(GetType() == MOD_TYPE_MT2)
- {
- if(param) chn.nOldFinePortaUpDown = param; else param = chn.nOldFinePortaUpDown;
- }
- if(chn.isFirstTick && chn.nPeriod && param)
- {
- DoFreqSlide(chn, chn.nPeriod, param * -4);
- if(chn.nPeriod > 0xFFFF && !m_playBehaviour[kPeriodsAreHertz] && (!m_SongFlags[SONG_LINEARSLIDES] || GetType() == MOD_TYPE_XM))
- chn.nPeriod = 0xFFFF;
- }
- }
- void CSoundFile::ExtraFinePortamentoUp(ModChannel &chn, ModCommand::PARAM param) const
- {
- MPT_ASSERT(!chn.HasCustomTuning());
- if(GetType() == MOD_TYPE_XM)
- {
- // FT2 compatibility: E1x / E2x / X1x / X2x memory is not linked
- // Test case: Porta-LinkMem.xm
- if(param) chn.nOldExtraFinePortaUpDown = (chn.nOldExtraFinePortaUpDown & 0x0F) | (param << 4); else param = (chn.nOldExtraFinePortaUpDown >> 4);
- } else if(GetType() == MOD_TYPE_MT2)
- {
- if(param) chn.nOldFinePortaUpDown = param; else param = chn.nOldFinePortaUpDown;
- }
- if(chn.isFirstTick && chn.nPeriod && param)
- DoFreqSlide(chn, chn.nPeriod, param);
- }
- void CSoundFile::ExtraFinePortamentoDown(ModChannel &chn, ModCommand::PARAM param) const
- {
- MPT_ASSERT(!chn.HasCustomTuning());
- if(GetType() == MOD_TYPE_XM)
- {
- // FT2 compatibility: E1x / E2x / X1x / X2x memory is not linked
- // Test case: Porta-LinkMem.xm
- if(param) chn.nOldExtraFinePortaUpDown = (chn.nOldExtraFinePortaUpDown & 0xF0) | (param & 0x0F); else param = (chn.nOldExtraFinePortaUpDown & 0x0F);
- } else if(GetType() == MOD_TYPE_MT2)
- {
- if(param) chn.nOldFinePortaUpDown = param; else param = chn.nOldFinePortaUpDown;
- }
- if(chn.isFirstTick && chn.nPeriod && param)
- {
- DoFreqSlide(chn, chn.nPeriod, -static_cast<int32>(param));
- if(chn.nPeriod > 0xFFFF && !m_playBehaviour[kPeriodsAreHertz] && (!m_SongFlags[SONG_LINEARSLIDES] || GetType() == MOD_TYPE_XM))
- chn.nPeriod = 0xFFFF;
- }
- }
- void CSoundFile::SetFinetune(CHANNELINDEX channel, PlayState &playState, bool isSmooth) const
- {
- ModChannel &chn = playState.Chn[channel];
- int16 newTuning = mpt::saturate_cast<int16>(static_cast<int32>(CalculateXParam(playState.m_nPattern, playState.m_nRow, channel, nullptr)) - 0x8000);
- if(isSmooth)
- {
- const int32 ticksLeft = playState.TicksOnRow() - playState.m_nTickCount;
- if(ticksLeft > 1)
- {
- const int32 step = (newTuning - chn.microTuning) / ticksLeft;
- newTuning = mpt::saturate_cast<int16>(chn.microTuning + step);
- }
- }
- chn.microTuning = newTuning;
- }
- // Implemented for IMF / PTM / OKT compatibility, can't actually save this in any formats
- // Slide up / down every x ticks by y semitones
- // Oktalyzer: Slide down on first tick only, or on every tick
- void CSoundFile::NoteSlide(ModChannel &chn, uint32 param, bool slideUp, bool retrig) const
- {
- if(m_SongFlags[SONG_FIRSTTICK])
- {
- if(param & 0xF0)
- chn.noteSlideParam = static_cast<uint8>(param & 0xF0) | (chn.noteSlideParam & 0x0F);
- if(param & 0x0F)
- chn.noteSlideParam = (chn.noteSlideParam & 0xF0) | static_cast<uint8>(param & 0x0F);
- chn.noteSlideCounter = (chn.noteSlideParam >> 4);
- }
- bool doTrigger = false;
- if(GetType() == MOD_TYPE_OKT)
- doTrigger = ((chn.noteSlideParam & 0xF0) == 0x10) || m_SongFlags[SONG_FIRSTTICK];
- else
- doTrigger = !m_SongFlags[SONG_FIRSTTICK] && (--chn.noteSlideCounter == 0);
- if(doTrigger)
- {
- const uint8 speed = (chn.noteSlideParam >> 4), steps = (chn.noteSlideParam & 0x0F);
- chn.noteSlideCounter = speed;
- // update it
- const int32 delta = (slideUp ? steps : -steps);
- if(chn.HasCustomTuning())
- chn.m_PortamentoFineSteps += delta * chn.pModInstrument->pTuning->GetFineStepCount();
- else
- chn.nPeriod = GetPeriodFromNote(delta + GetNoteFromPeriod(chn.nPeriod, chn.nFineTune, chn.nC5Speed), chn.nFineTune, chn.nC5Speed);
- if(retrig)
- chn.position.Set(0);
- }
- }
- std::pair<uint16, bool> CSoundFile::GetVolCmdTonePorta(const ModCommand &m, uint32 startTick) const
- {
- if(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_AMS | MOD_TYPE_DMF | MOD_TYPE_DBM | MOD_TYPE_IMF | MOD_TYPE_PSM | MOD_TYPE_J2B | MOD_TYPE_ULT | MOD_TYPE_OKT | MOD_TYPE_MT2 | MOD_TYPE_MDL))
- {
- return {ImpulseTrackerPortaVolCmd[m.vol & 0x0F], false};
- } else
- {
- bool clearEffectColumn = false;
- uint16 vol = m.vol;
- if(m.command == CMD_TONEPORTAMENTO && GetType() == MOD_TYPE_XM)
- {
- // Yes, FT2 is *that* weird. If there is a Mx command in the volume column
- // and a normal 3xx command, the 3xx command is ignored but the Mx command's
- // effectiveness is doubled.
- // Test case: TonePortamentoMemory.xm
- clearEffectColumn = true;
- vol *= 2;
- }
- // FT2 compatibility: If there's a portamento and a note delay, execute the portamento, but don't update the parameter
- // Test case: PortaDelay.xm
- if(m_playBehaviour[kFT2PortaDelay] && startTick != 0)
- return {uint16(0), clearEffectColumn};
- else
- return {static_cast<uint16>(vol * 16), clearEffectColumn};
- }
- }
- // Portamento Slide
- void CSoundFile::TonePortamento(ModChannel &chn, uint16 param) const
- {
- chn.dwFlags.set(CHN_PORTAMENTO);
- //IT compatibility 03: Share effect memory with portamento up/down
- if((!m_SongFlags[SONG_ITCOMPATGXX] && m_playBehaviour[kITPortaMemoryShare]) || GetType() == MOD_TYPE_PLM)
- {
- if(param == 0) param = chn.nOldPortaUp;
- chn.nOldPortaUp = chn.nOldPortaDown = static_cast<uint8>(param);
- }
- if(param)
- chn.portamentoSlide = param;
- if(chn.HasCustomTuning())
- {
- //Behavior: Param tells number of finesteps(or 'fullsteps'(notes) with glissando)
- //to slide per row(not per tick).
- if(chn.portamentoSlide == 0)
- return;
- const int32 oldPortamentoTickSlide = (m_PlayState.m_nTickCount != 0) ? chn.m_PortamentoTickSlide : 0;
- int32 delta = chn.portamentoSlide;
- if(chn.nPortamentoDest < 0)
- delta = -delta;
- chn.m_PortamentoTickSlide = static_cast<int32>((m_PlayState.m_nTickCount + 1.0) * delta / m_PlayState.m_nMusicSpeed);
- if(chn.dwFlags[CHN_GLISSANDO])
- {
- chn.m_PortamentoTickSlide *= chn.pModInstrument->pTuning->GetFineStepCount() + 1;
- //With glissando interpreting param as notes instead of finesteps.
- }
- const int32 slide = chn.m_PortamentoTickSlide - oldPortamentoTickSlide;
- if(std::abs(chn.nPortamentoDest) <= std::abs(slide))
- {
- if(chn.nPortamentoDest != 0)
- {
- chn.m_PortamentoFineSteps += chn.nPortamentoDest;
- chn.nPortamentoDest = 0;
- chn.m_CalculateFreq = true;
- }
- } else
- {
- chn.m_PortamentoFineSteps += slide;
- chn.nPortamentoDest -= slide;
- chn.m_CalculateFreq = true;
- }
- return;
- }
- bool doPorta = !chn.isFirstTick
- || (GetType() & (MOD_TYPE_DBM | MOD_TYPE_669))
- || (m_PlayState.m_nMusicSpeed == 1 && m_playBehaviour[kSlidesAtSpeed1])
- || (GetType() == MOD_TYPE_MED && m_SongFlags[SONG_FASTVOLSLIDES]);
- int32 delta = chn.portamentoSlide;
- if(GetType() == MOD_TYPE_PLM && delta >= 0xF0)
- {
- delta -= 0xF0;
- doPorta = chn.isFirstTick;
- }
- if(chn.nPeriod && chn.nPortamentoDest && doPorta)
- {
- delta *= (GetType() == MOD_TYPE_669) ? 2 : 4;
- if(!PeriodsAreFrequencies())
- delta = -delta;
- if(chn.nPeriod < chn.nPortamentoDest || chn.portaTargetReached)
- {
- DoFreqSlide(chn, chn.nPeriod, delta, true);
- if(chn.nPeriod > chn.nPortamentoDest)
- chn.nPeriod = chn.nPortamentoDest;
- } else if(chn.nPeriod > chn.nPortamentoDest)
- {
- DoFreqSlide(chn, chn.nPeriod, -delta, true);
- if(chn.nPeriod < chn.nPortamentoDest)
- chn.nPeriod = chn.nPortamentoDest;
- // FT2 compatibility: Reaching portamento target from below forces subsequent portamentos on the same note to use the logic for reaching the note from above instead.
- // Test case: PortaResetDirection.xm
- if(chn.nPeriod == chn.nPortamentoDest && m_playBehaviour[kFT2PortaResetDirection])
- chn.portaTargetReached = true;
- }
- }
- // IT compatibility 23. Portamento with no note
- // ProTracker also disables portamento once the target is reached.
- // Test case: PortaTarget.mod
- if(chn.nPeriod == chn.nPortamentoDest && (m_playBehaviour[kITPortaTargetReached] || GetType() == MOD_TYPE_MOD))
- chn.nPortamentoDest = 0;
- }
- void CSoundFile::Vibrato(ModChannel &chn, uint32 param) const
- {
- if (param & 0x0F) chn.nVibratoDepth = (param & 0x0F) * 4;
- if (param & 0xF0) chn.nVibratoSpeed = (param >> 4) & 0x0F;
- chn.dwFlags.set(CHN_VIBRATO);
- }
- void CSoundFile::FineVibrato(ModChannel &chn, uint32 param) const
- {
- if (param & 0x0F) chn.nVibratoDepth = param & 0x0F;
- if (param & 0xF0) chn.nVibratoSpeed = (param >> 4) & 0x0F;
- chn.dwFlags.set(CHN_VIBRATO);
- // ST3 compatibility: Do not distinguish between vibrato types in effect memory
- // Test case: VibratoTypeChange.s3m
- if(m_playBehaviour[kST3VibratoMemory] && (param & 0x0F))
- {
- chn.nVibratoDepth *= 4u;
- }
- }
- void CSoundFile::Panbrello(ModChannel &chn, uint32 param) const
- {
- if (param & 0x0F) chn.nPanbrelloDepth = param & 0x0F;
- if (param & 0xF0) chn.nPanbrelloSpeed = (param >> 4) & 0x0F;
- }
- void CSoundFile::Panning(ModChannel &chn, uint32 param, PanningType panBits) const
- {
- // No panning in ProTracker mode
- if(m_playBehaviour[kMODIgnorePanning])
- {
- return;
- }
- // IT Compatibility (and other trackers as well): panning disables surround (unless panning in rear channels is enabled, which is not supported by the original trackers anyway)
- if (!m_SongFlags[SONG_SURROUNDPAN] && (panBits == Pan8bit || m_playBehaviour[kPanOverride]))
- {
- chn.dwFlags.reset(CHN_SURROUND);
- }
- if(panBits == Pan4bit)
- {
- // 0...15 panning
- chn.nPan = (param * 256 + 8) / 15;
- } else if(panBits == Pan6bit)
- {
- // 0...64 panning
- if(param > 64) param = 64;
- chn.nPan = param * 4;
- } else
- {
- if(!(GetType() & (MOD_TYPE_S3M | MOD_TYPE_DSM | MOD_TYPE_AMF0 | MOD_TYPE_AMF | MOD_TYPE_MTM)))
- {
- // Real 8-bit panning
- chn.nPan = param;
- } else
- {
- // 7-bit panning + surround
- if(param <= 0x80)
- {
- chn.nPan = param << 1;
- } else if(param == 0xA4)
- {
- chn.dwFlags.set(CHN_SURROUND);
- chn.nPan = 0x80;
- }
- }
- }
- chn.dwFlags.set(CHN_FASTVOLRAMP);
- chn.nRestorePanOnNewNote = 0;
- //IT compatibility 20. Set pan overrides random pan
- if(m_playBehaviour[kPanOverride])
- {
- chn.nPanSwing = 0;
- chn.nPanbrelloOffset = 0;
- }
- }
- void CSoundFile::VolumeSlide(ModChannel &chn, ModCommand::PARAM param) const
- {
- if (param)
- chn.nOldVolumeSlide = param;
- else
- param = chn.nOldVolumeSlide;
- if((GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM | MOD_TYPE_MT2 | MOD_TYPE_MED | MOD_TYPE_DIGI | MOD_TYPE_STP | MOD_TYPE_DTM)))
- {
- // MOD / XM nibble priority
- if((param & 0xF0) != 0)
- {
- param &= 0xF0;
- } else
- {
- param &= 0x0F;
- }
- }
- int newVolume = chn.nVolume;
- if(!(GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM | MOD_TYPE_AMF0 | MOD_TYPE_MED | MOD_TYPE_DIGI)))
- {
- if ((param & 0x0F) == 0x0F) //Fine upslide or slide -15
- {
- if (param & 0xF0) //Fine upslide
- {
- FineVolumeUp(chn, (param >> 4), false);
- return;
- } else //Slide -15
- {
- if(chn.isFirstTick && !m_SongFlags[SONG_FASTVOLSLIDES])
- {
- newVolume -= 0x0F * 4;
- }
- }
- } else
- if ((param & 0xF0) == 0xF0) //Fine downslide or slide +15
- {
- if (param & 0x0F) //Fine downslide
- {
- FineVolumeDown(chn, (param & 0x0F), false);
- return;
- } else //Slide +15
- {
- if(chn.isFirstTick && !m_SongFlags[SONG_FASTVOLSLIDES])
- {
- newVolume += 0x0F * 4;
- }
- }
- }
- }
- if(!chn.isFirstTick || m_SongFlags[SONG_FASTVOLSLIDES] || (m_PlayState.m_nMusicSpeed == 1 && GetType() == MOD_TYPE_DBM))
- {
- // IT compatibility: Ignore slide commands with both nibbles set.
- if (param & 0x0F)
- {
- if(!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) || (param & 0xF0) == 0)
- newVolume -= (int)((param & 0x0F) * 4);
- }
- else
- {
- newVolume += (int)((param & 0xF0) >> 2);
- }
- if (GetType() == MOD_TYPE_MOD) chn.dwFlags.set(CHN_FASTVOLRAMP);
- }
- newVolume = Clamp(newVolume, 0, 256);
- chn.nVolume = newVolume;
- }
- void CSoundFile::PanningSlide(ModChannel &chn, ModCommand::PARAM param, bool memory) const
- {
- if(memory)
- {
- // FT2 compatibility: Use effect memory (lxx and rxx in XM shouldn't use effect memory).
- // Test case: PanSlideMem.xm
- if(param)
- chn.nOldPanSlide = param;
- else
- param = chn.nOldPanSlide;
- }
- if((GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2)))
- {
- // XM nibble priority
- if((param & 0xF0) != 0)
- {
- param &= 0xF0;
- } else
- {
- param &= 0x0F;
- }
- }
- int32 nPanSlide = 0;
- if(!(GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2)))
- {
- if (((param & 0x0F) == 0x0F) && (param & 0xF0))
- {
- if(m_SongFlags[SONG_FIRSTTICK])
- {
- param = (param & 0xF0) / 4u;
- nPanSlide = - (int)param;
- }
- } else if (((param & 0xF0) == 0xF0) && (param & 0x0F))
- {
- if(m_SongFlags[SONG_FIRSTTICK])
- {
- nPanSlide = (param & 0x0F) * 4u;
- }
- } else if(!m_SongFlags[SONG_FIRSTTICK])
- {
- if (param & 0x0F)
- {
- // IT compatibility: Ignore slide commands with both nibbles set.
- if(!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) || (param & 0xF0) == 0)
- nPanSlide = (int)((param & 0x0F) * 4u);
- } else
- {
- nPanSlide = -(int)((param & 0xF0) / 4u);
- }
- }
- } else
- {
- if(!m_SongFlags[SONG_FIRSTTICK])
- {
- if (param & 0xF0)
- {
- nPanSlide = (int)((param & 0xF0) / 4u);
- } else
- {
- nPanSlide = -(int)((param & 0x0F) * 4u);
- }
- // FT2 compatibility: FT2's panning slide is like IT's fine panning slide (not as deep)
- if(m_playBehaviour[kFT2PanSlide])
- nPanSlide /= 4;
- }
- }
- if (nPanSlide)
- {
- nPanSlide += chn.nPan;
- nPanSlide = Clamp(nPanSlide, 0, 256);
- chn.nPan = nPanSlide;
- chn.nRestorePanOnNewNote = 0;
- }
- }
- void CSoundFile::FineVolumeUp(ModChannel &chn, ModCommand::PARAM param, bool volCol) const
- {
- if(GetType() == MOD_TYPE_XM)
- {
- // FT2 compatibility: EAx / EBx memory is not linked
- // Test case: FineVol-LinkMem.xm
- if(param) chn.nOldFineVolUpDown = (param << 4) | (chn.nOldFineVolUpDown & 0x0F); else param = (chn.nOldFineVolUpDown >> 4);
- } else if(volCol)
- {
- if(param) chn.nOldVolParam = param; else param = chn.nOldVolParam;
- } else
- {
- if(param) chn.nOldFineVolUpDown = param; else param = chn.nOldFineVolUpDown;
- }
- if(chn.isFirstTick)
- {
- chn.nVolume += param * 4;
- if(chn.nVolume > 256) chn.nVolume = 256;
- if(GetType() & MOD_TYPE_MOD) chn.dwFlags.set(CHN_FASTVOLRAMP);
- }
- }
- void CSoundFile::FineVolumeDown(ModChannel &chn, ModCommand::PARAM param, bool volCol) const
- {
- if(GetType() == MOD_TYPE_XM)
- {
- // FT2 compatibility: EAx / EBx memory is not linked
- // Test case: FineVol-LinkMem.xm
- if(param) chn.nOldFineVolUpDown = param | (chn.nOldFineVolUpDown & 0xF0); else param = (chn.nOldFineVolUpDown & 0x0F);
- } else if(volCol)
- {
- if(param) chn.nOldVolParam = param; else param = chn.nOldVolParam;
- } else
- {
- if(param) chn.nOldFineVolUpDown = param; else param = chn.nOldFineVolUpDown;
- }
- if(chn.isFirstTick)
- {
- chn.nVolume -= param * 4;
- if(chn.nVolume < 0) chn.nVolume = 0;
- if(GetType() & MOD_TYPE_MOD) chn.dwFlags.set(CHN_FASTVOLRAMP);
- }
- }
- void CSoundFile::Tremolo(ModChannel &chn, uint32 param) const
- {
- if (param & 0x0F) chn.nTremoloDepth = (param & 0x0F) << 2;
- if (param & 0xF0) chn.nTremoloSpeed = (param >> 4) & 0x0F;
- chn.dwFlags.set(CHN_TREMOLO);
- }
- void CSoundFile::ChannelVolSlide(ModChannel &chn, ModCommand::PARAM param) const
- {
- int32 nChnSlide = 0;
- if (param) chn.nOldChnVolSlide = param; else param = chn.nOldChnVolSlide;
- if (((param & 0x0F) == 0x0F) && (param & 0xF0))
- {
- if(m_SongFlags[SONG_FIRSTTICK]) nChnSlide = param >> 4;
- } else if (((param & 0xF0) == 0xF0) && (param & 0x0F))
- {
- if(m_SongFlags[SONG_FIRSTTICK]) nChnSlide = - (int)(param & 0x0F);
- } else
- {
- if(!m_SongFlags[SONG_FIRSTTICK])
- {
- if (param & 0x0F)
- {
- if(!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_J2B | MOD_TYPE_DBM)) || (param & 0xF0) == 0)
- nChnSlide = -(int)(param & 0x0F);
- } else
- {
- nChnSlide = (int)((param & 0xF0) >> 4);
- }
- }
- }
- if (nChnSlide)
- {
- nChnSlide += chn.nGlobalVol;
- nChnSlide = Clamp(nChnSlide, 0, 64);
- chn.nGlobalVol = nChnSlide;
- }
- }
- void CSoundFile::ExtendedMODCommands(CHANNELINDEX nChn, ModCommand::PARAM param)
- {
- ModChannel &chn = m_PlayState.Chn[nChn];
- uint8 command = param & 0xF0;
- param &= 0x0F;
- switch(command)
- {
- // E0x: Set Filter
- case 0x00:
- for(CHANNELINDEX channel = 0; channel < GetNumChannels(); channel++)
- {
- m_PlayState.Chn[channel].dwFlags.set(CHN_AMIGAFILTER, !(param & 1));
- }
- break;
- // E1x: Fine Portamento Up
- case 0x10: if ((param) || (GetType() & (MOD_TYPE_XM|MOD_TYPE_MT2))) FinePortamentoUp(chn, param); break;
- // E2x: Fine Portamento Down
- case 0x20: if ((param) || (GetType() & (MOD_TYPE_XM|MOD_TYPE_MT2))) FinePortamentoDown(chn, param); break;
- // E3x: Set Glissando Control
- case 0x30: chn.dwFlags.set(CHN_GLISSANDO, param != 0); break;
- // E4x: Set Vibrato WaveForm
- case 0x40: chn.nVibratoType = param & 0x07; break;
- // E5x: Set FineTune
- case 0x50: if(!m_SongFlags[SONG_FIRSTTICK])
- break;
- if(GetType() & (MOD_TYPE_MOD | MOD_TYPE_DIGI | MOD_TYPE_AMF0 | MOD_TYPE_MED))
- {
- chn.nFineTune = MOD2XMFineTune(param);
- if(chn.nPeriod && chn.rowCommand.IsNote()) chn.nPeriod = GetPeriodFromNote(chn.nNote, chn.nFineTune, chn.nC5Speed);
- } else if(GetType() == MOD_TYPE_MTM)
- {
- if(chn.rowCommand.IsNote() && chn.pModSample != nullptr)
- {
- // Effect is permanent in MultiTracker
- const_cast<ModSample *>(chn.pModSample)->nFineTune = param;
- chn.nFineTune = param;
- if(chn.nPeriod) chn.nPeriod = GetPeriodFromNote(chn.nNote, chn.nFineTune, chn.nC5Speed);
- }
- } else if(chn.rowCommand.IsNote())
- {
- chn.nFineTune = MOD2XMFineTune(param - 8);
- if(chn.nPeriod) chn.nPeriod = GetPeriodFromNote(chn.nNote, chn.nFineTune, chn.nC5Speed);
- }
- break;
- // E6x: Pattern Loop
- case 0x60:
- if(m_SongFlags[SONG_FIRSTTICK])
- PatternLoop(m_PlayState, chn, param & 0x0F);
- break;
- // E7x: Set Tremolo WaveForm
- case 0x70: chn.nTremoloType = param & 0x07; break;
- // E8x: Set 4-bit Panning
- case 0x80:
- if(m_SongFlags[SONG_FIRSTTICK])
- {
- Panning(chn, param, Pan4bit);
- }
- break;
- // E9x: Retrig
- case 0x90: RetrigNote(nChn, param); break;
- // EAx: Fine Volume Up
- case 0xA0: if ((param) || (GetType() & (MOD_TYPE_XM|MOD_TYPE_MT2))) FineVolumeUp(chn, param, false); break;
- // EBx: Fine Volume Down
- case 0xB0: if ((param) || (GetType() & (MOD_TYPE_XM|MOD_TYPE_MT2))) FineVolumeDown(chn, param, false); break;
- // ECx: Note Cut
- case 0xC0: NoteCut(nChn, param, false); break;
- // EDx: Note Delay
- // EEx: Pattern Delay
- case 0xF0:
- if(GetType() == MOD_TYPE_MOD) // MOD: Invert Loop
- {
- chn.nEFxSpeed = param;
- if(m_SongFlags[SONG_FIRSTTICK]) InvertLoop(chn);
- } else // XM: Set Active Midi Macro
- {
- chn.nActiveMacro = param;
- }
- break;
- }
- }
- void CSoundFile::ExtendedS3MCommands(CHANNELINDEX nChn, ModCommand::PARAM param)
- {
- ModChannel &chn = m_PlayState.Chn[nChn];
- uint8 command = param & 0xF0;
- param &= 0x0F;
- switch(command)
- {
- // S0x: Set Filter
- // S1x: Set Glissando Control
- case 0x10: chn.dwFlags.set(CHN_GLISSANDO, param != 0); break;
- // S2x: Set FineTune
- case 0x20: if(!m_SongFlags[SONG_FIRSTTICK])
- break;
- if(chn.HasCustomTuning())
- {
- chn.nFineTune = param - 8;
- chn.m_CalculateFreq = true;
- } else if(GetType() != MOD_TYPE_669)
- {
- chn.nC5Speed = S3MFineTuneTable[param];
- chn.nFineTune = MOD2XMFineTune(param);
- if(chn.nPeriod)
- chn.nPeriod = GetPeriodFromNote(chn.nNote, chn.nFineTune, chn.nC5Speed);
- } else if(chn.pModSample != nullptr)
- {
- chn.nC5Speed = chn.pModSample->nC5Speed + param * 80;
- }
- break;
- // S3x: Set Vibrato Waveform
- case 0x30: if(GetType() == MOD_TYPE_S3M)
- {
- chn.nVibratoType = param & 0x03;
- } else
- {
- // IT compatibility: Ignore waveform types > 3
- if(m_playBehaviour[kITVibratoTremoloPanbrello])
- chn.nVibratoType = (param < 0x04) ? param : 0;
- else
- chn.nVibratoType = param & 0x07;
- }
- break;
- // S4x: Set Tremolo Waveform
- case 0x40: if(GetType() == MOD_TYPE_S3M)
- {
- chn.nTremoloType = param & 0x03;
- } else
- {
- // IT compatibility: Ignore waveform types > 3
- if(m_playBehaviour[kITVibratoTremoloPanbrello])
- chn.nTremoloType = (param < 0x04) ? param : 0;
- else
- chn.nTremoloType = param & 0x07;
- }
- break;
- // S5x: Set Panbrello Waveform
- case 0x50:
- // IT compatibility: Ignore waveform types > 3
- if(m_playBehaviour[kITVibratoTremoloPanbrello])
- {
- chn.nPanbrelloType = (param < 0x04) ? param : 0;
- chn.nPanbrelloPos = 0;
- } else
- {
- chn.nPanbrelloType = param & 0x07;
- }
- break;
- // S6x: Pattern Delay for x frames
- case 0x60:
- if(m_SongFlags[SONG_FIRSTTICK] && m_PlayState.m_nTickCount == 0)
- {
- // Tick delays are added up.
- // Scream Tracker 3 does actually not support this command.
- // We'll use the same behaviour as for Impulse Tracker, as we can assume that
- // most S3Ms that make use of this command were made with Impulse Tracker.
- // MPT added this command to the XM format through the X6x effect, so we will use
- // the same behaviour here as well.
- // Test cases: PatternDelays.it, PatternDelays.s3m, PatternDelays.xm
- m_PlayState.m_nFrameDelay += param;
- }
- break;
- // S7x: Envelope Control / Instrument Control
- case 0x70: if(!m_SongFlags[SONG_FIRSTTICK]) break;
- switch(param)
- {
- case 0:
- case 1:
- case 2:
- {
- for (CHANNELINDEX i = m_nChannels; i < MAX_CHANNELS; i++)
- {
- ModChannel &bkChn = m_PlayState.Chn[i];
- if (bkChn.nMasterChn == nChn + 1)
- {
- if (param == 1)
- {
- KeyOff(bkChn);
- if(bkChn.dwFlags[CHN_ADLIB] && m_opl)
- m_opl->NoteOff(i);
- } else if (param == 2)
- {
- bkChn.dwFlags.set(CHN_NOTEFADE);
- if(bkChn.dwFlags[CHN_ADLIB] && m_opl)
- m_opl->NoteOff(i);
- } else
- {
- bkChn.dwFlags.set(CHN_NOTEFADE);
- bkChn.nFadeOutVol = 0;
- if(bkChn.dwFlags[CHN_ADLIB] && m_opl)
- m_opl->NoteCut(i);
- }
- #ifndef NO_PLUGINS
- const ModInstrument *pIns = bkChn.pModInstrument;
- IMixPlugin *pPlugin;
- if(pIns != nullptr && pIns->nMixPlug && (pPlugin = m_MixPlugins[pIns->nMixPlug - 1].pMixPlugin) != nullptr)
- {
- pPlugin->MidiCommand(*pIns, bkChn.nNote + NOTE_MAX_SPECIAL, 0, nChn);
- }
- #endif // NO_PLUGINS
- }
- }
- }
- break;
- default: // S73-S7E
- chn.InstrumentControl(param, *this);
- break;
- }
- break;
- // S8x: Set 4-bit Panning
- case 0x80:
- if(m_SongFlags[SONG_FIRSTTICK])
- {
- Panning(chn, param, Pan4bit);
- }
- break;
- // S9x: Sound Control
- case 0x90: ExtendedChannelEffect(chn, param); break;
- // SAx: Set 64k Offset
- case 0xA0: if(m_SongFlags[SONG_FIRSTTICK])
- {
- chn.nOldHiOffset = static_cast<uint8>(param);
- if (!m_playBehaviour[kITHighOffsetNoRetrig] && chn.rowCommand.IsNote())
- {
- SmpLength pos = param << 16;
- if (pos < chn.nLength) chn.position.SetInt(pos);
- }
- }
- break;
- // SBx: Pattern Loop
- case 0xB0:
- if(m_SongFlags[SONG_FIRSTTICK])
- PatternLoop(m_PlayState, chn, param & 0x0F);
- break;
- // SCx: Note Cut
- case 0xC0:
- if(param == 0)
- {
- //IT compatibility 22. SC0 == SC1
- if(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT))
- param = 1;
- // ST3 doesn't cut notes with SC0
- else if(GetType() == MOD_TYPE_S3M)
- return;
- }
- // S3M/IT compatibility: Note Cut really cuts notes and does not just mute them (so that following volume commands could restore the sample)
- // Test case: scx.it
- NoteCut(nChn, param, m_playBehaviour[kITSCxStopsSample] || GetType() == MOD_TYPE_S3M);
- break;
- // SDx: Note Delay
- // SEx: Pattern Delay for x rows
- // SFx: S3M: Not used, IT: Set Active Midi Macro
- case 0xF0:
- if(GetType() != MOD_TYPE_S3M)
- {
- chn.nActiveMacro = static_cast<uint8>(param);
- }
- break;
- }
- }
- void CSoundFile::ExtendedChannelEffect(ModChannel &chn, uint32 param)
- {
- // S9x and X9x commands (S3M/XM/IT only)
- if(!m_SongFlags[SONG_FIRSTTICK]) return;
- switch(param & 0x0F)
- {
- // S90: Surround Off
- case 0x00: chn.dwFlags.reset(CHN_SURROUND); break;
- // S91: Surround On
- case 0x01: chn.dwFlags.set(CHN_SURROUND); chn.nPan = 128; break;
- ////////////////////////////////////////////////////////////
- // ModPlug Extensions
- // S98: Reverb Off
- case 0x08:
- chn.dwFlags.reset(CHN_REVERB);
- chn.dwFlags.set(CHN_NOREVERB);
- break;
- // S99: Reverb On
- case 0x09:
- chn.dwFlags.reset(CHN_NOREVERB);
- chn.dwFlags.set(CHN_REVERB);
- break;
- // S9A: 2-Channels surround mode
- case 0x0A:
- m_SongFlags.reset(SONG_SURROUNDPAN);
- break;
- // S9B: 4-Channels surround mode
- case 0x0B:
- m_SongFlags.set(SONG_SURROUNDPAN);
- break;
- // S9C: IT Filter Mode
- case 0x0C:
- m_SongFlags.reset(SONG_MPTFILTERMODE);
- break;
- // S9D: MPT Filter Mode
- case 0x0D:
- m_SongFlags.set(SONG_MPTFILTERMODE);
- break;
- // S9E: Go forward
- case 0x0E:
- chn.dwFlags.reset(CHN_PINGPONGFLAG);
- break;
- // S9F: Go backward (and set playback position to the end if sample just started)
- case 0x0F:
- if(chn.position.IsZero() && chn.nLength && (chn.rowCommand.IsNote() || !chn.dwFlags[CHN_LOOP]))
- {
- chn.position.Set(chn.nLength - 1, SamplePosition::fractMax);
- }
- chn.dwFlags.set(CHN_PINGPONGFLAG);
- break;
- }
- }
- void CSoundFile::InvertLoop(ModChannel &chn)
- {
- // EFx implementation for MOD files (PT 1.1A and up: Invert Loop)
- // This effect trashes samples. Thanks to 8bitbubsy for making this work. :)
- if(GetType() != MOD_TYPE_MOD || chn.nEFxSpeed == 0)
- return;
- ModSample *pModSample = const_cast<ModSample *>(chn.pModSample);
- if(pModSample == nullptr || !pModSample->HasSampleData() || !pModSample->uFlags[CHN_LOOP | CHN_SUSTAINLOOP])
- return;
- chn.nEFxDelay += ModEFxTable[chn.nEFxSpeed & 0x0F];
- if(chn.nEFxDelay < 128)
- return;
- chn.nEFxDelay = 0;
- const SmpLength loopStart = pModSample->uFlags[CHN_LOOP] ? pModSample->nLoopStart : pModSample->nSustainStart;
- const SmpLength loopEnd = pModSample->uFlags[CHN_LOOP] ? pModSample->nLoopEnd : pModSample->nSustainEnd;
- if(++chn.nEFxOffset >= loopEnd - loopStart)
- chn.nEFxOffset = 0;
- // TRASH IT!!! (Yes, the sample!)
- const uint8 bps = pModSample->GetBytesPerSample();
- uint8 *begin = mpt::byte_cast<uint8 *>(pModSample->sampleb()) + (loopStart + chn.nEFxOffset) * bps;
- for(auto &sample : mpt::as_span(begin, bps))
- {
- sample = ~sample;
- }
- pModSample->PrecomputeLoops(*this, false);
- }
- // Process a MIDI Macro.
- // Parameters:
- // playState: The playback state to operate on.
- // nChn: Mod channel to apply macro on
- // isSmooth: If true, internal macros are interpolated between two rows
- // macro: MIDI Macro string to process
- // param: Parameter for parametric macros (Zxx / \xx parameter)
- // plugin: Plugin to send MIDI message to (if not specified but needed, it is autodetected)
- void CSoundFile::ProcessMIDIMacro(PlayState &playState, CHANNELINDEX nChn, bool isSmooth, const MIDIMacroConfigData::Macro ¯o, uint8 param, PLUGINDEX plugin)
- {
- playState.m_midiMacroScratchSpace.resize(macro.Length() + 1);
- auto out = mpt::as_span(playState.m_midiMacroScratchSpace);
- ParseMIDIMacro(playState, nChn, isSmooth, macro, out, param, plugin);
- // Macro string has been parsed and translated, now send the message(s)...
- uint32 outSize = static_cast<uint32>(out.size());
- uint32 sendPos = 0;
- uint8 runningStatus = 0;
- while(sendPos < out.size())
- {
- uint32 sendLen = 0;
- if(out[sendPos] == 0xF0)
- {
- // SysEx start
- if((outSize - sendPos >= 4) && (out[sendPos + 1] == 0xF0 || out[sendPos + 1] == 0xF1))
- {
- // Internal macro (normal (F0F0) or extended (F0F1)), 4 bytes long
- sendLen = 4;
- } else
- {
- // SysEx message, find end of message
- for(uint32 i = sendPos + 1; i < outSize; i++)
- {
- if(out[i] == 0xF7)
- {
- // Found end of SysEx message
- sendLen = i - sendPos + 1;
- break;
- }
- }
- if(sendLen == 0)
- {
- // Didn't find end, so "invent" end of SysEx message
- out[outSize++] = 0xF7;
- sendLen = outSize - sendPos;
- }
- }
- } else if(!(out[sendPos] & 0x80))
- {
- // Missing status byte? Try inserting running status
- if(runningStatus != 0)
- {
- sendPos--;
- out[sendPos] = runningStatus;
- } else
- {
- // No running status to re-use; skip this byte
- sendPos++;
- }
- continue;
- } else
- {
- // Other MIDI messages
- sendLen = std::min(static_cast<uint32>(MIDIEvents::GetEventLength(out[sendPos])), outSize - sendPos);
- }
- if(sendLen == 0)
- break;
- if(out[sendPos] < 0xF0)
- {
- runningStatus = out[sendPos];
- }
- const auto midiMsg = out.subspan(sendPos, sendLen);
- SendMIDIData(playState, nChn, isSmooth, midiMsg, plugin);
- sendPos += sendLen;
- }
- }
- void CSoundFile::ParseMIDIMacro(PlayState &playState, CHANNELINDEX nChn, bool isSmooth, const mpt::span<const char> macro, mpt::span<uint8> &out, uint8 param, PLUGINDEX plugin) const
- {
- ModChannel &chn = playState.Chn[nChn];
- const ModInstrument *pIns = chn.pModInstrument;
- const uint8 lastZxxParam = chn.lastZxxParam; // always interpolate based on original value in case z appears multiple times in macro string
- uint8 updateZxxParam = 0xFF; // avoid updating lastZxxParam immediately if macro contains both internal and external MIDI message
- bool firstNibble = true;
- size_t outPos = 0; // output buffer position, which also equals the number of complete bytes
- for(size_t pos = 0; pos < macro.size() && outPos < out.size(); pos++)
- {
- bool isNibble = false; // did we parse a nibble or a byte value?
- uint8 data = 0; // data that has just been parsed
- // Parse next macro byte... See Impulse Tracker's MIDI.TXT for detailed information on each possible character.
- if(macro[pos] >= '0' && macro[pos] <= '9')
- {
- isNibble = true;
- data = static_cast<uint8>(macro[pos] - '0');
- } else if(macro[pos] >= 'A' && macro[pos] <= 'F')
- {
- isNibble = true;
- data = static_cast<uint8>(macro[pos] - 'A' + 0x0A);
- } else if(macro[pos] == 'c')
- {
- // MIDI channel
- isNibble = true;
- data = 0xFF;
- #ifndef NO_PLUGINS
- const PLUGINDEX plug = (plugin != 0) ? plugin : GetBestPlugin(playState, nChn, PrioritiseChannel, EvenIfMuted);
- if(plug > 0 && plug <= MAX_MIXPLUGINS)
- {
- auto midiPlug = dynamic_cast<const IMidiPlugin *>(m_MixPlugins[plug - 1u].pMixPlugin);
- if(midiPlug)
- data = midiPlug->GetMidiChannel(playState.Chn[nChn], nChn);
- }
- #endif // NO_PLUGINS
- if(data == 0xFF)
- {
- // Fallback if no plugin was found
- if(pIns)
- data = pIns->GetMIDIChannel(playState.Chn[nChn], nChn);
- else
- data = 0;
- }
- } else if(macro[pos] == 'n')
- {
- // Last triggered note
- if(ModCommand::IsNote(chn.nLastNote))
- {
- data = chn.nLastNote - NOTE_MIN;
- }
- } else if(macro[pos] == 'v')
- {
- // Velocity
- // This is "almost" how IT does it - apparently, IT seems to lag one row behind on global volume or channel volume changes.
- const int swing = (m_playBehaviour[kITSwingBehaviour] || m_playBehaviour[kMPTOldSwingBehaviour]) ? chn.nVolSwing : 0;
- const int vol = Util::muldiv((chn.nVolume + swing) * m_PlayState.m_nGlobalVolume, chn.nGlobalVol * chn.nInsVol, 1 << 20);
- data = static_cast<uint8>(Clamp(vol / 2, 1, 127));
- //data = (unsigned char)std::min((chn.nVolume * chn.nGlobalVol * m_nGlobalVolume) >> (1 + 6 + 8), 127);
- } else if(macro[pos] == 'u')
- {
- // Calculated volume
- // Same note as with velocity applies here, but apparently also for instrument / sample volumes?
- const int vol = Util::muldiv(chn.nCalcVolume * m_PlayState.m_nGlobalVolume, chn.nGlobalVol * chn.nInsVol, 1 << 26);
- data = static_cast<uint8>(Clamp(vol / 2, 1, 127));
- //data = (unsigned char)std::min((chn.nCalcVolume * chn.nGlobalVol * m_nGlobalVolume) >> (7 + 6 + 8), 127);
- } else if(macro[pos] == 'x')
- {
- // Pan set
- data = static_cast<uint8>(std::min(static_cast<int>(chn.nPan / 2), 127));
- } else if(macro[pos] == 'y')
- {
- // Calculated pan
- data = static_cast<uint8>(std::min(static_cast<int>(chn.nRealPan / 2), 127));
- } else if(macro[pos] == 'a')
- {
- // High byte of bank select
- if(pIns && pIns->wMidiBank)
- {
- data = static_cast<uint8>(((pIns->wMidiBank - 1) >> 7) & 0x7F);
- }
- } else if(macro[pos] == 'b')
- {
- // Low byte of bank select
- if(pIns && pIns->wMidiBank)
- {
- data = static_cast<uint8>((pIns->wMidiBank - 1) & 0x7F);
- }
- } else if(macro[pos] == 'o')
- {
- // Offset (ignoring high offset)
- data = static_cast<uint8>((chn.oldOffset >> 8) & 0xFF);
- } else if(macro[pos] == 'h')
- {
- // Host channel number
- data = static_cast<uint8>((nChn >= GetNumChannels() ? (chn.nMasterChn - 1) : nChn) & 0x7F);
- } else if(macro[pos] == 'm')
- {
- // Loop direction (judging from the character, it was supposed to be loop type, though)
- data = chn.dwFlags[CHN_PINGPONGFLAG] ? 1 : 0;
- } else if(macro[pos] == 'p')
- {
- // Program select
- if(pIns && pIns->nMidiProgram)
- {
- data = static_cast<uint8>((pIns->nMidiProgram - 1) & 0x7F);
- }
- } else if(macro[pos] == 'z')
- {
- // Zxx parameter
- data = param;
- if(isSmooth && chn.lastZxxParam < 0x80
- && (outPos < 3 || out[outPos - 3] != 0xF0 || out[outPos - 2] < 0xF0))
- {
- // Interpolation for external MIDI messages - interpolation for internal messages
- // is handled separately to allow for more than 7-bit granularity where it's possible
- data = static_cast<uint8>(CalculateSmoothParamChange(playState, lastZxxParam, data));
- chn.lastZxxParam = data;
- updateZxxParam = 0x80;
- } else if(updateZxxParam == 0xFF)
- {
- updateZxxParam = data;
- }
- } else if(macro[pos] == 's')
- {
- // SysEx Checksum (not an original Impulse Tracker macro variable, but added for convenience)
- auto startPos = outPos;
- while(startPos > 0 && out[--startPos] != 0xF0);
- if(outPos - startPos < 5 || out[startPos] != 0xF0)
- {
- continue;
- }
- for(auto p = startPos + 5u; p != outPos; p++)
- {
- data += out[p];
- }
- data = (~data + 1) & 0x7F;
- } else
- {
- // Unrecognized byte (e.g. space char)
- continue;
- }
- // Append parsed data
- if(isNibble) // parsed a nibble (constant or 'c' variable)
- {
- if(firstNibble)
- {
- out[outPos] = data;
- } else
- {
- out[outPos] = (out[outPos] << 4) | data;
- outPos++;
- }
- firstNibble = !firstNibble;
- } else // parsed a byte (variable)
- {
- if(!firstNibble) // From MIDI.TXT: '9n' is exactly the same as '09 n' or '9 n' -- so finish current byte first
- {
- outPos++;
- }
- out[outPos++] = data;
- firstNibble = true;
- }
- }
- if(!firstNibble)
- {
- // Finish current byte
- outPos++;
- }
- if(updateZxxParam < 0x80)
- chn.lastZxxParam = updateZxxParam;
- out = out.first(outPos);
- }
- // Calculate smooth MIDI macro slide parameter for current tick.
- float CSoundFile::CalculateSmoothParamChange(const PlayState &playState, float currentValue, float param)
- {
- MPT_ASSERT(playState.TicksOnRow() > playState.m_nTickCount);
- const uint32 ticksLeft = playState.TicksOnRow() - playState.m_nTickCount;
- if(ticksLeft > 1)
- {
- // Slide param
- const float step = (param - currentValue) / static_cast<float>(ticksLeft);
- return (currentValue + step);
- } else
- {
- // On last tick, set exact value.
- return param;
- }
- }
- // Process exactly one MIDI message parsed by ProcessMIDIMacro. Returns bytes sent on success, 0 on (parse) failure.
- void CSoundFile::SendMIDIData(PlayState &playState, CHANNELINDEX nChn, bool isSmooth, const mpt::span<const uint8> macro, PLUGINDEX plugin)
- {
- if(macro.size() < 1)
- return;
- // Don't do anything that modifies state outside of the playState itself.
- const bool localOnly = playState.m_midiMacroEvaluationResults.has_value();
- if(macro[0] == 0xFA || macro[0] == 0xFC || macro[0] == 0xFF)
- {
- // Start Song, Stop Song, MIDI Reset - both interpreted internally and sent to plugins
- for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++)
- {
- playState.Chn[chn].nCutOff = 0x7F;
- playState.Chn[chn].nResonance = 0x00;
- }
- }
- ModChannel &chn = playState.Chn[nChn];
- if(macro.size() == 4 && macro[0] == 0xF0 && (macro[1] == 0xF0 || macro[1] == 0xF1))
- {
- // Internal device.
- const bool isExtended = (macro[1] == 0xF1);
- const uint8 macroCode = macro[2];
- const uint8 param = macro[3];
- if(macroCode == 0x00 && !isExtended && param < 0x80)
- {
- // F0.F0.00.xx: Set CutOff
- if(!isSmooth)
- chn.nCutOff = param;
- else
- chn.nCutOff = mpt::saturate_round<uint8>(CalculateSmoothParamChange(playState, chn.nCutOff, param));
- chn.nRestoreCutoffOnNewNote = 0;
- int cutoff = SetupChannelFilter(chn, !chn.dwFlags[CHN_FILTER]);
- if(cutoff >= 0 && chn.dwFlags[CHN_ADLIB] && m_opl && !localOnly)
- {
- // Cutoff doubles as modulator intensity for FM instruments
- m_opl->Volume(nChn, static_cast<uint8>(cutoff / 4), true);
- }
- } else if(macroCode == 0x01 && !isExtended && param < 0x80)
- {
- // F0.F0.01.xx: Set Resonance
- if(!isSmooth)
- chn.nResonance = param;
- else
- chn.nResonance = mpt::saturate_round<uint8>(CalculateSmoothParamChange(playState, chn.nResonance, param));
- chn.nRestoreResonanceOnNewNote = 0;
- SetupChannelFilter(chn, !chn.dwFlags[CHN_FILTER]);
- } else if(macroCode == 0x02 && !isExtended)
- {
- // F0.F0.02.xx: Set filter mode (high nibble determines filter mode)
- if(param < 0x20)
- {
- chn.nFilterMode = static_cast<FilterMode>(param >> 4);
- SetupChannelFilter(chn, !chn.dwFlags[CHN_FILTER]);
- }
- #ifndef NO_PLUGINS
- } else if(macroCode == 0x03 && !isExtended)
- {
- // F0.F0.03.xx: Set plug dry/wet
- PLUGINDEX plug = (plugin != 0) ? plugin : GetBestPlugin(playState, nChn, PrioritiseChannel, EvenIfMuted);
- if(plug > 0 && plug <= MAX_MIXPLUGINS && param < 0x80)
- {
- plug--;
- if(IMixPlugin* pPlugin = m_MixPlugins[plug].pMixPlugin; pPlugin)
- {
- const float newRatio = (127 - param) / 127.0f;
- if(localOnly)
- playState.m_midiMacroEvaluationResults->pluginDryWetRatio[plug] = newRatio;
- else if(!isSmooth)
- pPlugin->SetDryRatio(newRatio);
- else
- pPlugin->SetDryRatio(CalculateSmoothParamChange(playState, m_MixPlugins[plug].fDryRatio, newRatio));
- }
- }
- } else if((macroCode & 0x80) || isExtended)
- {
- // F0.F0.{80|n}.xx / F0.F1.n.xx: Set VST effect parameter n to xx
- PLUGINDEX plug = (plugin != 0) ? plugin : GetBestPlugin(playState, nChn, PrioritiseChannel, EvenIfMuted);
- if(plug > 0 && plug <= MAX_MIXPLUGINS && param < 0x80)
- {
- plug--;
- if(IMixPlugin *pPlugin = m_MixPlugins[plug].pMixPlugin; pPlugin)
- {
- const PlugParamIndex plugParam = isExtended ? (0x80 + macroCode) : (macroCode & 0x7F);
- const PlugParamValue value = param / 127.0f;
- if(localOnly)
- playState.m_midiMacroEvaluationResults->pluginParameter[{plug, plugParam}] = value;
- else if(!isSmooth)
- pPlugin->SetParameter(plugParam, value);
- else
- pPlugin->SetParameter(plugParam, CalculateSmoothParamChange(playState, pPlugin->GetParameter(plugParam), value));
- }
- }
- #endif // NO_PLUGINS
- }
- } else if(!localOnly)
- {
- #ifndef NO_PLUGINS
- // Not an internal device. Pass on to appropriate plugin.
- const CHANNELINDEX plugChannel = (nChn < GetNumChannels()) ? nChn + 1 : chn.nMasterChn;
- if(plugChannel > 0 && plugChannel <= GetNumChannels()) // XXX do we need this? I guess it might be relevant for previewing notes in the pattern... Or when using this mechanism for volume/panning!
- {
- PLUGINDEX plug = 0;
- if(!chn.dwFlags[CHN_NOFX])
- {
- plug = (plugin != 0) ? plugin : GetBestPlugin(playState, nChn, PrioritiseChannel, EvenIfMuted);
- }
- if(plug > 0 && plug <= MAX_MIXPLUGINS)
- {
- if(IMixPlugin *pPlugin = m_MixPlugins[plug - 1].pMixPlugin; pPlugin != nullptr)
- {
- if(macro[0] == 0xF0)
- {
- pPlugin->MidiSysexSend(mpt::byte_cast<mpt::const_byte_span>(macro));
- } else
- {
- size_t len = std::min(static_cast<size_t>(MIDIEvents::GetEventLength(macro[0])), macro.size());
- uint32 curData = 0;
- memcpy(&curData, macro.data(), len);
- pPlugin->MidiSend(curData);
- }
- }
- }
- }
- #else
- MPT_UNREFERENCED_PARAMETER(plugin);
- #endif // NO_PLUGINS
- }
- }
- void CSoundFile::SendMIDINote(CHANNELINDEX chn, uint16 note, uint16 volume)
- {
- #ifndef NO_PLUGINS
- auto &channel = m_PlayState.Chn[chn];
- const ModInstrument *pIns = channel.pModInstrument;
- // instro sends to a midi chan
- if (pIns && pIns->HasValidMIDIChannel())
- {
- PLUGINDEX plug = pIns->nMixPlug;
- if(plug > 0 && plug <= MAX_MIXPLUGINS)
- {
- IMixPlugin *pPlug = m_MixPlugins[plug - 1].pMixPlugin;
- if (pPlug != nullptr)
- {
- pPlug->MidiCommand(*pIns, note, volume, chn);
- if(note < NOTE_MIN_SPECIAL)
- channel.nLeftVU = channel.nRightVU = 0xFF;
- }
- }
- }
- #endif // NO_PLUGINS
- }
- void CSoundFile::ProcessSampleOffset(ModChannel &chn, CHANNELINDEX nChn, const PlayState &playState) const
- {
- const ModCommand &m = chn.rowCommand;
- uint32 extendedRows = 0;
- SmpLength offset = CalculateXParam(playState.m_nPattern, playState.m_nRow, nChn, &extendedRows), highOffset = 0;
- if(!extendedRows)
- {
- // No X-param (normal behaviour)
- const bool isPercentageOffset = (m.volcmd == VOLCMD_OFFSET && m.vol == 0);
- offset <<= 8;
- if(offset)
- chn.oldOffset = offset;
- else if(m.volcmd != VOLCMD_OFFSET)
- offset = chn.oldOffset;
- if(!isPercentageOffset)
- highOffset = static_cast<SmpLength>(chn.nOldHiOffset) << 16;
- }
- if(m.volcmd == VOLCMD_OFFSET)
- {
- if(m.vol == 0)
- offset = Util::muldivr_unsigned(chn.nLength, offset, 256u << (8u * std::max(uint32(1), extendedRows))); // o00 + Oxx = Percentage Offset
- else if(m.vol <= std::size(ModSample().cues) && chn.pModSample != nullptr)
- offset += chn.pModSample->cues[m.vol - 1]; // Offset relative to cue point
- chn.oldOffset = offset;
- }
- SampleOffset(chn, offset + highOffset);
- }
- void CSoundFile::SampleOffset(ModChannel &chn, SmpLength param) const
- {
- // ST3 compatibility: Instrument-less note recalls previous note's offset
- // Test case: OxxMemory.s3m
- if(m_playBehaviour[kST3OffsetWithoutInstrument])
- chn.prevNoteOffset = 0;
-
- chn.prevNoteOffset += param;
- if(param >= chn.nLoopEnd && (GetType() & (MOD_TYPE_S3M | MOD_TYPE_MTM)) && chn.dwFlags[CHN_LOOP] && chn.nLoopEnd > 0)
- {
- // Offset wrap-around
- // Note that ST3 only does this in GUS mode. SoundBlaster stops the sample entirely instead.
- // Test case: OffsetLoopWraparound.s3m
- param = (param - chn.nLoopStart) % (chn.nLoopEnd - chn.nLoopStart) + chn.nLoopStart;
- }
- if(GetType() == MOD_TYPE_MDL && chn.dwFlags[CHN_16BIT])
- {
- // Digitrakker really uses byte offsets, not sample offsets. WTF!
- param /= 2u;
- }
- if(chn.rowCommand.IsNote() || m_playBehaviour[kApplyOffsetWithoutNote])
- {
- // IT compatibility: If this note is not mapped to a sample, ignore it.
- // Test case: empty_sample_offset.it
- if(chn.pModInstrument != nullptr && chn.rowCommand.IsNote())
- {
- SAMPLEINDEX smp = chn.pModInstrument->Keyboard[chn.rowCommand.note - NOTE_MIN];
- if(smp == 0 || smp > GetNumSamples())
- return;
- }
- if(m_SongFlags[SONG_PT_MODE])
- {
- // ProTracker compatbility: PT1/2-style funky 9xx offset command
- // Test case: ptoffset.mod
- chn.position.Set(chn.prevNoteOffset);
- chn.prevNoteOffset += param;
- } else
- {
- chn.position.Set(param);
- }
- if (chn.position.GetUInt() >= chn.nLength || (chn.dwFlags[CHN_LOOP] && chn.position.GetUInt() >= chn.nLoopEnd))
- {
- // Offset beyond sample size
- if(m_playBehaviour[kFT2ST3OffsetOutOfRange] || GetType() == MOD_TYPE_MTM)
- {
- // FT2 Compatibility: Don't play note if offset is beyond sample length
- // ST3 Compatibility: Don't play note if offset is beyond sample length (non-looped samples only)
- // Test cases: 3xx-no-old-samp.xm, OffsetPastSampleEnd.s3m
- chn.dwFlags.set(CHN_FASTVOLRAMP);
- chn.nPeriod = 0;
- } else if(!(GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2 | MOD_TYPE_MOD)))
- {
- // IT Compatibility: Offset
- if(m_playBehaviour[kITOffset])
- {
- if(m_SongFlags[SONG_ITOLDEFFECTS])
- chn.position.Set(chn.nLength); // Old FX: Clip to end of sample
- else
- chn.position.Set(0); // Reset to beginning of sample
- } else
- {
- chn.position.Set(chn.nLoopStart);
- if(m_SongFlags[SONG_ITOLDEFFECTS] && chn.nLength > 4)
- {
- chn.position.Set(chn.nLength - 2);
- }
- }
- } else if(GetType() == MOD_TYPE_MOD && chn.dwFlags[CHN_LOOP])
- {
- chn.position.Set(chn.nLoopStart);
- }
- }
- } else if ((param < chn.nLength) && (GetType() & (MOD_TYPE_MTM | MOD_TYPE_DMF | MOD_TYPE_MDL | MOD_TYPE_PLM)))
- {
- // Some trackers can also call offset effects without notes next to them...
- chn.position.Set(param);
- }
- }
- void CSoundFile::ReverseSampleOffset(ModChannel &chn, ModCommand::PARAM param) const
- {
- if(chn.pModSample != nullptr && chn.pModSample->nLength > 0)
- {
- chn.dwFlags.set(CHN_PINGPONGFLAG);
- chn.dwFlags.reset(CHN_LOOP);
- chn.nLength = chn.pModSample->nLength; // If there was a loop, extend sample to whole length.
- chn.position.Set((chn.nLength - 1) - std::min(SmpLength(param) << 8, chn.nLength - SmpLength(1)), 0);
- }
- }
- void CSoundFile::DigiBoosterSampleReverse(ModChannel &chn, ModCommand::PARAM param) const
- {
- if(chn.isFirstTick && chn.pModSample != nullptr && chn.pModSample->nLength > 0)
- {
- chn.dwFlags.set(CHN_PINGPONGFLAG);
- chn.nLength = chn.pModSample->nLength; // If there was a loop, extend sample to whole length.
- chn.position.Set(chn.nLength - 1, 0);
- chn.dwFlags.set(CHN_LOOP | CHN_PINGPONGLOOP, param > 0);
- if(param > 0)
- {
- chn.nLoopStart = 0;
- chn.nLoopEnd = chn.nLength;
- // TODO: When the sample starts playing in forward direction again, the loop should be updated to the normal sample loop.
- }
- }
- }
- void CSoundFile::HandleDigiSamplePlayDirection(PlayState &state, CHANNELINDEX chn) const
- {
- // Digi Booster mixes two channels into one Paula channel, and when a note is triggered on one of them it resets the reverse play flag on the other.
- if(GetType() == MOD_TYPE_DIGI)
- {
- state.Chn[chn].dwFlags.reset(CHN_PINGPONGFLAG);
- const CHANNELINDEX otherChn = chn ^ 1;
- if(otherChn < GetNumChannels())
- state.Chn[otherChn].dwFlags.reset(CHN_PINGPONGFLAG);
- }
- }
- void CSoundFile::RetrigNote(CHANNELINDEX nChn, int param, int offset)
- {
- // Retrig: bit 8 is set if it's the new XM retrig
- ModChannel &chn = m_PlayState.Chn[nChn];
- int retrigSpeed = param & 0x0F;
- uint8 retrigCount = chn.nRetrigCount;
- bool doRetrig = false;
- // IT compatibility 15. Retrigger
- if(m_playBehaviour[kITRetrigger])
- {
- if(m_PlayState.m_nTickCount == 0 && chn.rowCommand.note)
- {
- chn.nRetrigCount = param & 0x0F;
- } else if(!chn.nRetrigCount || !--chn.nRetrigCount)
- {
- chn.nRetrigCount = param & 0x0F;
- doRetrig = true;
- }
- } else if(m_playBehaviour[kFT2Retrigger] && (param & 0x100))
- {
- // Buggy-like-hell FT2 Rxy retrig!
- // Test case: retrig.xm
- if(m_SongFlags[SONG_FIRSTTICK])
- {
- // Here are some really stupid things FT2 does on the first tick.
- // Test case: RetrigTick0.xm
- if(chn.rowCommand.instr > 0 && chn.rowCommand.IsNoteOrEmpty())
- retrigCount = 1;
- if(chn.rowCommand.volcmd == VOLCMD_VOLUME && chn.rowCommand.vol != 0)
- {
- // I guess this condition simply checked if the volume byte was != 0 in FT2.
- chn.nRetrigCount = retrigCount;
- return;
- }
- }
- if(retrigCount >= retrigSpeed)
- {
- if(!m_SongFlags[SONG_FIRSTTICK] || !chn.rowCommand.IsNote())
- {
- doRetrig = true;
- retrigCount = 0;
- }
- }
- } else
- {
- // old routines
- if (GetType() & (MOD_TYPE_S3M|MOD_TYPE_IT|MOD_TYPE_MPT))
- {
- if(!retrigSpeed)
- retrigSpeed = 1;
- if(retrigCount && !(retrigCount % retrigSpeed))
- doRetrig = true;
- retrigCount++;
- } else if(GetType() == MOD_TYPE_MOD)
- {
- // ProTracker-style retrigger
- // Test case: PTRetrigger.mod
- const auto tick = m_PlayState.m_nTickCount % m_PlayState.m_nMusicSpeed;
- if(!tick && chn.rowCommand.IsNote())
- return;
- if(retrigSpeed && !(tick % retrigSpeed))
- doRetrig = true;
- } else if(GetType() == MOD_TYPE_MTM)
- {
- // In MultiTracker, E9x retriggers the last note at exactly the x-th tick of the row
- doRetrig = m_PlayState.m_nTickCount == static_cast<uint32>(param & 0x0F) && retrigSpeed != 0;
- } else
- {
- int realspeed = retrigSpeed;
- // FT2 bug: if a retrig (Rxy) occurs together with a volume command, the first retrig interval is increased by one tick
- if((param & 0x100) && (chn.rowCommand.volcmd == VOLCMD_VOLUME) && (chn.rowCommand.param & 0xF0))
- realspeed++;
- if(!m_SongFlags[SONG_FIRSTTICK] || (param & 0x100))
- {
- if(!realspeed)
- realspeed = 1;
- if(!(param & 0x100) && m_PlayState.m_nMusicSpeed && !(m_PlayState.m_nTickCount % realspeed))
- doRetrig = true;
- retrigCount++;
- } else if(GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2))
- retrigCount = 0;
- if (retrigCount >= realspeed)
- {
- if(m_PlayState.m_nTickCount || ((param & 0x100) && !chn.rowCommand.note))
- doRetrig = true;
- }
- if(m_playBehaviour[kFT2Retrigger] && param == 0)
- {
- // E90 = Retrig instantly, and only once
- doRetrig = (m_PlayState.m_nTickCount == 0);
- }
- }
- }
- // IT compatibility: If a sample is shorter than the retrig time (i.e. it stops before the retrig counter hits zero), it is not retriggered.
- // Test case: retrig-short.it
- if(chn.nLength == 0 && m_playBehaviour[kITShortSampleRetrig] && !chn.HasMIDIOutput())
- return;
- // ST3 compatibility: No retrig after Note Cut
- // Test case: RetrigAfterNoteCut.s3m
- if(m_playBehaviour[kST3RetrigAfterNoteCut] && !chn.nFadeOutVol)
- return;
- if(doRetrig)
- {
- uint32 dv = (param >> 4) & 0x0F;
- int vol = chn.nVolume;
- if(dv)
- {
- // FT2 compatibility: Retrig + volume will not change volume of retrigged notes
- if(!m_playBehaviour[kFT2Retrigger] || !(chn.rowCommand.volcmd == VOLCMD_VOLUME))
- {
- if(retrigTable1[dv])
- vol = (vol * retrigTable1[dv]) / 16;
- else
- vol += ((int)retrigTable2[dv]) * 4;
- }
- Limit(vol, 0, 256);
- chn.dwFlags.set(CHN_FASTVOLRAMP);
- }
- uint32 note = chn.nNewNote;
- int32 oldPeriod = chn.nPeriod;
- // ST3 doesn't retrigger OPL notes
- // Test case: RetrigSlide.s3m
- const bool oplRealRetrig = chn.dwFlags[CHN_ADLIB] && m_playBehaviour[kOPLRealRetrig];
- if(note >= NOTE_MIN && note <= NOTE_MAX && chn.nLength && (GetType() != MOD_TYPE_S3M || oplRealRetrig))
- CheckNNA(nChn, 0, note, true);
- bool resetEnv = false;
- if(GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2))
- {
- if(chn.rowCommand.instr && param < 0x100)
- {
- InstrumentChange(chn, chn.rowCommand.instr, false, false);
- resetEnv = true;
- }
- if(param < 0x100)
- resetEnv = true;
- }
- const bool fading = chn.dwFlags[CHN_NOTEFADE];
- const auto oldPrevNoteOffset = chn.prevNoteOffset;
- chn.prevNoteOffset = 0; // Retriggered notes should not use previous offset (test case: OxxMemoryWithRetrig.s3m)
- // IT compatibility: Really weird combination of envelopes and retrigger (see Storlek's q.it testcase)
- // Test cases: retrig.it, RetrigSlide.s3m
- const bool itS3Mstyle = m_playBehaviour[kITRetrigger] || (GetType() == MOD_TYPE_S3M && chn.nLength && !oplRealRetrig);
- NoteChange(chn, note, itS3Mstyle, resetEnv, false, nChn);
- if(!chn.rowCommand.instr)
- chn.prevNoteOffset = oldPrevNoteOffset;
- // XM compatibility: Prevent NoteChange from resetting the fade flag in case an instrument number + note-off is present.
- // Test case: RetrigFade.xm
- if(fading && GetType() == MOD_TYPE_XM)
- chn.dwFlags.set(CHN_NOTEFADE);
- chn.nVolume = vol;
- if(m_nInstruments)
- {
- chn.rowCommand.note = static_cast<ModCommand::NOTE>(note); // No retrig without note...
- #ifndef NO_PLUGINS
- ProcessMidiOut(nChn); //Send retrig to Midi
- #endif // NO_PLUGINS
- }
- if((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) && chn.rowCommand.note == NOTE_NONE && oldPeriod != 0)
- chn.nPeriod = oldPeriod;
- if(!(GetType() & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT)))
- retrigCount = 0;
- // IT compatibility: see previous IT compatibility comment =)
- if(itS3Mstyle)
- chn.position.Set(0);
- offset--;
- if(chn.pModSample != nullptr && offset >= 0 && offset <= static_cast<int>(std::size(chn.pModSample->cues)))
- {
- if(offset == 0)
- offset = chn.oldOffset;
- else
- offset = chn.oldOffset = chn.pModSample->cues[offset - 1];
- SampleOffset(chn, offset);
- }
- }
- // buggy-like-hell FT2 Rxy retrig!
- if(m_playBehaviour[kFT2Retrigger] && (param & 0x100))
- retrigCount++;
- // Now we can also store the retrig value for IT...
- if(!m_playBehaviour[kITRetrigger])
- chn.nRetrigCount = retrigCount;
- }
- // Execute a frequency slide on given channel.
- // Positive amounts increase the frequency, negative amounts decrease it.
- // The period or frequency that is read and written is in the period variable, chn.nPeriod is not touched.
- void CSoundFile::DoFreqSlide(ModChannel &chn, int32 &period, int32 amount, bool isTonePorta) const
- {
- if(!period || !amount)
- return;
- MPT_ASSERT(!chn.HasCustomTuning());
- if(GetType() == MOD_TYPE_669)
- {
- // Like other oldskool trackers, Composer 669 doesn't have linear slides...
- // But the slides are done in Hertz rather than periods, meaning that they
- // are more effective in the lower notes (rather than the higher notes).
- period += amount * 20;
- } else if(GetType() == MOD_TYPE_FAR)
- {
- period += (amount * 36318 / 1024);
- } else if(m_SongFlags[SONG_LINEARSLIDES] && GetType() != MOD_TYPE_XM)
- {
- // IT Linear slides
- const auto oldPeriod = period;
- uint32 n = std::abs(amount);
- LimitMax(n, 255u * 4u);
- // Note: IT ignores the lower 2 bits when abs(mount) > 16 (it either uses the fine *or* the regular table, not both)
- // This means that vibratos are slightly less accurate in this range than they could be.
- // Other code paths will *either* have an amount that's a multiple of 4 *or* it's less than 16.
- if(amount > 0)
- {
- if(n < 16)
- period = Util::muldivr(period, GetFineLinearSlideUpTable(this, n), 65536);
- else
- period = Util::muldivr(period, GetLinearSlideUpTable(this, n / 4u), 65536);
- } else
- {
- if(n < 16)
- period = Util::muldivr(period, GetFineLinearSlideDownTable(this, n), 65536);
- else
- period = Util::muldivr(period, GetLinearSlideDownTable(this, n / 4u), 65536);
- }
- if(period == oldPeriod)
- {
- const bool incPeriod = m_playBehaviour[kPeriodsAreHertz] == (amount > 0);
- if(incPeriod && period < Util::MaxValueOfType(period))
- period++;
- else if(!incPeriod && period > 1)
- period--;
- }
- } else if(!m_SongFlags[SONG_LINEARSLIDES] && m_playBehaviour[kPeriodsAreHertz])
- {
- // IT Amiga slides
- if(amount < 0)
- {
- // Go down
- period = mpt::saturate_cast<int32>(Util::mul32to64_unsigned(1712 * 8363, period) / (Util::mul32to64_unsigned(period, -amount) + 1712 * 8363));
- } else if(amount > 0)
- {
- // Go up
- const auto periodDiv = 1712 * 8363 - Util::mul32to64(period, amount);
- if(periodDiv <= 0)
- {
- if(isTonePorta)
- {
- period = int32_max;
- return;
- } else
- {
- period = 0;
- chn.nFadeOutVol = 0;
- chn.dwFlags.set(CHN_NOTEFADE | CHN_FASTVOLRAMP);
- }
- return;
- }
- period = mpt::saturate_cast<int32>(Util::mul32to64_unsigned(1712 * 8363, period) / periodDiv);
- }
- } else
- {
- period -= amount;
- }
- if(period < 1)
- {
- period = 1;
- if(GetType() == MOD_TYPE_S3M && !isTonePorta)
- {
- chn.nFadeOutVol = 0;
- chn.dwFlags.set(CHN_NOTEFADE | CHN_FASTVOLRAMP);
- }
- }
- }
- void CSoundFile::NoteCut(CHANNELINDEX nChn, uint32 nTick, bool cutSample)
- {
- if (m_PlayState.m_nTickCount == nTick)
- {
- ModChannel &chn = m_PlayState.Chn[nChn];
- if(cutSample)
- {
- chn.increment.Set(0);
- chn.nFadeOutVol = 0;
- chn.dwFlags.set(CHN_NOTEFADE);
- } else
- {
- chn.nVolume = 0;
- }
- chn.dwFlags.set(CHN_FASTVOLRAMP);
- // instro sends to a midi chan
- SendMIDINote(nChn, /*chn.nNote+*/NOTE_MAX_SPECIAL, 0);
-
- if(chn.dwFlags[CHN_ADLIB] && m_opl)
- {
- m_opl->NoteCut(nChn, false);
- }
- }
- }
- void CSoundFile::KeyOff(ModChannel &chn) const
- {
- const bool keyIsOn = !chn.dwFlags[CHN_KEYOFF];
- chn.dwFlags.set(CHN_KEYOFF);
- if(chn.pModInstrument != nullptr && !chn.VolEnv.flags[ENV_ENABLED])
- {
- chn.dwFlags.set(CHN_NOTEFADE);
- }
- if (!chn.nLength) return;
- if (chn.dwFlags[CHN_SUSTAINLOOP] && chn.pModSample && keyIsOn)
- {
- const ModSample *pSmp = chn.pModSample;
- if(pSmp->uFlags[CHN_LOOP])
- {
- if (pSmp->uFlags[CHN_PINGPONGLOOP])
- chn.dwFlags.set(CHN_PINGPONGLOOP);
- else
- chn.dwFlags.reset(CHN_PINGPONGLOOP | CHN_PINGPONGFLAG);
- chn.dwFlags.set(CHN_LOOP);
- chn.nLength = pSmp->nLength;
- chn.nLoopStart = pSmp->nLoopStart;
- chn.nLoopEnd = pSmp->nLoopEnd;
- if (chn.nLength > chn.nLoopEnd) chn.nLength = chn.nLoopEnd;
- if(chn.position.GetUInt() > chn.nLength)
- {
- // Test case: SusAfterLoop.it
- chn.position.Set(chn.nLoopStart + ((chn.position.GetInt() - chn.nLoopStart) % (chn.nLoopEnd - chn.nLoopStart)));
- }
- } else
- {
- chn.dwFlags.reset(CHN_LOOP | CHN_PINGPONGLOOP | CHN_PINGPONGFLAG);
- chn.nLength = pSmp->nLength;
- }
- }
- if (chn.pModInstrument)
- {
- const ModInstrument *pIns = chn.pModInstrument;
- if((pIns->VolEnv.dwFlags[ENV_LOOP] || (GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2 | MOD_TYPE_MDL))) && pIns->nFadeOut != 0)
- {
- chn.dwFlags.set(CHN_NOTEFADE);
- }
- if (pIns->VolEnv.nReleaseNode != ENV_RELEASE_NODE_UNSET && chn.VolEnv.nEnvValueAtReleaseJump == NOT_YET_RELEASED)
- {
- chn.VolEnv.nEnvValueAtReleaseJump = mpt::saturate_cast<int16>(pIns->VolEnv.GetValueFromPosition(chn.VolEnv.nEnvPosition, 256));
- chn.VolEnv.nEnvPosition = pIns->VolEnv[pIns->VolEnv.nReleaseNode].tick;
- }
- }
- }
- //////////////////////////////////////////////////////////
- // CSoundFile: Global Effects
- void CSoundFile::SetSpeed(PlayState &playState, uint32 param) const
- {
- #ifdef MODPLUG_TRACKER
- // FT2 appears to be decrementing the tick count before checking for zero,
- // so it effectively counts down 65536 ticks with speed = 0 (song speed is a 16-bit variable in FT2)
- if(GetType() == MOD_TYPE_XM && !param)
- {
- playState.m_nMusicSpeed = uint16_max;
- }
- #endif // MODPLUG_TRACKER
- if(param > 0) playState.m_nMusicSpeed = param;
- if(GetType() == MOD_TYPE_STM && param > 0)
- {
- playState.m_nMusicSpeed = std::max(param >> 4, uint32(1));
- playState.m_nMusicTempo = ConvertST2Tempo(static_cast<uint8>(param));
- }
- }
- // Convert a ST2 tempo byte to classic tempo and speed combination
- TEMPO CSoundFile::ConvertST2Tempo(uint8 tempo)
- {
- static constexpr uint8 ST2TempoFactor[] = { 140, 50, 25, 15, 10, 7, 6, 4, 3, 3, 2, 2, 2, 2, 1, 1 };
- static constexpr uint32 st2MixingRate = 23863; // Highest possible setting in ST2
- // This underflows at tempo 06...0F, and the resulting tick lengths depend on the mixing rate.
- // Note: ST2.3 uses the constant 50 below, earlier versions use 49 but they also play samples at a different speed.
- int32 samplesPerTick = st2MixingRate / (50 - ((ST2TempoFactor[tempo >> 4u] * (tempo & 0x0F)) >> 4u));
- if(samplesPerTick <= 0)
- samplesPerTick += 65536;
- return TEMPO().SetRaw(Util::muldivrfloor(st2MixingRate, 5 * TEMPO::fractFact, samplesPerTick * 2));
- }
- void CSoundFile::SetTempo(TEMPO param, bool setFromUI)
- {
- const CModSpecifications &specs = GetModSpecifications();
- // Anything lower than the minimum tempo is considered to be a tempo slide
- const TEMPO minTempo = (GetType() & (MOD_TYPE_MDL | MOD_TYPE_MED | MOD_TYPE_MOD)) ? TEMPO(1, 0) : TEMPO(32, 0);
- if(setFromUI)
- {
- // Set tempo from UI - ignore slide commands and such.
- m_PlayState.m_nMusicTempo = Clamp(param, specs.GetTempoMin(), specs.GetTempoMax());
- } else if(param >= minTempo && m_SongFlags[SONG_FIRSTTICK] == !m_playBehaviour[kMODTempoOnSecondTick])
- {
- // ProTracker sets the tempo after the first tick.
- // Note: The case of one tick per row is handled in ProcessRow() instead.
- // Test case: TempoChange.mod
- m_PlayState.m_nMusicTempo = std::min(param, specs.GetTempoMax());
- } else if(param < minTempo && !m_SongFlags[SONG_FIRSTTICK])
- {
- // Tempo Slide
- TEMPO tempDiff(param.GetInt() & 0x0F, 0);
- if((param.GetInt() & 0xF0) == 0x10)
- m_PlayState.m_nMusicTempo += tempDiff;
- else
- m_PlayState.m_nMusicTempo -= tempDiff;
- TEMPO tempoMin = specs.GetTempoMin(), tempoMax = specs.GetTempoMax();
- if(m_playBehaviour[kTempoClamp]) // clamp tempo correctly in compatible mode
- {
- tempoMax.Set(255);
- }
- Limit(m_PlayState.m_nMusicTempo, tempoMin, tempoMax);
- }
- }
- void CSoundFile::PatternLoop(PlayState &state, ModChannel &chn, ModCommand::PARAM param) const
- {
- if(m_playBehaviour[kST3NoMutedChannels] && chn.dwFlags[CHN_MUTE | CHN_SYNCMUTE])
- return; // not even effects are processed on muted S3M channels
- if(!param)
- {
- // Loop Start
- chn.nPatternLoop = state.m_nRow;
- return;
- }
- // Loop Repeat
- if(chn.nPatternLoopCount)
- {
- // There's a loop left
- chn.nPatternLoopCount--;
- if(!chn.nPatternLoopCount)
- {
- // IT compatibility 10. Pattern loops (+ same fix for S3M files)
- // When finishing a pattern loop, the next loop without a dedicated SB0 starts on the first row after the previous loop.
- if(m_playBehaviour[kITPatternLoopTargetReset] || (GetType() == MOD_TYPE_S3M))
- chn.nPatternLoop = state.m_nRow + 1;
- return;
- }
- } else
- {
- // First time we get into the loop => Set loop count.
- // IT compatibility 10. Pattern loops (+ same fix for XM / MOD / S3M files)
- if(!m_playBehaviour[kITFT2PatternLoop] && !(GetType() & (MOD_TYPE_MOD | MOD_TYPE_S3M)))
- {
- auto p = std::cbegin(state.Chn);
- for(CHANNELINDEX i = 0; i < GetNumChannels(); i++, p++)
- {
- // Loop on other channel
- if(p != &chn && p->nPatternLoopCount)
- return;
- }
- }
- chn.nPatternLoopCount = param;
- }
- state.m_nextPatStartRow = chn.nPatternLoop; // Nasty FT2 E60 bug emulation!
- const auto loopTarget = chn.nPatternLoop;
- if(loopTarget != ROWINDEX_INVALID)
- {
- // FT2 compatibility: E6x overwrites jump targets of Dxx effects that are located left of the E6x effect.
- // Test cases: PatLoop-Jumps.xm, PatLoop-Various.xm
- if(state.m_breakRow != ROWINDEX_INVALID && m_playBehaviour[kFT2PatternLoopWithJumps])
- state.m_breakRow = loopTarget;
- state.m_patLoopRow = loopTarget;
- // IT compatibility: SBx is prioritized over Position Jump (Bxx) effects that are located left of the SBx effect.
- // Test case: sbx-priority.it, LoopBreak.it
- if(m_playBehaviour[kITPatternLoopWithJumps])
- state.m_posJump = ORDERINDEX_INVALID;
- }
- if(GetType() == MOD_TYPE_S3M)
- {
- // ST3 doesn't have per-channel pattern loop memory, so spam all changes to other channels as well.
- for(CHANNELINDEX i = 0; i < GetNumChannels(); i++)
- {
- state.Chn[i].nPatternLoop = chn.nPatternLoop;
- state.Chn[i].nPatternLoopCount = chn.nPatternLoopCount;
- }
- }
- }
- void CSoundFile::GlobalVolSlide(ModCommand::PARAM param, uint8 &nOldGlobalVolSlide)
- {
- int32 nGlbSlide = 0;
- if (param) nOldGlobalVolSlide = param; else param = nOldGlobalVolSlide;
- if((GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2)))
- {
- // XM nibble priority
- if((param & 0xF0) != 0)
- {
- param &= 0xF0;
- } else
- {
- param &= 0x0F;
- }
- }
- if (((param & 0x0F) == 0x0F) && (param & 0xF0))
- {
- if(m_SongFlags[SONG_FIRSTTICK]) nGlbSlide = (param >> 4) * 2;
- } else
- if (((param & 0xF0) == 0xF0) && (param & 0x0F))
- {
- if(m_SongFlags[SONG_FIRSTTICK]) nGlbSlide = - (int)((param & 0x0F) * 2);
- } else
- {
- if(!m_SongFlags[SONG_FIRSTTICK])
- {
- if (param & 0xF0)
- {
- // IT compatibility: Ignore slide commands with both nibbles set.
- if(!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_IMF | MOD_TYPE_J2B | MOD_TYPE_MID | MOD_TYPE_AMS | MOD_TYPE_DBM)) || (param & 0x0F) == 0)
- nGlbSlide = (int)((param & 0xF0) >> 4) * 2;
- } else
- {
- nGlbSlide = -(int)((param & 0x0F) * 2);
- }
- }
- }
- if (nGlbSlide)
- {
- if(!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_IMF | MOD_TYPE_J2B | MOD_TYPE_MID | MOD_TYPE_AMS | MOD_TYPE_DBM))) nGlbSlide *= 2;
- nGlbSlide += m_PlayState.m_nGlobalVolume;
- Limit(nGlbSlide, 0, 256);
- m_PlayState.m_nGlobalVolume = nGlbSlide;
- }
- }
- //////////////////////////////////////////////////////
- // Note/Period/Frequency functions
- // Find lowest note which has same or lower period as a given period (i.e. the note has the same or higher frequency)
- uint32 CSoundFile::GetNoteFromPeriod(uint32 period, int32 nFineTune, uint32 nC5Speed) const
- {
- if(!period) return 0;
- if(m_playBehaviour[kFT2Periods])
- {
- // FT2's "RelocateTon" function actually rounds up and down, while GetNoteFromPeriod normally just truncates.
- nFineTune += 64;
- }
- // This essentially implements std::lower_bound, with the difference that we don't need an iterable container.
- uint32 minNote = NOTE_MIN, maxNote = NOTE_MAX, count = maxNote - minNote + 1;
- const bool periodIsFreq = PeriodsAreFrequencies();
- while(count > 0)
- {
- const uint32 step = count / 2, midNote = minNote + step;
- uint32 n = GetPeriodFromNote(midNote, nFineTune, nC5Speed);
- if((n > period && !periodIsFreq) || (n < period && periodIsFreq) || !n)
- {
- minNote = midNote + 1;
- count -= step + 1;
- } else
- {
- count = step;
- }
- }
- return minNote;
- }
- uint32 CSoundFile::GetPeriodFromNote(uint32 note, int32 nFineTune, uint32 nC5Speed) const
- {
- if (note == NOTE_NONE || (note >= NOTE_MIN_SPECIAL)) return 0;
- note -= NOTE_MIN;
- if(!UseFinetuneAndTranspose())
- {
- if(GetType() & (MOD_TYPE_MDL | MOD_TYPE_DTM))
- {
- // MDL uses non-linear slides, but their effectiveness does not depend on the middle-C frequency.
- return (FreqS3MTable[note % 12u] << 4) >> (note / 12);
- }
- if(!nC5Speed)
- nC5Speed = 8363;
- if(PeriodsAreFrequencies())
- {
- // Compute everything in Hertz rather than periods.
- uint32 freq = Util::muldiv_unsigned(nC5Speed, LinearSlideUpTable[(note % 12u) * 16u] << (note / 12u), 65536 << 5);
- LimitMax(freq, static_cast<uint32>(int32_max));
- return freq;
- } else if(m_SongFlags[SONG_LINEARSLIDES])
- {
- return (FreqS3MTable[note % 12u] << 5) >> (note / 12);
- } else
- {
- LimitMax(nC5Speed, uint32_max >> (note / 12u));
- //(a*b)/c
- return Util::muldiv_unsigned(8363, (FreqS3MTable[note % 12u] << 5), nC5Speed << (note / 12u));
- //8363 * freq[note%12] / nC5Speed * 2^(5-note/12)
- }
- } else if(GetType() & (MOD_TYPE_XM | MOD_TYPE_MTM))
- {
- if (note < 12) note = 12;
- note -= 12;
- if(GetType() == MOD_TYPE_MTM)
- {
- nFineTune *= 16;
- } else if(m_playBehaviour[kFT2FinetunePrecision])
- {
- // FT2 Compatibility: The lower three bits of the finetune are truncated.
- // Test case: Finetune-Precision.xm
- nFineTune &= ~7;
- }
- if(m_SongFlags[SONG_LINEARSLIDES])
- {
- int l = ((NOTE_MAX - note) << 6) - (nFineTune / 2);
- if (l < 1) l = 1;
- return static_cast<uint32>(l);
- } else
- {
- int finetune = nFineTune;
- uint32 rnote = (note % 12) << 3;
- uint32 roct = note / 12;
- int rfine = finetune / 16;
- int i = rnote + rfine + 8;
- Limit(i , 0, 103);
- uint32 per1 = XMPeriodTable[i];
- if(finetune < 0)
- {
- rfine--;
- finetune = -finetune;
- } else rfine++;
- i = rnote+rfine+8;
- if (i < 0) i = 0;
- if (i >= 104) i = 103;
- uint32 per2 = XMPeriodTable[i];
- rfine = finetune & 0x0F;
- per1 *= 16-rfine;
- per2 *= rfine;
- return ((per1 + per2) << 1) >> roct;
- }
- } else
- {
- nFineTune = XM2MODFineTune(nFineTune);
- if ((nFineTune) || (note < 24) || (note >= 24 + std::size(ProTrackerPeriodTable)))
- return (ProTrackerTunedPeriods[nFineTune * 12u + note % 12u] << 5) >> (note / 12u);
- else
- return (ProTrackerPeriodTable[note - 24] << 2);
- }
- }
- // Converts period value to sample frequency. Return value is fixed point, with FREQ_FRACBITS fractional bits.
- uint32 CSoundFile::GetFreqFromPeriod(uint32 period, uint32 c5speed, int32 nPeriodFrac) const
- {
- if (!period) return 0;
- if (GetType() & (MOD_TYPE_XM | MOD_TYPE_MTM))
- {
- if(m_playBehaviour[kFT2Periods])
- {
- // FT2 compatibility: Period is a 16-bit value in FT2, and it overflows happily.
- // Test case: FreqWraparound.xm
- period &= 0xFFFF;
- }
- if(m_SongFlags[SONG_LINEARSLIDES])
- {
- uint32 octave;
- if(m_playBehaviour[kFT2Periods])
- {
- // Under normal circumstances, this calculation returns the same values as the non-compatible one.
- // However, once the 12 octaves are exceeded (through portamento slides), the octave shift goes
- // crazy in FT2, meaning that the frequency wraps around randomly...
- // The entries in FT2's conversion table are four times as big, hence we have to do an additional shift by two bits.
- // Test case: FreqWraparound.xm
- // 12 octaves * (12 * 64) LUT entries = 9216, add 767 for rounding
- uint32 div = ((9216u + 767u - period) / 768);
- octave = ((14 - div) & 0x1F);
- } else
- {
- octave = (period / 768) + 2;
- }
- return (XMLinearTable[period % 768] << (FREQ_FRACBITS + 2)) >> octave;
- } else
- {
- if(!period) period = 1;
- return ((8363 * 1712L) << FREQ_FRACBITS) / period;
- }
- } else if(UseFinetuneAndTranspose())
- {
- return ((3546895L * 4) << FREQ_FRACBITS) / period;
- } else if(GetType() == MOD_TYPE_669)
- {
- // We only really use c5speed for the finetune pattern command. All samples in 669 files have the same middle-C speed (imported as 8363 Hz).
- return (period + c5speed - 8363) << FREQ_FRACBITS;
- } else if(GetType() & (MOD_TYPE_MDL | MOD_TYPE_DTM))
- {
- LimitMax(period, Util::MaxValueOfType(period) >> 8);
- if (!c5speed) c5speed = 8363;
- return Util::muldiv_unsigned(c5speed, (1712L << 7) << FREQ_FRACBITS, (period << 8) + nPeriodFrac);
- } else
- {
- LimitMax(period, Util::MaxValueOfType(period) >> 8);
- if(PeriodsAreFrequencies())
- {
- // Input is already a frequency in Hertz, not a period.
- static_assert(FREQ_FRACBITS <= 8, "Check this shift operator");
- return uint32(((uint64(period) << 8) + nPeriodFrac) >> (8 - FREQ_FRACBITS));
- } else if(m_SongFlags[SONG_LINEARSLIDES])
- {
- if(!c5speed)
- c5speed = 8363;
- return Util::muldiv_unsigned(c5speed, (1712L << 8) << FREQ_FRACBITS, (period << 8) + nPeriodFrac);
- } else
- {
- return Util::muldiv_unsigned(8363, (1712L << 8) << FREQ_FRACBITS, (period << 8) + nPeriodFrac);
- }
- }
- }
- PLUGINDEX CSoundFile::GetBestPlugin(const PlayState &playState, CHANNELINDEX nChn, PluginPriority priority, PluginMutePriority respectMutes) const
- {
- if (nChn >= MAX_CHANNELS) //Check valid channel number
- {
- return 0;
- }
- //Define search source order
- PLUGINDEX plugin = 0;
- switch (priority)
- {
- case ChannelOnly:
- plugin = GetChannelPlugin(playState, nChn, respectMutes);
- break;
- case InstrumentOnly:
- plugin = GetActiveInstrumentPlugin(playState.Chn[nChn], respectMutes);
- break;
- case PrioritiseInstrument:
- plugin = GetActiveInstrumentPlugin(playState.Chn[nChn], respectMutes);
- if(!plugin || plugin > MAX_MIXPLUGINS)
- {
- plugin = GetChannelPlugin(playState, nChn, respectMutes);
- }
- break;
- case PrioritiseChannel:
- plugin = GetChannelPlugin(playState, nChn, respectMutes);
- if(!plugin || plugin > MAX_MIXPLUGINS)
- {
- plugin = GetActiveInstrumentPlugin(playState.Chn[nChn], respectMutes);
- }
- break;
- }
- return plugin; // 0 Means no plugin found.
- }
- PLUGINDEX CSoundFile::GetChannelPlugin(const PlayState &playState, CHANNELINDEX nChn, PluginMutePriority respectMutes) const
- {
- const ModChannel &channel = playState.Chn[nChn];
- PLUGINDEX plugin;
- if((respectMutes == RespectMutes && channel.dwFlags[CHN_MUTE | CHN_SYNCMUTE]) || channel.dwFlags[CHN_NOFX])
- {
- plugin = 0;
- } else
- {
- // If it looks like this is an NNA channel, we need to find the master channel.
- // This ensures we pick up the right ChnSettings.
- if(channel.nMasterChn > 0)
- {
- nChn = channel.nMasterChn - 1;
- }
- if(nChn < MAX_BASECHANNELS)
- {
- plugin = ChnSettings[nChn].nMixPlugin;
- } else
- {
- plugin = 0;
- }
- }
- return plugin;
- }
- PLUGINDEX CSoundFile::GetActiveInstrumentPlugin(const ModChannel &chn, PluginMutePriority respectMutes)
- {
- // Unlike channel settings, pModInstrument is copied from the original chan to the NNA chan,
- // so we don't need to worry about finding the master chan.
- PLUGINDEX plug = 0;
- if(chn.pModInstrument != nullptr)
- {
- // TODO this looks fishy. Shouldn't it check the mute status of the instrument itself?!
- if(respectMutes == RespectMutes && chn.pModSample && chn.pModSample->uFlags[CHN_MUTE])
- {
- plug = 0;
- } else
- {
- plug = chn.pModInstrument->nMixPlug;
- }
- }
- return plug;
- }
- // Retrieve the plugin that is associated with the channel's current instrument.
- // No plugin is returned if the channel is muted or if the instrument doesn't have a MIDI channel set up,
- // As this is meant to be used with instrument plugins.
- IMixPlugin *CSoundFile::GetChannelInstrumentPlugin(const ModChannel &chn) const
- {
- #ifndef NO_PLUGINS
- if(chn.dwFlags[CHN_MUTE | CHN_SYNCMUTE])
- {
- // Don't process portamento on muted channels. Note that this might have a side-effect
- // on other channels which trigger notes on the same MIDI channel of the same plugin,
- // as those won't be pitch-bent anymore.
- return nullptr;
- }
- if(chn.HasMIDIOutput())
- {
- const ModInstrument *pIns = chn.pModInstrument;
- // Instrument sends to a MIDI channel
- if(pIns->nMixPlug != 0 && pIns->nMixPlug <= MAX_MIXPLUGINS)
- {
- return m_MixPlugins[pIns->nMixPlug - 1].pMixPlugin;
- }
- }
- #else
- MPT_UNREFERENCED_PARAMETER(chn);
- #endif // NO_PLUGINS
- return nullptr;
- }
- #ifdef MODPLUG_TRACKER
- void CSoundFile::HandlePatternTransitionEvents()
- {
- // MPT sequence override
- if(m_PlayState.m_nSeqOverride != ORDERINDEX_INVALID && m_PlayState.m_nSeqOverride < Order().size())
- {
- if(m_SongFlags[SONG_PATTERNLOOP])
- {
- m_PlayState.m_nPattern = Order()[m_PlayState.m_nSeqOverride];
- }
- m_PlayState.m_nCurrentOrder = m_PlayState.m_nSeqOverride;
- m_PlayState.m_nSeqOverride = ORDERINDEX_INVALID;
- }
- // Channel mutes
- for (CHANNELINDEX chan = 0; chan < GetNumChannels(); chan++)
- {
- if (m_bChannelMuteTogglePending[chan])
- {
- if(GetpModDoc())
- {
- GetpModDoc()->MuteChannel(chan, !GetpModDoc()->IsChannelMuted(chan));
- }
- m_bChannelMuteTogglePending[chan] = false;
- }
- }
- }
- #endif // MODPLUG_TRACKER
- // Update time signatures (global or pattern-specific). Don't forget to call this when changing the RPB/RPM settings anywhere!
- void CSoundFile::UpdateTimeSignature()
- {
- if(!Patterns.IsValidIndex(m_PlayState.m_nPattern) || !Patterns[m_PlayState.m_nPattern].GetOverrideSignature())
- {
- m_PlayState.m_nCurrentRowsPerBeat = m_nDefaultRowsPerBeat;
- m_PlayState.m_nCurrentRowsPerMeasure = m_nDefaultRowsPerMeasure;
- } else
- {
- m_PlayState.m_nCurrentRowsPerBeat = Patterns[m_PlayState.m_nPattern].GetRowsPerBeat();
- m_PlayState.m_nCurrentRowsPerMeasure = Patterns[m_PlayState.m_nPattern].GetRowsPerMeasure();
- }
- }
- void CSoundFile::PortamentoMPT(ModChannel &chn, int param)
- {
- //Behavior: Modifies portamento by param-steps on every tick.
- //Note that step meaning depends on tuning.
- chn.m_PortamentoFineSteps += param;
- chn.m_CalculateFreq = true;
- }
- void CSoundFile::PortamentoFineMPT(ModChannel &chn, int param)
- {
- //Behavior: Divides portamento change between ticks/row. For example
- //if Ticks/row == 6, and param == +-6, portamento goes up/down by one tuning-dependent
- //fine step every tick.
- if(m_PlayState.m_nTickCount == 0)
- chn.nOldFinePortaUpDown = 0;
- const int tickParam = static_cast<int>((m_PlayState.m_nTickCount + 1.0) * param / m_PlayState.m_nMusicSpeed);
- chn.m_PortamentoFineSteps += (param >= 0) ? tickParam - chn.nOldFinePortaUpDown : tickParam + chn.nOldFinePortaUpDown;
- if(m_PlayState.m_nTickCount + 1 == m_PlayState.m_nMusicSpeed)
- chn.nOldFinePortaUpDown = static_cast<int8>(std::abs(param));
- else
- chn.nOldFinePortaUpDown = static_cast<int8>(std::abs(tickParam));
- chn.m_CalculateFreq = true;
- }
- void CSoundFile::PortamentoExtraFineMPT(ModChannel &chn, int param)
- {
- // This kinda behaves like regular fine portamento.
- // It changes the pitch by n finetune steps on the first tick.
- if(chn.isFirstTick)
- {
- chn.m_PortamentoFineSteps += param;
- chn.m_CalculateFreq = true;
- }
- }
- OPENMPT_NAMESPACE_END
|