├── .prettierrc.json ├── style └── style.css ├── audio ├── tom-808.ogg ├── tom-fm.ogg ├── clap-808.ogg ├── clap-fat.ogg ├── clap-tape.ogg ├── crash-808.ogg ├── hihat-808.ogg ├── kick-808.ogg ├── kick-big.ogg ├── kick-deep.ogg ├── kick-dry.ogg ├── kick-tape.ogg ├── kick-tron.ogg ├── perc-808.ogg ├── snare-808.ogg ├── snare-big.ogg ├── tom-lofi.ogg ├── tom-short.ogg ├── clap-analog.ogg ├── clap-crushed.ogg ├── clap-slapper.ogg ├── cowbell-808.ogg ├── crash-noise.ogg ├── crash-tape.ogg ├── hihat-analog.ogg ├── hihat-dist01.ogg ├── hihat-dist02.ogg ├── hihat-plain.ogg ├── hihat-reso.ogg ├── hihat-ring.ogg ├── kick-classic.ogg ├── kick-floppy.ogg ├── kick-gritty.ogg ├── kick-heavy.ogg ├── kick-newwave.ogg ├── kick-plain.ogg ├── kick-softy.ogg ├── kick-stomp.ogg ├── kick-thump.ogg ├── kick-tight.ogg ├── kick-vinyl01.ogg ├── kick-vinyl02.ogg ├── kick-zapper.ogg ├── openhat-808.ogg ├── perc-chirpy.ogg ├── perc-hollow.ogg ├── perc-laser.ogg ├── perc-metal.ogg ├── perc-nasty.ogg ├── perc-short.ogg ├── perc-tambo.ogg ├── perc-tribal.ogg ├── perc-weirdo.ogg ├── snare-analog.ogg ├── snare-block.ogg ├── snare-brute.ogg ├── snare-dist01.ogg ├── snare-dist02.ogg ├── snare-dist03.ogg ├── snare-lofi01.ogg ├── snare-lofi02.ogg ├── snare-noise.ogg ├── snare-pinch.ogg ├── snare-punch.ogg ├── snare-sumo.ogg ├── snare-tape.ogg ├── tom-analog.ogg ├── tom-chiptune.ogg ├── tom-rototom.ogg ├── crash-acoustic.ogg ├── hihat-digital.ogg ├── hihat-electro.ogg ├── kick-electro01.ogg ├── kick-electro02.ogg ├── kick-oldschool.ogg ├── kick-slapback.ogg ├── openhat-analog.ogg ├── openhat-slick.ogg ├── openhat-tight.ogg ├── shaker-analog.ogg ├── shaker-shuffle.ogg ├── shaker-suckup.ogg ├── snare-electro.ogg ├── snare-modular.ogg ├── snare-smasher.ogg ├── snare-vinyl01.ogg ├── snare-vinyl02.ogg ├── tom-acoustic01.ogg ├── tom-acoustic02.ogg ├── hihat-acoustic01.ogg ├── hihat-acoustic02.ogg ├── kick-acoustic01.ogg ├── kick-acoustic02.ogg ├── kick-cultivator.ogg ├── ride-acoustic01.ogg ├── ride-acoustic02.ogg ├── snare-acoustic01.ogg ├── snare-acoustic02.ogg └── openhat-acoustic01.ogg ├── .gitignore ├── README.md ├── index.html ├── elm ├── Ports.elm ├── Data │ ├── Note.elm │ ├── Instrument.elm │ ├── Sequence.elm │ ├── Track.elm │ ├── Scale.elm │ ├── Drum │ │ └── Kit.elm │ └── Drum.elm └── Main.elm ├── elm.json ├── package.json └── js ├── meter.js ├── index.js └── instruments.js /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120 3 | } 4 | -------------------------------------------------------------------------------- /style/style.css: -------------------------------------------------------------------------------- 1 | .vuMeter { 2 | width: 300px; 3 | height: 300px; 4 | } 5 | -------------------------------------------------------------------------------- /audio/tom-808.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/tom-808.ogg -------------------------------------------------------------------------------- /audio/tom-fm.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/tom-fm.ogg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | /dist 3 | /node_modules 4 | /elm-stuff 5 | /package-lock.json 6 | -------------------------------------------------------------------------------- /audio/clap-808.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/clap-808.ogg -------------------------------------------------------------------------------- /audio/clap-fat.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/clap-fat.ogg -------------------------------------------------------------------------------- /audio/clap-tape.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/clap-tape.ogg -------------------------------------------------------------------------------- /audio/crash-808.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/crash-808.ogg -------------------------------------------------------------------------------- /audio/hihat-808.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/hihat-808.ogg -------------------------------------------------------------------------------- /audio/kick-808.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-808.ogg -------------------------------------------------------------------------------- /audio/kick-big.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-big.ogg -------------------------------------------------------------------------------- /audio/kick-deep.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-deep.ogg -------------------------------------------------------------------------------- /audio/kick-dry.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-dry.ogg -------------------------------------------------------------------------------- /audio/kick-tape.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-tape.ogg -------------------------------------------------------------------------------- /audio/kick-tron.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-tron.ogg -------------------------------------------------------------------------------- /audio/perc-808.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/perc-808.ogg -------------------------------------------------------------------------------- /audio/snare-808.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/snare-808.ogg -------------------------------------------------------------------------------- /audio/snare-big.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/snare-big.ogg -------------------------------------------------------------------------------- /audio/tom-lofi.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/tom-lofi.ogg -------------------------------------------------------------------------------- /audio/tom-short.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/tom-short.ogg -------------------------------------------------------------------------------- /audio/clap-analog.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/clap-analog.ogg -------------------------------------------------------------------------------- /audio/clap-crushed.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/clap-crushed.ogg -------------------------------------------------------------------------------- /audio/clap-slapper.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/clap-slapper.ogg -------------------------------------------------------------------------------- /audio/cowbell-808.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/cowbell-808.ogg -------------------------------------------------------------------------------- /audio/crash-noise.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/crash-noise.ogg -------------------------------------------------------------------------------- /audio/crash-tape.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/crash-tape.ogg -------------------------------------------------------------------------------- /audio/hihat-analog.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/hihat-analog.ogg -------------------------------------------------------------------------------- /audio/hihat-dist01.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/hihat-dist01.ogg -------------------------------------------------------------------------------- /audio/hihat-dist02.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/hihat-dist02.ogg -------------------------------------------------------------------------------- /audio/hihat-plain.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/hihat-plain.ogg -------------------------------------------------------------------------------- /audio/hihat-reso.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/hihat-reso.ogg -------------------------------------------------------------------------------- /audio/hihat-ring.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/hihat-ring.ogg -------------------------------------------------------------------------------- /audio/kick-classic.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-classic.ogg -------------------------------------------------------------------------------- /audio/kick-floppy.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-floppy.ogg -------------------------------------------------------------------------------- /audio/kick-gritty.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-gritty.ogg -------------------------------------------------------------------------------- /audio/kick-heavy.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-heavy.ogg -------------------------------------------------------------------------------- /audio/kick-newwave.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-newwave.ogg -------------------------------------------------------------------------------- /audio/kick-plain.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-plain.ogg -------------------------------------------------------------------------------- /audio/kick-softy.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-softy.ogg -------------------------------------------------------------------------------- /audio/kick-stomp.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-stomp.ogg -------------------------------------------------------------------------------- /audio/kick-thump.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-thump.ogg -------------------------------------------------------------------------------- /audio/kick-tight.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-tight.ogg -------------------------------------------------------------------------------- /audio/kick-vinyl01.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-vinyl01.ogg -------------------------------------------------------------------------------- /audio/kick-vinyl02.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-vinyl02.ogg -------------------------------------------------------------------------------- /audio/kick-zapper.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-zapper.ogg -------------------------------------------------------------------------------- /audio/openhat-808.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/openhat-808.ogg -------------------------------------------------------------------------------- /audio/perc-chirpy.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/perc-chirpy.ogg -------------------------------------------------------------------------------- /audio/perc-hollow.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/perc-hollow.ogg -------------------------------------------------------------------------------- /audio/perc-laser.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/perc-laser.ogg -------------------------------------------------------------------------------- /audio/perc-metal.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/perc-metal.ogg -------------------------------------------------------------------------------- /audio/perc-nasty.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/perc-nasty.ogg -------------------------------------------------------------------------------- /audio/perc-short.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/perc-short.ogg -------------------------------------------------------------------------------- /audio/perc-tambo.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/perc-tambo.ogg -------------------------------------------------------------------------------- /audio/perc-tribal.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/perc-tribal.ogg -------------------------------------------------------------------------------- /audio/perc-weirdo.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/perc-weirdo.ogg -------------------------------------------------------------------------------- /audio/snare-analog.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/snare-analog.ogg -------------------------------------------------------------------------------- /audio/snare-block.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/snare-block.ogg -------------------------------------------------------------------------------- /audio/snare-brute.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/snare-brute.ogg -------------------------------------------------------------------------------- /audio/snare-dist01.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/snare-dist01.ogg -------------------------------------------------------------------------------- /audio/snare-dist02.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/snare-dist02.ogg -------------------------------------------------------------------------------- /audio/snare-dist03.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/snare-dist03.ogg -------------------------------------------------------------------------------- /audio/snare-lofi01.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/snare-lofi01.ogg -------------------------------------------------------------------------------- /audio/snare-lofi02.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/snare-lofi02.ogg -------------------------------------------------------------------------------- /audio/snare-noise.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/snare-noise.ogg -------------------------------------------------------------------------------- /audio/snare-pinch.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/snare-pinch.ogg -------------------------------------------------------------------------------- /audio/snare-punch.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/snare-punch.ogg -------------------------------------------------------------------------------- /audio/snare-sumo.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/snare-sumo.ogg -------------------------------------------------------------------------------- /audio/snare-tape.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/snare-tape.ogg -------------------------------------------------------------------------------- /audio/tom-analog.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/tom-analog.ogg -------------------------------------------------------------------------------- /audio/tom-chiptune.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/tom-chiptune.ogg -------------------------------------------------------------------------------- /audio/tom-rototom.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/tom-rototom.ogg -------------------------------------------------------------------------------- /audio/crash-acoustic.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/crash-acoustic.ogg -------------------------------------------------------------------------------- /audio/hihat-digital.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/hihat-digital.ogg -------------------------------------------------------------------------------- /audio/hihat-electro.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/hihat-electro.ogg -------------------------------------------------------------------------------- /audio/kick-electro01.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-electro01.ogg -------------------------------------------------------------------------------- /audio/kick-electro02.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-electro02.ogg -------------------------------------------------------------------------------- /audio/kick-oldschool.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-oldschool.ogg -------------------------------------------------------------------------------- /audio/kick-slapback.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-slapback.ogg -------------------------------------------------------------------------------- /audio/openhat-analog.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/openhat-analog.ogg -------------------------------------------------------------------------------- /audio/openhat-slick.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/openhat-slick.ogg -------------------------------------------------------------------------------- /audio/openhat-tight.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/openhat-tight.ogg -------------------------------------------------------------------------------- /audio/shaker-analog.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/shaker-analog.ogg -------------------------------------------------------------------------------- /audio/shaker-shuffle.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/shaker-shuffle.ogg -------------------------------------------------------------------------------- /audio/shaker-suckup.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/shaker-suckup.ogg -------------------------------------------------------------------------------- /audio/snare-electro.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/snare-electro.ogg -------------------------------------------------------------------------------- /audio/snare-modular.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/snare-modular.ogg -------------------------------------------------------------------------------- /audio/snare-smasher.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/snare-smasher.ogg -------------------------------------------------------------------------------- /audio/snare-vinyl01.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/snare-vinyl01.ogg -------------------------------------------------------------------------------- /audio/snare-vinyl02.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/snare-vinyl02.ogg -------------------------------------------------------------------------------- /audio/tom-acoustic01.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/tom-acoustic01.ogg -------------------------------------------------------------------------------- /audio/tom-acoustic02.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/tom-acoustic02.ogg -------------------------------------------------------------------------------- /audio/hihat-acoustic01.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/hihat-acoustic01.ogg -------------------------------------------------------------------------------- /audio/hihat-acoustic02.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/hihat-acoustic02.ogg -------------------------------------------------------------------------------- /audio/kick-acoustic01.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-acoustic01.ogg -------------------------------------------------------------------------------- /audio/kick-acoustic02.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-acoustic02.ogg -------------------------------------------------------------------------------- /audio/kick-cultivator.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/kick-cultivator.ogg -------------------------------------------------------------------------------- /audio/ride-acoustic01.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/ride-acoustic01.ogg -------------------------------------------------------------------------------- /audio/ride-acoustic02.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/ride-acoustic02.ogg -------------------------------------------------------------------------------- /audio/snare-acoustic01.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/snare-acoustic01.ogg -------------------------------------------------------------------------------- /audio/snare-acoustic02.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/snare-acoustic02.ogg -------------------------------------------------------------------------------- /audio/openhat-acoustic01.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n1k0/jujube/master/audio/openhat-acoustic01.ogg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jujube 2 | ====== 3 | 4 | [Generative Music](https://teropa.info/loop/#/title) experiment written with [Elm](https://elm-lang.org/). 5 | 6 | ## Install 7 | 8 | npm i && npm start 9 | open http://localhost:3000 10 | 11 | ## Why this name? 12 | 13 | Why not? 14 | 15 | ## License 16 | 17 | WTFPL 18 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | jujube 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /elm/Ports.elm: -------------------------------------------------------------------------------- 1 | port module Ports exposing (bar, load, ready, setBpm, setDrumTracks, setTrack, start, stop) 2 | 3 | import Json.Encode as Encode 4 | 5 | 6 | 7 | --Commands 8 | 9 | 10 | port load : () -> Cmd msg 11 | 12 | 13 | port start : () -> Cmd msg 14 | 15 | 16 | port stop : () -> Cmd msg 17 | 18 | 19 | port setBpm : Int -> Cmd msg 20 | 21 | 22 | port setDrumTracks : Encode.Value -> Cmd msg 23 | 24 | 25 | port setTrack : Encode.Value -> Cmd msg 26 | 27 | 28 | 29 | -- Subscriptions 30 | 31 | 32 | port bar : (Float -> msg) -> Sub msg 33 | 34 | 35 | port ready : (Bool -> msg) -> Sub msg 36 | -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": ["elm"], 4 | "elm-version": "0.19.0", 5 | "dependencies": { 6 | "direct": { 7 | "elm/browser": "1.0.1", 8 | "elm/core": "1.0.2", 9 | "elm/html": "1.0.0", 10 | "elm/json": "1.1.3", 11 | "elm/random": "1.0.0", 12 | "elm/time": "1.0.0", 13 | "elm-community/random-extra": "3.1.0" 14 | }, 15 | "indirect": { 16 | "elm/url": "1.0.0", 17 | "elm/virtual-dom": "1.0.2", 18 | "owanturist/elm-union-find": "1.0.0" 19 | } 20 | }, 21 | "test-dependencies": { 22 | "direct": {}, 23 | "indirect": {} 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jujube", 3 | "version": "1.0.0", 4 | "description": "", 5 | "private": true, 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "parcel serve -p 3000 index.html", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "Nicolas Perriault ", 12 | "license": "ISC", 13 | "staticFiles": { 14 | "staticPath": "audio" 15 | }, 16 | "browserslist": [ 17 | "last 1 Chrome version" 18 | ], 19 | "devDependencies": { 20 | "elm": "0.19.0-no-deps", 21 | "elm-format": "0.8.1", 22 | "elm-hot": "^1.1.1", 23 | "node-elm-compiler": "^5.0.4", 24 | "parcel-bundler": "^1.12.3", 25 | "parcel-plugin-static-files-copy": "^2.2.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /elm/Data/Note.elm: -------------------------------------------------------------------------------- 1 | module Data.Note exposing (Note, encode, randomFromPitch) 2 | 3 | import Json.Encode as Encode 4 | import Random exposing (Generator) 5 | import Random.List as RandomList 6 | 7 | 8 | type alias Note = 9 | { pitch : String 10 | , dur : String 11 | , vel : Float 12 | } 13 | 14 | 15 | durations : List String 16 | durations = 17 | [ "8n", "12n", "16n", "32n" ] 18 | 19 | 20 | encode : Note -> Encode.Value 21 | encode v = 22 | Encode.object 23 | [ ( "dur", Encode.string v.dur ) 24 | , ( "vel", Encode.float v.vel ) 25 | , ( "pitch", Encode.string v.pitch ) 26 | ] 27 | 28 | 29 | randomFromPitch : String -> Generator Note 30 | randomFromPitch pitch = 31 | Random.map2 (Note pitch) 32 | (RandomList.choose durations |> Random.andThen (Tuple.first >> Maybe.withDefault "16n" >> Random.constant)) 33 | (Random.float 0.2 1) 34 | -------------------------------------------------------------------------------- /elm/Data/Instrument.elm: -------------------------------------------------------------------------------- 1 | module Data.Instrument exposing (Instrument(..), encode, random, toString) 2 | 3 | import Json.Encode as Encode 4 | import Random exposing (Generator) 5 | import Random.List as RandomList 6 | 7 | 8 | type Instrument 9 | = Bass 10 | | Brass 11 | | Cello 12 | | Kalimba 13 | | Marimba 14 | | Piano 15 | | Steelpan 16 | | Wind 17 | 18 | 19 | encode : Instrument -> Encode.Value 20 | encode = 21 | toString >> Encode.string 22 | 23 | 24 | tonal : List Instrument 25 | tonal = 26 | [ Bass, Brass, Cello, Kalimba, Marimba, Piano, Steelpan, Wind ] 27 | 28 | 29 | random : Generator Instrument 30 | random = 31 | RandomList.choose tonal 32 | |> Random.andThen (Tuple.first >> Maybe.withDefault Piano >> Random.constant) 33 | 34 | 35 | toString : Instrument -> String 36 | toString instrument = 37 | case instrument of 38 | Bass -> 39 | "bass" 40 | 41 | Brass -> 42 | "brass" 43 | 44 | Cello -> 45 | "cello" 46 | 47 | Kalimba -> 48 | "kalimba" 49 | 50 | Marimba -> 51 | "marimba" 52 | 53 | Piano -> 54 | "piano" 55 | 56 | Steelpan -> 57 | "steelpan" 58 | 59 | Wind -> 60 | "wind" 61 | -------------------------------------------------------------------------------- /js/meter.js: -------------------------------------------------------------------------------- 1 | function clamp(x, lower, upper) { 2 | if (Number.isNaN(x)) return NaN; 3 | if (Number.isNaN(lower)) return NaN; 4 | if (Number.isNaN(upper)) return NaN; 5 | 6 | const max = Math.max(x, lower); 7 | const min = Math.min(max, upper); 8 | 9 | return min; 10 | } 11 | 12 | function scale(x, inLow, inHigh, outLow, outHigh) { 13 | if (Number.isNaN(x)) return NaN; 14 | if (Number.isNaN(inLow)) return NaN; 15 | if (Number.isNaN(inHigh)) return NaN; 16 | if (Number.isNaN(outLow)) return NaN; 17 | if (Number.isNaN(outHigh)) return NaN; 18 | 19 | if (x === Infinity || x === -Infinity) return x; 20 | 21 | let result = x - inLow; 22 | result *= outHigh - outLow; 23 | result /= inHigh - inLow; 24 | result += outLow; 25 | return result; 26 | } 27 | 28 | export default function vuMeter(tone, canvas) { 29 | const split = new Tone.Split(); 30 | tone.connect(split); 31 | const meters = [new Tone.Meter(0.9), new Tone.Meter(0.9)]; 32 | split.left.connect(meters[0]); 33 | split.right.connect(meters[1]); 34 | 35 | const context = canvas.getContext("2d"); 36 | 37 | loop(); 38 | 39 | function loop() { 40 | requestAnimationFrame(loop); 41 | const { width, height } = canvas; 42 | const values = meters.map(m => m.getLevel()); 43 | const barHeights = values.map(value => clamp(scale(value, -100, 6, 0, height), 4, height)); 44 | context.clearRect(0, 0, width, height); 45 | const barWidth = width / meters.length; 46 | context.fillStyle = "#777"; 47 | const margin = meters.length > 1 ? 5 : 0; 48 | barHeights.forEach((barHeight, i) => { 49 | context.fillRect(i * barWidth + margin * i, height - barHeight, barWidth - margin, barHeight); 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /elm/Data/Sequence.elm: -------------------------------------------------------------------------------- 1 | module Data.Sequence exposing (Sequence(..), encode, random) 2 | 3 | import Array 4 | import Data.Note as Note exposing (Note) 5 | import Json.Encode as Encode 6 | import Random exposing (Generator) 7 | import Random.Array as RandomArray 8 | import Random.List as RandomList 9 | 10 | 11 | type Sequence 12 | = Silence 13 | | Single Note 14 | | Multiple (List Sequence) 15 | 16 | 17 | encode : Sequence -> Encode.Value 18 | encode seq = 19 | case seq of 20 | Silence -> 21 | Encode.null 22 | 23 | Single note -> 24 | Note.encode note 25 | 26 | Multiple subSeq -> 27 | Encode.list encode subSeq 28 | 29 | 30 | pickOne : List String -> Generator Sequence 31 | pickOne = 32 | RandomList.choose 33 | >> Random.andThen 34 | (Tuple.first 35 | >> Maybe.map (Note.randomFromPitch >> Random.andThen (Single >> Random.constant)) 36 | >> Maybe.withDefault (Random.constant Silence) 37 | ) 38 | 39 | 40 | pickMany : Int -> List String -> Generator Sequence 41 | pickMany length = 42 | pickOne 43 | >> RandomArray.array length 44 | >> Random.andThen (Array.toList >> Multiple >> Random.constant) 45 | 46 | 47 | decideWhat : List String -> Float -> Generator Sequence 48 | decideWhat notes prob = 49 | if prob > 0.97 then 50 | pickMany 3 notes 51 | 52 | else if prob > 0.8 then 53 | Random.constant Silence 54 | 55 | else if prob > 0.6 then 56 | pickOne notes 57 | 58 | else if prob > 0.2 then 59 | pickMany 2 notes 60 | 61 | else 62 | pickMany 4 notes 63 | 64 | 65 | random : Int -> List String -> Generator Sequence 66 | random bars notes = 67 | Random.float 0 1 68 | |> Random.andThen (decideWhat notes) 69 | |> RandomArray.rangeLengthArray bars bars 70 | |> Random.andThen (Array.toList >> Multiple >> Random.constant) 71 | -------------------------------------------------------------------------------- /elm/Data/Track.elm: -------------------------------------------------------------------------------- 1 | module Data.Track exposing (Track, encode, random) 2 | 3 | import Data.Drum as Drum exposing (Drum) 4 | import Data.Instrument as Instrument exposing (Instrument) 5 | import Data.Scale as Scale 6 | import Data.Sequence as Sequence exposing (Sequence) 7 | import Json.Encode as Encode 8 | import Random exposing (Generator) 9 | import Random.List as RandomList 10 | 11 | 12 | type alias Track = 13 | { id : String 14 | , resolution : String 15 | , instrument : Instrument 16 | , pan : Float 17 | , volume : Float 18 | , probability : Float 19 | , sequence : Sequence 20 | } 21 | 22 | 23 | type alias Config = 24 | { id : String 25 | , bars : Maybe Int 26 | , instrument : Maybe Instrument 27 | , octaves : Maybe ( Int, Int ) 28 | , pan : Maybe Float 29 | , scale : List String 30 | , probability : Maybe Float 31 | , volume : Maybe Float 32 | } 33 | 34 | 35 | encode : Track -> Encode.Value 36 | encode v = 37 | Encode.object 38 | [ ( "id", Encode.string v.id ) 39 | , ( "resolution", Encode.string v.resolution ) 40 | , ( "instrument", Instrument.encode v.instrument ) 41 | , ( "pan", Encode.float v.pan ) 42 | , ( "probability", Encode.float v.probability ) 43 | , ( "volume", Encode.float v.volume ) 44 | , ( "resolution", Encode.string v.resolution ) 45 | , ( "sequence", Sequence.encode v.sequence ) 46 | ] 47 | 48 | 49 | random : Config -> Generator Track 50 | random config = 51 | Random.map5 (Track config.id "4n") 52 | (config.instrument |> Maybe.map Random.constant >> Maybe.withDefault Instrument.random) 53 | (config.pan |> Maybe.map Random.constant |> Maybe.withDefault (Random.float -1 1)) 54 | (config.volume |> Maybe.map Random.constant |> Maybe.withDefault (Random.float -20 -10)) 55 | (config.probability |> Maybe.withDefault 1 >> Random.constant) 56 | (Sequence.random (config.bars |> Maybe.withDefault 4) (Scale.range (config.octaves |> Maybe.withDefault ( 1, 6 )) config.scale)) 57 | -------------------------------------------------------------------------------- /elm/Data/Scale.elm: -------------------------------------------------------------------------------- 1 | module Data.Scale exposing 2 | ( diat 3 | , fifth 4 | , first 5 | , fourth 6 | , penta 7 | , randomNext 8 | , range 9 | , second 10 | , seventh 11 | , sixth 12 | , third 13 | ) 14 | 15 | import Random exposing (Generator) 16 | import Random.List as RandomList 17 | 18 | 19 | first : List String 20 | first = 21 | [ "C", "E", "G", "B" ] 22 | 23 | 24 | second : List String 25 | second = 26 | [ "D", "F", "A", "C" ] 27 | 28 | 29 | third : List String 30 | third = 31 | [ "E", "G", "B", "D" ] 32 | 33 | 34 | fourth : List String 35 | fourth = 36 | [ "F", "A", "C", "E" ] 37 | 38 | 39 | fifth : List String 40 | fifth = 41 | [ "G", "B", "D", "F" ] 42 | 43 | 44 | sixth : List String 45 | sixth = 46 | [ "A", "C", "E", "G" ] 47 | 48 | 49 | seventh : List String 50 | seventh = 51 | [ "B", "D", "F", "A" ] 52 | 53 | 54 | diat : List String 55 | diat = 56 | [ "C", "D", "E", "F", "G", "A", "B" ] 57 | 58 | 59 | penta : List String 60 | penta = 61 | [ "A", "C", "D", "E", "F", "G" ] 62 | 63 | 64 | range : ( Int, Int ) -> List String -> List String 65 | range ( min, max ) notes = 66 | List.range min max 67 | |> List.map (\oct -> notes |> List.map (\n -> n ++ String.fromInt oct)) 68 | |> List.concat 69 | 70 | 71 | takeSeventh : List String -> Generator (List String) 72 | takeSeventh scale = 73 | Random.float 0 1 74 | |> Random.andThen 75 | (\prob -> 76 | if prob > 0.9 then 77 | Random.constant scale 78 | 79 | else 80 | Random.constant (List.take 3 scale) 81 | ) 82 | 83 | 84 | pickOne : List (List String) -> Generator (List String) 85 | pickOne choices = 86 | RandomList.choose choices 87 | |> Random.andThen (Tuple.first >> Maybe.withDefault first >> Random.constant) 88 | |> Random.andThen takeSeventh 89 | 90 | 91 | randomNext : List String -> Generator (List String) 92 | randomNext scale = 93 | if scale == first then 94 | pickOne [ second, fourth, sixth ] 95 | 96 | else if scale == second then 97 | pickOne [ third, fourth, fifth, sixth ] 98 | 99 | else if scale == third then 100 | pickOne [ first, second, fourth, fifth, sixth ] 101 | 102 | else if scale == fourth then 103 | pickOne [ first, second, fifth, sixth ] 104 | 105 | else if scale == fifth then 106 | pickOne [ first, fourth ] 107 | 108 | else if scale == sixth then 109 | pickOne [ first, second, third, fourth, fifth ] 110 | 111 | else 112 | Random.constant first 113 | -------------------------------------------------------------------------------- /js/index.js: -------------------------------------------------------------------------------- 1 | import { Elm } from "./../elm/Main.elm"; 2 | import instruments from "./instruments.js"; 3 | import vuMeter from "./meter.js"; 4 | 5 | if (module.hot) { 6 | module.hot.dispose(() => { 7 | window.location.reload(); 8 | }); 9 | } 10 | 11 | const app = Elm.Main.init({ node: document.querySelector("main") }); 12 | 13 | let drumKit; 14 | const tracks = {}; 15 | 16 | function setTrack(id, sequence, handler) { 17 | if (tracks.hasOwnProperty(id)) { 18 | tracks[id].mute = true; 19 | tracks[id].probability = 0; 20 | tracks[id].loop = 0; 21 | tracks[id].callback = () => {}; 22 | tracks[id] 23 | .cancel(0) 24 | .stop(0) 25 | .removeAll() 26 | .dispose(); 27 | } 28 | delete tracks[id]; 29 | tracks[id] = new Tone.Sequence(handler, sequence).start(0); 30 | return tracks[id]; 31 | } 32 | 33 | function drumTrack(id, sequence) { 34 | setTrack(id, sequence, function(time, note) { 35 | try { 36 | drumKit.get(note.pitch).start(time); 37 | } catch (e) { 38 | console.warn("cannot get drum sample", note.pitch); 39 | } 40 | }).probability = 0.99; 41 | } 42 | 43 | async function setupDrumKit() { 44 | const reverb = new Tone.Reverb(1.1); 45 | reverb.wet.value = 0.2; 46 | await reverb.generate(); 47 | const panVol = new Tone.PanVol(0, -6); 48 | const sounds = instruments.drumSamples.reduce((acc, sample) => ({ ...acc, [sample]: sample }), {}); 49 | drumKit = await new Promise(resolve => { 50 | new Tone.Players(sounds, function(kit) { 51 | resolve(kit.chain(panVol, reverb, Tone.Master)); 52 | }); 53 | }); 54 | } 55 | 56 | app.ports.load.subscribe(async function() { 57 | Tone.start(); 58 | Tone.Master.chain(new Tone.Limiter(-6)); 59 | vuMeter(Tone.Master, document.querySelector(".vuMeter")); 60 | await setupDrumKit(); 61 | app.ports.ready.send(true); 62 | }); 63 | 64 | app.ports.start.subscribe(function() { 65 | Tone.Transport.start(); 66 | Tone.Transport.scheduleRepeat(app.ports.bar.send, "4m"); 67 | }); 68 | 69 | app.ports.stop.subscribe(function() { 70 | Tone.Transport.stop(); 71 | Tone.Transport.position = 0; 72 | }); 73 | 74 | app.ports.setBpm.subscribe(function(bpm) { 75 | Tone.Transport.bpm.value = bpm; 76 | }); 77 | 78 | app.ports.setDrumTracks.subscribe(function(drums) { 79 | for (const key in drums) { 80 | drumTrack(key, drums[key].sequence); 81 | } 82 | }); 83 | 84 | app.ports.setTrack.subscribe(function(track) { 85 | const panVol = new Tone.PanVol(track.pan, track.volume); 86 | const pingPong = new Tone.PingPongDelay("8n", 0); 87 | const instrument = new Tone.PolySynth(4, Tone.Synth, instruments[track.instrument]).chain( 88 | pingPong, 89 | panVol, 90 | Tone.Master 91 | ); 92 | 93 | setTrack(track.id, track.sequence, function(time, note) { 94 | instrument.triggerAttackRelease(note.pitch, note.dur, time, note.vel); 95 | }).probability = track.probability; 96 | }); 97 | -------------------------------------------------------------------------------- /elm/Data/Drum/Kit.elm: -------------------------------------------------------------------------------- 1 | module Data.Drum.Kit exposing 2 | ( randomClap 3 | , randomCrash 4 | , randomHihat 5 | , randomKick 6 | , randomOpenhat 7 | , randomPerc 8 | , randomRide 9 | , randomShaker 10 | , randomSnare 11 | , randomTom 12 | ) 13 | 14 | import Random exposing (Generator) 15 | import Random.List as RandomList 16 | 17 | 18 | pick : List String -> Generator String 19 | pick = 20 | RandomList.choose 21 | >> Random.andThen 22 | (Tuple.first 23 | >> Maybe.withDefault "" 24 | >> Random.constant 25 | ) 26 | 27 | 28 | randomClap : Generator String 29 | randomClap = 30 | pick 31 | [ "clap-808.ogg" 32 | , "clap-analog.ogg" 33 | , "clap-crushed.ogg" 34 | , "clap-fat.ogg" 35 | , "clap-slapper.ogg" 36 | , "clap-tape.ogg" 37 | ] 38 | 39 | 40 | randomCrash : Generator String 41 | randomCrash = 42 | pick 43 | [ "crash-808.ogg" 44 | , "crash-acoustic.ogg" 45 | , "crash-noise.ogg" 46 | , "crash-tape.ogg" 47 | ] 48 | 49 | 50 | randomHihat : Generator String 51 | randomHihat = 52 | pick 53 | [ "hihat-808.ogg" 54 | , "hihat-digital.ogg" 55 | , "hihat-electro.ogg" 56 | , "hihat-plain.ogg" 57 | ] 58 | 59 | 60 | randomKick : Generator String 61 | randomKick = 62 | pick 63 | [ "kick-deep.ogg" 64 | , "kick-electro01.ogg" 65 | , "kick-floppy.ogg" 66 | , "kick-newwave.ogg" 67 | , "kick-oldschool.ogg" 68 | , "kick-tape.ogg" 69 | , "kick-tight.ogg" 70 | , "kick-tron.ogg" 71 | ] 72 | 73 | 74 | randomOpenhat : Generator String 75 | randomOpenhat = 76 | pick 77 | [ "openhat-808.ogg" 78 | , "openhat-acoustic01.ogg" 79 | , "openhat-analog.ogg" 80 | , "openhat-slick.ogg" 81 | , "openhat-tight.ogg" 82 | ] 83 | 84 | 85 | randomPerc : Generator String 86 | randomPerc = 87 | pick 88 | [ "perc-808.ogg" 89 | , "perc-chirpy.ogg" 90 | , "perc-hollow.ogg" 91 | , "perc-laser.ogg" 92 | , "perc-metal.ogg" 93 | , "perc-nasty.ogg" 94 | , "perc-short.ogg" 95 | , "perc-tambo.ogg" 96 | , "perc-tribal.ogg" 97 | , "perc-weirdo.ogg" 98 | ] 99 | 100 | 101 | randomRide : Generator String 102 | randomRide = 103 | pick 104 | [ "ride-acoustic01.ogg" 105 | , "ride-acoustic02.ogg" 106 | ] 107 | 108 | 109 | randomShaker : Generator String 110 | randomShaker = 111 | pick 112 | [ "shaker-analog.ogg" 113 | , "shaker-shuffle.ogg" 114 | , "shaker-suckup.ogg" 115 | ] 116 | 117 | 118 | randomSnare : Generator String 119 | randomSnare = 120 | pick 121 | [ "snare-electro.ogg" 122 | , "snare-lofi01.ogg" 123 | , "snare-noise.ogg" 124 | , "snare-pinch.ogg" 125 | ] 126 | 127 | 128 | randomTom : Generator String 129 | randomTom = 130 | pick 131 | [ "tom-808.ogg" 132 | , "tom-acoustic01.ogg" 133 | , "tom-acoustic02.ogg" 134 | , "tom-analog.ogg" 135 | , "tom-chiptune.ogg" 136 | , "tom-fm.ogg" 137 | , "tom-lofi.ogg" 138 | , "tom-rototom.ogg" 139 | , "tom-short.ogg" 140 | ] 141 | -------------------------------------------------------------------------------- /elm/Data/Drum.elm: -------------------------------------------------------------------------------- 1 | module Data.Drum exposing (Drum, encode, random) 2 | 3 | import Data.Drum.Kit as DrumKit 4 | import Data.Note as Note exposing (Note) 5 | import Data.Sequence as Sequence exposing (Sequence(..)) 6 | import Json.Encode as Encode 7 | import Random exposing (Generator) 8 | import Random.List as RandomList 9 | 10 | 11 | type alias Drum = 12 | { kick : Track 13 | , snare : Track 14 | , hihat : Track 15 | } 16 | 17 | 18 | type alias Track = 19 | { sample : String 20 | , sequence : Sequence 21 | } 22 | 23 | 24 | encode : Drum -> Encode.Value 25 | encode v = 26 | Encode.object 27 | [ ( "kick", encodeTrack v.kick ) 28 | , ( "snare", encodeTrack v.snare ) 29 | , ( "hihat", encodeTrack v.hihat ) 30 | ] 31 | 32 | 33 | encodeTrack : Track -> Encode.Value 34 | encodeTrack v = 35 | Encode.object 36 | [ ( "sample", Encode.string v.sample ) 37 | , ( "sequence", Sequence.encode v.sequence ) 38 | ] 39 | 40 | 41 | random : Generator Drum 42 | random = 43 | Random.map3 Drum 44 | randomKick 45 | randomSnare 46 | randomHihat 47 | 48 | 49 | randomSeq : (Sequence -> Float -> Sequence) -> Generator String -> Generator Track 50 | randomSeq handler = 51 | Random.andThen 52 | (\note -> 53 | Random.map (Track note) 54 | (Random.float 0 1 55 | |> Random.andThen (handler (Single (Note note "16n" 1)) >> Random.constant) 56 | ) 57 | ) 58 | 59 | 60 | m : List Sequence -> Sequence 61 | m = 62 | Multiple 63 | 64 | 65 | x : Sequence 66 | x = 67 | Silence 68 | 69 | 70 | randomKick : Generator Track 71 | randomKick = 72 | DrumKit.randomKick 73 | |> randomSeq 74 | (\h prob -> 75 | if prob < 0.2 then 76 | m [ h, x ] 77 | 78 | else if prob < 0.4 then 79 | m 80 | [ m [ h, x, x, h ] 81 | , m [ x, x, h, x ] 82 | , m [ h, x, x, x ] 83 | , x 84 | ] 85 | 86 | else if prob < 0.6 then 87 | m 88 | [ m [ h, x, x, x ] 89 | , m [ x, x, x, h ] 90 | , m [ x, x, h, x ] 91 | , x 92 | ] 93 | 94 | else if prob < 0.8 then 95 | m 96 | [ m [ h, h, h ] 97 | , x 98 | , h 99 | , x 100 | ] 101 | 102 | else 103 | m [ h ] 104 | ) 105 | 106 | 107 | randomSnare : Generator Track 108 | randomSnare = 109 | DrumKit.randomSnare 110 | |> randomSeq 111 | (\h prob -> 112 | if prob < 0.5 then 113 | m [ x, h ] 114 | 115 | else if prob < 0.75 then 116 | m [ x, h, x, h, x, h, x, m [ h, h ] ] 117 | 118 | else if prob < 0.75 then 119 | m [ x, h, x, h, x, h, x, m [ h, m [ h, h ] ] ] 120 | 121 | else 122 | m [ x, h, x, h, x, h, x, m [ h, m [ h, h, x, h ] ] ] 123 | ) 124 | 125 | 126 | randomHihat : Generator Track 127 | randomHihat = 128 | DrumKit.randomHihat 129 | |> randomSeq 130 | (\h prob -> 131 | if prob > 0.7 then 132 | m [ h, h, x, h ] 133 | 134 | else if prob > 0.5 then 135 | m [ m [ h, h ] ] 136 | 137 | else 138 | m [ h ] 139 | ) 140 | -------------------------------------------------------------------------------- /js/instruments.js: -------------------------------------------------------------------------------- 1 | const bass = { 2 | envelope: { 3 | attack: 0.1, 4 | decay: 0.3, 5 | release: 2 6 | }, 7 | filterEnvelope: { 8 | attack: 0.001, 9 | decay: 0.01, 10 | sustain: 0.5, 11 | octaves: 2.6 12 | } 13 | }; 14 | 15 | const brass = { 16 | portamento: 0.01, 17 | oscillator: { 18 | type: "sawtooth" 19 | }, 20 | filter: { 21 | Q: 2, 22 | type: "lowpass", 23 | rolloff: -24 24 | }, 25 | envelope: { 26 | attack: 0.1, 27 | decay: 0.1, 28 | sustain: 0.6, 29 | release: 0.5 30 | }, 31 | filterEnvelope: { 32 | attack: 0.05, 33 | decay: 0.8, 34 | sustain: 0.4, 35 | release: 1.5, 36 | baseFrequency: 2000, 37 | octaves: 1.5 38 | } 39 | }; 40 | 41 | const cello = { 42 | harmonicity: 3.01, 43 | modulationIndex: 14, 44 | oscillator: { 45 | type: "triangle" 46 | }, 47 | envelope: { 48 | attack: 0.2, 49 | decay: 0.3, 50 | sustain: 0.1, 51 | release: 1.2 52 | }, 53 | modulation: { 54 | type: "square" 55 | }, 56 | modulationEnvelope: { 57 | attack: 0.01, 58 | decay: 0.5, 59 | sustain: 0.2, 60 | release: 0.1 61 | } 62 | }; 63 | 64 | const drumSamples = [ 65 | "clap-808.ogg", 66 | "clap-analog.ogg", 67 | "clap-crushed.ogg", 68 | "clap-fat.ogg", 69 | "clap-slapper.ogg", 70 | "clap-tape.ogg", 71 | "cowbell-808.ogg", 72 | "crash-808.ogg", 73 | "crash-acoustic.ogg", 74 | "crash-noise.ogg", 75 | "crash-tape.ogg", 76 | "hihat-808.ogg", 77 | "hihat-acoustic01.ogg", 78 | "hihat-acoustic02.ogg", 79 | "hihat-analog.ogg", 80 | "hihat-digital.ogg", 81 | "hihat-dist01.ogg", 82 | "hihat-dist02.ogg", 83 | "hihat-electro.ogg", 84 | "hihat-plain.ogg", 85 | "hihat-reso.ogg", 86 | "hihat-ring.ogg", 87 | "kick-808.ogg", 88 | "kick-acoustic01.ogg", 89 | "kick-acoustic02.ogg", 90 | "kick-big.ogg", 91 | "kick-classic.ogg", 92 | "kick-cultivator.ogg", 93 | "kick-deep.ogg", 94 | "kick-dry.ogg", 95 | "kick-electro01.ogg", 96 | "kick-electro02.ogg", 97 | "kick-floppy.ogg", 98 | "kick-gritty.ogg", 99 | "kick-heavy.ogg", 100 | "kick-newwave.ogg", 101 | "kick-oldschool.ogg", 102 | "kick-plain.ogg", 103 | "kick-slapback.ogg", 104 | "kick-softy.ogg", 105 | "kick-stomp.ogg", 106 | "kick-tape.ogg", 107 | "kick-thump.ogg", 108 | "kick-tight.ogg", 109 | "kick-tron.ogg", 110 | "kick-vinyl01.ogg", 111 | "kick-vinyl02.ogg", 112 | "kick-zapper.ogg", 113 | "openhat-808.ogg", 114 | "openhat-acoustic01.ogg", 115 | "openhat-analog.ogg", 116 | "openhat-slick.ogg", 117 | "openhat-tight.ogg", 118 | "perc-808.ogg", 119 | "perc-chirpy.ogg", 120 | "perc-hollow.ogg", 121 | "perc-laser.ogg", 122 | "perc-metal.ogg", 123 | "perc-nasty.ogg", 124 | "perc-short.ogg", 125 | "perc-tambo.ogg", 126 | "perc-tribal.ogg", 127 | "perc-weirdo.ogg", 128 | "ride-acoustic01.ogg", 129 | "ride-acoustic02.ogg", 130 | "shaker-analog.ogg", 131 | "shaker-shuffle.ogg", 132 | "shaker-suckup.ogg", 133 | "snare-808.ogg", 134 | "snare-acoustic01.ogg", 135 | "snare-acoustic02.ogg", 136 | "snare-analog.ogg", 137 | "snare-big.ogg", 138 | "snare-block.ogg", 139 | "snare-brute.ogg", 140 | "snare-dist01.ogg", 141 | "snare-dist02.ogg", 142 | "snare-dist03.ogg", 143 | "snare-electro.ogg", 144 | "snare-lofi01.ogg", 145 | "snare-lofi02.ogg", 146 | "snare-modular.ogg", 147 | "snare-noise.ogg", 148 | "snare-pinch.ogg", 149 | "snare-punch.ogg", 150 | "snare-smasher.ogg", 151 | "snare-sumo.ogg", 152 | "snare-tape.ogg", 153 | "snare-vinyl01.ogg", 154 | "snare-vinyl02.ogg", 155 | "tom-808.ogg", 156 | "tom-acoustic01.ogg", 157 | "tom-acoustic02.ogg", 158 | "tom-analog.ogg", 159 | "tom-chiptune.ogg", 160 | "tom-fm.ogg", 161 | "tom-lofi.ogg", 162 | "tom-rototom.ogg", 163 | "tom-short.ogg" 164 | ]; 165 | 166 | const kalimba = { 167 | harmonicity: 8, 168 | modulationIndex: 2, 169 | oscillator: { 170 | type: "sine" 171 | }, 172 | envelope: { 173 | attack: 0.001, 174 | decay: 2, 175 | sustain: 0.1, 176 | release: 2 177 | }, 178 | modulation: { 179 | type: "square" 180 | }, 181 | modulationEnvelope: { 182 | attack: 0.002, 183 | decay: 0.2, 184 | sustain: 0, 185 | release: 0.2 186 | } 187 | }; 188 | 189 | const marimba = { 190 | oscillator: { 191 | partials: [1, 0, 2, 0, 3] 192 | }, 193 | envelope: { 194 | attack: 0.001, 195 | decay: 1.2, 196 | sustain: 0, 197 | release: 1.2 198 | } 199 | }; 200 | 201 | const piano = { 202 | harmonicity: 2, 203 | oscillator: { 204 | type: "amsine2", 205 | modulationType: "sine", 206 | harmonicity: 1.01 207 | }, 208 | envelope: { 209 | attack: 0.006, 210 | decay: 4, 211 | sustain: 0.04, 212 | release: 1.2 213 | }, 214 | modulation: { 215 | volume: 13, 216 | type: "amsine2", 217 | modulationType: "sine", 218 | harmonicity: 12 219 | }, 220 | modulationEnvelope: { 221 | attack: 0.006, 222 | decay: 0.2, 223 | sustain: 0.2, 224 | release: 0.4 225 | } 226 | }; 227 | 228 | const steelpan = { 229 | oscillator: { 230 | type: "fatcustom", 231 | partials: [0.2, 1, 0, 0.5, 0.1], 232 | spread: 40, 233 | count: 3 234 | }, 235 | envelope: { 236 | attack: 0.001, 237 | decay: 1.6, 238 | sustain: 0, 239 | release: 1.6 240 | } 241 | }; 242 | 243 | const wind = { 244 | portamento: 0.0, 245 | oscillator: { 246 | type: "square4" 247 | }, 248 | envelope: { 249 | attack: 2, 250 | decay: 1, 251 | sustain: 0.2, 252 | release: 2 253 | } 254 | }; 255 | 256 | export default { 257 | bass, 258 | brass, 259 | cello, 260 | drumSamples, 261 | kalimba, 262 | marimba, 263 | steelpan, 264 | piano, 265 | wind 266 | }; 267 | -------------------------------------------------------------------------------- /elm/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Array exposing (Array) 4 | import Browser 5 | import Data.Drum as Drum exposing (Drum) 6 | import Data.Instrument as Instrument exposing (Instrument) 7 | import Data.Scale as Scale 8 | import Data.Sequence as Sequence exposing (Sequence(..)) 9 | import Data.Track as Track exposing (Track) 10 | import Html exposing (..) 11 | import Html.Attributes as HA exposing (..) 12 | import Html.Events exposing (..) 13 | import Json.Encode as Encode 14 | import Ports 15 | import Random exposing (Generator) 16 | import Random.Array as RandomArray 17 | import Random.List as RandomList 18 | import Time exposing (Posix) 19 | 20 | 21 | type alias Model = 22 | { bpm : Int 23 | , status : Status 24 | , tracks : List Track 25 | , scale : List String 26 | } 27 | 28 | 29 | type Status 30 | = Idle 31 | | Loading 32 | | Ready 33 | | Playing 34 | 35 | 36 | type Msg 37 | = Bar Float 38 | | NewDrumTracks Drum 39 | | NewScale (List String) 40 | | NewTrack Track 41 | | Play 42 | | SetReady Bool 43 | | SetBpm Int 44 | | ShouldVary ( Bool, Bool ) 45 | | Stop 46 | | VaryDrums 47 | | VaryScale 48 | 49 | 50 | init : () -> ( Model, Cmd Msg ) 51 | init _ = 52 | ( { bpm = 120 53 | , status = Idle 54 | , tracks = [] 55 | , scale = Scale.first 56 | } 57 | , Random.int 80 100 |> Random.generate SetBpm 58 | ) 59 | 60 | 61 | generateDrums : Cmd Msg 62 | generateDrums = 63 | Random.generate NewDrumTracks Drum.random 64 | 65 | 66 | generateTonalInstruments : Model -> Cmd Msg 67 | generateTonalInstruments model = 68 | Cmd.batch 69 | [ Track.random 70 | { id = "low" 71 | , bars = Just 4 72 | , instrument = Just Instrument.Piano 73 | , octaves = Just ( 2, 2 ) 74 | , pan = Just 0 75 | , probability = Just 0.95 76 | , scale = model.scale 77 | , volume = Just -8 78 | } 79 | |> Random.generate NewTrack 80 | , Track.random 81 | { id = "mid" 82 | , bars = Just 6 83 | , instrument = Just Instrument.Cello 84 | , octaves = Just ( 3, 3 ) 85 | , pan = Just -0.2 86 | , probability = Just 0.9 87 | , scale = model.scale 88 | , volume = Just -14 89 | } 90 | |> Random.generate NewTrack 91 | , Track.random 92 | { id = "high" 93 | , bars = Just 8 94 | , instrument = Just Instrument.Marimba 95 | , octaves = Just ( 4, 4 ) 96 | , pan = Just 0.2 97 | , probability = Just 0.8 98 | , scale = model.scale 99 | , volume = Just -12 100 | } 101 | |> Random.generate NewTrack 102 | , Track.random 103 | { id = "other" 104 | , bars = Just 12 105 | , instrument = Just Instrument.Kalimba 106 | , octaves = Just ( 5, 5 ) 107 | , pan = Just 0.4 108 | , probability = Just 0.7 109 | , scale = Scale.penta 110 | , volume = Just -16 111 | } 112 | |> Random.generate NewTrack 113 | ] 114 | 115 | 116 | percentChance : Int -> Random.Generator Bool 117 | percentChance odds = 118 | Random.map (\n -> n < odds) (Random.int 1 100) 119 | 120 | 121 | update : Msg -> Model -> ( Model, Cmd Msg ) 122 | update msg model = 123 | case msg of 124 | Bar _ -> 125 | ( model 126 | , Random.pair (percentChance 10) (percentChance 80) 127 | |> Random.generate ShouldVary 128 | ) 129 | 130 | NewDrumTracks drumTracks -> 131 | ( model, drumTracks |> Drum.encode |> Ports.setDrumTracks ) 132 | 133 | NewTrack track -> 134 | ( { model 135 | | tracks = 136 | model.tracks 137 | |> List.filter (.id >> (/=) track.id) 138 | |> (::) track 139 | } 140 | , track |> Track.encode |> Ports.setTrack 141 | ) 142 | 143 | NewScale scale -> 144 | let 145 | newModel = 146 | { model | scale = scale } 147 | in 148 | ( newModel, generateTonalInstruments newModel ) 149 | 150 | Play -> 151 | case model.status of 152 | Idle -> 153 | ( { model | status = Loading }, Ports.load () ) 154 | 155 | Ready -> 156 | ( { model | status = Playing }, Ports.start () ) 157 | 158 | _ -> 159 | ( model, Cmd.none ) 160 | 161 | SetReady _ -> 162 | ( { model | status = Playing } 163 | , Cmd.batch 164 | [ generateDrums 165 | , generateTonalInstruments model 166 | , Ports.start () 167 | ] 168 | ) 169 | 170 | SetBpm bpm -> 171 | ( { model | bpm = bpm }, Ports.setBpm bpm ) 172 | 173 | ShouldVary ( varyDrums, varyScale ) -> 174 | ( model 175 | , Cmd.batch 176 | [ if varyDrums then 177 | generateDrums 178 | 179 | else 180 | Cmd.none 181 | , if varyScale then 182 | Scale.randomNext model.scale 183 | |> Random.generate NewScale 184 | 185 | else 186 | Cmd.none 187 | ] 188 | ) 189 | 190 | Stop -> 191 | ( { model | status = Ready }, Ports.stop () ) 192 | 193 | VaryDrums -> 194 | ( model, generateDrums ) 195 | 196 | VaryScale -> 197 | ( { model | tracks = [] } 198 | , Scale.randomNext model.scale 199 | |> Random.generate NewScale 200 | ) 201 | 202 | 203 | viewTrack : Track -> Html Msg 204 | viewTrack track = 205 | let 206 | viewSequence seq = 207 | case seq of 208 | Silence -> 209 | li [] [ text "silence" ] 210 | 211 | Single { pitch } -> 212 | li [] [ text pitch ] 213 | 214 | Multiple subSeq -> 215 | subSeq |> List.map viewSequence |> ul [] 216 | in 217 | div [ style "float" "left" ] 218 | [ div [] [ text track.id ] 219 | , div [] [ track.instrument |> Instrument.toString |> text ] 220 | , div [] [ text (String.fromFloat track.pan) ] 221 | , div [] [ text (String.fromFloat track.volume) ] 222 | , div [] [ viewSequence track.sequence ] 223 | ] 224 | 225 | 226 | view : Model -> Html Msg 227 | view model = 228 | div [] 229 | [ div [] 230 | [ label [] 231 | [ text "BPM" 232 | , input 233 | [ type_ "range" 234 | , HA.min "70" 235 | , HA.max "160" 236 | , value <| String.fromInt model.bpm 237 | , onInput (String.toInt >> Maybe.withDefault 120 >> SetBpm) 238 | ] 239 | [] 240 | , text (String.fromInt model.bpm) 241 | ] 242 | ] 243 | , div [] 244 | [ case model.status of 245 | Idle -> 246 | button [ onClick Play ] [ text "▶" ] 247 | 248 | Loading -> 249 | button [ disabled True ] [ text "▶" ] 250 | 251 | Ready -> 252 | button [ onClick Play ] [ text "▶" ] 253 | 254 | Playing -> 255 | span [] 256 | [ button [ onClick Stop ] [ text "◼" ] 257 | , button [ onClick VaryDrums ] [ text "Vary drums" ] 258 | , button [ onClick VaryScale ] [ text "Vary notes" ] 259 | ] 260 | ] 261 | , model.tracks 262 | |> List.map viewTrack 263 | |> div [] 264 | ] 265 | 266 | 267 | subscriptions : Model -> Sub Msg 268 | subscriptions model = 269 | Sub.batch 270 | [ Ports.ready SetReady 271 | , Ports.bar Bar 272 | ] 273 | 274 | 275 | main : Program () Model Msg 276 | main = 277 | Browser.element 278 | { init = init 279 | , update = update 280 | , view = view 281 | , subscriptions = subscriptions 282 | } 283 | --------------------------------------------------------------------------------