Created
February 3, 2017 14:48
-
-
Save basile-henry/ac86f8bb8829e156d2041084bc6c3613 to your computer and use it in GitHub Desktop.
DailyProgrammer -- 301 [Hard] Guitar Tablature
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- stack exec -- ghc -O2 Main.hs | |
-- cat input1.txt | ./Main --sound | aplay | |
module Main where | |
import Control.Applicative | |
import Data.List | |
import Data.Maybe | |
import Data.WAVE | |
import Notes | |
import Safe (readMay) | |
import System.Environment | |
import System.IO (stdout) | |
type GuitarChord = [Pitch] | |
type Bar = [GuitarChord] | |
type Tab = [Bar] | |
data Option | |
= OutputNames | |
| OutputSound | |
main :: IO () | |
main = do | |
opt <- parseOptions <$> getArgs | |
tab <- parseTab <$> getContents | |
case opt of | |
OutputNames -> printTab tab | |
OutputSound -> playTab tab | |
printTab :: Tab -> IO () | |
printTab = print | |
. unwords | |
. map show | |
. concat | |
. concat | |
playTab :: Tab -> IO () | |
playTab = | |
hPutWAVE stdout | |
. WAVE (WAVEHeader 1 44100 16 $ Just 10000000) | |
-- Just ... is to lazily output the wave file and not run out of memory | |
. toSamples 44100 | |
. toTrack | |
parseOptions :: [String] -> Option | |
parseOptions [] = OutputNames | |
parseOptions ("-n":_) = OutputNames | |
parseOptions ("--names":_) = OutputNames | |
parseOptions ("-s":_) = OutputSound | |
parseOptions ("--sound":_) = OutputSound | |
parseOptions (_:opts) = parseOptions opts | |
parseTab :: String -> Tab | |
parseTab = map | |
( map catMaybes -- to GuitarChord | |
. transpose | |
. zipWith stringToPitch guitarStrings | |
. parseTabFrets | |
) | |
. transpose -- split into bars | |
. map separateBars | |
. concat -- make one long tab | |
. groupBy6 | |
. map (drop 1) -- remove the note names at the beginning of each line | |
. filter (/=[]) -- remove empty lines | |
. lines | |
. filter (`notElem` " \t") -- simple removal of whitespace | |
groupBy6 :: [a] -> [[a]] | |
groupBy6 [] = [] | |
groupBy6 xs = uncurry (:) . fmap groupBy6 . splitAt 6 $ xs | |
parseTabFrets :: [String] -> [[Maybe Int]] | |
parseTabFrets ["", "", "", "", "", ""] = [[], [], [], [], [], []] | |
parseTabFrets xs | |
| null $ catMaybes frets = next 1 | |
| maximum frets > Just 9 = next 2 | |
| otherwise = next 1 | |
where | |
frets = map parseFret xs | |
next skip = zipWith (:) frets | |
. parseTabFrets | |
. map (drop skip) | |
$ xs | |
stringToPitch :: Pitch -> [Maybe Int] -> [Maybe Pitch] | |
stringToPitch p = map (fmap (fretToPitch p)) | |
fretToPitch :: Pitch -> Int -> Pitch | |
fretToPitch (Pitch o n) f | |
| fromEnum n + f > 11 = fretToPitch (Pitch (o + 1) n) (f - 12) | |
| otherwise = Pitch o (toEnum $ fromEnum n + f) | |
-- This assumes there is always a '-' after '|' (except at the end of a line) | |
separateBars :: String -> [String] | |
separateBars "" = [] | |
separateBars ('|':xs) = "" : separateBars (drop 1 xs) | |
separateBars (x:xs) = | |
case separateBars xs of | |
[] -> [[x]] | |
(y:ys) -> (x:y) : ys | |
parseFret :: String -> Maybe Int | |
parseFret ('-':_) = Nothing -- make sure it isn't parsed as a negative number | |
parseFret xs = readMay (take 2 xs) <|> readMay (take 1 xs) | |
guitarStrings :: [Pitch] | |
guitarStrings = | |
[ Pitch 4 E | |
, Pitch 3 B | |
, Pitch 3 G | |
, Pitch 3 D | |
, Pitch 2 A | |
, Pitch 2 E | |
] | |
toTrack :: Tab -> [Chord] | |
toTrack = map (uncurry toChord) | |
. concatMap noteLengths | |
where | |
toChord :: [Pitch] -> Float -> Chord | |
toChord [] d = Chord [note d 0.0 $ Pitch 4 A] -- silence | |
toChord ps d = Chord $ map (note d a) ps | |
where | |
a = 1.0 / fromIntegral (length ps) | |
note :: Float -> Float -> Pitch -> Note | |
note d a = Note (d * basicDur) a env timbre | |
basicDur = 0.1 -- time equivalent to 1 '-' in the tab | |
env = Envelope 0.05 0.1 | |
timbre = map (/ sum harmonics) harmonics | |
harmonics = [10, 7, 4, 0, 3, 1.5, 1] | |
noteLengths :: Bar -> [([Pitch], Float)] | |
noteLengths = reverse . foldl' go [] | |
where | |
go :: [([Pitch], Float)] -> [Pitch] -> [([Pitch], Float)] | |
go [] [] = [([], 1.0)] | |
go ((xs,d):ys) [] = (xs, d + 1.0) : ys | |
go ys ps = (ps, 1.0) : ys |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{-# LANGUAGE MagicHash #-} | |
module Notes where | |
import Data.List | |
import Data.WAVE | |
data NoteName = C | C# | D | D# | E | F | F# | G | G# | A | A# | B | |
deriving (Enum, Show) | |
type Octave = Int | |
data Pitch = Pitch Octave NoteName | |
instance Show Pitch where | |
show (Pitch o n) = show n ++ show o | |
{- | |
----| duration |---- | |
----| attack |--------| release |---- | |
-} | |
data Envelope | |
= Envelope | |
{ attack :: Float -- in seconds | |
, release :: Float -- in seconds | |
} deriving Show | |
data Note | |
= Note | |
{ duration :: Float -- in seconds | |
, amplitude :: Float -- in [0..1] | |
, envelope :: Envelope | |
, timbre :: [Float] | |
, pitch :: Pitch | |
} deriving Show | |
newtype Chord = Chord { notes :: [Note] } deriving Show | |
class Samples a where | |
toSamples :: Float -> a -> WAVESamples | |
instance Samples a => Samples [a] where | |
toSamples rate = concatMap (toSamples rate) | |
instance Samples Chord where | |
toSamples rate = mix . map (toSamples rate) . notes | |
instance Samples Note where | |
toSamples rate note | |
| amplitude note == 0 = replicate size [0] | |
| otherwise = applyEnvelope rate (envelope note) | |
. take size | |
. (map . map) (amplitude note .*) | |
. applyTimbre (timbre note) | |
. toSamples rate | |
$ pitch note | |
where | |
size = floor $ duration note * rate | |
instance Samples Pitch where | |
toSamples rate pitch = | |
map (\x -> [floor $ maxAmplitude * sin ( 2 * pi * freq * fromIntegral x / rate)]) [0..] | |
where | |
freq = frequency pitch | |
maxAmplitude = 0.95 * fromIntegral (maxBound :: WAVESample) | |
infixl 6 .* | |
(.*) :: Float -> WAVESample -> WAVESample | |
x .* y = floor $ x * fromIntegral y | |
mix :: [WAVESamples] -> WAVESamples | |
mix = foldl' (zipWith (zipWith (+))) (repeat [0]) | |
applyTimbre :: [Float] -> WAVESamples -> WAVESamples | |
applyTimbre weights = mix | |
. zipWith (\w -> (map . map) (w .*)) weights | |
. zipWith skippingStep [0..] | |
. replicate (length weights) | |
where | |
skippingStep :: Int -> [a] -> [a] | |
skippingStep _ [] = [] | |
skippingStep n (x:xs) = x : skippingStep n (drop n xs) | |
applyEnvelope :: Float -> Envelope -> WAVESamples -> WAVESamples | |
applyEnvelope rate env samples = | |
concat | |
[ linear attackSize $ zip [0..] pre | |
, hold | |
, linear releaseSize $ zip [releaseSize, pred releaseSize..] post | |
] | |
where | |
attackSize = floor $ attack env * rate | |
releaseSize = floor $ release env * rate | |
(pre, rest) = splitAt attackSize samples | |
(hold, post) = splitAt (length rest - releaseSize) rest | |
linear :: Int -> [(Int, [WAVESample])] -> WAVESamples | |
linear n = map (\(x, s) -> map (fromIntegral x / fromIntegral n .*) s) | |
frequency :: Pitch -> Float | |
frequency pitch = referenceFreq * 1.0594630943592953 ** n | |
-- 1.0594630943592953 is 2 ** (1/12) | |
where | |
index (Pitch octave name) = 12 * octave + fromEnum name | |
referencePitch = Pitch 4 A | |
referenceFreq = 440.0 | |
n = fromIntegral $ index pitch - index referencePitch |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment