packages feed

herald-0.1.0.1: src/Herald/Performance/Instrument.hs

-- | Instruments render a note of a frequency and duration, peaking at most 1.
module Herald.Performance.Instrument
  ( Instrument (..),
    pureTone,
    additive,
    organ,
    harpsichord,
    partials,
    chord,
    trill,
  )
where

import Data.Vector.Unboxed qualified as V
import Herald.Sound

-- | Renders a note of a frequency and duration.
newtype Instrument = Instrument {renderNote :: Hz -> Seconds -> Sound}

-- | Sine partials as frequency and relative amplitude, with short attack and
-- release ramps. Partials above Nyquist are dropped to avoid aliasing.
partials :: [(Hz, Double)] -> Seconds -> Sound
partials ws t = mixSound [gain (a / total) (envelope (sine f t)) | (f, a) <- ws']
  where
    ws' = filter ((< sampleRate / 2) . fst) ws
    total = sum (snd <$> ws')
    len = sampleCount t
    ramp i = min 1.0 (fromIntegral i * 0.001)
    envelope = V.imap (\i x -> ramp i * ramp (len - 1 - i) * x)

-- | A single sine wave.
pureTone :: Instrument
pureTone = Instrument $ \f -> partials [(f, 1)]

-- | Five slightly inharmonic partials.
additive :: Instrument
additive = Instrument $ \f -> partials (take 5 $ zip (inharmonics 0.001 f) (iterate (* 0.5) 1.0))

-- | Pipe organ stops.
organ :: Instrument
organ = Instrument $ \f -> partials [(f * r, a) | (r, a) <- stops]
  where
    stops =
      [ (0.5, 0.5), -- 16'
        (1, 1), -- 8'
        (2, 0.6), -- 4'
        (3, 0.3), -- 2 2/3'
        (4, 0.4), -- 2'
        (8, 0.2) -- 1'
      ]

-- | Faded out over the last 5 ms so notes cut short don't click.
harpsichord :: Instrument
harpsichord = Instrument $ \f t ->
  let len = sampleCount t
      fade = fromIntegral (sampleCount 0.005)
   in V.imap (\i x -> min 1 (fromIntegral (len - 1 - i) / fade) * x) (pluck 0.996 f t)

-- | Notes sounding together.
chord :: Instrument -> [Hz] -> Seconds -> Sound
chord i fs t = balanceSound [renderNote i f t | f <- fs]

-- | @k@ equal notes alternating @f@ and @g@.
trill :: Instrument -> Int -> Hz -> Hz -> Seconds -> Sound
trill i k f g t = V.concat [renderNote i x (t / fromIntegral k') | x <- take k' (cycle [f, g])]
  where
    k' = max 1 k