And while I'm nearby, I might as well also share some voltage output calibration support code I've put in place.
For my uses, I don't need calibration throughout, say, a slow ramp, but for my uses I do need to hit specific voltages very accurately (to control delay times and such.) With that in mind, I opted to build some control-rate output calibration over on the Tidal side.
This can actually be used for all sorts of interpolated-transfer-function type silliness, to remap values well beyond the needs of calibrating voltages, so perhaps others might find it useful too.
I first wrote some Map helpers to retrieve nearest values, and some code to help with lookup and interpolation of values. These exist within my fork of Tidal, but it is unpublished so I'm just going to paste in the relevant module contents:
module Sound.TidalMV.Utils where
import Prelude
import qualified Data.Map as Map
import qualified Data.Map.Internal as MapInternal
data LookupNearestResult k a
= NoMatch
| LTMatch k a
| EQMatch a
| GTMatch k a
| RangeMatch k a k a
deriving (Eq, Show)
lookupNearest :: Ord k => k -> Map.Map k a -> LookupNearestResult k a
lookupNearest k (MapInternal.Bin _size key value left right) = case compare k key of
LT -> case left of
bin@MapInternal.Bin {} -> case lookupNearest k bin of
NoMatch -> GTMatch key value
LTMatch ltk ltv -> RangeMatch ltk ltv key value
eqMatch@EQMatch {} -> eqMatch
gtMatch@GTMatch {} -> gtMatch
rangeMatch@RangeMatch {} -> rangeMatch
MapInternal.Tip -> GTMatch key value
EQ -> EQMatch value
GT -> case right of
bin@MapInternal.Bin {} -> case lookupNearest k bin of
NoMatch -> LTMatch key value
ltMatch@LTMatch {} -> ltMatch
eqMatch@EQMatch {} -> eqMatch
GTMatch gtk gtv -> RangeMatch key value gtk gtv
rangeMatch@RangeMatch {} -> rangeMatch
MapInternal.Tip -> LTMatch key value
lookupNearest _ MapInternal.Tip = NoMatch
lerp :: (Fractional a, Ord a) => a -> Map.Map a a -> Maybe a
lerp k m = case lookupNearest k m of
NoMatch -> Nothing
LTMatch ltk0 ltv0 ->
if Map.size m == 1 then
Just ltv0
else do
let (ltk1, ltv1) = Map.elemAt 1 m
Just $ go k ltk0 ltv0 ltk1 ltv1
EQMatch eqv -> Just eqv
GTMatch gtk1 gtv1 -> do
let mapSize = Map.size m
if mapSize == 1 then
Just gtv1
else do
let (gtk0, gtv0) = Map.elemAt (mapSize - 2) m
Just $ go k gtk0 gtv0 gtk1 gtv1
RangeMatch ltk ltv gtk gtv -> Just $ go k ltk ltv gtk gtv
where go x x0 y0 x1 y1 = y0 + ((x - x0) * ((y1 - y0) / (x1 - x0)))
This is then used by:
- constructing a list of specified voltage and resulting actual voltage
- swapping those into (for calibration purposes) a map from resulting actual voltages to voltages which must be specified to the hardware interface, and then
- using a calibrate function to turn patterns of expected output voltages into whatever value the hardware interface needs to see, with interpolation and extrapolation where needed.
import qualified Data.Map.Strict as Map
import qualified Data.Tuple as Tuple
leftES3Ch4List =
[ ((-10), (-10.136))
, ((-9.8387), (-10))
, ((-9), (-9.147))
, ((-8.8585), (-9))
, ((-8), (-8.126))
, ((-7.879), (-8))
... etc.
, (8, 8.209)
, (8.773, 9)
, (9, 9.230)
, (9.7525, 10)
, (10, 10.240)
]
leftES3Ch4Map = Map.fromList $ Tuple.swap <$> leftES3Ch4List
calibrate :: (Fractional a, Ord a) => Map.Map a a -> Pattern a -> Pattern a
calibrate m = filterJust . fmap (flip lerp m)
With this all in place I can pattern voltages I expect to see at the physical output and the various code above will do its best to make it happen, ala
(...) calibrate leftES3Ch4Map "-2 0 2 3"
Which I generally wrap up into other helpers for controlling outputs destined for certain modules, so that by the time I'm done it looks more like:
ptapographictime "-1.724 2.06"
(Which I'm soon going to wrap up even more so that I can get replace the ugly numbers with nice strings, via yet more value mapping code.)
This does, of course, require that you go through a calibration process where you send uncalibrated voltages and then measure them on the other side, but (modulo temperature drift, power supply changes, etc.) that is generally a one-time process and you can provide as few or as many calibration values as you like.