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/acousticbrainz-client.cabal b/acousticbrainz-client.cabal
new file mode 100644
--- /dev/null
+++ b/acousticbrainz-client.cabal
@@ -0,0 +1,69 @@
+cabal-version:       2.2
+name:                acousticbrainz-client
+version:             0.1.0.0
+synopsis:            AcousticBrainz API client
+-- description:
+-- bug-reports:
+license:             CC0-1.0
+author:              koral
+maintainer:          chahine.moreau@gmail.com
+-- category:
+build-type:          Simple
+-- extra-source-files:  CHANGELOG.md
+tested-with: GHC ==8.6.3 || ==8.4.4 || ==8.2.2 || ==8.0.2
+
+
+common common
+  build-depends: base-noprelude >= 4.7, base-compat-batteries, relude
+  default-language: Haskell2010
+  other-modules:
+    Prelude
+
+library
+  import: common
+  exposed-modules:
+    AcousticBrainz.FiniteDistribution
+    AcousticBrainz.HighLevel
+    AcousticBrainz.HighLevel.Dortmund
+    AcousticBrainz.HighLevel.ElectronicClassification
+    AcousticBrainz.HighLevel.ISMIR04Rhythm
+    AcousticBrainz.HighLevel.MirexMood
+    AcousticBrainz.HighLevel.Rosamerica
+    AcousticBrainz.HighLevel.Tzanetakis
+    AcousticBrainz.LowLevel
+    AcousticBrainz.LowLevel.Algorithm.BpmHistogramDescriptors
+    AcousticBrainz.LowLevel.Algorithm.Chords
+    AcousticBrainz.LowLevel.Algorithm.EnergyBand
+    AcousticBrainz.LowLevel.Algorithm.HighResolutionFeatures
+    AcousticBrainz.LowLevel.Algorithm.HPCP
+    AcousticBrainz.LowLevel.Algorithm.Key
+    AcousticBrainz.LowLevel.Algorithm.SilenceRate
+    AcousticBrainz.LowLevel.BarkBands
+    AcousticBrainz.LowLevel.Bands
+    AcousticBrainz.LowLevel.CepstralCoefficients
+    AcousticBrainz.LowLevel.EquivalentRectangularBandwidth
+    AcousticBrainz.LowLevel.MelBands
+    AcousticBrainz.LowLevel.Rhythm
+    AcousticBrainz.LowLevel.Spectral
+    AcousticBrainz.LowLevel.Tonal
+    AcousticBrainz.MetaData
+    AcousticBrainz.StatisticalUnits
+    AcousticBrainz.Version
+    MusicBrainz
+  -- other-extensions:
+  build-depends:       aeson, bytestring, exceptions, microlens-platform, microlens-aeson, scientific, streaming-bytestring, streaming-utils, text
+  hs-source-dirs:      src
+
+test-suite golden-tests
+  import: common
+  type: exitcode-stdio-1.0
+  main-is: GoldenTests.hs
+  build-depends:
+    acousticbrainz-client -any,
+    aeson,
+    filepath,
+    pretty-simple,
+    tasty -any,
+    tasty-golden -any,
+    text
+  hs-source-dirs: test
diff --git a/src/AcousticBrainz/FiniteDistribution.hs b/src/AcousticBrainz/FiniteDistribution.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/FiniteDistribution.hs
@@ -0,0 +1,29 @@
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE PartialTypeSignatures #-}
+{-# LANGUAGE StandaloneDeriving    #-}
+{-# LANGUAGE TupleSections         #-}
+module AcousticBrainz.FiniteDistribution where
+
+import           Data.Aeson
+import           Data.Aeson.Types
+import           Data.Scientific
+
+newtype FiniteDistribution a = FiniteDistribution (Map a Scientific)
+
+deriving instance Eq a => Eq (FiniteDistribution a)
+deriving instance Ord a => Ord (FiniteDistribution a)
+deriving instance (Ord a, Read a) => Read (FiniteDistribution a)
+deriving instance Show a => Show (FiniteDistribution a)
+
+
+
+parseFiniteDistribution :: Ord a => [(a, Text)] -> Value -> Parser (FiniteDistribution a)
+parseFiniteDistribution valueKeys = withObject "all" $ \v ->
+  FiniteDistribution . fromList <$> sequence (do
+    (value, name) <- valueKeys
+    return $ (value,) <$> v .: name
+  )
+
+
+
diff --git a/src/AcousticBrainz/HighLevel.hs b/src/AcousticBrainz/HighLevel.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/HighLevel.hs
@@ -0,0 +1,186 @@
+{-# LANGUAGE FlexibleContexts      #-}
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE PartialTypeSignatures #-}
+{-# LANGUAGE StandaloneDeriving    #-}
+{-# LANGUAGE UndecidableInstances  #-}
+module AcousticBrainz.HighLevel where
+
+import           AcousticBrainz.FiniteDistribution
+import           AcousticBrainz.MetaData
+import qualified AcousticBrainz.HighLevel.Dortmund                 as Dortmund
+import qualified AcousticBrainz.HighLevel.ElectronicClassification as ElectronicClassification
+import qualified AcousticBrainz.HighLevel.ISMIR04Rhythm            as ISMIR04Rhythm
+import qualified AcousticBrainz.HighLevel.MirexMood                as Mirex
+import qualified AcousticBrainz.HighLevel.Rosamerica               as Rosamerica
+import qualified AcousticBrainz.HighLevel.Tzanetakis               as Tzanetakis
+import           AcousticBrainz.Version
+import           MusicBrainz
+
+import           Control.Monad.Catch
+import           Data.Aeson
+import           Data.Aeson.Types
+import qualified Data.ByteString.Streaming                         as Q
+import           Data.ByteString.Streaming.HTTP
+import           Data.Scientific
+import qualified Data.Text                                         as Text
+import           Lens.Micro.Aeson
+
+-- | Call the following endpoint: https://acousticbrainz.readthedocs.io/api.html#get--api-v1-(uuid-mbid)-high-level .
+getHighLevelData :: MonadIO m => MonadThrow m => MusicBrainzIdentifier -> m HighLevelResponse
+getHighLevelData (MusicBrainzIdentifier identifier) = io $ do
+  request <- parseRequest $ "https://acousticbrainz.org/api/v1/" <> Text.unpack identifier <> "/high-level"
+  manager <- newManager tlsManagerSettings
+  withHTTP request manager $ \r ->
+    r & responseBody & Q.toLazy_ <&> eitherDecode >>= liftEither
+  where liftEither (Left e) = throwM $ HighLevelException e
+        liftEither (Right a) = return a
+
+
+data Danceability = Danceable | NotDanceable deriving(Eq, Ord, Read, Show)
+
+instance FromJSON (FiniteDistribution Danceability) where
+  parseJSON = parseFiniteDistribution [(Danceable, "danceable"), (NotDanceable, "not_danceable")]
+
+
+data Gender = Female | Male deriving(Eq, Ord, Read, Show)
+
+instance FromJSON (FiniteDistribution Gender) where
+  parseJSON = parseFiniteDistribution [(Female, "female"), (Male, "male")]
+
+
+data Acoustic = Acoustic | NotAcoustic deriving(Eq, Ord, Read, Show)
+
+instance FromJSON (FiniteDistribution Acoustic) where
+  parseJSON = parseFiniteDistribution [(Acoustic, "acoustic"), (NotAcoustic, "not_acoustic")]
+
+
+data Aggressive = Aggressive | NotAggressive deriving(Eq, Ord, Read, Show)
+
+instance FromJSON (FiniteDistribution Aggressive) where
+  parseJSON = parseFiniteDistribution [(Aggressive, "aggressive"), (NotAggressive, "not_aggressive")]
+
+
+data Electronic = Electronic | NotElectronic deriving(Eq, Ord, Read, Show)
+
+instance FromJSON (FiniteDistribution Electronic) where
+  parseJSON = parseFiniteDistribution [(Electronic, "electronic"), (NotElectronic, "not_electronic")]
+
+
+data Happy = Happy | NotHappy deriving(Eq, Ord, Read, Show)
+
+instance FromJSON (FiniteDistribution Happy) where
+  parseJSON = parseFiniteDistribution [(Happy, "happy"), (NotHappy, "not_happy")]
+
+
+data Party = Party | NotParty deriving(Eq, Ord, Read, Show)
+
+instance FromJSON (FiniteDistribution Party) where
+  parseJSON = parseFiniteDistribution [(Party, "party"), (NotParty, "not_party")]
+
+
+data Relaxed = Relaxed | NotRelaxed deriving(Eq, Ord, Read, Show)
+
+instance FromJSON (FiniteDistribution Relaxed) where
+  parseJSON = parseFiniteDistribution [(Relaxed, "relaxed"), (NotRelaxed, "not_relaxed")]
+
+
+data Sad = Sad | NotSad deriving(Eq, Ord, Read, Show)
+
+instance FromJSON (FiniteDistribution Sad) where
+  parseJSON = parseFiniteDistribution [(Sad, "sad"), (NotSad, "not_sad")]
+
+
+data Timbre = Bright | Dark deriving(Eq, Ord, Read, Show)
+
+instance FromJSON (FiniteDistribution Timbre) where
+  parseJSON = parseFiniteDistribution [(Bright, "bright"), (Dark, "dark")]
+
+
+data Tonal = Atonal | Tonal deriving(Eq, Ord, Read, Show)
+
+instance FromJSON (FiniteDistribution Tonal) where
+  parseJSON = parseFiniteDistribution [(Atonal, "atonal"), (Tonal, "tonal")]
+
+
+data Vocal = Instrumental | Vocal deriving(Eq, Ord, Read, Show)
+
+instance FromJSON (FiniteDistribution Vocal) where
+  parseJSON = parseFiniteDistribution [(Instrumental, "instrumental"), (Vocal, "voice")]
+
+
+data Feature t = Feature Version (FiniteDistribution t)
+
+deriving instance Eq t => Eq (Feature t)
+deriving instance Ord t => Ord (Feature t)
+deriving instance (Ord t, Read t) => Read (Feature t)
+deriving instance Show t => Show (Feature t)
+
+instance (FromJSON (FiniteDistribution t)) => FromJSON (Feature t) where
+  parseJSON = withObject "Feature" $ \v -> Feature
+    <$> v .: "version"
+    <*> v .: "all"
+
+
+data HighLevelData = HighLevelData
+  { _danceability    :: Feature Danceability
+  , _gender          :: Feature Gender
+  , _genreDortmund   :: Feature Dortmund.Genre
+  , _genreElectronic :: Feature ElectronicClassification.Genre
+  , _genreRosamerica :: Feature Rosamerica.Genre
+  , _genreTzanetakis :: Feature Tzanetakis.Genre
+  , _ismir04rhythm   :: Feature ISMIR04Rhythm.Genre
+  , _mirexMood       :: Feature Mirex.Mood
+  , _moodAcoustic    :: Feature Acoustic
+  , _moodAggressive  :: Feature Aggressive
+  , _moodElectronic  :: Feature Electronic
+  , _moodHappy       :: Feature Happy
+  , _moodParty       :: Feature Party
+  , _moodRelaxed     :: Feature Relaxed
+  , _moodSad         :: Feature Sad
+  , _timbre          :: Feature Timbre
+  , _tonal           :: Feature Tonal
+  , _vocal           :: Feature Vocal
+  } deriving(Eq, Ord, Read, Show)
+
+instance FromJSON HighLevelData where
+  parseJSON = withObject "highlevel" $ \v -> HighLevelData
+    <$> v .: "danceability"
+    <*> v .: "gender"
+    <*> v .: "genre_dortmund"
+    <*> v .: "genre_electronic"
+    <*> v .: "genre_rosamerica"
+    <*> v .: "genre_tzanetakis"
+    <*> v .: "ismir04_rhythm"
+    <*> v .: "moods_mirex"
+    <*> v .: "mood_acoustic"
+    <*> v .: "mood_aggressive"
+    <*> v .: "mood_electronic"
+    <*> v .: "mood_happy"
+    <*> v .: "mood_party"
+    <*> v .: "mood_relaxed"
+    <*> v .: "mood_sad"
+    <*> v .: "timbre"
+    <*> v .: "tonal_atonal"
+    <*> v .: "voice_instrumental"
+
+
+
+parseMetaData = withObject "metadata" $ \v -> MetaData
+  <$> v .: "audio_properties"
+  <*> v .: "tags"
+  <*> (v .: "version" >>= withObject "version" (.: "highlevel"))
+
+data HighLevelResponse = HighLevelResponse
+  { _data     :: HighLevelData
+  , _metadata :: MetaData
+  } deriving(Eq, Read, Show)
+
+instance FromJSON HighLevelResponse where
+  parseJSON = withObject "response" $ \v -> HighLevelResponse
+    <$> v .: "highlevel"
+    <*> (v .: "metadata" >>= parseMetaData)
+
+
+newtype HighLevelException = HighLevelException String deriving(Eq, Ord, Read, Show)
+instance Exception HighLevelException
diff --git a/src/AcousticBrainz/HighLevel/Dortmund.hs b/src/AcousticBrainz/HighLevel/Dortmund.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/HighLevel/Dortmund.hs
@@ -0,0 +1,25 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE OverloadedStrings #-}
+-- | Dortmund genre classification: http://www-ai.cs.uni-dortmund.de/audio.html .
+module AcousticBrainz.HighLevel.Dortmund where
+
+import           AcousticBrainz.FiniteDistribution
+
+import           Data.Aeson
+import           Data.Aeson.Types
+
+data Genre = Alternative | Blues | Electronic | FolkCountry | FunkSoulRnB | Jazz | Pop | Raphiphop | Rock
+  deriving(Eq, Ord, Read, Show)
+
+instance FromJSON (FiniteDistribution Genre) where
+  parseJSON = parseFiniteDistribution
+    [(Alternative, "alternative")
+    , (Blues, "blues")
+    , (Electronic, "electronic")
+    , (FolkCountry, "folkcountry")
+    , (FunkSoulRnB, "funksoulrnb")
+    , (Jazz, "jazz")
+    , (Pop, "pop")
+    , (Raphiphop, "raphiphop")
+    , (Rock, "rock")
+    ]
diff --git a/src/AcousticBrainz/HighLevel/ElectronicClassification.hs b/src/AcousticBrainz/HighLevel/ElectronicClassification.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/HighLevel/ElectronicClassification.hs
@@ -0,0 +1,22 @@
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE OverloadedStrings     #-}
+module AcousticBrainz.HighLevel.ElectronicClassification where
+
+import           AcousticBrainz.FiniteDistribution
+
+import           Data.Aeson                        as JSON
+import           Data.Aeson.Types                  as JSON
+
+
+data Genre = Ambient | DnB | House | Techno | Trance
+  deriving(Eq, Ord, Read, Show)
+
+instance FromJSON (FiniteDistribution Genre) where
+  parseJSON = parseFiniteDistribution
+    [ (Ambient, "ambient")
+    , (DnB, "dnb")
+    , (House, "house")
+    , (Techno, "techno")
+    , (Trance, "trance")
+    ]
+
diff --git a/src/AcousticBrainz/HighLevel/ISMIR04Rhythm.hs b/src/AcousticBrainz/HighLevel/ISMIR04Rhythm.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/HighLevel/ISMIR04Rhythm.hs
@@ -0,0 +1,25 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE OverloadedStrings #-}
+-- | ISMIR 2004 rhythm classification.
+module AcousticBrainz.HighLevel.ISMIR04Rhythm where
+
+import           AcousticBrainz.FiniteDistribution
+
+import           Data.Aeson
+import           Data.Aeson.Types
+
+data Genre = ChaChaCha | Jive | Quickstep | RumbaAmerican | RumbaInternational | RumbaMisc | Samba | Tango | VienneseWaltz | Waltz
+  deriving(Eq, Ord, Read, Show)
+
+instance FromJSON (FiniteDistribution Genre) where
+  parseJSON = parseFiniteDistribution
+    [ (ChaChaCha, "ChaChaCha")
+    , (Jive, "Jive")
+    , (Quickstep, "Quickstep")
+    , (RumbaAmerican, "Rumba-American")
+    , (RumbaInternational, "Rumba-International")
+    , (RumbaMisc, "Rumba-Misc")
+    , (Samba, "Samba")
+    , (VienneseWaltz, "VienneseWaltz")
+    , (Waltz, "Waltz")
+    ]
diff --git a/src/AcousticBrainz/HighLevel/MirexMood.hs b/src/AcousticBrainz/HighLevel/MirexMood.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/HighLevel/MirexMood.hs
@@ -0,0 +1,21 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE OverloadedStrings #-}
+-- | K-Pop mood classification: https://www.music-ir.org/mirex/wiki/2019:Audio_K-POP_Mood_Classification .
+module AcousticBrainz.HighLevel.MirexMood where
+
+import           AcousticBrainz.FiniteDistribution
+
+import           Data.Aeson
+import           Data.Aeson.Types
+
+data Mood = Cluster1 | Cluster2 | Cluster3 | Cluster4 | Cluster5
+  deriving(Eq, Ord, Read, Show)
+
+instance FromJSON (FiniteDistribution Mood) where
+  parseJSON = parseFiniteDistribution
+    [ (Cluster1, "Cluster1")
+    , (Cluster2, "Cluster2")
+    , (Cluster3, "Cluster3")
+    , (Cluster4, "Cluster4")
+    , (Cluster5, "Cluster5")
+    ]
diff --git a/src/AcousticBrainz/HighLevel/Rosamerica.hs b/src/AcousticBrainz/HighLevel/Rosamerica.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/HighLevel/Rosamerica.hs
@@ -0,0 +1,23 @@
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE OverloadedStrings     #-}
+module AcousticBrainz.HighLevel.Rosamerica where
+
+import           AcousticBrainz.FiniteDistribution
+
+import           Data.Aeson                        as JSON
+import           Data.Aeson.Types                  as JSON
+
+data Genre = Cla | Dan | Hip | Jaz | Pop | Rhy | Roc | Spe
+  deriving(Eq, Ord, Read, Show)
+
+instance FromJSON (FiniteDistribution Genre) where
+  parseJSON = parseFiniteDistribution
+    [ (Cla, "cla")
+    , (Dan, "dan")
+    , (Hip, "hip")
+    , (Jaz, "jaz")
+    , (Pop, "pop")
+    , (Rhy, "rhy")
+    , (Roc, "roc")
+    , (Spe, "spe")
+    ]
diff --git a/src/AcousticBrainz/HighLevel/Tzanetakis.hs b/src/AcousticBrainz/HighLevel/Tzanetakis.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/HighLevel/Tzanetakis.hs
@@ -0,0 +1,26 @@
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE OverloadedStrings     #-}
+module AcousticBrainz.HighLevel.Tzanetakis where
+
+import           AcousticBrainz.FiniteDistribution
+
+import           Data.Aeson                        as JSON
+import           Data.Aeson.Types                  as JSON
+
+data Genre = Blu | Cla | Cou | Dis | Hip | Jaz | Met | Pop | Reg | Roc
+  deriving(Eq, Ord, Read, Show)
+
+instance FromJSON (FiniteDistribution Genre) where
+  parseJSON = parseFiniteDistribution
+    [ (Blu, "blu")
+    , (Cla, "cla")
+    , (Cou, "cou")
+    , (Dis, "dis")
+    , (Hip, "hip")
+    , (Jaz, "jaz")
+    , (Met, "met")
+    , (Pop, "pop")
+    , (Reg, "reg")
+    , (Roc, "roc")
+    ]
+
diff --git a/src/AcousticBrainz/LowLevel.hs b/src/AcousticBrainz/LowLevel.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/LowLevel.hs
@@ -0,0 +1,91 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- | Low-level descriptors: https://essentia.upf.edu/documentation/streaming_extractor_music.html#low-level .
+module AcousticBrainz.LowLevel where
+
+import           AcousticBrainz.StatisticalUnits
+import           AcousticBrainz.MetaData
+import qualified AcousticBrainz.LowLevel.Algorithm.SilenceRate as SilenceRate
+import           AcousticBrainz.LowLevel.BarkBands
+import           AcousticBrainz.LowLevel.CepstralCoefficients
+import qualified AcousticBrainz.LowLevel.EquivalentRectangularBandwidth as EquivalentRectangularBandwidth
+import           AcousticBrainz.LowLevel.MelBands
+import           AcousticBrainz.LowLevel.Rhythm
+import           AcousticBrainz.LowLevel.Spectral
+import           AcousticBrainz.LowLevel.Tonal
+import           MusicBrainz
+
+import           Control.Monad.Catch
+import           Data.Aeson                     as JSON
+import           Data.Aeson.Types               as JSON
+import qualified Data.ByteString.Lazy           as Lazy
+import qualified Data.ByteString.Streaming      as Q
+import           Data.ByteString.Streaming.HTTP
+import           Data.Scientific
+import qualified Data.Text                      as Text
+
+-- | Call the following endpoint: https://acousticbrainz.readthedocs.io/api.html#get--api-v1-(uuid-mbid)-low-level .
+getLowLevelData :: MonadIO m => MonadThrow m => MusicBrainzIdentifier -> m LowLevelResponse
+getLowLevelData (MusicBrainzIdentifier identifier) = io $ do
+  request <- parseRequest $ "https://acousticbrainz.org/api/v1/" <> Text.unpack identifier <> "/low-level"
+  manager <- newManager tlsManagerSettings
+  withHTTP request manager $ \r ->
+    r & responseBody & Q.toLazy_ <&> eitherDecode >>= liftEither
+  where liftEither (Left e) = throwM $ LowLevelException e
+        liftEither (Right a) = return a
+
+data LowLevelResponse = LowLevelResponse
+  { _data     :: LowLevelData
+  , _tonal    :: Tonal
+  , _rhythm   :: Rhythm
+  , _metadata :: MetaData
+  } deriving(Eq, Read, Show)
+
+instance FromJSON LowLevelResponse where
+  parseJSON = withObject "response" $ \v -> LowLevelResponse
+    <$> v .: "lowlevel"
+    <*> v .: "tonal"
+    <*> v .: "rhythm"
+    <*> (v .: "metadata" >>= parseMetaData)
+
+
+parseMetaData = withObject "metadata" $ \v -> MetaData
+  <$> v .: "audio_properties"
+  <*> v .: "tags"
+  <*> v .: "version"
+
+
+data LowLevelData = LowLevelData
+  { _averageLoudness :: Scientific
+  , _barkBands :: BarkBands
+  , _dissonance :: StatisticalUnits
+  , _dynamicComplexity :: Scientific
+  , _equivalentRectangularBandwidthBands :: EquivalentRectangularBandwidth.Bands
+  , _gfcc :: CepstralCoefficients
+  , _highFrequencyContent :: StatisticalUnits
+  , _melBands :: MelBands
+  , _mfcc :: CepstralCoefficients
+  , _pitchSalience :: StatisticalUnits
+  , _silenceRate :: SilenceRate.Output
+  , _spectral :: Spectral
+  , _zeroCrossingRate :: StatisticalUnits
+  } deriving(Eq, Ord, Read, Show)
+
+instance FromJSON LowLevelData where
+  parseJSON value = flip (withObject "lowlevel") value $ \v -> LowLevelData
+    <$> v .: "average_loudness"
+    <*> parseJSON value
+    <*> v .: "dissonance"
+    <*> v .: "dynamic_complexity"
+    <*> parseJSON value
+    <*> v .: "gfcc"
+    <*> v .: "hfc"
+    <*> parseJSON value
+    <*> v .: "mfcc"
+    <*> v .: "pitch_salience"
+    <*> parseJSON value
+    <*> parseJSON value
+    <*> v .: "zerocrossingrate"
+
+
+newtype LowLevelException = LowLevelException String deriving(Eq, Ord, Read, Show)
+instance Exception LowLevelException
diff --git a/src/AcousticBrainz/LowLevel/Algorithm/BpmHistogramDescriptors.hs b/src/AcousticBrainz/LowLevel/Algorithm/BpmHistogramDescriptors.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/LowLevel/Algorithm/BpmHistogramDescriptors.hs
@@ -0,0 +1,27 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- | BpmHistogramDescriptors algorithm: https://essentia.upf.edu/documentation/reference/streaming_BpmHistogramDescriptors.html .
+module AcousticBrainz.LowLevel.Algorithm.BpmHistogramDescriptors where
+
+import           AcousticBrainz.StatisticalUnits
+
+import           Data.Aeson
+import           Data.Aeson.Types
+
+
+data Output = Output
+  { _firstPeakBpm     :: StatisticalUnits
+  , _firstPeakSpread  :: StatisticalUnits
+  , _firstPeakWeight  :: StatisticalUnits
+  , _secondPeakBpm    :: StatisticalUnits
+  , _secondPeakSpread :: StatisticalUnits
+  , _secondPeakWeight :: StatisticalUnits
+  } deriving(Eq, Ord, Read, Show)
+
+instance FromJSON Output where
+  parseJSON = withObject "BpmHistogramDescriptors" $ \v -> Output
+    <$> v .: "bpm_histogram_first_peak_bpm"
+    <*> v .: "bpm_histogram_first_peak_spread"
+    <*> v .: "bpm_histogram_first_peak_weight"
+    <*> v .: "bpm_histogram_second_peak_bpm"
+    <*> v .: "bpm_histogram_second_peak_spread"
+    <*> v .: "bpm_histogram_second_peak_weight"
diff --git a/src/AcousticBrainz/LowLevel/Algorithm/Chords.hs b/src/AcousticBrainz/LowLevel/Algorithm/Chords.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/LowLevel/Algorithm/Chords.hs
@@ -0,0 +1,31 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- | Chords algorithms:
+-- - https://essentia.upf.edu/documentation/reference/streaming_ChordsDetection.html
+-- - https://essentia.upf.edu/documentation/reference/streaming_ChordsDescriptors.html
+module AcousticBrainz.LowLevel.Algorithm.Chords where
+
+import           AcousticBrainz.StatisticalUnits
+import qualified AcousticBrainz.LowLevel.Algorithm.Key as Key
+
+import           Data.Aeson
+import           Data.Aeson.Types
+import           Data.Scientific
+
+
+data Output = Output
+  { _changesRate :: Scientific
+  , _histogram   :: [Scientific]
+  , _key         :: Text
+  , _numberRate  :: Scientific
+  , _scale       :: Key.Scale
+  , _strength    :: StatisticalUnits
+  } deriving(Eq, Ord, Read, Show)
+
+instance FromJSON Output where
+  parseJSON = withObject "chords" $ \v -> Output
+    <$> v .: "chords_changes_rate"
+    <*> v .: "chords_histogram"
+    <*> v .: "chords_key"
+    <*> v .: "chords_number_rate"
+    <*> v .: "chords_scale"
+    <*> v .: "chords_strength"
diff --git a/src/AcousticBrainz/LowLevel/Algorithm/EnergyBand.hs b/src/AcousticBrainz/LowLevel/Algorithm/EnergyBand.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/LowLevel/Algorithm/EnergyBand.hs
@@ -0,0 +1,24 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- | EnergyBand algorithm: https://essentia.upf.edu/documentation/reference/streaming_EnergyBand.html .
+module AcousticBrainz.LowLevel.Algorithm.EnergyBand where
+
+import           AcousticBrainz.StatisticalUnits
+
+import           Data.Aeson
+import           Data.Aeson.Types
+
+
+data Output = Output
+  { _high       :: StatisticalUnits
+  , _low        :: StatisticalUnits
+  , _middleHigh :: StatisticalUnits
+  , _middleLow  :: StatisticalUnits
+
+  } deriving(Eq, Ord, Read, Show)
+
+instance FromJSON Output where
+  parseJSON = withObject "EnergyBand" $ \v -> Output
+    <$> v .: "spectral_energyband_high"
+    <*> v .: "spectral_energyband_low"
+    <*> v .: "spectral_energyband_middle_high"
+    <*> v .: "spectral_energyband_middle_low"
diff --git a/src/AcousticBrainz/LowLevel/Algorithm/HPCP.hs b/src/AcousticBrainz/LowLevel/Algorithm/HPCP.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/LowLevel/Algorithm/HPCP.hs
@@ -0,0 +1,20 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- | HPCP algorithm: https://essentia.upf.edu/documentation/reference/streaming_HPCP.html .
+module AcousticBrainz.LowLevel.Algorithm.HPCP where
+
+import           AcousticBrainz.StatisticalUnits
+
+import           Data.Aeson
+import           Data.Aeson.Types
+import           Data.Scientific
+
+
+data Output = Output
+  { _harmonicPitchClassProfile           :: [StatisticalUnits]
+  , _transposedHarmonicPitchClassProfile :: [Scientific]
+  } deriving(Eq, Ord, Read, Show)
+
+instance FromJSON Output where
+  parseJSON value = flip (withObject "HPCP") value $ \v -> Output
+    <$> (_statisticalUnitsList <$> v .: "hpcp")
+    <*> v .: "thpcp"
diff --git a/src/AcousticBrainz/LowLevel/Algorithm/HighResolutionFeatures.hs b/src/AcousticBrainz/LowLevel/Algorithm/HighResolutionFeatures.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/LowLevel/Algorithm/HighResolutionFeatures.hs
@@ -0,0 +1,18 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- | HighResolutionFeatures algorithm: https://essentia.upf.edu/documentation/reference/streaming_HighResolutionFeatures.html .
+module AcousticBrainz.LowLevel.Algorithm.HighResolutionFeatures where
+
+import           Data.Aeson
+import           Data.Aeson.Types
+import           Data.Scientific
+
+
+data Output = Output
+  { _tuningEqualTemperedDeviation :: Scientific
+  , _tuningNonTemperedEnergyRatio :: Scientific
+  } deriving(Eq, Ord, Read, Show)
+
+instance FromJSON Output where
+  parseJSON = withObject "HighResolutionFeatures" $ \v -> Output
+    <$> v .: "tuning_equal_tempered_deviation"
+    <*> v .: "tuning_nontempered_energy_ratio"
diff --git a/src/AcousticBrainz/LowLevel/Algorithm/Key.hs b/src/AcousticBrainz/LowLevel/Algorithm/Key.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/LowLevel/Algorithm/Key.hs
@@ -0,0 +1,38 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- | Key algorithm: https://essentia.upf.edu/documentation/reference/streaming_Key.html .
+module AcousticBrainz.LowLevel.Algorithm.Key where
+
+import           Data.Aeson
+import           Data.Aeson.Types
+import           Data.Scientific
+
+
+data Output = Output
+  { _key :: Text
+  , _scale :: Scale
+  , _strength :: Scientific
+  , _tuningDiatonicStrength :: Scientific
+  } deriving(Eq, Ord, Read, Show)
+
+instance FromJSON Output where
+  parseJSON = withObject "key" $ \v -> Output
+    <$> v .: "key_key"
+    <*> v .: "key_scale"
+    <*> v .: "key_strength"
+    <*> v .: "tuning_diatonic_strength"
+
+-- data Key = A | B | C | D | E | F | G deriving(Eq, Ord, Read, Show)
+
+-- instance FromJSON Key where
+--   parseJSON = withText "key" $ \t ->
+--     either (const $ fail $ "Invalid key: " <> toString t) return $ readEither t
+
+data Scale = Major | Minor deriving(Eq, Ord, Read, Show)
+
+instance FromJSON Scale where
+  parseJSON = withText "scale" $ \t ->
+    if t == "major"
+    then return Major
+    else if t == "minor"
+    then return Minor
+    else fail $ "Invalid key scale: " <> toString t
diff --git a/src/AcousticBrainz/LowLevel/Algorithm/SilenceRate.hs b/src/AcousticBrainz/LowLevel/Algorithm/SilenceRate.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/LowLevel/Algorithm/SilenceRate.hs
@@ -0,0 +1,21 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- | SilenceRate algorithm: https://essentia.upf.edu/documentation/reference/streaming_SilenceRate.html .
+module AcousticBrainz.LowLevel.Algorithm.SilenceRate where
+
+import           AcousticBrainz.StatisticalUnits
+
+import           Data.Aeson
+import           Data.Aeson.Types
+
+
+data Output = Output
+  { _20dB :: StatisticalUnits
+  , _30dB :: StatisticalUnits
+  , _60dB :: StatisticalUnits
+  } deriving(Eq, Ord, Read, Show)
+
+instance FromJSON Output where
+  parseJSON = withObject "SilenceRate" $ \v -> Output
+    <$> v .: "silence_rate_20dB"
+    <*> v .: "silence_rate_30dB"
+    <*> v .: "silence_rate_60dB"
diff --git a/src/AcousticBrainz/LowLevel/Bands.hs b/src/AcousticBrainz/LowLevel/Bands.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/LowLevel/Bands.hs
@@ -0,0 +1,12 @@
+module AcousticBrainz.LowLevel.Bands where
+
+import           AcousticBrainz.StatisticalUnits
+
+data Bands = Bands
+  { _bands      :: [StatisticalUnits]
+  , _crest      :: StatisticalUnits
+  , _flatnessDb :: StatisticalUnits
+  , _kurtosis   :: StatisticalUnits
+  , _skewness   :: StatisticalUnits
+  , _spread     :: StatisticalUnits
+  } deriving(Eq, Ord, Read, Show)
diff --git a/src/AcousticBrainz/LowLevel/BarkBands.hs b/src/AcousticBrainz/LowLevel/BarkBands.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/LowLevel/BarkBands.hs
@@ -0,0 +1,19 @@
+{-# LANGUAGE OverloadedStrings #-}
+module AcousticBrainz.LowLevel.BarkBands where
+
+import           AcousticBrainz.StatisticalUnits
+import           AcousticBrainz.LowLevel.Bands
+
+import           Data.Aeson                         as JSON
+import           Data.Aeson.Types                   as JSON
+
+newtype BarkBands = BarkBands Bands deriving(Eq, Ord, Read, Show)
+
+instance FromJSON BarkBands where
+  parseJSON = withObject "BarkBands" $ \v -> fmap BarkBands (Bands
+    <$> (_statisticalUnitsList <$> v .: "barkbands")
+    <*> v .: "barkbands_crest"
+    <*> v .: "barkbands_flatness_db"
+    <*> v .: "barkbands_kurtosis"
+    <*> v .: "barkbands_skewness"
+    <*> v .: "barkbands_spread")
diff --git a/src/AcousticBrainz/LowLevel/CepstralCoefficients.hs b/src/AcousticBrainz/LowLevel/CepstralCoefficients.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/LowLevel/CepstralCoefficients.hs
@@ -0,0 +1,19 @@
+{-# LANGUAGE OverloadedStrings #-}
+module AcousticBrainz.LowLevel.CepstralCoefficients where
+
+import           Data.Aeson
+import           Data.Aeson.Types
+import           Data.Scientific
+
+
+data CepstralCoefficients = CepstralCoefficients
+  { _covariance        :: [[Scientific]]
+  , _inverseCovariance :: [[Scientific]]
+  , _mean              :: [Scientific]
+  } deriving(Eq, Ord, Read, Show)
+
+instance FromJSON CepstralCoefficients where
+  parseJSON = withObject "CepstralCoefficients" $ \v -> CepstralCoefficients
+    <$> v .: "cov"
+    <*> v .: "icov"
+    <*> v .: "mean"
diff --git a/src/AcousticBrainz/LowLevel/EquivalentRectangularBandwidth.hs b/src/AcousticBrainz/LowLevel/EquivalentRectangularBandwidth.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/LowLevel/EquivalentRectangularBandwidth.hs
@@ -0,0 +1,19 @@
+{-# LANGUAGE OverloadedStrings #-}
+module AcousticBrainz.LowLevel.EquivalentRectangularBandwidth where
+
+import           AcousticBrainz.StatisticalUnits
+import qualified AcousticBrainz.LowLevel.Bands      as Generic
+
+import           Data.Aeson                         as JSON
+import           Data.Aeson.Types                   as JSON
+
+newtype Bands = Bands Generic.Bands deriving(Eq, Ord, Read, Show)
+
+instance FromJSON Bands where
+  parseJSON = withObject "Equivalent Rectangular Bandwidth Bands" $ \v -> fmap Bands (Generic.Bands
+    <$> (_statisticalUnitsList <$> v .: "erbbands")
+    <*> v .: "erbbands_crest"
+    <*> v .: "erbbands_flatness_db"
+    <*> v .: "erbbands_kurtosis"
+    <*> v .: "erbbands_skewness"
+    <*> v .: "erbbands_spread")
diff --git a/src/AcousticBrainz/LowLevel/MelBands.hs b/src/AcousticBrainz/LowLevel/MelBands.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/LowLevel/MelBands.hs
@@ -0,0 +1,19 @@
+{-# LANGUAGE OverloadedStrings #-}
+module AcousticBrainz.LowLevel.MelBands where
+
+import           AcousticBrainz.StatisticalUnits
+import           AcousticBrainz.LowLevel.Bands
+
+import           Data.Aeson
+import           Data.Aeson.Types
+
+newtype MelBands = MelBands Bands deriving(Eq, Ord, Read, Show)
+
+instance FromJSON MelBands where
+  parseJSON = withObject "MelBands" $ \v -> fmap MelBands (Bands
+    <$> (_statisticalUnitsList <$> v .: "melbands")
+    <*> v .: "melbands_crest"
+    <*> v .: "melbands_flatness_db"
+    <*> v .: "melbands_kurtosis"
+    <*> v .: "melbands_skewness"
+    <*> v .: "melbands_spread")
diff --git a/src/AcousticBrainz/LowLevel/Rhythm.hs b/src/AcousticBrainz/LowLevel/Rhythm.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/LowLevel/Rhythm.hs
@@ -0,0 +1,32 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- | Rhythm descriptors.
+module AcousticBrainz.LowLevel.Rhythm where
+
+import           AcousticBrainz.StatisticalUnits
+import qualified AcousticBrainz.LowLevel.Algorithm.BpmHistogramDescriptors as BpmHistogramDescriptors
+
+import           Data.Aeson
+import           Data.Aeson.Types
+import           Data.Scientific
+
+data Rhythm = Rhythm
+  { _beatsCount              :: Int
+  , _beatsLoudness           :: StatisticalUnits
+  , _beatsLoudnessBandRatio  :: [StatisticalUnits]
+  , _beatsPosition           :: [Scientific]
+  , _beatsPerMinute          :: Scientific
+  , _beatsPerMinuteHistogram :: BpmHistogramDescriptors.Output
+  , _danceability            :: Scientific
+  , _onsetRate               :: Scientific
+  } deriving(Eq, Read, Show)
+
+instance FromJSON Rhythm where
+  parseJSON value = flip (withObject "rhythm") value $ \v -> Rhythm
+    <$> v .: "beats_count"
+    <*> v .: "beats_loudness"
+    <*> (_statisticalUnitsList <$> v .: "beats_loudness_band_ratio")
+    <*> v .: "beats_position"
+    <*> v .: "bpm"
+    <*> parseJSON value
+    <*> v .: "danceability"
+    <*> v .: "onset_rate"
diff --git a/src/AcousticBrainz/LowLevel/Spectral.hs b/src/AcousticBrainz/LowLevel/Spectral.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/LowLevel/Spectral.hs
@@ -0,0 +1,44 @@
+{-# LANGUAGE OverloadedStrings #-}
+module AcousticBrainz.LowLevel.Spectral where
+
+import           AcousticBrainz.StatisticalUnits
+import qualified AcousticBrainz.LowLevel.Algorithm.EnergyBand as EnergyBand
+
+import           Data.Aeson
+import           Data.Aeson.Types
+
+data Spectral = Spectral
+  { _centroid             :: StatisticalUnits
+  , _complexity           :: StatisticalUnits
+  , _contrastCoefficients :: [StatisticalUnits]
+  , _contrastValleys      :: [StatisticalUnits]
+  , _decrease             :: StatisticalUnits
+  , _energy               :: StatisticalUnits
+  , _energyBand           :: EnergyBand.Output
+  , _entropy              :: StatisticalUnits
+  , _flux                 :: StatisticalUnits
+  , _kurtosis             :: StatisticalUnits
+  , _rms                  :: StatisticalUnits
+  , _rolloff              :: StatisticalUnits
+  , _skewness             :: StatisticalUnits
+  , _spread               :: StatisticalUnits
+  , _strongPeak           :: StatisticalUnits
+  } deriving(Eq, Ord, Read, Show)
+
+instance FromJSON Spectral where
+  parseJSON value = flip (withObject "spectral") value $ \v -> Spectral
+    <$> v .: "spectral_centroid"
+    <*> v .: "spectral_complexity"
+    <*> (_statisticalUnitsList <$> v .: "spectral_contrast_coeffs")
+    <*> (_statisticalUnitsList <$> v .: "spectral_contrast_valleys")
+    <*> v .: "spectral_decrease"
+    <*> v .: "spectral_energy"
+    <*> parseJSON value
+    <*> v .: "spectral_entropy"
+    <*> v .: "spectral_flux"
+    <*> v .: "spectral_kurtosis"
+    <*> v .: "spectral_rms"
+    <*> v .: "spectral_rolloff"
+    <*> v .: "spectral_skewness"
+    <*> v .: "spectral_spread"
+    <*> v .: "spectral_strongpeak"
diff --git a/src/AcousticBrainz/LowLevel/Tonal.hs b/src/AcousticBrainz/LowLevel/Tonal.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/LowLevel/Tonal.hs
@@ -0,0 +1,32 @@
+{-# LANGUAGE OverloadedStrings #-}
+-- | Tonal descriptors.
+module AcousticBrainz.LowLevel.Tonal where
+
+import           AcousticBrainz.StatisticalUnits
+import qualified AcousticBrainz.LowLevel.Algorithm.Chords                 as Chords
+import qualified AcousticBrainz.LowLevel.Algorithm.HighResolutionFeatures as HighResolutionFeatures
+import qualified AcousticBrainz.LowLevel.Algorithm.HPCP                   as HPCP
+import qualified AcousticBrainz.LowLevel.Algorithm.Key                    as Key
+
+import           Data.Aeson
+import           Data.Aeson.Types
+import           Data.Scientific
+
+data Tonal = Tonal
+  { _chords                            :: Chords.Output
+  , _highResolutionFeatures            :: HighResolutionFeatures.Output
+  , _hpcp                              :: HPCP.Output
+  , _harmonicPitchClassProfilesEntropy :: StatisticalUnits
+  , _key                               :: Key.Output
+  , _tuningFrequency                   :: Scientific
+  } deriving(Eq, Ord, Read, Show)
+
+instance FromJSON Tonal where
+  parseJSON value = flip (withObject "tonal") value $ \v -> Tonal
+    <$> parseJSON value
+    <*> parseJSON value
+    <*> parseJSON value
+    <*> v .: "hpcp_entropy"
+    <*> parseJSON value
+    <*> v .: "tuning_frequency"
+
diff --git a/src/AcousticBrainz/MetaData.hs b/src/AcousticBrainz/MetaData.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/MetaData.hs
@@ -0,0 +1,52 @@
+{-# LANGUAGE OverloadedStrings     #-}
+module AcousticBrainz.MetaData where
+
+import           AcousticBrainz.Version
+
+import           Data.Aeson
+import           Data.Aeson.Types
+import           Data.Scientific
+
+
+data MetaData = MetaData
+  { _audioProperties :: AudioProperties
+  , _tags            :: Tags
+  , _version         :: Version
+  } deriving(Eq, Read, Show)
+
+
+
+data AudioProperties = AudioProperties
+  { _analysisSampleRate :: Natural
+  , _bitRate            :: Natural
+  , _codec              :: Text
+  , _downmix            :: Text
+  , _equalLoudness      :: Natural
+  , _length             :: Scientific
+  , _lossless           :: Bool
+  , _md5Encoded         :: Text
+  , _replayGain         :: Scientific
+  , _sampleRate         :: Natural
+  } deriving(Eq, Ord, Read, Show)
+
+instance FromJSON AudioProperties where
+  parseJSON = withObject "audio_properties" $ \v -> AudioProperties
+    <$> v .: "analysis_sample_rate"
+    <*> v .: "bit_rate"
+    <*> v .: "codec"
+    <*> v .: "downmix"
+    <*> v .: "equal_loudness"
+    <*> v .: "length"
+    <*> ((int2bool <$> v .: "lossless") <|> v .: "lossless")
+    <*> v .: "md5_encoded"
+    <*> v .: "replay_gain"
+    <*> v .: "sample_rate"
+    where int2bool :: Int -> Bool
+          int2bool 0 = False
+          int2bool _ = True
+
+
+newtype Tags = Tags (HashMap Text Value) deriving(Eq, Read, Show)
+
+instance FromJSON Tags where
+  parseJSON = withObject "tags" $ pure . Tags
diff --git a/src/AcousticBrainz/StatisticalUnits.hs b/src/AcousticBrainz/StatisticalUnits.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/StatisticalUnits.hs
@@ -0,0 +1,58 @@
+{-# LANGUAGE FlexibleInstances     #-}
+{-# LANGUAGE OverloadedStrings     #-}
+{-# LANGUAGE PartialTypeSignatures #-}
+-- | Result of PoolAggregator algorithm: https://essentia.upf.edu/documentation/reference/streaming_PoolAggregator.html .
+module AcousticBrainz.StatisticalUnits where
+
+import           Data.Aeson
+import           Data.Aeson.Types
+import           Data.Scientific
+
+
+data StatisticalUnits = StatisticalUnits
+  { _meanOfDerivative :: Scientific
+  , _meanOfSecondDerivative :: Scientific
+  , _varianceOfDerivative :: Scientific
+  , _varianceOfSecondDerivative :: Scientific
+  , _maximum :: Scientific
+  , _mean :: Scientific
+  , _median :: Scientific
+  , _minimum :: Scientific
+  , _variance :: Scientific
+  } deriving(Eq, Ord, Read, Show)
+
+instance FromJSON StatisticalUnits where
+  parseJSON = withObject "distribution metrics" $ \v -> StatisticalUnits
+    <$> v .: "dmean"
+    <*> v .: "dmean2"
+    <*> v .: "dvar"
+    <*> v .: "dvar2"
+    <*> v .: "max"
+    <*> v .: "mean"
+    <*> v .: "median"
+    <*> v .: "min"
+    <*> v .: "var"
+
+-- | This newtype is only useful for the associated 'FromJSON' instance.
+newtype StatisticalUnitsList = StatisticalUnitsList { _statisticalUnitsList :: [StatisticalUnits] }
+
+instance FromJSON StatisticalUnitsList where
+  parseJSON = withObject "distribution metrics list" $ \v -> do
+    dmean  <- v .: "dmean"
+    dmean2 <- v .: "dmean2"
+    dvar <- v .: "dvar"
+    dvar2 <- v .: "dvar2"
+    max <- v .: "max"
+    mean <- v .: "mean"
+    median <- v .: "median"
+    min <- v .: "min"
+    var <- v .: "var"
+    let a = zipWith StatisticalUnits dmean dmean2
+        b = zipWith ($) a dvar
+        c = zipWith ($) b dvar2
+        d = zipWith ($) c max
+        e = zipWith ($) d mean
+        f = zipWith ($) e median
+        g = zipWith ($) f min
+        h = zipWith ($) g var
+    return $ StatisticalUnitsList h
diff --git a/src/AcousticBrainz/Version.hs b/src/AcousticBrainz/Version.hs
new file mode 100644
--- /dev/null
+++ b/src/AcousticBrainz/Version.hs
@@ -0,0 +1,44 @@
+{-# LANGUAGE OverloadedStrings #-}
+module AcousticBrainz.Version where
+
+import           Data.Aeson
+import           Data.Aeson.Types
+
+
+data Version = Version
+  { _essentia             :: EssentiaVersion
+  , _extractor            :: Text
+  , _gaia                 :: Maybe GaiaVersion
+  , _modelsEssentiaGitSha :: Text
+  } deriving(Eq, Ord, Read, Show)
+
+instance FromJSON Version where
+  parseJSON value = flip (withObject "version") value $ \v -> Version
+    <$> parseJSON value
+    <*> v .: "extractor"
+    <*> ((Just <$> parseJSON value) <|> pure Nothing)
+    <*> (v .: "models_essentia_git_sha" <|> pure mempty)
+
+-- | Cf https://github.com/MTG/essentia/releases .
+data EssentiaVersion = EssentiaVersion
+  { _essentiaVersion  :: Text
+  , _essentiaBuildSha :: Text
+  , _essentiaGitSha   :: Text
+  } deriving(Eq, Ord, Read, Show)
+
+instance FromJSON EssentiaVersion where
+  parseJSON = withObject "EssentiaVersion" $ \v -> EssentiaVersion
+    <$> v .: "essentia"
+    <*> v .: "essentia_build_sha"
+    <*> v .: "essentia_git_sha"
+
+-- | Cf https://github.com/MTG/gaia/releases .
+data GaiaVersion = GaiaVersion
+  { _gaiaVersion :: Text
+  , _gaiaGitSha  :: Text
+  } deriving(Eq, Ord, Read, Show)
+
+instance FromJSON GaiaVersion where
+  parseJSON = withObject "GaiaVersion" $ \v -> GaiaVersion
+    <$> v .: "gaia"
+    <*> v .: "gaia_git_sha"
diff --git a/src/MusicBrainz.hs b/src/MusicBrainz.hs
new file mode 100644
--- /dev/null
+++ b/src/MusicBrainz.hs
@@ -0,0 +1,16 @@
+module MusicBrainz where
+
+import           Control.Monad.Catch
+import           Data.Text           as Text
+
+
+-- | Cf https://musicbrainz.org/doc/MusicBrainz_Identifier .
+newtype MusicBrainzIdentifier = MusicBrainzIdentifier Text deriving(Eq, Ord, Read, Show)
+
+newtype InvalidMusicBrainzIdentifier = InvalidMusicBrainzIdentifier Text deriving(Eq, Ord, Read, Show)
+instance Exception InvalidMusicBrainzIdentifier
+
+mkMusicBrainzIdentifier :: MonadThrow m => Text -> m MusicBrainzIdentifier
+mkMusicBrainzIdentifier rawIdentifier = do
+  when (Text.length rawIdentifier /= 36) $ throwM $ InvalidMusicBrainzIdentifier rawIdentifier
+  return $ MusicBrainzIdentifier rawIdentifier
diff --git a/src/Prelude.hs b/src/Prelude.hs
new file mode 100644
--- /dev/null
+++ b/src/Prelude.hs
@@ -0,0 +1,6 @@
+module Prelude (module Relude, io) where
+
+import Relude
+
+io :: MonadIO m => IO a -> m a
+io = liftIO
diff --git a/test/GoldenTests.hs b/test/GoldenTests.hs
new file mode 100644
--- /dev/null
+++ b/test/GoldenTests.hs
@@ -0,0 +1,41 @@
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE OverloadedStrings #-}
+import           AcousticBrainz.HighLevel
+import           AcousticBrainz.LowLevel
+import           MusicBrainz
+
+import           Data.Aeson
+import           Data.String
+import qualified Data.Text                as Text
+import qualified Data.Text.IO             as Text
+import qualified Data.Text.Lazy.Encoding  as Lazy
+import           System.FilePath
+import           Test.Tasty
+import           Test.Tasty.Golden        (findByExtension, goldenVsString)
+import           Text.Pretty.Simple
+
+main :: IO ()
+main = do
+  a <- highLevelTests
+  b <- lowLevelTests
+  defaultMain $ testGroup "Golden tests" [a, b]
+
+highLevelTests :: IO TestTree
+highLevelTests = do
+  files <- findByExtension [".highlevel"] "test"
+
+  return $ testGroup "High level tests" $ do
+    file <- files
+    let goldenFile = addExtension file ".golden"
+    return $ goldenVsString file goldenFile $
+      Text.readFile file <&> Text.strip >>= mkMusicBrainzIdentifier >>= getHighLevelData <&> pShowNoColor <&> Lazy.encodeUtf8
+
+lowLevelTests :: IO TestTree
+lowLevelTests = do
+  files <- findByExtension [".lowlevel"] "test"
+
+  return $ testGroup "Low level tests" $ do
+    file <- files
+    let goldenFile = addExtension file ".golden"
+    return $ goldenVsString file goldenFile $
+      Text.readFile file <&> Text.strip >>= mkMusicBrainzIdentifier >>= getLowLevelData <&> pShowNoColor <&> Lazy.encodeUtf8
diff --git a/test/Prelude.hs b/test/Prelude.hs
new file mode 100644
--- /dev/null
+++ b/test/Prelude.hs
@@ -0,0 +1,6 @@
+module Prelude (module Relude, io) where
+
+import Relude
+
+io :: MonadIO m => IO a -> m a
+io = liftIO
