module Resource.Opus ( Config(..) , Source , load , loadPCM ) where import RIO import Foreign qualified import Foreign.C.Types (CFloat(..)) import GHC.Stack (withFrozenCallStack) import Sound.OpenAL.FFI.AL qualified as AL import Sound.OpenAL.FFI.ALC qualified as ALC import Sound.OpusFile qualified as OpusFile import Resource.Source qualified as Resource data Config = Config { gain :: Float , loopingMode :: Bool , byteSource :: Resource.Source } type Source = (Double, AL.Source) load :: ( MonadIO m , MonadReader env m , HasLogFunc env , HasCallStack ) => ALC.Device -> Config -> m Source load _device Config{..} = do pcm <- withFrozenCallStack $ Resource.load loadPCM byteSource alFormat <- case OpusFile.pcmChannels pcm of Right OpusFile.Stereo -> pure AL.FORMAT_STEREO16 Right OpusFile.Mono -> pure AL.FORMAT_MONO16 Left n -> do logError $ mconcat [ "unexpected channels in " , displayShow byteSource , ": " , displayShow n ] exitFailure buffer@(AL.Buffer bufId) <- liftIO $ Foreign.alloca \bufPtr -> do AL.alGenBuffers 1 bufPtr Foreign.peek bufPtr liftIO $ Foreign.withForeignPtr (OpusFile.pcmData pcm) \pcmData -> AL.alBufferData buffer alFormat (Foreign.castPtr pcmData) (fromIntegral $ OpusFile.pcmSize pcm) 48000 source <- liftIO $ Foreign.alloca \sourcePtr -> do AL.alGenSources 1 sourcePtr Foreign.peek sourcePtr liftIO do AL.alSourcei source AL.BUFFER (fromIntegral bufId) -- XXX: unsigned to signed conversion AL.alSourcei source AL.LOOPING (bool 0 1 loopingMode) Foreign.with (CFloat gain) $ AL.alSourcefv source AL.GAIN -- XXX: exempt from distance attenuation Foreign.with (CFloat 0) $ AL.alSourcefv source AL.ROLLOFF_FACTOR pure (OpusFile.pcmTime pcm, source) -- TODO: extract to `opusfile` loadPCM :: MonadIO m => ByteString -> m (OpusFile.Pcm Int16) loadPCM bytes = do !opusBytes <- liftIO $ OpusFile.openMemoryBS bytes >>= \case Left err -> throwString $ "Opus loader error: " <> show err Right res -> pure res !pcmInt16 <- liftIO $ OpusFile.decodeInt16 opusBytes liftIO $ OpusFile.free opusBytes pure pcmInt16