diff --git a/ChangeLog.md b/ChangeLog.md
new file mode 100644
--- /dev/null
+++ b/ChangeLog.md
@@ -0,0 +1,7 @@
+# Changelog for openal-ffi
+
+## [0.0.1]
+
+- Initial import.
+
+[0.0.1]: https://gitlab.com/dpwiz/openal-ffi/-/tree/v0.0.1
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,30 @@
+Copyright Author name here (c) 2021
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Author name here nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,16 @@
+# openal-ffi
+
+> Very low-level bindings to the [OpenAL] library.
+
+- No OpenGL dependencies.
+- No point/vector/tensor/spaces dependencies.
+- No state and no resource management.
+- Modest DLL size.
+
+## Examples
+
+- [C API overview](https://ffainelli.github.io/openal-example/)
+- [Buffer-playing example](https://gitlab.com/dpwiz/openal-ffi/-/blob/main/test/Spec.hs)
+- [Opus-playing example](https://gitlab.com/dpwiz/opusfile/-/blob/master/examples/openal-playfile/Main.hs)
+
+[OpenAL]: https://openal.org/
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/openal-ffi.cabal b/openal-ffi.cabal
new file mode 100644
--- /dev/null
+++ b/openal-ffi.cabal
@@ -0,0 +1,55 @@
+cabal-version: 1.12
+
+-- This file has been generated from package.yaml by hpack version 0.34.4.
+--
+-- see: https://github.com/sol/hpack
+
+name:           openal-ffi
+version:        0.0.1
+synopsis:       Low-level bindings to OpenAL.
+category:       Sound
+author:         IC Rainbow
+maintainer:     aenor.realm@gmail.com
+copyright:      2021 IC Rainbow
+license:        BSD3
+license-file:   LICENSE
+build-type:     Simple
+extra-source-files:
+    README.md
+    ChangeLog.md
+
+source-repository head
+  type: git
+  location: https://gitlab.com/dpwiz/openal-ffi
+
+library
+  exposed-modules:
+      Sound.OpenAL.FFI.AL
+      Sound.OpenAL.FFI.ALC
+      Sound.OpenAL.FFI.Utils
+  other-modules:
+      Paths_openal_ffi
+  hs-source-dirs:
+      src
+  ghc-options: -Wall
+  extra-libraries:
+      openal
+  build-depends:
+      base >=4.7 && <5
+  if os(darwin) || os(ios)
+    frameworks:
+        OpenAL
+  default-language: Haskell2010
+
+test-suite openal-ffi-test
+  type: exitcode-stdio-1.0
+  main-is: Spec.hs
+  other-modules:
+      Paths_openal_ffi
+  hs-source-dirs:
+      test
+  ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
+  build-depends:
+      base >=4.7 && <5
+    , openal-ffi
+  default-language: Haskell2010
diff --git a/src/Sound/OpenAL/FFI/AL.hs b/src/Sound/OpenAL/FFI/AL.hs
new file mode 100644
--- /dev/null
+++ b/src/Sound/OpenAL/FFI/AL.hs
@@ -0,0 +1,304 @@
+{-# LANGUAGE PatternSynonyms #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE GeneralisedNewtypeDeriving #-}
+
+module Sound.OpenAL.FFI.AL where
+
+import Foreign (FunPtr, Ptr, Storable)
+import Foreign.C.Types
+import Foreign.C.String (CString)
+
+import Sound.OpenAL.FFI.Utils (Dynamic)
+
+-- * Source
+
+newtype Source = Source CUInt
+  deriving stock (Eq, Ord, Show)
+  deriving newtype (Storable)
+
+foreign import ccall unsafe "alGenSources"
+  alGenSources :: CInt -> Ptr Source -> IO ()
+
+foreign import ccall unsafe "alDeleteSources"
+  alDeleteSources :: CInt -> Ptr Source -> IO ()
+
+foreign import ccall unsafe "alIsSource"
+  alIsSource :: Source -> IO CChar
+
+foreign import ccall unsafe "alGetSourcefv"
+  alGetSourcefv :: Source -> CInt -> Ptr CFloat -> IO ()
+
+foreign import ccall unsafe "alSourcefv"
+  alSourcefv :: Source -> CInt -> Ptr CFloat -> IO ()
+
+foreign import ccall unsafe "alGetSourceiv"
+  alGetSourceiv :: Source ->  CInt -> Ptr CInt -> IO ()
+
+foreign import ccall unsafe "alSourcei"
+  alSourcei :: Source -> CInt -> CInt -> IO ()
+
+foreign import ccall unsafe "alSourceQueueBuffers"
+  alSourceQueueBuffers :: Source -> CInt -> Ptr Buffer -> IO ()
+
+foreign import ccall unsafe "alSourceUnqueueBuffers"
+  alSourceUnqueueBuffers :: Source -> CInt -> Ptr Buffer -> IO ()
+
+foreign import ccall unsafe "alSourcePlayv"
+  alSourcePlayv :: CInt -> Ptr Source -> IO ()
+
+foreign import ccall unsafe "alSourcePausev"
+  alSourcePausev :: CInt -> Ptr Source -> IO ()
+
+foreign import ccall unsafe "alSourceStopv"
+  alSourceStopv :: CInt -> Ptr Source -> IO ()
+
+foreign import ccall unsafe "alSourceRewindv"
+  alSourceRewindv :: CInt -> Ptr Source -> IO ()
+
+-- * Buffer
+
+newtype Buffer = Buffer CUInt
+  deriving stock (Eq, Ord, Show)
+  deriving newtype (Storable)
+
+foreign import ccall unsafe "alGenBuffers"
+  alGenBuffers :: CInt -> Ptr Buffer -> IO ()
+
+foreign import ccall unsafe "alDeleteBuffers"
+  alDeleteBuffers :: CInt -> Ptr Buffer -> IO ()
+
+foreign import ccall unsafe "alBufferData"
+  alBufferData :: Buffer -> CInt -> Ptr () -> CInt -> CInt -> IO ()
+
+foreign import ccall unsafe "alGetBufferi"
+  alGetBufferi :: Buffer -> CInt -> Ptr CInt -> IO ()
+
+foreign import ccall unsafe "alIsBuffer"
+  alIsBuffer :: Buffer -> IO CChar
+
+-- * Errors
+
+foreign import ccall unsafe "alGetError"
+  alGetError :: IO CInt
+
+pattern INVALID_NAME :: (Eq a, Num a) => a
+pattern INVALID_NAME = 0xA001
+
+pattern INVALID_ENUM :: (Eq a, Num a) => a
+pattern INVALID_ENUM = 0xA002
+
+pattern INVALID_VALUE :: (Eq a, Num a) => a
+pattern INVALID_VALUE = 0xA003
+
+pattern INVALID_OPERATION :: (Eq a, Num a) => a
+pattern INVALID_OPERATION = 0xA004
+
+pattern OUT_OF_MEMORY :: (Eq a, Num a) => a
+pattern OUT_OF_MEMORY = 0xA005
+
+-- * Listener
+
+foreign import ccall unsafe "alListenerf"
+  alListenerf :: CInt -> CFloat -> IO ()
+
+foreign import ccall unsafe "alListenerfv"
+  alListenerfv :: CInt -> Ptr () -> IO ()
+
+foreign import ccall unsafe "alGetListenerfv"
+  alGetListenerfv :: CInt -> Ptr CFloat -> IO ()
+
+-- * Queries
+
+foreign import ccall unsafe "alGetEnumValue"
+  alGetEnumValue :: CString -> IO CInt
+
+foreign import ccall unsafe "alGetFloat"
+  alGetFloat :: CInt -> IO CFloat
+
+foreign import ccall unsafe "alGetIntegerv"
+  alGetIntegerv :: CInt -> Ptr CInt -> IO ()
+
+foreign import ccall unsafe "alDistanceModel"
+  alDistanceModel :: CInt -> IO ()
+
+-- ** Strings
+
+foreign import ccall unsafe "alGetString"
+  alGetString :: CInt -> IO CString
+
+pattern VERSION :: (Eq a, Num a) => a
+pattern VERSION = 0xB002
+
+pattern RENDERER :: (Eq a, Num a) => a
+pattern RENDERER = 0xB003
+
+pattern VENDOR :: (Eq a, Num a) => a
+pattern VENDOR = 0xB001
+
+pattern EXTENSIONS :: (Eq a, Num a) => a
+pattern EXTENSIONS = 0xB004
+
+-- ** Extensions
+
+foreign import ccall unsafe "alIsExtensionPresent"
+  alIsExtensionPresent_ :: CString -> IO CBool
+
+foreign import ccall unsafe "alGetProcAddress"
+  alGetProcAddress :: CString -> IO (FunPtr a)
+
+foreign import ccall unsafe "dynamic"
+  invokeWithFloat :: Dynamic (CFloat -> IO ())
+
+-- * Enums
+
+pattern DISTANCE_MODEL :: (Eq a, Num a) => a
+pattern DISTANCE_MODEL = 0xD000
+
+pattern DOPPLER_FACTOR :: (Eq a, Num a) => a
+pattern DOPPLER_FACTOR = 0xC000
+
+pattern SPEED_OF_SOUND :: (Eq a, Num a) => a
+pattern SPEED_OF_SOUND = 0xC003
+
+
+pattern INVERSE_DISTANCE :: (Eq a, Num a) => a
+pattern INVERSE_DISTANCE = 0xD001
+
+pattern INVERSE_DISTANCE_CLAMPED :: (Eq a, Num a) => a
+pattern INVERSE_DISTANCE_CLAMPED = 0xD002
+
+pattern LINEAR_DISTANCE :: (Eq a, Num a) => a
+pattern LINEAR_DISTANCE = 0xD003
+
+pattern LINEAR_DISTANCE_CLAMPED :: (Eq a, Num a) => a
+pattern LINEAR_DISTANCE_CLAMPED = 0xD004
+
+pattern EXPONENT_DISTANCE :: (Eq a, Num a) => a
+pattern EXPONENT_DISTANCE = 0xD005
+
+pattern EXPONENT_DISTANCE_CLAMPED :: (Eq a, Num a) => a
+pattern EXPONENT_DISTANCE_CLAMPED = 0xD006
+
+
+pattern POSITION :: (Eq a, Num a) => a
+pattern POSITION = 0x1004
+
+pattern VELOCITY :: (Eq a, Num a) => a
+pattern VELOCITY = 0x1006
+
+pattern GAIN :: (Eq a, Num a) => a
+pattern GAIN = 0x100A
+
+
+pattern ORIENTATION :: (Eq a, Num a) => a
+pattern ORIENTATION = 0x100F
+
+
+pattern SOURCE_RELATIVE :: (Eq a, Num a) => a
+pattern SOURCE_RELATIVE = 0x0202
+
+pattern SOURCE_TYPE :: (Eq a, Num a) => a
+pattern SOURCE_TYPE = 0x1027
+
+pattern LOOPING :: (Eq a, Num a) => a
+pattern LOOPING = 0x1007
+
+pattern BUFFER :: (Eq a, Num a) => a
+pattern BUFFER = 0x1009
+
+pattern BUFFERS_QUEUED :: (Eq a, Num a) => a
+pattern BUFFERS_QUEUED = 0x1015
+
+pattern BUFFERS_PROCESSED :: (Eq a, Num a) => a
+pattern BUFFERS_PROCESSED = 0x1016
+
+pattern MIN_GAIN :: (Eq a, Num a) => a
+pattern MIN_GAIN = 0x100D
+
+pattern MAX_GAIN :: (Eq a, Num a) => a
+pattern MAX_GAIN = 0x100E
+
+pattern REFERENCE_DISTANCE :: (Eq a, Num a) => a
+pattern REFERENCE_DISTANCE = 0x1020
+
+pattern ROLLOFF_FACTOR :: (Eq a, Num a) => a
+pattern ROLLOFF_FACTOR = 0x1021
+
+pattern MAX_DISTANCE :: (Eq a, Num a) => a
+pattern MAX_DISTANCE = 0x1023
+
+pattern PITCH :: (Eq a, Num a) => a
+pattern PITCH = 0x1003
+
+pattern DIRECTION :: (Eq a, Num a) => a
+pattern DIRECTION = 0x1005
+
+pattern CONE_INNER_ANGLE :: (Eq a, Num a) => a
+pattern CONE_INNER_ANGLE = 0x1001
+
+pattern CONE_OUTER_ANGLE :: (Eq a, Num a) => a
+pattern CONE_OUTER_ANGLE = 0x1002
+
+pattern CONE_OUTER_GAIN :: (Eq a, Num a) => a
+pattern CONE_OUTER_GAIN = 0x1022
+
+pattern SEC_OFFSET :: (Eq a, Num a) => a
+pattern SEC_OFFSET = 0x1024
+
+pattern SAMPLE_OFFSET :: (Eq a, Num a) => a
+pattern SAMPLE_OFFSET = 0x1025
+
+pattern BYTE_OFFSET :: (Eq a, Num a) => a
+pattern BYTE_OFFSET = 0x1026
+
+pattern SOURCE_STATE :: (Eq a, Num a) => a
+pattern SOURCE_STATE = 0x1010
+
+
+pattern UNDETERMINED :: (Eq a, Num a) => a
+pattern UNDETERMINED = 0x1030
+
+pattern STATIC :: (Eq a, Num a) => a
+pattern STATIC = 0x1028
+
+pattern STREAMING :: (Eq a, Num a) => a
+pattern STREAMING = 0x1029
+
+
+pattern INITIAL :: (Eq a, Num a) => a
+pattern INITIAL = 0x1011
+
+pattern PLAYING :: (Eq a, Num a) => a
+pattern PLAYING = 0x1012
+
+pattern PAUSED :: (Eq a, Num a) => a
+pattern PAUSED  = 0x1013
+
+pattern STOPPED :: (Eq a, Num a) => a
+pattern STOPPED = 0x1014
+
+
+pattern FREQUENCY :: (Eq a, Num a) => a
+pattern FREQUENCY = 0x2001
+
+pattern SIZE :: (Eq a, Num a) => a
+pattern SIZE = 0x2004
+
+pattern BITS :: (Eq a, Num a) => a
+pattern BITS = 0x2002
+
+pattern CHANNELS :: (Eq a, Num a) => a
+pattern CHANNELS = 0x2003
+
+
+pattern FORMAT_MONO8 :: (Eq a, Num a) => a
+pattern FORMAT_MONO8 = 0x1100
+
+pattern FORMAT_MONO16 :: (Eq a, Num a) => a
+pattern FORMAT_MONO16 = 0x1101
+
+pattern FORMAT_STEREO8 :: (Eq a, Num a) => a
+pattern FORMAT_STEREO8 = 0x1102
+
+pattern FORMAT_STEREO16 :: (Eq a, Num a) => a
+pattern FORMAT_STEREO16 = 0x1103
diff --git a/src/Sound/OpenAL/FFI/ALC.hs b/src/Sound/OpenAL/FFI/ALC.hs
new file mode 100644
--- /dev/null
+++ b/src/Sound/OpenAL/FFI/ALC.hs
@@ -0,0 +1,151 @@
+{-# LANGUAGE PatternSynonyms #-}
+
+module Sound.OpenAL.FFI.ALC where
+
+import Foreign (FunPtr, Ptr, nullPtr)
+import Foreign.C.Types (CChar(..), CInt(..), CUInt(..))
+import Foreign.C.String (CString)
+import Sound.OpenAL.FFI.Utils (Dynamic)
+
+-- * Context
+
+newtype Context = Context (Ptr Context)
+  deriving (Eq, Ord, Show)
+
+foreign import ccall unsafe "alcCreateContext"
+  alcCreateContext :: Device -> Ptr CInt -> IO Context
+
+foreign import ccall unsafe "alcDestroyContext"
+  alcDestroyContext :: Context -> IO ()
+
+foreign import ccall unsafe "alcGetCurrentContext"
+  alcGetCurrentContext :: IO Context
+
+foreign import ccall unsafe "alcMakeContextCurrent"
+  alcMakeContextCurrent :: Context -> IO CChar
+
+foreign import ccall unsafe "alcSuspendContext"
+  alcSuspendContext :: Context -> IO ()
+
+foreign import ccall unsafe "alcProcessContext"
+  alcProcessContext :: Context -> IO ()
+
+foreign import ccall unsafe "alcGetContextsDevice"
+  alcGetContextsDevice :: Context -> IO Device
+
+-- * Device
+
+newtype Device = Device (Ptr Device)
+  deriving (Eq, Ord, Show)
+
+nullDevice :: Device
+nullDevice = Device nullPtr
+
+foreign import ccall unsafe "alcOpenDevice"
+  alcOpenDevice :: CString -> IO Device
+
+foreign import ccall unsafe "alcCloseDevice"
+  alcCloseDevice :: Device -> IO CChar
+
+-- * Errors
+
+foreign import ccall unsafe "alcGetError"
+  alcGetError :: Device -> IO CInt
+
+pattern INVALID_DEVICE :: (Eq a, Num a) => a
+pattern INVALID_DEVICE = 0xA001
+
+pattern INVALID_CONTEXT :: (Eq a, Num a) => a
+pattern INVALID_CONTEXT = 0xA002
+
+pattern INVALID_ENUM :: (Eq a, Num a) => a
+pattern INVALID_ENUM = 0xA003
+
+pattern INVALID_VALUE :: (Eq a, Num a) => a
+pattern INVALID_VALUE = 0xA004
+
+pattern INVALID_OPERATION :: (Eq a, Num a) => a
+pattern INVALID_OPERATION = 0xA006
+
+pattern OUT_OF_MEMORY :: (Eq a, Num a) => a
+pattern OUT_OF_MEMORY = 0xA005
+
+-- * Extensions
+
+foreign import ccall unsafe "alcGetProcAddress"
+  alcGetProcAddress :: Device -> CString -> IO (FunPtr a)
+
+foreign import ccall unsafe "alcGetEnumValue"
+  alcGetEnumValue :: Device -> CString -> IO CInt
+
+-- * Queries
+
+foreign import ccall unsafe "alcGetString"
+  alcGetString :: Device -> CInt -> IO CString
+
+foreign import ccall unsafe "alcGetIntegerv"
+  alcGetIntegerv :: Device -> CInt -> CInt -> Ptr CInt -> IO ()
+
+foreign import ccall unsafe "alcIsExtensionPresent"
+  alcIsExtensionPresent_ :: Device -> CString -> IO CChar
+
+-- * Capture
+
+foreign import ccall unsafe "dynamic"
+  invokeCaptureOpenDevice :: Dynamic (CString -> CUInt -> CInt -> CInt -> IO Device)
+
+foreign import ccall unsafe "dynamic"
+  invokeCaptureStartStop :: Dynamic (Device -> IO ())
+
+foreign import ccall unsafe "dynamic"
+  invokeCaptureSamples :: Dynamic (Device -> Ptr a -> CInt -> IO ())
+
+foreign import ccall unsafe "dynamic"
+  invokeCaptureCloseDevice :: Dynamic (Device -> IO CChar)
+
+-- * Enums
+
+pattern FREQUENCY :: (Eq a, Num a) => a
+pattern FREQUENCY = 0x1007
+
+pattern REFRESH :: (Eq a, Num a) => a
+pattern REFRESH = 0x1008
+
+pattern SYNC :: (Eq a, Num a) => a
+pattern SYNC = 0x1009
+
+pattern MONO_SOURCES :: (Eq a, Num a) => a
+pattern MONO_SOURCES = 0x1010
+
+pattern STEREO_SOURCES :: (Eq a, Num a) => a
+pattern STEREO_SOURCES = 0x1011
+
+pattern DEFAULT_DEVICE_SPECIFIER :: (Eq a, Num a) => a
+pattern DEFAULT_DEVICE_SPECIFIER = 0x1004
+
+pattern DEVICE_SPECIFIER :: (Eq a, Num a) => a
+pattern DEVICE_SPECIFIER = 0x1005
+
+pattern EXTENSIONS :: (Eq a, Num a) => a
+pattern EXTENSIONS = 0x1006
+
+pattern CAPTURE_DEFAULT_DEVICE_SPECIFIER :: (Eq a, Num a) => a
+pattern CAPTURE_DEFAULT_DEVICE_SPECIFIER = 0x0311
+
+pattern CAPTURE_DEVICE_SPECIFIER :: (Eq a, Num a) => a
+pattern CAPTURE_DEVICE_SPECIFIER = 0x0310
+
+pattern ATTRIBUTES_SIZE :: (Eq a, Num a) => a
+pattern ATTRIBUTES_SIZE = 0x1002
+
+pattern ALL_ATTRIBUTES :: (Eq a, Num a) => a
+pattern ALL_ATTRIBUTES = 0x1003
+
+pattern MAJOR_VERSION :: (Eq a, Num a) => a
+pattern MAJOR_VERSION = 0x1000
+
+pattern MINOR_VERSION :: (Eq a, Num a) => a
+pattern MINOR_VERSION = 0x1001
+
+pattern CAPTURE_SAMPLES :: (Eq a, Num a) => a
+pattern CAPTURE_SAMPLES = 0x0312
diff --git a/src/Sound/OpenAL/FFI/Utils.hs b/src/Sound/OpenAL/FFI/Utils.hs
new file mode 100644
--- /dev/null
+++ b/src/Sound/OpenAL/FFI/Utils.hs
@@ -0,0 +1,20 @@
+module Sound.OpenAL.FFI.Utils
+  ( Dynamic
+  , peekCStrings
+  ) where
+
+import Foreign (FunPtr, lengthArray0, plusPtr)
+import Foreign.C.String (CString, peekCAString)
+
+type Dynamic a = FunPtr a -> a
+
+peekCStrings :: CString -> IO [String]
+peekCStrings = go []
+  where
+    go acc ptr = do
+      str <- peekCAString ptr
+      if null str then
+        pure $ reverse acc
+      else do
+        len <- lengthArray0 0 ptr
+        go (str : acc) $ plusPtr ptr (len + 1)
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,93 @@
+{-# LANGUAGE BlockArguments #-}
+{-# LANGUAGE TupleSections #-}
+
+import Control.Concurrent (threadDelay)
+import Control.Monad (when)
+import Data.Int (Int16)
+import Foreign.C.String (peekCString, withCString)
+import qualified Foreign
+
+import qualified Sound.OpenAL.FFI.AL as AL
+import qualified Sound.OpenAL.FFI.ALC as ALC
+import Sound.OpenAL.FFI.Utils (peekCStrings)
+
+main :: IO ()
+main = do
+  extensions <- fmap words $
+    ALC.alcGetString ALC.nullDevice ALC.EXTENSIONS >>= peekCString
+  putStrLn $ "ALC: " <> show extensions
+
+  enumerationExt <- fmap (== 1) . withCString "ALC_ENUMERATION_EXT" $
+    ALC.alcIsExtensionPresent_ ALC.nullDevice
+  putStrLn $ "Enumeration available: " <> show enumerationExt
+
+  when enumerationExt do
+    allDevices <- ALC.alcGetString ALC.nullDevice ALC.DEVICE_SPECIFIER >>= peekCStrings
+    putStrLn $ "All devices: " <> show allDevices
+
+  defaultDevice <- ALC.alcGetString ALC.nullDevice ALC.DEFAULT_DEVICE_SPECIFIER >>= peekCString
+  putStrLn $ "Default device: " <> show defaultDevice
+
+  device <- ALC.alcOpenDevice Foreign.nullPtr
+  context <- ALC.alcCreateContext device Foreign.nullPtr
+
+  ok <- fmap (== 1) $ ALC.alcMakeContextCurrent context
+  putStrLn $ "Context OK: " <> show ok
+  when ok do
+    buf@(AL.Buffer bufId) <- Foreign.alloca \bufPtr -> do
+      AL.alGenBuffers 1 bufPtr
+      Foreign.peek bufPtr
+    AL.alGetError >>= print . (buf,)
+
+    Foreign.withArrayLen soundData \bufSize bufData ->
+      AL.alBufferData
+        buf
+        AL.FORMAT_MONO16
+        (Foreign.castPtr bufData)
+        (fromIntegral bufSize)
+        48000
+    AL.alGetError >>= print . (buf,)
+
+    source <- Foreign.alloca \sourcePtr -> do
+      AL.alGenSources 1 sourcePtr
+      Foreign.peek sourcePtr
+    AL.alGetError >>= print . (source,)
+
+    AL.alSourcei source AL.BUFFER (fromIntegral bufId) -- XXX: unsigned to signed conversion
+    AL.alGetError >>= print . (buf,)
+
+    Foreign.with source $
+      AL.alSourcePlayv 1
+
+    threadDelay 1000000
+
+    Foreign.with source $ AL.alDeleteSources 1
+    Foreign.with buf $ AL.alDeleteBuffers 1
+
+  ALC.alcDestroyContext context
+  ALC.alcCloseDevice device >>= print
+
+-- | Some funky sample
+soundData :: [Int16]
+soundData =
+  map convert $
+  zipWith
+    (+)
+    (sine 220) $
+  zipWith
+    (*)
+    (sine 440)
+    (sine 3)
+  where
+    convert d = round $ range * 0.5 * d
+    range = fromIntegral (maxBound :: Int16)
+
+    duration = 10.0
+    rate = 48000
+
+    sine freq =
+      take (round $ duration * rate) $
+        map sin [0.0 :: Double, step ..]
+      where
+        step =
+          freq * 2 * pi / rate
