├── .gitignore ├── LICENSE ├── README.md ├── elm.json ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.elm ├── Encode.elm ├── Lib.elm ├── index.js └── main.css └── tests ├── Tests.elm └── elm-package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Distribution 2 | dist/ 3 | 4 | # elm-package generated files 5 | elm-stuff 6 | 7 | # elm-repl generated files 8 | repl-temp-* 9 | 10 | # Dependency directories 11 | node_modules 12 | 13 | # Desktop Services Store on macOS 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, Darren Prentice 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elm-audio-graph 2 | A declarative Elm interface to the Web Audio node API. 3 | 4 | Powered by [virtual-audio-graph](https://github.com/benji6/virtual-audio-graph) on the JS side. 5 | 6 | ```sh 7 | npm i 8 | npx elm-app start 9 | ``` 10 | 11 | ## Demo 12 | Volume Warning! 13 | 14 | [dpren.github.io/elm-audio-graph](https://dpren.github.io/elm-audio-graph) 15 | 16 | (you may need to enable "Sound" in your browser's site settings, this demo doesn't obey the policies for autoplay very well; which requires an initial user interaction) 17 | 18 | 19 | ## Build scripts 20 | This project is bootstrapped with [Create Elm App](https://github.com/halfzebra/create-elm-app). 21 | You can read the [full guide here](https://github.com/halfzebra/create-elm-app/blob/master/template/README.md). 22 | 23 | In the project directory you can run: 24 | 25 | #### `elm-app start` 26 | Runs the app in development mode at [http://localhost:3000](http://localhost:3000) 27 | 28 | The page will reload if you make edits. 29 | 30 | #### `elm-app build` 31 | Builds the app for production to the `dist` folder. 32 | The build is minified, and the filenames include the hashes. 33 | 34 | #### `elm-app test` 35 | Run tests with [node-test-runner](https://github.com/rtfeldman/node-test-runner/tree/master) 36 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src/" 5 | ], 6 | "elm-version": "0.19.0", 7 | "dependencies": { 8 | "direct": { 9 | "elm/browser": "1.0.1", 10 | "elm/core": "1.0.0", 11 | "elm/html": "1.0.0", 12 | "elm/json": "1.1.2" 13 | }, 14 | "indirect": { 15 | "elm/time": "1.0.0", 16 | "elm/url": "1.0.0", 17 | "elm/virtual-dom": "1.0.2" 18 | } 19 | }, 20 | "test-dependencies": { 21 | "direct": {}, 22 | "indirect": {} 23 | } 24 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elm-audio-graph", 3 | "version": "0.0.1", 4 | "private": true, 5 | "dependencies": { 6 | "elm": "^0.19.0-bugfix6", 7 | "elm-upgrade": "^0.19.6", 8 | "virtual-audio-graph": "0.19.0" 9 | }, 10 | "devDependencies": { 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dpren/elm-audio-graph/af88d950f87b057f4d8951536fb4fce4c7d05efc/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Elm App 8 | 9 | 10 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /src/App.elm: -------------------------------------------------------------------------------- 1 | module App exposing (Model, Msg(..), audioGraph, init, main, subscriptions, update, view) 2 | 3 | import Browser 4 | import Browser.Events exposing (onMouseMove) 5 | import Encode exposing (nodeToString, updateAudioGraph) 6 | import Html exposing (Html, br, div, text) 7 | import Json.Decode 8 | import Lib exposing (..) 9 | 10 | 11 | audioGraph : Model -> AudioGraph 12 | audioGraph model = 13 | let 14 | lfo = 15 | oscillatorParams 0 { oscillatorDefaults | frequency = model.y / 100 } 16 | 17 | lfoGain = 18 | gainParams 1 { gainDefaults | volume = model.y } 19 | 20 | saw = 21 | oscillatorParams 2 { oscillatorDefaults | waveform = Sawtooth, frequency = model.x } 22 | 23 | lowpass = 24 | filterParams 3 { filterDefaults | frequency = 900, q = 10 } 25 | 26 | master = 27 | gainParams 4 { gainDefaults | volume = model.y * 0.001 } 28 | in 29 | [ Oscillator lfo [ input lfoGain ] 30 | , Gain lfoGain [ frequency lowpass ] 31 | , Oscillator saw [ input lowpass ] 32 | , Filter lowpass [ input master ] 33 | , Gain master [ Output ] 34 | ] 35 | 36 | 37 | 38 | ----- PROGRAM ---- 39 | 40 | 41 | main : Program Flags Model Msg 42 | main = 43 | Browser.element 44 | { init = init 45 | , view = view 46 | , update = update 47 | , subscriptions = subscriptions 48 | } 49 | 50 | 51 | 52 | -- MODEL 53 | 54 | 55 | type alias Model = 56 | { x : Float 57 | , y : Float 58 | } 59 | 60 | 61 | type alias Flags = 62 | () 63 | 64 | 65 | init : Flags -> ( Model, Cmd Msg ) 66 | init = 67 | always ( { x = 0, y = 0 }, Cmd.none ) 68 | 69 | 70 | 71 | -- UPDATE 72 | 73 | 74 | type Msg 75 | = Position Int Int 76 | 77 | 78 | update : Msg -> Model -> ( Model, Cmd Msg ) 79 | update msg model = 80 | case msg of 81 | Position x y -> 82 | ( { model | x = toFloat x, y = toFloat y } 83 | , updateAudioGraph (audioGraph model) 84 | ) 85 | 86 | 87 | 88 | -- SUBSCRIPTIONS 89 | 90 | 91 | subscriptions : Model -> Sub Msg 92 | subscriptions model = 93 | onMouseMove 94 | (Json.Decode.map2 Position 95 | (Json.Decode.field "clientX" Json.Decode.int) 96 | (Json.Decode.field "clientY" Json.Decode.int) 97 | ) 98 | 99 | 100 | 101 | -- VIEW 102 | 103 | 104 | view : Model -> Html Msg 105 | view model = 106 | div [] 107 | [ div [] [ text ("x: " ++ String.fromFloat model.x ++ " y: " ++ String.fromFloat model.y) ] 108 | , br [] [] 109 | , div [] 110 | (List.map 111 | (\node -> div [] [ text (nodeToString node) ]) 112 | (audioGraph model) 113 | ) 114 | ] 115 | -------------------------------------------------------------------------------- /src/Encode.elm: -------------------------------------------------------------------------------- 1 | port module Encode exposing (nodeToString, updateAudioGraph) 2 | 3 | import Json.Encode exposing (Value, float, int, list, object, string) 4 | import Lib exposing (..) 5 | 6 | 7 | updateAudioGraph : AudioGraph -> Cmd msg 8 | updateAudioGraph = 9 | renderContextJs << encodeGraph 10 | 11 | 12 | port renderContextJs : Value -> Cmd msg 13 | 14 | 15 | encodeGraph : AudioGraph -> Value 16 | encodeGraph = 17 | object << List.map encodeNode 18 | 19 | 20 | encodeNode : AudioNode -> ( String, Value ) 21 | encodeNode node = 22 | case node of 23 | Gain params edges -> 24 | nodePatternMatch params.id "gain" edges (encodeGainParams params) 25 | 26 | Oscillator params edges -> 27 | nodePatternMatch params.id "oscillator" edges (encodeOscillatorParams params) 28 | 29 | Filter params edges -> 30 | nodePatternMatch params.id "biquadFilter" edges (encodeFilterParams params) 31 | 32 | Delay params edges -> 33 | nodePatternMatch params.id "delay" edges (encodeDelayParams params) 34 | 35 | 36 | nodeToString : AudioNode -> String 37 | nodeToString audioNode = 38 | Tuple.mapSecond (Json.Encode.encode 2) (encodeNode audioNode) 39 | |> (\( name, values ) -> name ++ ": " ++ values) 40 | 41 | 42 | nodePatternMatch : NodeId -> String -> NodeEdges -> Value -> ( String, Value ) 43 | nodePatternMatch id apiName edges encodedParams = 44 | -- matches virtual-audio-graph api 45 | ( String.fromInt id 46 | , list identity [ string apiName, encodeEdges edges, encodedParams ] 47 | ) 48 | 49 | 50 | encodeEdges : NodeEdges -> Value 51 | encodeEdges = 52 | list encodeNodePort 53 | 54 | 55 | encodeNodePort : NodePort NodeId -> Value 56 | encodeNodePort nodePort = 57 | let 58 | keyDestObject id param = 59 | object 60 | [ ( "key", toStringValue id ) 61 | , ( "destination", string param ) 62 | ] 63 | in 64 | case nodePort of 65 | Output -> 66 | string "output" 67 | 68 | Input id -> 69 | toStringValue id 70 | 71 | Volume id -> 72 | keyDestObject id "gain" 73 | 74 | Frequency id -> 75 | keyDestObject id "frequency" 76 | 77 | Detune id -> 78 | keyDestObject id "detune" 79 | 80 | Q id -> 81 | keyDestObject id "q" 82 | 83 | DelayTime id -> 84 | keyDestObject id "delayTime" 85 | 86 | 87 | encodeGainParams : GainParams -> Value 88 | encodeGainParams node = 89 | object 90 | [ ( "gain", float node.volume ) ] 91 | 92 | 93 | encodeOscillatorParams : OscillatorParams -> Value 94 | encodeOscillatorParams node = 95 | object 96 | [ ( "type", encodeWaveform node.waveform ) 97 | , ( "frequency", float node.frequency ) 98 | , ( "detune", float node.detune ) 99 | ] 100 | 101 | 102 | encodeFilterParams : BiquadFilterParams -> Value 103 | encodeFilterParams node = 104 | object 105 | [ ( "type", encodeFilterMode node.mode ) 106 | , ( "frequency", float node.frequency ) 107 | , ( "Q", float node.q ) 108 | , ( "detune", float node.detune ) 109 | ] 110 | 111 | 112 | encodeDelayParams : DelayParams -> Value 113 | encodeDelayParams node = 114 | object 115 | [ ( "delayTime", float node.delayTime ) 116 | , ( "maxDelayTime", float node.maxDelayTime ) 117 | ] 118 | 119 | 120 | toStringValue : Int -> Value 121 | toStringValue = 122 | string << String.fromInt 123 | 124 | 125 | toLowerStringValue : Int -> Value 126 | toLowerStringValue = 127 | string << String.toLower << String.fromInt 128 | -------------------------------------------------------------------------------- /src/Lib.elm: -------------------------------------------------------------------------------- 1 | module Lib exposing (AudioGraph, AudioInput(..), AudioNode(..), BiquadFilterDefaults, BiquadFilterParams, DelayDefaults, DelayParams, FilterMode(..), GainDefaults, GainParams, NodeEdges, NodeId, NodePort(..), OscillatorDefaults, OscillatorParams, Waveform(..), delayDefaults, delayParams, delayTime, detune, encodeFilterMode, encodeWaveform, filterDefaults, filterParams, frequency, gainDefaults, gainParams, input, oscillatorDefaults, oscillatorParams, q, volume) 2 | 3 | import Json.Encode 4 | 5 | 6 | type alias AudioGraph = 7 | List AudioNode 8 | 9 | 10 | type AudioNode 11 | = Gain GainParams NodeEdges 12 | | Oscillator OscillatorParams NodeEdges 13 | | Filter BiquadFilterParams NodeEdges 14 | | Delay DelayParams NodeEdges 15 | 16 | 17 | type alias NodeEdges = 18 | List (NodePort NodeId) 19 | 20 | 21 | type alias NodeId = 22 | Int 23 | 24 | 25 | type AudioInput 26 | = AudioInput 27 | 28 | 29 | type Waveform 30 | = Sine 31 | | Square 32 | | Sawtooth 33 | | Triangle 34 | 35 | 36 | encodeWaveform : Waveform -> Json.Encode.Value 37 | encodeWaveform v = 38 | Json.Encode.string <| 39 | String.toLower <| 40 | case v of 41 | Sine -> 42 | "Sine" 43 | 44 | Square -> 45 | "Square" 46 | 47 | Sawtooth -> 48 | "Sawtooth" 49 | 50 | Triangle -> 51 | "Triangle" 52 | 53 | 54 | type FilterMode 55 | = Lowpass 56 | | Highpass 57 | | Bandpass 58 | | Lowshelf 59 | | Highshelf 60 | | Peaking 61 | | Notch 62 | | Allpass 63 | 64 | 65 | encodeFilterMode : FilterMode -> Json.Encode.Value 66 | encodeFilterMode v = 67 | Json.Encode.string <| 68 | String.toLower <| 69 | case v of 70 | Lowpass -> 71 | "Lowpass" 72 | 73 | Highpass -> 74 | "Highpass" 75 | 76 | Bandpass -> 77 | "Bandpass" 78 | 79 | Lowshelf -> 80 | "Lowshelf" 81 | 82 | Highshelf -> 83 | "Highshelf" 84 | 85 | Peaking -> 86 | "Peaking" 87 | 88 | Notch -> 89 | "Notch" 90 | 91 | Allpass -> 92 | "Allpass" 93 | 94 | 95 | 96 | -------- Node Constructors -------- 97 | 98 | 99 | {-| 100 | -} 101 | type alias GainDefaults = 102 | { volume : Float 103 | } 104 | 105 | 106 | gainDefaults : GainDefaults 107 | gainDefaults = 108 | { volume = 1 109 | } 110 | 111 | 112 | type alias GainParams = 113 | { id : NodeId 114 | , input : AudioInput 115 | , volume : Float 116 | } 117 | 118 | 119 | gainParams : NodeId -> GainDefaults -> GainParams 120 | gainParams id defaults = 121 | { id = id 122 | , input = AudioInput 123 | , volume = defaults.volume 124 | } 125 | 126 | 127 | {-| 128 | -} 129 | type alias OscillatorDefaults = 130 | { waveform : Waveform 131 | , frequency : Float 132 | , detune : Float 133 | } 134 | 135 | 136 | oscillatorDefaults : OscillatorDefaults 137 | oscillatorDefaults = 138 | { waveform = Sine 139 | , frequency = 440 140 | , detune = 0 141 | } 142 | 143 | 144 | type alias OscillatorParams = 145 | { id : NodeId 146 | , waveform : Waveform 147 | , frequency : Float 148 | , detune : Float 149 | } 150 | 151 | 152 | oscillatorParams : NodeId -> OscillatorDefaults -> OscillatorParams 153 | oscillatorParams id defaults = 154 | { id = id 155 | , waveform = defaults.waveform 156 | , frequency = defaults.frequency 157 | , detune = defaults.detune 158 | } 159 | 160 | 161 | {-| 162 | -} 163 | type alias BiquadFilterDefaults = 164 | { mode : FilterMode 165 | , frequency : Float 166 | , q : Float 167 | , detune : Float 168 | } 169 | 170 | 171 | filterDefaults : BiquadFilterDefaults 172 | filterDefaults = 173 | { mode = Lowpass 174 | , frequency = 350 175 | , q = 1 176 | , detune = 0 177 | } 178 | 179 | 180 | type alias BiquadFilterParams = 181 | { id : NodeId 182 | , input : AudioInput 183 | , mode : FilterMode 184 | , frequency : Float 185 | , q : Float 186 | , detune : Float 187 | } 188 | 189 | 190 | filterParams : NodeId -> BiquadFilterDefaults -> BiquadFilterParams 191 | filterParams id defaults = 192 | { id = id 193 | , input = AudioInput 194 | , mode = defaults.mode 195 | , frequency = defaults.frequency 196 | , q = defaults.q 197 | , detune = defaults.detune 198 | } 199 | 200 | 201 | {-| 202 | -} 203 | type alias DelayDefaults = 204 | { delayTime : Float 205 | , maxDelayTime : Float 206 | } 207 | 208 | 209 | delayDefaults : DelayDefaults 210 | delayDefaults = 211 | { delayTime = 0 212 | , maxDelayTime = 0 213 | } 214 | 215 | 216 | type alias DelayParams = 217 | { id : NodeId 218 | , delayTime : Float 219 | , maxDelayTime : Float 220 | } 221 | 222 | 223 | delayParams : NodeId -> DelayDefaults -> DelayParams 224 | delayParams id defaults = 225 | { id = id 226 | , delayTime = 0 227 | , maxDelayTime = 0 228 | } 229 | 230 | 231 | 232 | -------- Param Ports -------- 233 | 234 | 235 | type NodePort id 236 | = Output 237 | | Input id 238 | | Volume id 239 | | Frequency id 240 | | Detune id 241 | | Q id 242 | | DelayTime id 243 | 244 | 245 | input : { a | input : AudioInput, id : NodeId } -> NodePort NodeId 246 | input a = 247 | Input a.id 248 | 249 | 250 | volume : { a | volume : Float, id : NodeId } -> NodePort NodeId 251 | volume a = 252 | Volume a.id 253 | 254 | 255 | frequency : { a | frequency : Float, id : NodeId } -> NodePort NodeId 256 | frequency a = 257 | Frequency a.id 258 | 259 | 260 | detune : { a | detune : Float, id : NodeId } -> NodePort NodeId 261 | detune a = 262 | Detune a.id 263 | 264 | 265 | q : { a | q : Float, id : NodeId } -> NodePort NodeId 266 | q a = 267 | Q a.id 268 | 269 | 270 | delayTime : { a | delayTime : Float, id : NodeId } -> NodePort NodeId 271 | delayTime a = 272 | DelayTime a.id 273 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import './main.css' 2 | const { Elm } = require('./App.elm'); 3 | const createVirtualAudioGraph = require('virtual-audio-graph'); 4 | 5 | window.AudioContext = window.AudioContext || window.webkitAudioContext; 6 | if (typeof window.AudioContext === 'undefined') { 7 | alert('Sorry, this browser does not support the Web Audio API.'); 8 | } 9 | 10 | const root = document.getElementById('root'); 11 | var app = Elm.App.init({ 12 | node: root 13 | }); 14 | 15 | const audioContext = new AudioContext(); 16 | const virtualAudioGraph = createVirtualAudioGraph({ 17 | audioContext: audioContext, 18 | output: audioContext.destination 19 | }); 20 | 21 | app.ports.renderContextJs.subscribe(function(graph) { 22 | virtualAudioGraph.update(graph); 23 | }); 24 | -------------------------------------------------------------------------------- /src/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Source Sans Pro', 'Trebuchet MS', 'Lucida Grande', 'Bitstream Vera Sans', 'Helvetica Neue', sans-serif; 3 | font-size: 20px; 4 | margin: 0; 5 | color: #293c4b; 6 | } 7 | -------------------------------------------------------------------------------- /tests/Tests.elm: -------------------------------------------------------------------------------- 1 | module Tests exposing (..) 2 | 3 | import Test exposing (..) 4 | import Expect 5 | import String 6 | import App 7 | 8 | 9 | all : Test 10 | all = 11 | describe "A Test Suite" 12 | [ test "App.model.message should be set properly" <| 13 | \() -> 14 | (Tuple.first (App.init "../src/logo.svg") |> .message) 15 | |> Expect.equal "Your Elm App is working!" 16 | , test "Addition" <| 17 | \() -> 18 | Expect.equal 10 (3 + 7) 19 | , test "String.left" <| 20 | \() -> 21 | Expect.equal "a" (String.left 1 "abcdefg") 22 | , test "This test should fail" <| 23 | \() -> 24 | Expect.fail "failed as expected!" 25 | ] 26 | -------------------------------------------------------------------------------- /tests/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "Test Suites", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | ".", 8 | "..", 9 | "../src/" 10 | ], 11 | "exposed-modules": [], 12 | "dependencies": { 13 | "elm-lang/core": "5.0.0 <= v < 6.0.0", 14 | "elm-community/elm-test": "4.0.0 <= v < 5.0.0", 15 | "elm-lang/html": "2.0.0 <= v < 3.0.0" 16 | }, 17 | "elm-version": "0.18.0 <= v < 0.19.0" 18 | } 19 | --------------------------------------------------------------------------------