Slide 1

Slide 1 text

Make yourself a web audio player with Purescript and Halogen Justin Woo 11 May 2017

Slide 2

Slide 2 text

Why? ● I want to listen to my 10 - 50 min mp3s ○ ...and seek forward 5/10/30 seconds ○ ...and seek backward 5/10/30 seconds ● Haven’t found anything I like ● Don’t want to make native app ● Want it to be easy to update

Slide 3

Slide 3 text

What is web audio? ● My working definition: ○ Thingies for audio to be played back on browser ● What I need: ○ Way to use local files for sources ○ Use built-in play/pause, seek ○ Need to be able to get and set current time

Slide 4

Slide 4 text

What is Purescript? ● Strongly typed language inspired by and written in Haskell ● Strictly evaluated ● Higher Kinded Types ● Row types ● Mature JS backend ● Allows easy FFI

Slide 5

Slide 5 text

● Browsers ● Node.js ● Toasters (e.g. rpi, hopefully not internet of shit) ● Reuse and adapt existing libraries ○ E.g. purescript-echarts - do you really want to rewrite this? ■ Purescript-echarts ● 9 people 115 commits from Jan 2015 ■ Baidu EFE echarts ● 37 people 3780 commits from May 2013 Why JS and easy FFI?

Slide 6

Slide 6 text

What is Halogen? ● Component-based UI library by Slamdata ● Based on common ideas, with user-defined: ○ State types ○ Query type ○ Input types (when a component used as child) ○ Output message types ● Render of State -> HTML Query ○ Parameterized HTML since before even Elm got on board ● Query eval eval :: forall m. Query ~> H.ComponentDSL State Query Message m

Slide 7

Slide 7 text

Query eval ● We get put/modify/get for state-related things ● LiftEff/LiftAff as needed ● And some more, but these are all I usually need

Slide 8

Slide 8 text

Gettin’ them types ready First, it’d be nice to use a newtype for ObjectURL The only sensible state to keep is our file ObjectURL Then we only need to handle two query types: setting a file and skipping newtype ObjectURL = ObjectURL String type State = { file :: Maybe ObjectURL } data Query a = FileSet a | Skip SkipDir SkipSize a data SkipDir = Bck | Fwd data SkipSize = Sm | Md | Lg

Slide 9

Slide 9 text

Just a bit of EFFin’ ceremony We need to declare the core row of effects in our application In our app, we’ll be writing to the console and using DOM methods a lot Of course, some qualified imports to keep in mind type AppEffects eff = ( console :: CONSOLE , dom :: DOM | eff ) import Halogen as H import Halogen.Aff as HA import Halogen.HTML as HH import Halogen.HTML.Events as HE import Halogen.HTML.Properties as HP import Halogen.VDom.Driver as D

Slide 10

Slide 10 text

Component Types Since we don’t need any fancy parent/child components for this project, the params are simple: ● HTML ● Our Query type ● Input type (no inputs) ● Output type (no outputs) ● Our type for non-component effects, using Aff for asynchronous effects ui :: forall eff. H.Component HH.HTML Query Unit Void (Aff (AppEffects eff))

Slide 11

Slide 11 text

Component Spec Now we can create our component spec! Initial state is defined simply as such, and the rest we will define as we go along Receiver used for inputs, which we won’t be using ui = H.component { initialState: const initialState , render , eval , receiver: const Nothing } where initialState = { file: Nothing }

Slide 12

Slide 12 text

Rendering Renders from state to Query-parameterized HTML Ref: for grabbing HTML reference Rest are normal indexed properties onChange: attached event handler to input our FileSet data render :: State -> H.ComponentHTML Query -- ... [ HH.input [ HP.ref (wrap "input") , HP.type_ HP.InputFile , HP.prop (wrap "accept") "audio/*" , HE.onChange (HE.input_ FileSet) ]]

Slide 13

Slide 13 text

Rendering audio Well… yup [ HH.audio [ HP.ref $ wrap "audio" , HP.src $ fromMaybe "" (unwrap <$> state.file) , HP.controls true , HP.autoplay true ] []

Slide 14

Slide 14 text

Rendering buttons More of the same HH.button [ HE.onClick $ HE.input_ (Skip Bck Lg) ] [ HH.label_ [HH.text "<<<"] ]

Slide 15

Slide 15 text

Eval First, let’s see our type Natural transformation of our Query into the Component DSL using our State, Query, no outgoing messages, and non-component Aff of AppEffects eff. eval :: Query ~> H.ComponentDSL State Query Void (Aff (AppEffects eff))

Slide 16

Slide 16 text

Eval (FileSet next) Originally written at 2AM in the simplest way possible Uses the ref we defined earlier to get the generic HTML element Then uses purescript-dom-classy to read the input as an input element On success, we then continue handling our input eval (FileSet next) = do input <- H.getHTMLElementRef $ wrap "input" case fromHTMLElement =<< input of Nothing -> log' "No input ref found" Just el -> handleInput el pure next

Slide 17

Slide 17 text

Eval (FileSet next) continued More ugly code censored for your viewing pleasure window.URL is used create an object url string from it and set it to our state This is then put into the component state using modify handleInput el = -- ... handleFile file = do url <- H.liftEff $ url =<< window blob <- H.liftEff $ createObjectURL file url -- ... H.modify \s -> s {file = pure $ wrap blob}

Slide 18

Slide 18 text

Eval (Skip dir size next) More straightforward, since we just need to find this element and set the time eval (Skip dir size next) = do audio <- H.getHTMLElementRef $ wrap "audio" case htmlAudioElementToHTMLMediaElement <$> (fromHTMLElement =<< audio) of Just el -> do current <- H.liftEff $ currentTime el H.liftEff $ setCurrentTime (current + delta) el _ -> log' "No audio ref found" pure next

Slide 19

Slide 19 text

Getting the delta for skip Might as well do it the easiest way possible skip = case size of Lg -> 30.0 Md -> 10.0 Sm -> 5.0 delta = skip * case dir of Bck -> -1.0 _ -> 1.0

Slide 20

Slide 20 text

Main runHalogenAff just runs our Aff in a fire-and-forget way We grab the document.body and run our component on it That’s it! main = HA.runHalogenAff do body <- HA.awaitBody io <- D.runUI ui unit body log "Running"

Slide 21

Slide 21 text

That’s all, folks!

Slide 22

Slide 22 text

Result

Slide 23

Slide 23 text

Links ● Repo ○ github.com/justinwoo/purescript-web-audio-player-demo ● Blog post ○ http://qiita.com/kimagure/items/653c52e77d7cd3567498 ● Halogen ○ https://github.com/slamdata/purescript-halogen