diff --git a/.gitignore b/.gitignore index 2a17dc5..c1c98b7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.log *.swp *.nes +*.prof diff --git a/app/Main.hs b/app/Main.hs index cfcc63b..b15c07c 100644 --- a/app/Main.hs +++ b/app/Main.hs @@ -1,7 +1,12 @@ +{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} + module Main (main) where import Control.Monad +import Data.IORef +import qualified Data.Vector.Storable.Mutable as V import Events +import Nes.APU.State.Filter.Constants import Nes.Bus import Nes.Bus.Monad (runBusM) import Nes.CPU.Interpreter @@ -15,6 +20,12 @@ import SDL.Internal.Types import qualified SDL.Raw as Raw import System.Environment +vectorSize :: Int +vectorSize = 4 * sampleCount + +sampleCount :: Int +sampleCount = 1024 + main :: IO () main = do romPath <- do @@ -25,7 +36,10 @@ main = do rom <- do res <- fromFile romPath either fail return res + vectorCursor <- newIORef 0 + sampleVector <- V.new vectorSize initializeAll + let windowConfig = defaultWindow { windowInitialSize = @@ -34,23 +48,48 @@ main = do (240 * 3) , windowPosition = Centered } + (device, _) <- + openAudioDevice + OpenDeviceSpec + { SDL.openDeviceFreq = Mandate $ floor defaultOutputRate + , SDL.openDeviceFormat = Mandate FloatingLEAudio + , SDL.openDeviceChannels = Mandate Mono + , SDL.openDeviceSamples = fromIntegral sampleCount + , SDL.openDeviceCallback = audioCallback sampleVector vectorCursor + , SDL.openDeviceUsage = ForPlayback + , SDL.openDeviceName = Nothing + } window <- createWindow "FuNes" windowConfig renderer@(Renderer rendererPtr) <- createRenderer window (-1) defaultRenderer - _ <- setHintWithPriority NormalPriority HintRenderVSync DisableVSync + -- _ <- setHintWithPriority NormalPriority HintRenderVSync DisableVSync _ <- Raw.renderSetScale rendererPtr 3 3 texture <- createTexture renderer RGB24 TextureAccessTarget (V2 256 240) + setAudioDevicePlaybackState device Play frame <- newFrameState - bus <- newBus rom (onDrawFrame frame texture renderer) tickCallback + bus <- + newBus + rom + (onDrawFrame frame texture renderer) + (sampleCallback sampleVector vectorCursor) + tickCallback void $ runProgram bus (pure ()) + closeAudioDevice device destroyRenderer renderer tickCallback :: Double -> Int -> IO (Double, Int) tickCallback lastSleepTime_ ticks_ = return (lastSleepTime_, ticks_) +sampleCallback :: V.IOVector Float -> IORef Int -> Float -> IO () +sampleCallback vec cursorRef sample = do + cursor <- readIORef cursorRef + when (cursor < vectorSize) $ do + V.write vec cursor sample + writeIORef cursorRef (cursor + 1) + -- !currentTime <- getCPUTimeUs -- let !totalTickDurationUs = tickDurationUs * fromIntegral ticks_ -- !deltaTimeUs = currentTime - lastSleepTime @@ -76,6 +115,25 @@ tickCallback lastSleepTime_ ticks_ = return (lastSleepTime_, ticks_) -- -- Frequency in Hz -- cpuFrequency = 1.789773 * 1000000 +audioCallback :: V.IOVector Float -> IORef Int -> AudioFormat sampleType -> V.IOVector sampleType -> IO () +audioCallback samples cursorRef fmt buffer = case fmt of + FloatingLEAudio -> do + cursor <- readIORef cursorRef + let bufferLen = V.length buffer + nToCopy = min bufferLen cursor + when (cursor < bufferLen) $ do + V.set buffer 0 + V.copy (V.slice 0 nToCopy buffer) (V.slice 0 nToCopy samples) + -- If more samples are ready + if cursor > bufferLen + then do + let toShift = cursor - bufferLen + V.unsafeCopy (V.slice 0 toShift samples) (V.slice (cursor - 1) toShift samples) + writeIORef cursorRef toShift + else + writeIORef cursorRef 0 + _ -> error "Unsupported audio format" + onDrawFrame :: FrameState -> Texture -> Renderer -> Bus -> IO Bus onDrawFrame frame texture renderer bus = do bs <- runRender (render bus R.>> toSDL2ByteString) frame diff --git a/examples/Snake.hs b/examples/Snake.hs index 5da1a87..de50a51 100644 --- a/examples/Snake.hs +++ b/examples/Snake.hs @@ -48,7 +48,7 @@ main = do texture <- createTexture renderer RGB24 TextureAccessTarget (V2 32 32) frame <- newArray @IOUArray (0, frameSize) (0 :: Word8) let cpuState = newCPUState{programCounter = programOffset} - bus <- newBus unsafeEmptyRom pure (\a b -> pure (a, b)) + bus <- newBus unsafeEmptyRom pure (\_ -> pure ()) (\a b -> pure (a, b)) loadProgramToMemory gameCode bus _ <- runProgram' cpuState bus (callback frame texture renderer) destroyRenderer renderer diff --git a/funes.cabal b/funes.cabal index 1d34038..514ad7a 100644 --- a/funes.cabal +++ b/funes.cabal @@ -24,9 +24,34 @@ source-repository head library exposed-modules: + Nes.APU.BusInterface + Nes.APU.BusInterface.DMC + Nes.APU.BusInterface.FrameCounter + Nes.APU.BusInterface.Noise + Nes.APU.BusInterface.Pulse + Nes.APU.BusInterface.Status + Nes.APU.BusInterface.Triangle + Nes.APU.Mixer + Nes.APU.Monad + Nes.APU.State + Nes.APU.State.DMC + Nes.APU.State.Envelope + Nes.APU.State.Filter.Chain + Nes.APU.State.Filter.Class + Nes.APU.State.Filter.Constants + Nes.APU.State.Filter.Fir + Nes.APU.State.Filter.Iir + Nes.APU.State.Filter.Sampled + Nes.APU.State.FrameCounter + Nes.APU.State.LengthCounter + Nes.APU.State.Noise + Nes.APU.State.Pulse + Nes.APU.State.Triangle + Nes.APU.Tick Nes.Bus Nes.Bus.Constants Nes.Bus.Monad + Nes.Bus.SideEffect Nes.Controller Nes.CPU.Instructions.Access Nes.CPU.Instructions.Addressing @@ -69,6 +94,7 @@ library hs-source-dirs: src default-extensions: + Strict BinaryLiterals LambdaCase GeneralizedNewtypeDeriving @@ -79,7 +105,7 @@ library DeriveFunctor DataKinds QualifiedDo - ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -O2 + ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -O3 build-depends: array , base >=4.7 && <5 @@ -98,6 +124,7 @@ executable fake-snake hs-source-dirs: examples default-extensions: + Strict BinaryLiterals LambdaCase GeneralizedNewtypeDeriving @@ -108,7 +135,7 @@ executable fake-snake DeriveFunctor DataKinds QualifiedDo - ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -O2 -threaded -rtsopts -with-rtsopts=-N + ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -O3 -threaded -rtsopts -with-rtsopts=-N build-depends: array , base >=4.7 && <5 @@ -131,6 +158,7 @@ executable funes-exe hs-source-dirs: app default-extensions: + Strict BinaryLiterals LambdaCase GeneralizedNewtypeDeriving @@ -141,7 +169,7 @@ executable funes-exe DeriveFunctor DataKinds QualifiedDo - ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -O2 -threaded -rtsopts -with-rtsopts=-N + ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -O3 -threaded -rtsopts -with-rtsopts=-N build-depends: array , base >=4.7 && <5 @@ -163,6 +191,7 @@ test-suite nestest hs-source-dirs: test/nestest default-extensions: + Strict BinaryLiterals LambdaCase GeneralizedNewtypeDeriving @@ -173,7 +202,7 @@ test-suite nestest DeriveFunctor DataKinds QualifiedDo - ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -O2 -threaded -rtsopts -with-rtsopts=-N + ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -O3 -threaded -rtsopts -with-rtsopts=-N build-depends: array , base >=4.7 && <5 @@ -210,6 +239,7 @@ test-suite unit hs-source-dirs: test/unit default-extensions: + Strict BinaryLiterals LambdaCase GeneralizedNewtypeDeriving @@ -220,7 +250,7 @@ test-suite unit DeriveFunctor DataKinds QualifiedDo - ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -O2 -threaded -rtsopts -with-rtsopts=-N + ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -O3 -threaded -rtsopts -with-rtsopts=-N build-depends: array , base >=4.7 && <5 diff --git a/package.yaml b/package.yaml index ab47dc8..80ae1ac 100644 --- a/package.yaml +++ b/package.yaml @@ -19,6 +19,7 @@ dependencies: - vector default-extensions: + - Strict - BinaryLiterals - LambdaCase - GeneralizedNewtypeDeriving @@ -40,7 +41,7 @@ ghc-options: - -Wmissing-home-modules - -Wpartial-fields - -Wredundant-constraints - - -O2 + - -O3 library: source-dirs: src diff --git a/src/Nes/APU/BusInterface.hs b/src/Nes/APU/BusInterface.hs new file mode 100644 index 0000000..21359ab --- /dev/null +++ b/src/Nes/APU/BusInterface.hs @@ -0,0 +1,53 @@ +module Nes.APU.BusInterface ( + writeToAPU, + readFromAPU, +) where + +import Nes.APU.BusInterface.DMC +import Nes.APU.BusInterface.FrameCounter +import Nes.APU.BusInterface.Noise +import Nes.APU.BusInterface.Pulse +import Nes.APU.BusInterface.Status +import Nes.APU.BusInterface.Triangle +import Nes.APU.Monad +import Nes.Memory (Addr, Byte (..)) + +{-# INLINE writeToAPU #-} +writeToAPU :: Addr -> Byte -> APU r () +writeToAPU addr = case addr of + -- Pulse 1 + 0x4000 -> write4000 + 0x4001 -> write4001 + 0x4002 -> write4002 + 0x4003 -> write4003 + -- Pulse 2 + 0x4004 -> write4004 + 0x4005 -> write4005 + 0x4006 -> write4006 + 0x4007 -> write4007 + -- Triangle + 0x4008 -> write4008 + 0x400A -> write400A + 0x400B -> write400B + -- Noise + 0x400C -> write400C + 0x400E -> write400E + 0x400F -> write400F + -- DMC + 0x4010 -> write4010 + 0x4011 -> write4011 + 0x4012 -> write4012 + 0x4013 -> write4013 + -- Status + 0x4015 -> write4015 + -- Frame Counter + 0x4017 -> write4017 + _ -> const (return ()) + +{-# INLINE readFromAPU #-} +readFromAPU :: Addr -> APU r (Maybe Byte) +readFromAPU = \case + -- TODO Not open bus + -- TODO Bit 5 is open bus. + 0x4015 -> Just <$> read4015 + _ -> return Nothing diff --git a/src/Nes/APU/BusInterface/DMC.hs b/src/Nes/APU/BusInterface/DMC.hs new file mode 100644 index 0000000..921a5a4 --- /dev/null +++ b/src/Nes/APU/BusInterface/DMC.hs @@ -0,0 +1,40 @@ +module Nes.APU.BusInterface.DMC (write4010, write4011, write4012, write4013) where + +import Data.Bits +import Nes.APU.Monad +import Nes.APU.State (modifyDMC) +import Nes.APU.State.DMC +import Nes.Memory + +{-# INLINE write4010 #-} +write4010 :: Byte -> APU r () +write4010 byte = do + let irq = byte `testBit` 7 + loop = byte `testBit` 6 + rateIdx = byteToInt $ byte .&. 0b1111 + rate = getPeriodValue rateIdx + modifyAPUState $ modifyDMC $ \dmc -> + dmc + { irqEnabledFlag = irq + , loopFlag = loop + , period = rate + } + +{-# INLINE write4011 #-} +write4011 :: Byte -> APU r () +write4011 byte = do + let directLoad = byteToInt $ byte .&. 0b1111111 + -- TODO If the timer is outputting a clock at the same time, the output level is occasionally not changed properly. + modifyAPUState $ modifyDMC $ \dmc -> dmc{outputLevel = directLoad} + +{-# INLINE write4012 #-} +write4012 :: Byte -> APU r () +write4012 byte = do + let sampleAddr = 0xC000 + (byteToAddr byte * 64) + modifyAPUState $ modifyDMC $ \dmc -> dmc{sampleOgAddr = sampleAddr} + +{-# INLINE write4013 #-} +write4013 :: Byte -> APU r () +write4013 byte = do + let sampleLength = (byteToInt byte * 16) + 1 + modifyAPUState $ modifyDMC $ \dmc -> dmc{sampleOgLength = sampleLength} diff --git a/src/Nes/APU/BusInterface/FrameCounter.hs b/src/Nes/APU/BusInterface/FrameCounter.hs new file mode 100644 index 0000000..638aaa0 --- /dev/null +++ b/src/Nes/APU/BusInterface/FrameCounter.hs @@ -0,0 +1,24 @@ +module Nes.APU.BusInterface.FrameCounter (write4017) where + +import Control.Monad +import Data.Bits +import Nes.APU.Monad +import Nes.APU.State +import Nes.APU.State.FrameCounter +import Nes.APU.Tick +import Nes.Memory + +-- | Callback when a byte is written to 0x4017 through the Bus +{-# INLINE write4017 #-} +write4017 :: Byte -> APU r () +write4017 byte = do + c <- withAPUState Nes.APU.State.cycle + let seqMode = sequenceModeFromBool $ byte `testBit` 7 + inhibit = byte `testBit` 6 + delay = if even c then 4 else 3 + modifyAPUState $ + modifyFrameCounter $ + \fc -> fc{sequenceMode = seqMode, inhibitInterrupt = inhibit, delayedWriteSideEffectCycle = Just delay} + -- If the mode flag is set, then both "quarter frame" and "half frame" signals are also generated + when inhibit $ do + setFrameInterruptFlag False diff --git a/src/Nes/APU/BusInterface/Noise.hs b/src/Nes/APU/BusInterface/Noise.hs new file mode 100644 index 0000000..65fbf2b --- /dev/null +++ b/src/Nes/APU/BusInterface/Noise.hs @@ -0,0 +1,38 @@ +module Nes.APU.BusInterface.Noise (write400C, write400E, write400F) where + +import Data.Bits +import Nes.APU.Monad +import Nes.APU.State +import Nes.APU.State.Envelope +import Nes.APU.State.LengthCounter +import Nes.APU.State.Noise +import Nes.Memory + +{-# INLINE write400C #-} +write400C :: Byte -> APU r () +write400C byte = do + let haltLC = byte `testBit` 5 + constVol = byte `testBit` 4 + vol = byte .&. 0b1111 + modifyAPUState $ + modifyNoise $ + withLengthCounter + (\lc -> lc{isHalted = haltLC}) + . withEnvelope + (\e -> e{constantVolume = byteToInt vol, useConstantVolume = constVol, loopFlag = haltLC}) + +{-# INLINE write400E #-} +write400E :: Byte -> APU r () +write400E byte = do + let modeFlag = byte `testBit` 7 + periodIndex = byteToInt $ byte .&. 0b1111 + modifyAPUState $ modifyNoise $ \t -> t{period = getPeriodValue periodIndex, useBit6ForFeedback = modeFlag} + +{-# INLINE write400F #-} +write400F :: Byte -> APU r () +write400F byte = do + let newLCLoad = byteToInt $ byte `shiftR` 3 + modifyAPUState $ + modifyNoise $ + withLengthCounter (loadLengthCounter newLCLoad) + . withEnvelope (\e -> e{startFlag = True}) diff --git a/src/Nes/APU/BusInterface/Pulse.hs b/src/Nes/APU/BusInterface/Pulse.hs new file mode 100644 index 0000000..12d3f7b --- /dev/null +++ b/src/Nes/APU/BusInterface/Pulse.hs @@ -0,0 +1,114 @@ +module Nes.APU.BusInterface.Pulse ( + -- * Pulse 1 + write4000, + write4001, + write4002, + write4003, + + -- * Pulse 2 + write4004, + write4005, + write4006, + write4007, +) where + +import Data.Bits +import Nes.APU.Monad +import Nes.APU.State +import Nes.APU.State.Envelope +import Nes.APU.State.LengthCounter +import Nes.APU.State.Pulse +import Nes.Memory + +{-# INLINE write4000 #-} +write4000 :: Byte -> APU r () +write4000 = writePulseFirstByte modifyPulse1 + +{-# INLINE write4004 #-} +write4004 :: Byte -> APU r () +write4004 = writePulseFirstByte modifyPulse2 + +{-# INLINE writePulseFirstByte #-} +writePulseFirstByte :: ((Pulse -> Pulse) -> APUState -> APUState) -> Byte -> APU r () +writePulseFirstByte setter byte = do + let duty = byte `shiftR` 6 + haltLC = byte `testBit` 5 + constVol = byte `testBit` 4 + vol = byte .&. 0b1111 + modifyAPUState $ setter $ \p -> + withEnvelope + ( \e -> + e + { constantVolume = byteToInt vol + , useConstantVolume = constVol + , loopFlag = haltLC + } + ) + $ withLengthCounter (\lc -> lc{isHalted = haltLC}) + $ p{dutyIndex = fromIntegral $ unByte duty} + +{-# INLINE write4001 #-} +write4001 :: Byte -> APU r () +write4001 = writePulseSecondByte modifyPulse1 + +{-# INLINE write4005 #-} +write4005 :: Byte -> APU r () +write4005 = writePulseSecondByte modifyPulse2 + +{-# INLINE writePulseSecondByte #-} +writePulseSecondByte :: ((Pulse -> Pulse) -> APUState -> APUState) -> Byte -> APU r () +writePulseSecondByte setter byte = do + let enabledFlag = byte `testBit` 7 + divPeriod = 1 + ((byte `shiftR` 4) .&. 0b111) + negateFlag = byte `testBit` 3 + shiftC = byte .&. 0b111 + sweepIsEnabled = enabledFlag && shiftC > 0 + modifyAPUState $ + setter $ + updateTargetPeriod + . modifySweep + ( \s -> + s + { reloadFlag = True + , enabled = sweepIsEnabled + , dividerPeriod = byteToInt divPeriod + , negateDelta = negateFlag + , shiftCount = byteToInt shiftC + } + ) + +{-# INLINE write4002 #-} +write4002 :: Byte -> APU r () +write4002 = writePulseThirdByte modifyPulse1 + +{-# INLINE write4006 #-} +write4006 :: Byte -> APU r () +write4006 = writePulseThirdByte modifyPulse2 + +{-# INLINE writePulseThirdByte #-} +writePulseThirdByte :: ((Pulse -> Pulse) -> APUState -> APUState) -> Byte -> APU r () +writePulseThirdByte setter byte = modifyAPUState $ setter $ \p -> + let newPeriod = (period p .&. 0b11100000000) .|. byteToInt byte + in updateTargetPeriod $ p{period = newPeriod} + +{-# INLINE write4003 #-} +write4003 :: Byte -> APU r () +write4003 = writePulseFourthByte modifyPulse1 + +{-# INLINE write4007 #-} +write4007 :: Byte -> APU r () +write4007 = writePulseFourthByte modifyPulse2 + +{-# INLINE writePulseFourthByte #-} +writePulseFourthByte :: ((Pulse -> Pulse) -> APUState -> APUState) -> Byte -> APU r () +writePulseFourthByte setter byte = modifyAPUState $ setter $ \p -> + let newPeriod = ((byteToInt byte .&. 0b111) `shiftL` 8) .|. (period p .&. 0b11111111) + newLCLoad = byteToInt byte `shiftR` 3 + in updateTargetPeriod $ + withEnvelope (\e -> e{startFlag = True}) $ + withLengthCounter + (loadLengthCounter newLCLoad) + p + { period = newPeriod + , dutyStep = 0 + } diff --git a/src/Nes/APU/BusInterface/Status.hs b/src/Nes/APU/BusInterface/Status.hs new file mode 100644 index 0000000..48aa36a --- /dev/null +++ b/src/Nes/APU/BusInterface/Status.hs @@ -0,0 +1,65 @@ +module Nes.APU.BusInterface.Status (write4015, read4015) where + +import Control.Monad +import Data.Bits +import Nes.APU.Monad +import Nes.APU.State +import Nes.APU.State.DMC +import Nes.APU.State.LengthCounter +import Nes.APU.Tick (setFrameInterruptFlag) +import Nes.Bus.SideEffect +import Nes.FlagRegister (getFlag) +import Nes.Memory + +{-# INLINE write4015 #-} +write4015 :: Byte -> APU r () +write4015 byte = do + let enablePulse1Lc = byte `testBit` 0 + enablePulse2Lc = byte `testBit` 1 + enableTriangleLc = byte `testBit` 2 + enableNoiseLc = byte `testBit` 3 + enableDmc = byte `testBit` 4 + toggleLengthCounter enablePulse1Lc modifyPulse1 + toggleLengthCounter enablePulse2Lc modifyPulse2 + toggleLengthCounter enableTriangleLc modifyTriangle + toggleLengthCounter enableNoiseLc modifyNoise + modifyAPUState $ modifyDMC $ \t -> + if enableDmc + -- TODO If there are bits remaining in the 1-byte sample buffer, these will finish playing before the next sample is fetched. + then if sampleBytesRemaining t == 0 then restartSample t else t + else t{sampleBytesRemaining = 0} + +{-# INLINE toggleLengthCounter #-} +toggleLengthCounter :: (HasLengthCounter a) => Bool -> ((a -> a) -> APUState -> APUState) -> APU r () +toggleLengthCounter enable f = + modifyAPUState $ + f $ + withLengthCounter $ + if enable then enableLengthCounter else disableLengthCounter . clearAndHaltLengthCounter + +{-# INLINE read4015 #-} +read4015 :: APU r Byte +read4015 = do + noiseBit <- withAPUState $ lengthCounterBit . noise + triangleBit <- withAPUState $ lengthCounterBit . triangle + pulse1Bit <- withAPUState $ lengthCounterBit . pulse1 + pulse2Bit <- withAPUState $ lengthCounterBit . pulse2 + dmcBit <- withAPUState $ \st -> sampleBytesRemaining (dmc st) > 0 + frameInterruptBit <- withSideEffect $ getFlag IRQ + dmcInterruptBit <- withSideEffect $ getFlag DMCDMA + when frameInterruptBit $ do + setFrameInterruptFlag False + return $ + setBit' dmcInterruptBit 7 $ + setBit' frameInterruptBit 6 $ + setBit' dmcBit 4 $ + setBit' noiseBit 3 $ + setBit' triangleBit 2 $ + setBit' pulse2Bit 1 $ + setBit' + pulse1Bit + 0 + 0 + where + setBit' b i a = if b then a `setBit` i else a `clearBit` i + lengthCounterBit st = let lc = getLengthCounter st in isEnabled lc diff --git a/src/Nes/APU/BusInterface/Triangle.hs b/src/Nes/APU/BusInterface/Triangle.hs new file mode 100644 index 0000000..136214e --- /dev/null +++ b/src/Nes/APU/BusInterface/Triangle.hs @@ -0,0 +1,41 @@ +module Nes.APU.BusInterface.Triangle ( + -- * Triangle + write4008, + write400A, + write400B, +) where + +import Data.Bits +import Nes.APU.Monad +import Nes.APU.State +import Nes.APU.State.LengthCounter +import Nes.APU.State.Triangle +import Nes.Memory + +{-# INLINE write4008 #-} +write4008 :: Byte -> APU r () +write4008 byte = do + let control = byte `testBit` 7 + reload = byteToInt $ byte `clearBit` 7 + modifyAPUState $ + modifyTriangle $ + withLengthCounter (\lc -> lc{isHalted = control}) + . \t -> t{controlFlag = control, reloadValue = reload} + +{-# INLINE write400A #-} +write400A :: Byte -> APU r () +write400A periodLow = modifyAPUState $ modifyTriangle $ \t -> + let newPeriod = (period t .&. 0b11100000000) .|. byteToInt periodLow + in t{period = newPeriod} + +{-# INLINE write400B #-} +write400B :: Byte -> APU r () +write400B byte = modifyAPUState $ modifyTriangle $ \t -> + let timerHigh = byteToInt $ byte .&. 0b111 + newPeriod = (timerHigh `shiftL` 8) .|. (period t .&. 0b11111111) + newLcLoad = byteToInt $ byte `shiftR` 3 + in withLengthCounter (loadLengthCounter newLcLoad) $ + t + { reloadFlag = True + , period = newPeriod + } diff --git a/src/Nes/APU/Mixer.hs b/src/Nes/APU/Mixer.hs new file mode 100644 index 0000000..a45d7d5 --- /dev/null +++ b/src/Nes/APU/Mixer.hs @@ -0,0 +1,34 @@ +{-# LANGUAGE RecordWildCards #-} + +module Nes.APU.Mixer (getMixerOutput) where + +import Nes.APU.State +import Nes.APU.State.DMC (getDMCOutput) +import Nes.APU.State.Noise (getNoiseOutput) +import Nes.APU.State.Pulse (getPulseOutput) +import Nes.APU.State.Triangle (getTriangleOutput) +import Prelude hiding (cycle) + +getMixerOutput :: APUState -> Float +getMixerOutput MkAPUState{..} = + let + !pulse1Out = getPulseOutput pulse1 + !pulse2Out = getPulseOutput pulse2 + !triangleOut = getTriangleOutput triangle + !noiseOut = getNoiseOutput noise + !dmcOut = getDMCOutput dmc + !pulseOut = pulseTable (pulse1Out + pulse2Out) + !tndOut = tndTable (3 * triangleOut + 2 * noiseOut + dmcOut) + !mixerOutput = pulseOut + tndOut + in + mixerOutput + +{-# INLINE pulseTable #-} +pulseTable :: Int -> Float +pulseTable 0 = 0 +pulseTable n = 95.52 / ((8128.0 / fromIntegral n) + 100) + +{-# INLINE tndTable #-} +tndTable :: Int -> Float +tndTable 0 = 0 +tndTable n = 163.67 / ((24329.0 / fromIntegral n) + 100) diff --git a/src/Nes/APU/Monad.hs b/src/Nes/APU/Monad.hs new file mode 100644 index 0000000..d870f8d --- /dev/null +++ b/src/Nes/APU/Monad.hs @@ -0,0 +1,71 @@ +module Nes.APU.Monad ( + APU (..), + runAPU, + modifyAPUState, + modifyAPUStateWithSideEffect, + withAPUState, + modifyFilterChain, + setSideEffect, + withSideEffect, +) where + +import Control.Monad.IO.Class +import Nes.APU.State +import Nes.APU.State.Filter.Chain (FilterChain) +import Nes.Bus.SideEffect + +newtype APU r a = MkAPU + { unAPU :: APUState -> CPUSideEffect -> (APUState -> CPUSideEffect -> a -> IO r) -> IO r + } + deriving (Functor) + +instance Applicative (APU r) where + {-# INLINE pure #-} + pure a = MkAPU $ \(!st) (!cpuEff) cont -> cont st cpuEff a + + {-# INLINE liftA2 #-} + liftA2 f (MkAPU a) (MkAPU b) = MkAPU $ \(!st) (!cpuEff) cont -> + a st cpuEff $ \(!st') (!cpuEff') !a' -> b st' cpuEff' $ \(!st'') (!cpuEff'') !b' -> cont st'' cpuEff'' (f a' b') + +instance Monad (APU r) where + {-# INLINE (>>=) #-} + (MkAPU a) >>= next = MkAPU $ \st cpuEff cont -> + a st cpuEff $ \(!st') (!cpuEff') (!a') -> unAPU (next a') st' cpuEff' cont + +instance MonadIO (APU r) where + {-# INLINE liftIO #-} + liftIO io = MkAPU $ \st cpuEff cont -> io >>= cont st cpuEff + +instance MonadFail (APU r) where + {-# INLINE fail #-} + fail = liftIO . fail + +{-# INLINE runAPU #-} +runAPU :: APUState -> APU (a, APUState, CPUSideEffect) a -> IO (a, APUState, CPUSideEffect) +runAPU !st f = unAPU f st mempty $ \(!st') (!cpuEff) a -> return (a, st', cpuEff) + +{-# INLINE modifyAPUState #-} +modifyAPUState :: (APUState -> APUState) -> APU r () +modifyAPUState f = MkAPU $ \(!st) (!cpuEff) cont -> cont (f st) cpuEff () + +{-# INLINE modifyAPUStateWithSideEffect #-} +modifyAPUStateWithSideEffect :: (APUState -> (APUState, CPUSideEffect)) -> APU r () +modifyAPUStateWithSideEffect f = MkAPU $ \(!st) !cpuEff cont -> + let (st', sideEff) = f st in cont st' (cpuEff <> sideEff) () + +{-# INLINE withAPUState #-} +withAPUState :: (APUState -> a) -> APU r a +withAPUState f = MkAPU $ \(!st) !cpuEff cont -> cont st cpuEff (f st) + +{-# INLINE modifyFilterChain #-} +modifyFilterChain :: (FilterChain -> FilterChain) -> APU r () +modifyFilterChain f = MkAPU $ \(!st) !cpuEff cont -> + cont st{filterChain = f $ filterChain st} cpuEff () + +{-# INLINE setSideEffect #-} +setSideEffect :: (CPUSideEffect -> CPUSideEffect) -> APU r () +setSideEffect f = MkAPU $ \(!st) !cpuEff cont -> cont st (f cpuEff) () + +{-# INLINE withSideEffect #-} +withSideEffect :: (CPUSideEffect -> a) -> APU r a +withSideEffect f = MkAPU $ \(!st) !cpuEff cont -> cont st cpuEff (f cpuEff) diff --git a/src/Nes/APU/State.hs b/src/Nes/APU/State.hs new file mode 100644 index 0000000..4f605ff --- /dev/null +++ b/src/Nes/APU/State.hs @@ -0,0 +1,85 @@ +{-# LANGUAGE RecordWildCards #-} + +module Nes.APU.State ( + -- * Definition + APUState (..), + newAPUState, + + -- * Setters + modifyFrameCounter, + modifyPulse1, + modifyPulse2, + modifyTriangle, + modifyNoise, + modifyDMC, + modifyDMC', +) where + +import Nes.APU.State.DMC +import Nes.APU.State.Filter.Chain +import Nes.APU.State.Filter.Constants (defaultOutputRate) +import Nes.APU.State.FrameCounter +import Nes.APU.State.Noise +import Nes.APU.State.Pulse +import Nes.APU.State.Triangle +import Nes.Bus.SideEffect (CPUSideEffect) +import Prelude hiding (cycle) + +data APUState = MkAPUState + { frameCounter :: !FrameCounter + , pulse1 :: !Pulse + , pulse2 :: !Pulse + , triangle :: !Triangle + , noise :: !Noise + , dmc :: !DMC + , cycle :: {-# UNPACK #-} !Int + -- ^ Number of CPU cycles since the start + , filterChain :: !FilterChain + , sampleTimer :: {-# UNPACK #-} !Float + -- ^ The number of CPU cycles since the last call to 'pushSampleCallback' + , samplePeriod :: {-# UNPACK #-} !Float + -- ^ The number of CPU cycles between each call to 'pushSampleCallback' + , pushSampleCallback :: Float -> IO () + } + +newAPUState :: (Float -> IO ()) -> APUState +newAPUState pushSampleCallback = MkAPUState{..} + where + frameCounter = newFrameCounter + cycle = 0 + pulse1 = newPulse True + pulse2 = newPulse False + triangle = newTriangle + noise = newNoise + dmc = newDMC + filterChain = newFilterChain defaultOutputRate + samplePeriod = (21477272 / 12) / defaultOutputRate + sampleTimer = samplePeriod + +{-# INLINE modifyPulse1 #-} +modifyPulse1 :: (Pulse -> Pulse) -> APUState -> APUState +modifyPulse1 f st = st{pulse1 = f (pulse1 st)} + +{-# INLINE modifyPulse2 #-} +modifyPulse2 :: (Pulse -> Pulse) -> APUState -> APUState +modifyPulse2 f st = st{pulse2 = f (pulse2 st)} + +{-# INLINE modifyTriangle #-} +modifyTriangle :: (Triangle -> Triangle) -> APUState -> APUState +modifyTriangle f st = st{triangle = f (triangle st)} + +{-# INLINE modifyNoise #-} +modifyNoise :: (Noise -> Noise) -> APUState -> APUState +modifyNoise f st = st{noise = f (noise st)} + +{-# INLINE modifyDMC #-} +modifyDMC :: (DMC -> DMC) -> APUState -> APUState +modifyDMC f st = let dmc' = f $ dmc st in st{dmc = dmc'} + +{-# INLINE modifyDMC' #-} +modifyDMC' :: (DMC -> (DMC, CPUSideEffect)) -> APUState -> (APUState, CPUSideEffect) +modifyDMC' f st = let (dmc', sideEff) = f $ dmc st in (st{dmc = dmc'}, sideEff) + +{-# INLINE modifyFrameCounter #-} +modifyFrameCounter :: (FrameCounter -> FrameCounter) -> APUState -> APUState +modifyFrameCounter f st = st{frameCounter = f (frameCounter st)} diff --git a/src/Nes/APU/State/DMC.hs b/src/Nes/APU/State/DMC.hs new file mode 100644 index 0000000..39c4219 --- /dev/null +++ b/src/Nes/APU/State/DMC.hs @@ -0,0 +1,134 @@ +{-# LANGUAGE RecordWildCards #-} + +module Nes.APU.State.DMC ( + DMC (..), + newDMC, + tickDMC, + getPeriodValue, + + -- * Actions + restartSample, + loadSampleBuffer, + + -- * Output + getDMCOutput, +) where + +import Data.Array +import Data.Bits +import Data.List ((!?)) +import Data.Maybe (fromMaybe, isNothing) +import Nes.Bus.SideEffect +import Nes.FlagRegister +import Nes.Memory + +data DMC = MkDMC + { irqEnabledFlag :: {-# UNPACK #-} !Bool + , loopFlag :: {-# UNPACK #-} !Bool + , period :: {-# UNPACK #-} !Int + , timer :: {-# UNPACK #-} !Int + , sampleOgAddr :: {-# UNPACK #-} !Addr + , sampleOgLength :: {-# UNPACK #-} !Int + , sampleBufferAddr :: {-# UNPACK #-} !Addr -- Addr in memory of the sample buffer's byte + , sampleBytesRemaining :: {-# UNPACK #-} !Int + , sampleBuffer :: !(Maybe Byte) + , outputLevel :: {-# UNPACK #-} !Int + , enableChannel :: {-# UNPACK #-} !Bool + , shouldClock :: {-# UNPACK #-} !Bool + , sleepingCycles :: {-# UNPACK #-} !Int + , shiftRegister :: {-# UNPACK #-} !Byte + , remainingBits :: {-# UNPACK #-} !Byte + , silentFlag :: {-# UNPACK #-} !Bool + } + +newDMC :: DMC +newDMC = MkDMC{..} + where + irqEnabledFlag = False + loopFlag = False + period = 0 + timer = 0 + sampleOgAddr = 0 + sampleOgLength = 0 + sampleBufferAddr = 0 + sampleBytesRemaining = 0 + remainingBits = 0 + shiftRegister = 0 + silentFlag = False + sampleBuffer = Nothing + enableChannel = True + outputLevel = 0 + shouldClock = False + sleepingCycles = 0 + +getPeriodValue :: Int -> Int +getPeriodValue idx = fromMaybe 428 ([428, 380, 340, 286, 254, 226, 214, 190, 160, 142, 128, 106, 84, 72, 54] !? idx) + +-- | When a sample is (re)started, the current address is set to the sample address, and bytes remaining is set to the sample length. +restartSample :: DMC -> DMC +restartSample dmc = + dmc + { sampleBufferAddr = sampleOgAddr dmc + , sampleBytesRemaining = sampleOgLength dmc + , shouldClock = sampleOgLength dmc > 0 + } + +{-# INLINE getDMCOutput #-} +getDMCOutput :: DMC -> Int +getDMCOutput dmc = if silentFlag dmc then 0 else outputLevel dmc + +tickDMC :: DMC -> (DMC, CPUSideEffect) +tickDMC dmc = + (if clocks then tickOutputUnit else (,mempty)) + dmc + { timer = newTimer + , outputLevel = newOutputLevel + , shiftRegister = newShiftRegister + } + where + clocks = timer dmc == 0 + newTimer = if timer dmc == 0 then period dmc else timer dmc - 1 + newShiftRegister = if clocks then shiftRegister dmc `shiftR` 1 else shiftRegister dmc + newOutputLevel = + if clocks && not (silentFlag dmc) + then + let delta = if shiftRegister dmc `testBit` 0 then 2 else (-2) + tmpOutLevel = outputLevel dmc + delta + in if (0, 127) `inRange` tmpOutLevel then tmpOutLevel else outputLevel dmc + else outputLevel dmc + +tickOutputUnit :: DMC -> (DMC, CPUSideEffect) +tickOutputUnit dmc = if isEndOfOutputCycle then onOutputCycleEnd dmc1 else (dmc1, mempty) + where + newRemainingBits = max 0 (remainingBits dmc - 1) + isEndOfOutputCycle = newRemainingBits == 0 + dmc1 = dmc{remainingBits = newRemainingBits} + +onOutputCycleEnd :: DMC -> (DMC, CPUSideEffect) +onOutputCycleEnd dmc = (dmc1, sideEffect) + where + dmc0 = dmc{remainingBits = 8} + dmc1 = case sampleBuffer dmc0 of + Nothing -> dmc0{silentFlag = True} + Just b -> dmc0{shiftRegister = b, sampleBuffer = Nothing} + sideEffect = setFlag' DMCDMA (isNothing (sampleBuffer dmc1) && sampleBytesRemaining dmc1 > 0) mempty + +-- | Loads the byte into the sample buffer and shift the sample buffer-related values +loadSampleBuffer :: Byte -> DMC -> (DMC, CPUSideEffect) +loadSampleBuffer byte dmc = + let + newSampleBufferAddr = let addr = sampleBufferAddr dmc + 1 in if addr >= 0xffff then addr - 0x8000 else addr + newRemainingLength = max 0 (sampleBytesRemaining dmc - 1) + dmc1 = + dmc + { sampleBuffer = Just byte + , sampleBytesRemaining = newRemainingLength + , sampleBufferAddr = newSampleBufferAddr + , shouldClock = newRemainingLength > 0 + } + shouldRestartSample = newRemainingLength == 0 && loopFlag dmc + shouldIRQ = newRemainingLength == 0 && irqEnabledFlag dmc + in + if shouldRestartSample + then (restartSample dmc1, mempty) + else (dmc1, setFlag' IRQ shouldIRQ mempty) diff --git a/src/Nes/APU/State/Envelope.hs b/src/Nes/APU/State/Envelope.hs new file mode 100644 index 0000000..8acf79e --- /dev/null +++ b/src/Nes/APU/State/Envelope.hs @@ -0,0 +1,58 @@ +module Nes.APU.State.Envelope ( + -- * Type + Envelope (..), + newEnvelope, + + -- * Type class + HasEnvelope (..), + withEnvelope, + + -- * Clock + tickEnvelope, + + -- * Output + getEnvelopeOutput, +) where + +data Envelope = MkE + { startFlag :: {-# UNPACK #-} !Bool + , useConstantVolume :: {-# UNPACK #-} !Bool + , constantVolume :: {-# UNPACK #-} !Int + , decayLevel :: {-# UNPACK #-} !Int + , divider :: {-# UNPACK #-} !Int + , loopFlag :: {-# UNPACK #-} !Bool + } + +newEnvelope :: Envelope +newEnvelope = MkE False False 0 0 0 False + +class HasEnvelope a where + getEnvelope :: a -> Envelope + setEnvelope :: Envelope -> a -> a + +{-# INLINE withEnvelope #-} +withEnvelope :: (HasEnvelope a) => (Envelope -> Envelope) -> a -> a +withEnvelope f a = setEnvelope (f $ getEnvelope a) a + +tickEnvelope :: Envelope -> Envelope +tickEnvelope e = + if startFlag e + then e{startFlag = False, decayLevel = 15, divider = constantVolume e} + else tickDivider e + +tickDivider :: Envelope -> Envelope +tickDivider e = + if divider e == 0 + then e{divider = constantVolume e, decayLevel = newDecay} + else e{divider = divider e - 1} + where + newDecay = + if decayLevel e == 0 + then if loopFlag e then 15 else 0 + else decayLevel e - 1 + +getEnvelopeOutput :: Envelope -> Int +getEnvelopeOutput e = + if useConstantVolume e + then constantVolume e + else decayLevel e diff --git a/src/Nes/APU/State/Filter/Chain.hs b/src/Nes/APU/State/Filter/Chain.hs new file mode 100644 index 0000000..efa729b --- /dev/null +++ b/src/Nes/APU/State/Filter/Chain.hs @@ -0,0 +1,81 @@ +{-# LANGUAGE RecordWildCards #-} + +module Nes.APU.State.Filter.Chain (FilterChain (..), newFilterChain) where + +import Nes.APU.State.Filter.Class +import Nes.APU.State.Filter.Constants +import Nes.APU.State.Filter.Fir +import Nes.APU.State.Filter.Iir +import Nes.APU.State.Filter.Sampled +import Prelude hiding (filter) + +data FilterChain = MkFC + { filters :: ![SampledFilter] + , dt :: {-# UNPACK #-} !Float + } + +newFilterChain :: OutputRate -> FilterChain +newFilterChain outputRate = MkFC{..} + where + clockRate = 21477272 / 12 + intermediateSampleRate = outputRate * 2 + (pi / 32) + intermediateCutoff = outputRate * 0.4 + dt = 1 / clockRate + filters = + [ newSampledFilter (Left identityIirFilter) 1.0 + , newSampledFilter (Left $ lowPassIirFilter clockRate intermediateCutoff) clockRate + , newSampledFilter (Left $ highPassIirFilter intermediateSampleRate 90) intermediateSampleRate + , newSampledFilter (Left $ highPassIirFilter intermediateSampleRate 440) intermediateSampleRate + , newSampledFilter (Left $ lowPassIirFilter intermediateSampleRate 14000) intermediateSampleRate + , newSampledFilter (Right $ lowPassFirFilter intermediateSampleRate (outputRate * 0.45) 160) intermediateSampleRate + ] + +instance Filter FilterChain where + consume = filterChainConsumeSample + output = filterChainOutput + +filterChainConsumeSample :: Sample -> FilterChain -> FilterChain +filterChainConsumeSample sample fc = + let + fc1 = modifyFilterAtIndex 0 (consume sample) fc + updatedFilters = go (filters fc1) (dt fc1) + in + fc1{filters = updatedFilters} + where + go :: [SampledFilter] -> Float -> [SampledFilter] + go [] _ = [] + go [a] _ = [a] + go (prev : curr : rest) dt = + let + newCurr = filterChainConsumeIteration prev curr dt + in + prev : go (newCurr : rest) dt + +filterChainConsumeIteration :: SampledFilter -> SampledFilter -> Float -> SampledFilter +filterChainConsumeIteration prev current dt = + if periodCounter current >= samplePeriod current + then + let + newPeriodCounter = periodCounter current - samplePeriod current + previousOutput = output $ filter prev + newCurrent = consume previousOutput $ current{periodCounter = newPeriodCounter} + in + filterChainConsumeIteration + prev + newCurrent + dt + else + let newPeriodCounter = periodCounter current + dt + in current{periodCounter = newPeriodCounter} + +{-# INLINE modifyFilterAtIndex #-} +modifyFilterAtIndex :: Int -> (SampledFilter -> SampledFilter) -> FilterChain -> FilterChain +modifyFilterAtIndex idx f fc = case splitAt idx $ filters fc of + (_, []) -> fc + (left, item : right) -> fc{filters = left ++ (f item : right)} + +{-# INLINE filterChainOutput #-} +filterChainOutput :: FilterChain -> Sample +filterChainOutput fc = case filters fc of + [] -> 0 + l -> either output output . filter $ last l diff --git a/src/Nes/APU/State/Filter/Class.hs b/src/Nes/APU/State/Filter/Class.hs new file mode 100644 index 0000000..966fcf2 --- /dev/null +++ b/src/Nes/APU/State/Filter/Class.hs @@ -0,0 +1,11 @@ +module Nes.APU.State.Filter.Class (Filter (..)) where + +import Nes.APU.State.Filter.Constants + +class Filter a where + consume :: Sample -> a -> a + output :: a -> Sample + +instance (Filter a, Filter b) => Filter (Either a b) where + consume sample = either (Left . consume sample) (Right . consume sample) + output = either output output diff --git a/src/Nes/APU/State/Filter/Constants.hs b/src/Nes/APU/State/Filter/Constants.hs new file mode 100644 index 0000000..f55bd17 --- /dev/null +++ b/src/Nes/APU/State/Filter/Constants.hs @@ -0,0 +1,18 @@ +module Nes.APU.State.Filter.Constants ( + -- * Constants + defaultOutputRate, + + -- * Type alias + Sample, + SampleRate, + Cutoff, + OutputRate, +) where + +type Sample = Float +type SampleRate = Float +type Cutoff = Float +type OutputRate = Float + +defaultOutputRate :: OutputRate +defaultOutputRate = 44100 diff --git a/src/Nes/APU/State/Filter/Fir.hs b/src/Nes/APU/State/Filter/Fir.hs new file mode 100644 index 0000000..46c9c34 --- /dev/null +++ b/src/Nes/APU/State/Filter/Fir.hs @@ -0,0 +1,92 @@ +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE NoStrict #-} + +-- Using laziness to build cyclic lists for the output + +module Nes.APU.State.Filter.Fir (FirFilter (..), lowPassFirFilter) where + +import Data.Functor ((<&>)) +import qualified Data.Vector.Unboxed as V +import Nes.APU.State.Filter.Class +import Nes.APU.State.Filter.Constants + +-- | Finite impulse response (FIR) filter +data FirFilter = MkFirF + { kernel :: !(V.Vector Float) + , inputs :: !(V.Vector Float) + , inputIndex :: {-# UNPACK #-} !Int + } + +instance Filter FirFilter where + output f = + let + kernelL = V.toList $ kernel f + inputsL = drop (inputIndex f) $ Prelude.cycle $ V.toList $ inputs f + in + sum (zipWith (*) kernelL inputsL) + consume sample f = + f + { inputIndex = newInputIndex + , inputs = newInputs + } + where + newInputIndex = (inputIndex f + 1) `mod` V.length (inputs f) + newInputs = inputs f V.// [(inputIndex f, sample)] + +lowPassFirFilter :: SampleRate -> Cutoff -> Int -> FirFilter +lowPassFirFilter sampleRate cutoff windowSize = MkFirF{..} + where + inputIndex = 0 + inputs = V.replicate (windowSize + 1) 0 + kernel = windowedSincKernel sampleRate cutoff windowSize + +windowedSincKernel :: SampleRate -> Cutoff -> Int -> V.Vector Float +windowedSincKernel sampleRate cutoff windowSize = + let + fc = cutoff / sampleRate + kernelL :: [Float] + kernelL = + [0 .. windowSize] <&> \i -> + (sinc i fc windowSize) * blackmanWindow i windowSize + kernelV = V.fromList kernelL + in + normalise kernelV + where + blackmanWindow :: Int -> Int -> Float + blackmanWindow idx winSize = + let + fIdx = fromIntegral idx + fWinSize = fromIntegral winSize + tau = 2 * pi + in + 0.42 + - 0.5 + * ((cos ((tau * fIdx) / fWinSize)) + 0.08 * (cos ((2 * tau * fIdx / fWinSize)))) + sinc :: Int -> Float -> Int -> Float + sinc idx fc winSize = + let + fIdx = fromIntegral idx + fWinSize = fromIntegral winSize + shiftedIndex = fIdx - (fWinSize / 2) + tau = 2 * pi + in + if idx == (windowSize `div` 2) + then tau * fc + else (mySin (tau * fc * shiftedIndex)) / shiftedIndex + normalise :: V.Vector Float -> V.Vector Float + normalise vec = + let + vecSum = V.sum vec + in + V.map (/ vecSum) vec + +-- | Faster implementation of the sin function, +-- +-- Stolen from https://www.youtube.com/watch?v=72dI7dB3ZvQ +mySin :: Float -> Float +mySin t = + let + j0 = t * 0.15915 + j1 = j0 - fromIntegral (floor j0 :: Int) + in + 20.785 * j1 * (j1 - 0.5) * (j1 - 1) diff --git a/src/Nes/APU/State/Filter/Iir.hs b/src/Nes/APU/State/Filter/Iir.hs new file mode 100644 index 0000000..d291b78 --- /dev/null +++ b/src/Nes/APU/State/Filter/Iir.hs @@ -0,0 +1,67 @@ +module Nes.APU.State.Filter.Iir ( + IirFilter (..), + + -- * Build predefined filters + identityIirFilter, + highPassIirFilter, + lowPassIirFilter, +) where + +import Nes.APU.State.Filter.Class +import Nes.APU.State.Filter.Constants + +-- | Infinite impulse response (IIR) filter +data IirFilter = MkIirF + { alpha :: {-# UNPACK #-} !Float + , previousOutput :: {-# UNPACK #-} !Sample + , previousInput :: {-# UNPACK #-} !Sample + , delta :: {-# UNPACK #-} !Float + , outputF :: !(IirFilter -> Sample) + } + +identityIirFilter :: IirFilter +identityIirFilter = + MkIirF + { alpha = 0 + , previousInput = 0 + , previousOutput = 0 + , delta = 0 + , outputF = previousInput + } + +highPassIirFilter :: SampleRate -> Cutoff -> IirFilter +highPassIirFilter sampleRate cutoff = + MkIirF + { alpha = cutoffPeriod / (cutoffPeriod + period) + , previousOutput = 0 + , previousInput = 0 + , delta = 0 + , outputF = \f -> alpha f * previousOutput f + alpha f * delta f + } + where + period = 1 / sampleRate + cutoffPeriod = 1 / cutoff + +lowPassIirFilter :: SampleRate -> Cutoff -> IirFilter +lowPassIirFilter sampleRate cutoff = + MkIirF + { alpha = cutoffPeriod / (cutoffPeriod + period) + , previousOutput = 0 + , previousInput = 0 + , delta = 0 + , outputF = \f -> previousOutput f + alpha f * delta f + } + where + period = 1 / sampleRate + cutoffPeriod = 1 / (2 * pi * cutoff) + +instance Filter IirFilter where + {-# INLINE output #-} + output f = outputF f f + {-# INLINE consume #-} + consume sample f = + f + { previousOutput = output f + , delta = sample - previousInput f + , previousInput = sample + } diff --git a/src/Nes/APU/State/Filter/Sampled.hs b/src/Nes/APU/State/Filter/Sampled.hs new file mode 100644 index 0000000..02bb9e8 --- /dev/null +++ b/src/Nes/APU/State/Filter/Sampled.hs @@ -0,0 +1,32 @@ +{-# LANGUAGE RecordWildCards #-} + +module Nes.APU.State.Filter.Sampled (SampledFilter (..), newSampledFilter) where + +import Nes.APU.State.Filter.Class +import Nes.APU.State.Filter.Constants +import Nes.APU.State.Filter.Fir +import Nes.APU.State.Filter.Iir +import Prelude hiding (filter) + +data SampledFilter = MkSF + { filter :: Either IirFilter FirFilter + , samplePeriod :: {-# UNPACK #-} !Float + , periodCounter :: {-# UNPACK #-} !Float + } + +newSampledFilter :: Either IirFilter FirFilter -> SampleRate -> SampledFilter +newSampledFilter filter sampleRate = MkSF{..} + where + periodCounter = 1 + samplePeriod = 1 / sampleRate + +instance Filter SampledFilter where + consume = sampledFilterConsumeSample + output sf = output $ filter sf + +{-# INLINE sampledFilterConsumeSample #-} +sampledFilterConsumeSample :: Sample -> SampledFilter -> SampledFilter +sampledFilterConsumeSample sample sf = + sf + { filter = consume sample $ filter sf + } diff --git a/src/Nes/APU/State/FrameCounter.hs b/src/Nes/APU/State/FrameCounter.hs new file mode 100644 index 0000000..f77bbc4 --- /dev/null +++ b/src/Nes/APU/State/FrameCounter.hs @@ -0,0 +1,86 @@ +module Nes.APU.State.FrameCounter ( + FrameCounter (..), + newFrameCounter, + + -- * Sequence mode + SequenceMode (..), + sequenceModeFromBool, + + -- * Utils + shouldIncrementSequenceStep, + shouldResetSequenceStep, + incrementSequenceStep, + resetSequence, + setCycles, +) where + +import Data.List ((!?)) + +data SequenceMode = FourStep | FiveStep deriving (Eq, Show, Enum) + +{-# INLINE sequenceModeFromBool #-} +sequenceModeFromBool :: Bool -> SequenceMode +sequenceModeFromBool = toEnum . fromEnum + +{-# INLINE sequenceModeStepCount #-} +sequenceModeStepCount :: SequenceMode -> Int +sequenceModeStepCount = \case + FourStep -> 4 + FiveStep -> 5 + +sequenceStepCycles :: SequenceMode -> [Int] +sequenceStepCycles = \case + FourStep -> [3728, 7456, 11185, 14914, 14915] + FiveStep -> [3728, 7456, 11185, 14914, 18640, 18641] + +{-# INLINE shouldIncrementSequenceStep #-} +shouldIncrementSequenceStep :: FrameCounter -> Bool +shouldIncrementSequenceStep fc = + let + table = sequenceStepCycles $ sequenceMode fc + in + case table !? sequenceStep fc of + Nothing -> False + Just s -> cycles fc >= s + +{-# INLINE shouldResetSequenceStep #-} +shouldResetSequenceStep :: FrameCounter -> Bool +shouldResetSequenceStep fc = + let + table = sequenceStepCycles $ sequenceMode fc + in + case table !? 5 of + Nothing -> False + Just s -> cycles fc >= s + +data FrameCounter = MkFC + { sequenceMode :: {-# UNPACK #-} !SequenceMode + , frameInterruptFlag :: {-# UNPACK #-} !Bool + , inhibitInterrupt :: {-# UNPACK #-} !Bool + , sequenceStep :: {-# UNPACK #-} !Int + , cycles :: {-# UNPACK #-} !Int + , delayedWriteSideEffectCycle :: !(Maybe Int) + } + +newFrameCounter :: FrameCounter +newFrameCounter = MkFC FourStep False False 0 0 Nothing + +{-# INLINE resetSequence #-} +resetSequence :: FrameCounter -> FrameCounter +resetSequence fc = fc{cycles = 0, sequenceStep = 0} + +{-# INLINE setCycles #-} +setCycles :: (Int -> Int) -> FrameCounter -> FrameCounter +setCycles f fc = fc{cycles = f $ cycles fc} + +-- | Increment 'sequenceStep', or set to zero when sequence ends +{-# INLINE incrementSequenceStep #-} +incrementSequenceStep :: FrameCounter -> FrameCounter +incrementSequenceStep fc = + fc + { sequenceStep = nextStep `mod` (maxStep + 1) + -- To get end of sequence + } + where + nextStep = sequenceStep fc + 1 + maxStep = sequenceModeStepCount (sequenceMode fc) diff --git a/src/Nes/APU/State/LengthCounter.hs b/src/Nes/APU/State/LengthCounter.hs new file mode 100644 index 0000000..c276f65 --- /dev/null +++ b/src/Nes/APU/State/LengthCounter.hs @@ -0,0 +1,99 @@ +module Nes.APU.State.LengthCounter ( + LengthCounter (..), + newLengthCounter, + tickLengthCounter, + loadLengthCounter, + clearAndHaltLengthCounter, + enableLengthCounter, + disableLengthCounter, + + -- * Class + HasLengthCounter (..), + withLengthCounter, + isSilencedByLengthCounter, +) where + +import Data.List ((!?)) + +data LengthCounter = MkLC + { remainingLength :: {-# UNPACK #-} !Int + , isHalted :: {-# UNPACK #-} !Bool + , isEnabled :: {-# UNPACK #-} !Bool + } + +newLengthCounter :: LengthCounter +newLengthCounter = MkLC 0 False False + +tickLengthCounter :: LengthCounter -> LengthCounter +tickLengthCounter lc = + if remainingLength lc > 0 && not (isHalted lc) + then lc{remainingLength = remainingLength lc - 1} + else lc + +{-# INLINE clearAndHaltLengthCounter #-} +clearAndHaltLengthCounter :: LengthCounter -> LengthCounter +clearAndHaltLengthCounter lc = lc{remainingLength = 0, isHalted = True} + +-- | Load Length using the argument a an index in the length table +-- +-- Note: It must not be done when the enabled bit (4015) is clear +loadLengthCounter :: Int -> LengthCounter -> LengthCounter +loadLengthCounter idx lc + | not $ isEnabled lc = lc + | otherwise = case lengthTable !? idx of + Just l -> lc{remainingLength = l} + Nothing -> lc -- Index is invalid + +disableLengthCounter :: LengthCounter -> LengthCounter +disableLengthCounter lc = lc{isEnabled = False, remainingLength = 0} + +enableLengthCounter :: LengthCounter -> LengthCounter +enableLengthCounter lc = lc{isEnabled = True} + +class HasLengthCounter a where + getLengthCounter :: a -> LengthCounter + setLengthCounter :: LengthCounter -> a -> a + +{-# INLINE withLengthCounter #-} +withLengthCounter :: (HasLengthCounter a) => (LengthCounter -> LengthCounter) -> a -> a +withLengthCounter f a = setLengthCounter (f $ getLengthCounter a) a + +{-# INLINE isSilencedByLengthCounter #-} +isSilencedByLengthCounter :: (HasLengthCounter a) => a -> Bool +isSilencedByLengthCounter = (== 0) . remainingLength . getLengthCounter + +lengthTable :: [Int] +lengthTable = + [ 10 + , 254 + , 20 + , 2 + , 40 + , 4 + , 80 + , 6 + , 160 + , 8 + , 60 + , 10 + , 14 + , 12 + , 26 + , 14 + , 12 + , 16 + , 24 + , 18 + , 48 + , 20 + , 96 + , 22 + , 192 + , 24 + , 72 + , 26 + , 16 + , 28 + , 32 + , 30 + ] diff --git a/src/Nes/APU/State/Noise.hs b/src/Nes/APU/State/Noise.hs new file mode 100644 index 0000000..6851612 --- /dev/null +++ b/src/Nes/APU/State/Noise.hs @@ -0,0 +1,76 @@ +{-# LANGUAGE RecordWildCards #-} + +module Nes.APU.State.Noise ( + -- * Data type + Noise (..), + newNoise, + getNoiseOutput, + + -- * Clock + tickNoise, + tickShiftRegister, + + -- * Utils + getPeriodValue, +) where + +import Data.Bits +import Data.List ((!?)) +import Data.Maybe (fromMaybe) +import Data.Word +import Nes.APU.State.Envelope +import Nes.APU.State.LengthCounter + +data Noise = MkN + { useBit6ForFeedback :: {-# UNPACK #-} !Bool + -- ^ AKA Mode flag + , envelope :: !Envelope + , lengthCounter :: !LengthCounter + , shiftRegister :: {-# UNPACK #-} !Word16 + , period :: {-# UNPACK #-} !Int + , timer :: {-# UNPACK #-} !Int + } + +newNoise :: Noise +newNoise = MkN{..} + where + envelope = newEnvelope + useBit6ForFeedback = False + shiftRegister = 1 + lengthCounter = newLengthCounter + period = 0 + timer = 0 + +getPeriodValue :: Int -> Int +getPeriodValue idx = fromMaybe 4 ([4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068] !? idx) + +instance HasLengthCounter Noise where + getLengthCounter = lengthCounter + setLengthCounter lc n = n{lengthCounter = lc} + +instance HasEnvelope Noise where + getEnvelope = envelope + setEnvelope e n = n{envelope = e} + +tickNoise :: Noise -> Noise +tickNoise n = tickCallback $ n{timer = newTimer} + where + newTimer = if timer n == 0 then period n else timer n - 1 + tickCallback = if timer n == 0 then tickShiftRegister else id + +tickShiftRegister :: Noise -> Noise +tickShiftRegister n = n{shiftRegister = shift2} + where + shift0 = shiftRegister n + xorBit = if useBit6ForFeedback n then 6 else 1 + feeback = (shift0 `testBit` 0) .^. (shift0 `testBit` xorBit) + shift1 = shift0 `shiftR` 1 + shift2 = if feeback then shift1 `setBit` 14 else shift1 + +getNoiseOutput :: Noise -> Int +getNoiseOutput n = + if shiftBit0IsSet || isSilencedByLengthCounter n + then 0 + else getEnvelopeOutput $ envelope n + where + shiftBit0IsSet = shiftRegister n `testBit` 0 diff --git a/src/Nes/APU/State/Pulse.hs b/src/Nes/APU/State/Pulse.hs new file mode 100644 index 0000000..92e9dc5 --- /dev/null +++ b/src/Nes/APU/State/Pulse.hs @@ -0,0 +1,151 @@ +{-# LANGUAGE RecordWildCards #-} + +module Nes.APU.State.Pulse ( + -- * Pulse + Pulse (..), + newPulse, + tickPulse, + modifySweep, + withSweep, + + -- * Sweep Unit + SweepUnit (..), + tickSweepUnit, + updateTargetPeriod, + + -- * Output + getPulseOutput, +) where + +import Data.Bits +import Data.List ((!?)) +import Data.Maybe (fromMaybe) +import Nes.APU.State.Envelope +import Nes.APU.State.LengthCounter + +data Pulse = MkP + { dutyIndex :: {-# UNPACK #-} !Int + -- ^ Index for the 'dutySequences' table + , dutyStep :: {-# UNPACK #-} !Int + -- ^ Index for a row's element in the 'dutySequences' table + , lengthCounter :: !LengthCounter + , period :: {-# UNPACK #-} !Int + -- ^ Max value of the timer + , timer :: {-# UNPACK #-} !Int + -- ^ Decreases each tick, from 'period' to 0 and loops + , sweepUnit :: !SweepUnit + , envelope :: {-# UNPACK #-} !Envelope + } + +-- | Args is true if building pulse 1 +newPulse :: Bool -> Pulse +newPulse isPulseOne = MkP{..} + where + dutyIndex = 0 + dutyStep = 0 + lengthCounter = newLengthCounter + period = 0 + timer = 0 + sweepUnit = MkSU False 0 0 False 0 0 False isPulseOne + envelope = newEnvelope + +-- + +data SweepUnit = MkSU + { enabled :: Bool + , dividerPeriod :: Int + , dividerCounter :: Int + , negateDelta :: Bool + , targetPeriod :: Int + , shiftCount :: Int + , reloadFlag :: Bool + , isPulse1 :: Bool + } + +{-# INLINE modifySweep #-} +modifySweep :: (SweepUnit -> SweepUnit) -> Pulse -> Pulse +modifySweep f p = p{sweepUnit = f (sweepUnit p)} + +{-# INLINE withSweep #-} +withSweep :: (SweepUnit -> a) -> Pulse -> a +withSweep f p = f (sweepUnit p) + +-- | Update the target period in the Sweep unit of the pulse +updateTargetPeriod :: Pulse -> Pulse +updateTargetPeriod p = + modifySweep + ( \s -> + let + delta = period p `shiftR` shiftCount s + in + s + { targetPeriod = + if negateDelta s + then period p - delta - fromEnum (isPulse1 s) + else period p + delta + } + ) + p + +tickPulse :: Pulse -> Pulse +tickPulse p = p{dutyStep = newDutyStep, timer = newTimer} + where + newDutyStep = if timer p == 0 then (dutyStep p + 1) `mod` 8 else dutyStep p + newTimer = if timer p == 0 then period p else timer p - 1 + +tickSweepUnit :: Pulse -> Pulse +tickSweepUnit p = p2 + where + sweep = sweepUnit p + p1 = + if dividerCounter sweep == 0 && enabled sweep && shiftCount sweep > 0 + then + -- If sweep unit is not muting channel + if period p >= 8 && targetPeriod sweep <= 0x7ff + then + updateTargetPeriod $ p{period = targetPeriod sweep} + else + modifySweep + (\s -> s{dividerCounter = dividerPeriod s}) + p + else p + p2 = + -- TODO Not sure if should use p1 or p0 + if (reloadFlag . sweepUnit) p1 || (dividerCounter . sweepUnit) p1 == 0 + then + modifySweep + (\s -> s{dividerCounter = dividerPeriod s, reloadFlag = False}) + p1 + else modifySweep (\s -> s{dividerCounter = dividerCounter s - 1}) p1 + +-- + +instance HasLengthCounter Pulse where + getLengthCounter = lengthCounter + setLengthCounter lc a = a{lengthCounter = lc} + +instance HasEnvelope Pulse where + getEnvelope = envelope + setEnvelope e a = a{envelope = e} + +{-# INLINE getPulseOutput #-} +getPulseOutput :: Pulse -> Int +getPulseOutput p = + let dutyValue = fromMaybe 0 ((dutySequences !? dutyIndex p) >>= (!? dutyStep p)) + periodOverflows = not (negateDelta $ sweepUnit p) && (targetPeriod . sweepUnit) p > 0x7ff + isSilenced = + isSilencedByLengthCounter p + || (period p < 8) + || dutyValue == 0 + || periodOverflows + in if isSilenced + then 0 + else getEnvelopeOutput (envelope p) + +dutySequences :: [[Int]] +dutySequences = + [ [0, 1, 0, 0, 0, 0, 0, 0] + , [0, 1, 1, 0, 0, 0, 0, 0] + , [0, 1, 1, 1, 0, 0, 0, 0] + , [1, 0, 0, 1, 1, 1, 1, 1] + ] diff --git a/src/Nes/APU/State/Triangle.hs b/src/Nes/APU/State/Triangle.hs new file mode 100644 index 0000000..4b2a8ae --- /dev/null +++ b/src/Nes/APU/State/Triangle.hs @@ -0,0 +1,72 @@ +{-# LANGUAGE RecordWildCards #-} + +module Nes.APU.State.Triangle ( + -- * Definition + Triangle (..), + newTriangle, + + -- * Output + getTriangleOutput, + + -- * Clock + tickTriangle, + tickTriangleLinearCounter, +) where + +import Nes.APU.State.LengthCounter + +data Triangle = MkT + { controlFlag :: {-# UNPACK #-} !Bool + , reloadFlag :: {-# UNPACK #-} !Bool + , reloadValue :: {-# UNPACK #-} !Int + , lengthCounter :: !LengthCounter + , linearCounter :: {-# UNPACK #-} !Int + , period :: {-# UNPACK #-} !Int + , timer :: {-# UNPACK #-} !Int + , sequenceStep :: {-# UNPACK #-} !Int + } + +newTriangle :: Triangle +newTriangle = MkT{..} + where + controlFlag = False + reloadFlag = False + reloadValue = 0 + lengthCounter = newLengthCounter + period = 0 + linearCounter = 0 + sequenceStep = 0 + timer = 0 + +{-# INLINE getSequenceValue #-} +getSequenceValue :: Triangle -> Int +getSequenceValue t = if step <= 15 then 15 - step else step - 16 + where + step = sequenceStep t + +{-# INLINE getTriangleOutput #-} +getTriangleOutput :: Triangle -> Int +getTriangleOutput t = if remainingLength (lengthCounter t) /= 0 then getSequenceValue t else 0 + +instance HasLengthCounter Triangle where + getLengthCounter = lengthCounter + setLengthCounter lc t = t{lengthCounter = lc} + +tickTriangle :: Triangle -> Triangle +tickTriangle t = t{timer = newTimer, sequenceStep = newSequenceStep} + where + newTimer = if timer t == 0 then period t else timer t - 1 + tickSequence = timer t == 0 && linearCounter t /= 0 && remainingLength (lengthCounter t) /= 0 + newSequenceStep = if tickSequence then (sequenceStep t + 1) `mod` 32 else sequenceStep t + +tickTriangleLinearCounter :: Triangle -> Triangle +tickTriangleLinearCounter t = t2 + where + t1 = + if reloadFlag t + then t{linearCounter = reloadValue t} + else t{linearCounter = max 0 (linearCounter t - 1)} + t2 = + if not $ controlFlag t1 + then t1{reloadFlag = False} + else t1 diff --git a/src/Nes/APU/Tick.hs b/src/Nes/APU/Tick.hs new file mode 100644 index 0000000..7730d79 --- /dev/null +++ b/src/Nes/APU/Tick.hs @@ -0,0 +1,159 @@ +module Nes.APU.Tick ( + -- * Ticking + -- $semantic + tick, + tickOnce, + IsAPUCycle, + + -- * Internal ticking + tickFrameCounter, + runHalfFrameEvent, + runQuarterFrameEvent, + setFrameInterruptFlag, +) where + +import Control.Monad +import Control.Monad.IO.Class +import Nes.APU.Mixer +import Nes.APU.Monad +import Nes.APU.State +import qualified Nes.APU.State as S +import Nes.APU.State.DMC +import Nes.APU.State.Envelope +import Nes.APU.State.Filter.Class +import Nes.APU.State.FrameCounter +import qualified Nes.APU.State.FrameCounter as FC +import Nes.APU.State.LengthCounter +import Nes.APU.State.Noise +import Nes.APU.State.Pulse +import Nes.APU.State.Triangle +import Nes.Bus.SideEffect +import Nes.FlagRegister +import Prelude hiding (cycle) + +-- $use +-- The APU being a part of the CPU, they both tick at the same time. However, some ticks are updated every other CPU cycles. +-- Here the 'tick' function should be called every CPU tick, and pass as parameter whether the tick is on an even CPU cycle or not. +-- Same goes for 'tickMany'. + +type IsAPUCycle = Bool + +-- | Calls 'tick' n amount of time +-- +-- the first parameter says whether the first tick is an APU cycle or not +tick :: IsAPUCycle -> Int -> APU r () +tick _ 0 = return () +tick b n = tickOnce b >> tick (not b) (n - 1) + +{-# INLINE tickOnce #-} +tickOnce :: IsAPUCycle -> APU r () +tickOnce isAPUCycle = do + -- Ticks + tickDelayedWriteBuffer + modifyAPUStateWithSideEffect $ modifyDMC' tickDMC + modifyAPUState $ + modifyTriangle tickTriangle + . modifyNoise tickNoise + when isAPUCycle $ do + modifyAPUState $ + modifyPulse1 tickPulse + . modifyPulse2 tickPulse + tickFrameCounter + + -- Mixing + sample <- withAPUState getMixerOutput + modifyFilterChain $ consume sample + modifyAPUState $ \st -> st{sampleTimer = sampleTimer st - 1} + sampleTimer' <- withAPUState sampleTimer + when (sampleTimer' <= 1) $ do + filterOut <- withAPUState $ output . filterChain + callback <- withAPUState pushSampleCallback + liftIO $ callback filterOut + modifyAPUState $ + \st -> st{sampleTimer = S.sampleTimer st + S.samplePeriod st} + modifyAPUState $ \st -> st{cycle = cycle st + 1} + +-- | Tells the frame counter to tick channels +-- +-- Source: https://www.nesdev.org/wiki/APU_Frame_Counter +tickFrameCounter :: APU r () +tickFrameCounter = do + reset <- withAPUState $ shouldResetSequenceStep . frameCounter + seqMode <- withAPUState $ sequenceMode . frameCounter + if reset + then resetFrameCounterSequence + else do + fc <- withAPUState frameCounter + when (shouldIncrementSequenceStep fc) $ do + case seqMode of + FourStep -> tickFrameCounterFourStep + FiveStep -> tickFrameCounterFiveStep + modifyAPUState $ modifyFrameCounter incrementSequenceStep + modifyAPUState $ modifyFrameCounter $ setCycles (+ 1) + +tickDelayedWriteBuffer :: APU r () +tickDelayedWriteBuffer = do + fc <- withAPUState frameCounter + case delayedWriteSideEffectCycle fc of + Nothing -> return () + Just 0 -> do + seqMode <- withAPUState $ sequenceMode . frameCounter + modifyAPUState $ + modifyFrameCounter $ + const fc{delayedWriteSideEffectCycle = Nothing, FC.sequenceStep = 0, cycles = 0} + when (seqMode == FiveStep) $ do + runQuarterFrameEvent + runHalfFrameEvent + Just n -> + modifyAPUState $ + modifyFrameCounter $ + const fc{delayedWriteSideEffectCycle = Just $ n - 1} + +resetFrameCounterSequence :: APU r () +resetFrameCounterSequence = do + modifyAPUState $ modifyFrameCounter resetSequence + seqMode <- withAPUState $ sequenceMode . frameCounter + inhibitFrameInterrupt <- withAPUState $ inhibitInterrupt . frameCounter + when (seqMode == FourStep && not inhibitFrameInterrupt) $ do + setFrameInterruptFlag True + +tickFrameCounterFourStep :: APU r () +tickFrameCounterFourStep = do + step <- withAPUState $ FC.sequenceStep . frameCounter + inhibitFrameInterrupt <- withAPUState $ inhibitInterrupt . frameCounter + when (step < 4) runQuarterFrameEvent + when (step == 1 || step == 3) runHalfFrameEvent + when (step == 3 && not inhibitFrameInterrupt) $ + setFrameInterruptFlag True + +tickFrameCounterFiveStep :: APU r () +tickFrameCounterFiveStep = do + step <- withAPUState $ FC.sequenceStep . frameCounter + when (step < 5 && step /= 3) runQuarterFrameEvent + when (step == 1 || step == 4) runHalfFrameEvent + +runQuarterFrameEvent :: APU r () +runQuarterFrameEvent = do + modifyAPUState $ + modifyPulse1 (withEnvelope tickEnvelope) + . modifyPulse2 (withEnvelope tickEnvelope) + . modifyNoise (withEnvelope tickEnvelope) + . modifyTriangle tickTriangleLinearCounter + +runHalfFrameEvent :: APU r () +runHalfFrameEvent = modifyAPUState $ \st -> + st + { pulse1 = withLengthCounter tickLengthCounter $ tickSweepUnit (pulse1 st) + , pulse2 = withLengthCounter tickLengthCounter $ tickSweepUnit (pulse2 st) + , triangle = withLengthCounter tickLengthCounter $ triangle st + , noise = withLengthCounter tickLengthCounter $ noise st + } + +-- | Set the Frame Counter's Frame flag +{-# INLINE setFrameInterruptFlag #-} +setFrameInterruptFlag :: Bool -> APU r () +setFrameInterruptFlag b = do + setSideEffect $ setFlag IRQ + modifyAPUState $ + modifyFrameCounter $ + \fc -> fc{frameInterruptFlag = b} diff --git a/src/Nes/Bus.hs b/src/Nes/Bus.hs index 9839534..9d6444d 100644 --- a/src/Nes/Bus.hs +++ b/src/Nes/Bus.hs @@ -10,6 +10,8 @@ module Nes.Bus ( newBus, ) where +import Nes.APU.State (APUState, newAPUState) +import Nes.Bus.SideEffect (CPUSideEffect) import Nes.Controller import Nes.Internal import Nes.Memory @@ -23,9 +25,9 @@ import Nes.Rom (Rom (..)) data Bus = Bus { cpuVram :: {-# UNPACK #-} !MemoryPointer -- ^ Pointer to writeable memory - , cartridge :: {-# UNPACK #-} !Rom + , cartridge :: !Rom -- ^ Read-only memory, see 'Rom' - , controller :: {-# UNPACK #-} !Controller + , controller :: !Controller -- ^ Aka Joypad , cycles :: {-# UNPACK #-} !Integer , unsleptCycles :: {-# UNPACK #-} !Int @@ -34,16 +36,19 @@ data Bus = Bus -- ^ The function to call 'threadDelay' according to 'unsleptCycles' (> 'unsleptCyclesThreshold') -- The return value is the new number of unslept cycles , lastSleepTime :: {-# UNPACK #-} !Double - , ppuState :: {-# UNPACK #-} !PPUState + , ppuState :: !PPUState -- ^ The state of the PPU - , ppuPointers :: {-# UNPACK #-} !PPUPointers + , ppuPointers :: !PPUPointers -- ^ Memory dedicated to PPU , onNewFrame :: Bus -> IO Bus - , lastReadByte :: Byte + , lastReadByte :: {-# UNPACK #-} !Byte + -- ^ For open bus behaviour. Can be seen as data bus + , apuState :: !APUState + , cpuSideEffect :: {-# UNPACK #-} !CPUSideEffect } -newBus :: Rom -> (Bus -> IO Bus) -> (Double -> Int -> IO (Double, Int)) -> IO Bus -newBus rom_ onNewFrame_ tickCallback_ = do +newBus :: Rom -> (Bus -> IO Bus) -> (Float -> IO ()) -> (Double -> Int -> IO (Double, Int)) -> IO Bus +newBus rom_ onNewFrame_ pushSample_ tickCallback_ = do fptr <- callocForeignPtr vramSize ppuPtrs <- newPPUPointers let ppuSt = newPPUState (mirroring rom_) @@ -60,3 +65,5 @@ newBus rom_ onNewFrame_ tickCallback_ = do ppuPtrs onNewFrame_ 0 + (newAPUState pushSample_) + mempty diff --git a/src/Nes/Bus/Monad.hs b/src/Nes/Bus/Monad.hs index a51f995..520b49b 100644 --- a/src/Nes/Bus/Monad.hs +++ b/src/Nes/Bus/Monad.hs @@ -1,23 +1,29 @@ {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE MultiParamTypeClasses #-} -module Nes.Bus.Monad (BusM (..), runBusM, tick, withBus, withPPU, withController) where +module Nes.Bus.Monad (BusM (..), runBusM, tick, modifyBus, withBus, withPPU, withAPU, withController) where import Control.Monad import Control.Monad.IO.Class import qualified Data.ByteString as BS import qualified Data.ByteString.Internal as BS +import Data.Functor (($>)) import Data.Ix import Foreign +import Nes.APU.BusInterface +import Nes.APU.Monad +import Nes.APU.State +import qualified Nes.APU.Tick as APU import Nes.Bus import Nes.Bus.Constants +import Nes.Bus.SideEffect (CPUSideEffect) import Nes.Controller import Nes.FlagRegister (clearFlag) import Nes.Memory import Nes.PPU.Constants (oamDataSize) import Nes.PPU.Monad hiding (tick) import qualified Nes.PPU.Monad as PPUM -import Nes.PPU.State +import Nes.PPU.State hiding (cycles) import Nes.Rom newtype BusM r a = MkBusM {unBusM :: Bus -> (Bus -> a -> IO r) -> IO r} deriving (Functor) @@ -61,6 +67,12 @@ withPPU f = MkBusM $ \bus cont -> do (res, ppuSt) <- runPPU (ppuState bus) (ppuPointers bus) (cartridge bus) f cont (bus{ppuState = ppuSt}) res +{-# INLINE withAPU #-} +withAPU :: APU (a, APUState, CPUSideEffect) a -> BusM r a +withAPU f = MkBusM $ \bus cont -> do + (!res, !apuSt, !cpuEff) <- runAPU (apuState bus) f + cont (bus{apuState = apuSt, cpuSideEffect = cpuEff}) res + {-# INLINE withController #-} withController :: ControllerM (a, Controller) a -> BusM r a withController f = MkBusM $ \bus cont -> @@ -79,26 +91,37 @@ tick n = MkBusM $ \bus cont -> do isNewFrame <- PPUM.tick (n * 3) after <- withPPUState nmiInterrupt return (isNewFrame, before, after) - - let bus' = bus{unsleptCycles = newUnsleptCycles, ppuState = ppuSt, Nes.Bus.cycles = fromIntegral n + Nes.Bus.cycles bus, lastSleepTime = newLastSleepTime} + ((), !apuSt, !cpuEff) <- runAPU (apuState bus) $ APU.tick (odd (Nes.Bus.cycles bus)) n + let bus' = + bus + { unsleptCycles = newUnsleptCycles + , ppuState = ppuSt + , apuState = apuSt + , cycles = fromIntegral n + cycles bus + , lastSleepTime = newLastSleepTime + , cpuSideEffect = cpuEff + } if not nmiBefore && nmiAfter then onNewFrame bus' bus' >>= flip cont () else cont bus' () +data BusReadOutput = OpenBus | DataBus Byte | Internal Byte + instance MemoryInterface () (BusM r) where - readByte idx () = do - res <- go - modifyBus $ \b -> b{lastReadByte = res} - return res + readByte idx () = + go >>= \case + DataBus byte -> modifyBus (\b -> b{lastReadByte = byte}) $> byte + OpenBus -> withBus lastReadByte + Internal byte -> return byte where go | inRange ramRange idx = do let mirroredDownAddr = idx .&. 0b11111111111 -- 11 bits - liftIO . readByte mirroredDownAddr =<< withBus cpuVram + fmap DataBus . liftIO . readByte mirroredDownAddr =<< withBus cpuVram | inRange ppuRegisters idx = do let mirroredIdx = Addr . fromIntegral $ addrToInt (idx - fst ppuRegisters) `mod` 8 - onInvalidRead = return 0 + onInvalidRead = return $ DataBus 0 case mirroredIdx of 0 -> if idx == 0x2000 @@ -107,26 +130,38 @@ instance MemoryInterface () (BusM r) where let addr1 = idx .&. 0b0010000000000111 in - readByte addr1 () + DataBus <$> readByte addr1 () 1 -> onInvalidRead 2 -> withPPU $ do st <- readStatus -- https://www.nesdev.org/wiki/PPU_registers#PPUSTATUS_-_Rendering_events_($2002_read) modifyPPUState $ modifyStatusRegister $ clearFlag VBlankStarted - return st + return $ DataBus st 3 -> onInvalidRead - 4 -> withPPU readOamData + 4 -> DataBus <$> withPPU readOamData 5 -> onInvalidRead 6 -> onInvalidRead - 7 -> withPPU readData + 7 -> DataBus <$> withPPU readData _ -> error "Cannot happen" | inRange prgRomRange idx = do rom <- withBus cartridge - readPrgRomAddr (idx - fst prgRomRange) rom readByte - | idx == 0x4014 = return 0 - | idx == 0x4016 = withController readButtonStatus - | idx == 0x4017 = return 0 -- Second joypad, ignore - | otherwise = withBus lastReadByte + DataBus <$> readPrgRomAddr (idx - fst prgRomRange) rom readByte + | idx == 0x4014 = return $ DataBus 0 + | idx == 0x4016 = DataBus <$> withController readButtonStatus + | idx == 0x4017 = return $ DataBus 0 -- Second joypad, ignore + | (0x4000, 0x4017) `inRange` idx = do + res <- withAPU $ readFromAPU idx + case res of + Nothing -> return OpenBus + Just b -> do + b' <- do + if idx == 0x4015 + then do + bit5 <- withBus $ (`testBit` 5) . lastReadByte + return $ if bit5 then b `setBit` 5 else b `clearBit` 5 + else return b + return $ Internal b' + | otherwise = return OpenBus writeByte byte idx () = guardWriteBound idx go where @@ -168,7 +203,7 @@ instance MemoryInterface () (BusM r) where -- TODO 2) Not sure about about the tick count tick (513 + fromEnum (odd cycles_)) | idx == 0x4016 = withController $ setStrobe byte - | idx == 0x4017 = pure () -- Second joypad, ignore + | (0x4000, 0x4017) `inRange` idx = withAPU $ writeToAPU idx byte | otherwise = pure () -- liftIO $ printf "Ignoring write at %4x\n" $ unAddr idx readAddr idx () = do low <- readByte idx () diff --git a/src/Nes/Bus/SideEffect.hs b/src/Nes/Bus/SideEffect.hs new file mode 100644 index 0000000..d737768 --- /dev/null +++ b/src/Nes/Bus/SideEffect.hs @@ -0,0 +1,23 @@ +module Nes.Bus.SideEffect (CPUSideEffect (..), CPUSideEffectFlag (..)) where + +import Data.Bits ((.|.)) +import Nes.FlagRegister +import Nes.Memory + +newtype CPUSideEffect = MkSE {unSE :: Byte} + +data CPUSideEffectFlag = IRQ | DMCDMA deriving (Eq, Show, Enum) + +instance FlagRegister CPUSideEffect where + type Flag CPUSideEffect = CPUSideEffectFlag + fromByte = MkSE + toByte = unSE + flagToBitOffset = fromEnum + +instance Semigroup CPUSideEffect where + {-# INLINE (<>) #-} + MkSE se1 <> MkSE se2 = MkSE (se1 .|. se2) + +instance Monoid CPUSideEffect where + {-# INLINE mempty #-} + mempty = MkSE 0 diff --git a/src/Nes/CPU/Monad.hs b/src/Nes/CPU/Monad.hs index 0718359..455c840 100644 --- a/src/Nes/CPU/Monad.hs +++ b/src/Nes/CPU/Monad.hs @@ -7,6 +7,8 @@ module Nes.CPU.Monad ( -- * Interracting with bus withBus, + withBusState, + setSideEffect, -- * State modifyCPUState, @@ -35,12 +37,17 @@ module Nes.CPU.Monad ( unsafeWithBus, ) where +import Control.Monad import Control.Monad.IO.Class import Data.Bits (Bits (shiftR), testBit) +import Nes.APU.Monad (modifyAPUState) +import Nes.APU.State (APUState (dmc), modifyDMC) +import Nes.APU.State.DMC (DMC (sampleBuffer, sampleBufferAddr)) import Nes.Bus (Bus (..)) import Nes.Bus.Constants import Nes.Bus.Monad (BusM, runBusM) import qualified Nes.Bus.Monad as BusM +import Nes.Bus.SideEffect import Nes.CPU.State import Nes.FlagRegister import Nes.Interrupt @@ -58,16 +65,16 @@ newtype CPU r a = MkCPU instance Applicative (CPU r) where {-# INLINE pure #-} - pure a = MkCPU $ \st prog cont -> cont st prog a + pure a = MkCPU $ \st bus cont -> cont st bus a {-# INLINE (<*>) #-} - (MkCPU f) <*> (MkCPU a) = MkCPU $ \st prog cont -> f st prog $ + (MkCPU f) <*> (MkCPU a) = MkCPU $ \st bus cont -> f st bus $ \st' prog' f' -> a st' prog' $ \st'' prog'' a' -> cont st'' prog'' $ f' a' instance Monad (CPU r) where {-# INLINE (>>=) #-} - (MkCPU a) >>= next = MkCPU $ \st prog cont -> a st prog $ - \st' prog' a' -> unCPU (next a') st' prog' cont + (MkCPU a) >>= next = MkCPU $ \st bus cont -> a st bus $ + \st' bus' a' -> unCPU (next a') st' bus' cont instance MonadFail (CPU r) where {-# INLINE fail #-} @@ -75,19 +82,25 @@ instance MonadFail (CPU r) where instance MonadIO (CPU r) where {-# INLINE liftIO #-} - liftIO io = MkCPU $ \st prog cont -> io >>= cont st prog + liftIO io = MkCPU $ \st bus cont -> io >>= cont st bus {-# INLINE modifyCPUState #-} modifyCPUState :: (CPUState -> CPUState) -> CPU r () -modifyCPUState f = MkCPU $ \st prog cont -> cont (f st) prog () +modifyCPUState f = MkCPU $ \st bus cont -> cont (f st) bus () {-# INLINE withCPUState #-} withCPUState :: (CPUState -> a) -> CPU r a -withCPUState f = MkCPU $ \st prog cont -> cont st prog (f st) +withCPUState f = MkCPU $ \st bus cont -> cont st bus (f st) + +withBusState :: (Bus -> a) -> CPU r a +withBusState f = MkCPU $ \st bus cont -> cont st bus (f bus) {-# INLINE getCycles #-} getCycles :: CPU r Integer -getCycles = MkCPU $ \st bus cont -> cont st bus (cycles bus) +getCycles = withBusState cycles + +setSideEffect :: (CPUSideEffect -> CPUSideEffect) -> CPU r () +setSideEffect f = MkCPU $ \st bus cont -> cont st bus{cpuSideEffect = f $ cpuSideEffect bus} () {-# INLINE getPC #-} @@ -95,7 +108,6 @@ getCycles = MkCPU $ \st bus cont -> cont st bus (cycles bus) getPC :: CPU r Addr getPC = withCPUState programCounter -{-# INLINE setPC #-} setPC :: Addr -> CPU r () setPC addr = modifyCPUState $ \st -> st{programCounter = addr} @@ -134,9 +146,12 @@ pushAddrStack addr = do {-# INLINE withBus #-} withBus :: BusM (a, Bus) a -> CPU r a -withBus f = MkCPU $ \st bus cont -> do - (res, bus') <- runBusM bus f - cont st bus' res +withBus f = do + res <- MkCPU $ \st bus cont -> do + (res, bus') <- runBusM bus f + cont st bus' res + handleSideEffect + return res -- | Unsafe action that provides access to Bus -- @@ -194,10 +209,17 @@ instance MemoryInterface () (CPU r) where {-# INLINE tick #-} tick :: Int -> CPU r () -tick n = MkCPU $ \st bus cont -> do - ((), newbus) <- runBusM bus $ BusM.tick n - cont st newbus () +tick = withBus . BusM.tick {-# INLINE tickOnce #-} tickOnce :: CPU r () tickOnce = Nes.CPU.Monad.tick 1 + +handleSideEffect :: CPU r () +handleSideEffect = do + hasDMCDMA <- withBusState $ getFlag DMCDMA . cpuSideEffect + when hasDMCDMA $ withBus $ do + sampleByteAddr <- BusM.withBus $ sampleBufferAddr . dmc . apuState + sample <- Nes.Memory.readByte sampleByteAddr () + BusM.withAPU $ modifyAPUState $ modifyDMC $ \d -> d{sampleBuffer = Just sample} + BusM.modifyBus $ \b -> b{cpuSideEffect = clearFlag DMCDMA (cpuSideEffect b)} diff --git a/test/nestest/Spec.hs b/test/nestest/Spec.hs index 793bc6e..821fba2 100644 --- a/test/nestest/Spec.hs +++ b/test/nestest/Spec.hs @@ -38,7 +38,7 @@ spec = it "Trace should match logfile" $ do rom <- do eitherRom <- fromFile "test/assets/rom.nes" either fail return eitherRom - bus <- newBus rom pure (\a b -> return (a, b)) + bus <- newBus rom pure (\_ -> pure ()) (\a b -> return (a, b)) traceRef <- newIORef (T [] 0) let st = newCPUState{programCounter = 0xc000} -- TODO why is the tick count set to 7 ? Reset? diff --git a/test/unit/Internal.hs b/test/unit/Internal.hs index 7027cec..58a8a32 100644 --- a/test/unit/Internal.hs +++ b/test/unit/Internal.hs @@ -21,7 +21,7 @@ withStateAndMemorySetup :: (CPUState -> Bus -> IO r') -> IO () withStateAndMemorySetup program st memSetup post = do - bus <- newBus unsafeEmptyRom pure (\a b -> return (a, b)) + bus <- newBus unsafeEmptyRom pure (\_ -> pure ()) (\a b -> return (a, b)) loadProgramToMemory program bus _ <- memSetup bus -- Not we do not read 0xfffc because it's out of the bus read