├── tests
├── .gitignore
├── Main.elm
├── Tests.elm
├── elm-package.json
└── Test
│ ├── AbcPerformance.elm
│ └── MidiPerformance.elm
├── abc
├── tie.abc
├── justnotes.abc
└── lillasystern.abc
├── .gitattributes
├── compilee.sh
├── compilep.sh
├── compilet.sh
├── distjs
└── placeholder.txt
├── compileec.sh
├── assets
├── images
│ ├── tutorial
│ │ ├── tie.png
│ │ ├── balkan.png
│ │ ├── chords.png
│ │ ├── klezmer.png
│ │ ├── meter.png
│ │ ├── modes.png
│ │ ├── notes.png
│ │ ├── octaves.png
│ │ ├── repeats.png
│ │ ├── rests.png
│ │ ├── rhythm.png
│ │ ├── tempo.png
│ │ ├── title.png
│ │ ├── triplet.png
│ │ ├── hornpipes.png
│ │ ├── naturals.png
│ │ ├── unitnote.png
│ │ ├── accidentals.png
│ │ ├── information.png
│ │ ├── keychanges.png
│ │ ├── keysignature.png
│ │ ├── quadruplet.png
│ │ ├── strathspeys.png
│ │ ├── complextriplet.png
│ │ ├── repeatvariants.png
│ │ ├── fractionalnotes.png
│ │ ├── longnotesandbars.png
│ │ ├── sharpandflatkeys.png
│ │ └── keychangetransient.png
│ └── player
│ │ └── ABC-editor.jpg
└── css
│ ├── abcplayer.css
│ └── vextab.css
├── .gitignore
├── src
├── examples
│ ├── editor-controller
│ │ ├── VexTab
│ │ │ ├── Config.elm
│ │ │ └── Ports.elm
│ │ ├── FileIO
│ │ │ └── Ports.elm
│ │ ├── Midi
│ │ │ ├── Track.elm
│ │ │ └── Player.elm
│ │ ├── VexScore
│ │ │ ├── Score.elm
│ │ │ ├── Canonical.elm
│ │ │ └── Translate.elm
│ │ └── VexTab.elm
│ ├── SoundFont
│ │ ├── Msg.elm
│ │ ├── Subscriptions.elm
│ │ ├── Types.elm
│ │ └── Ports.elm
│ ├── simpleplayer
│ │ └── AbcPlayer.elm
│ ├── tutorial
│ │ ├── AbcTutorial.elm
│ │ └── Lessons.elm
│ └── editor
│ │ └── AbcEditor.elm
├── MidiNotes.elm
├── RepeatTypes.elm
├── Melody.elm
├── MidiTypes.elm
├── MidiMelody.elm
├── Notable.elm
├── Repeats.elm
└── AbcPerformance.elm
├── simpleplayer.html
├── abceditor.html
├── abctutorial.html
├── elm-package.json
├── js
├── nativeVexTab.js
├── nativeFileIO.js
├── nativeSoundFont.js
└── soundfont-player.js
├── abceditorcontroller.html
├── LICENCE
└── README.md
/tests/.gitignore:
--------------------------------------------------------------------------------
1 | /elm-stuff/
2 |
--------------------------------------------------------------------------------
/abc/tie.abc:
--------------------------------------------------------------------------------
1 | | AB-Bc de-ec |
2 | | c4 |
3 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | js/*.js linguist-vendored
2 |
--------------------------------------------------------------------------------
/abc/justnotes.abc:
--------------------------------------------------------------------------------
1 | | ABc z2 def z/2 |
2 | | fed z3 cBA z/2 |
3 |
--------------------------------------------------------------------------------
/compilee.sh:
--------------------------------------------------------------------------------
1 | elm-make src/examples/editor/AbcEditor.elm --output=distjs/elmAbcEditor.js
2 |
--------------------------------------------------------------------------------
/compilep.sh:
--------------------------------------------------------------------------------
1 | elm-make src/examples/simpleplayer/AbcPlayer.elm --output=distjs/elmAbcPlayer.js
2 |
--------------------------------------------------------------------------------
/compilet.sh:
--------------------------------------------------------------------------------
1 | elm-make src/examples/tutorial/AbcTutorial.elm --output=distjs/elmAbcTutorial.js
2 |
--------------------------------------------------------------------------------
/distjs/placeholder.txt:
--------------------------------------------------------------------------------
1 | This is merely a placeholder for the target directory for generated js
2 |
--------------------------------------------------------------------------------
/compileec.sh:
--------------------------------------------------------------------------------
1 | elm-make src/examples/editor-controller/AbcEditorController.elm --output=distjs/elmAbcEditorController.js
2 |
--------------------------------------------------------------------------------
/assets/images/tutorial/tie.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/tie.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | deprecated
2 | elm-stuff
3 | elm.js
4 | Main.html
5 | index.html
6 | abc-player.js
7 | /deprecated/
8 | distjs/*.js
9 |
--------------------------------------------------------------------------------
/assets/images/tutorial/balkan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/balkan.png
--------------------------------------------------------------------------------
/assets/images/tutorial/chords.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/chords.png
--------------------------------------------------------------------------------
/assets/images/tutorial/klezmer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/klezmer.png
--------------------------------------------------------------------------------
/assets/images/tutorial/meter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/meter.png
--------------------------------------------------------------------------------
/assets/images/tutorial/modes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/modes.png
--------------------------------------------------------------------------------
/assets/images/tutorial/notes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/notes.png
--------------------------------------------------------------------------------
/assets/images/tutorial/octaves.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/octaves.png
--------------------------------------------------------------------------------
/assets/images/tutorial/repeats.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/repeats.png
--------------------------------------------------------------------------------
/assets/images/tutorial/rests.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/rests.png
--------------------------------------------------------------------------------
/assets/images/tutorial/rhythm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/rhythm.png
--------------------------------------------------------------------------------
/assets/images/tutorial/tempo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/tempo.png
--------------------------------------------------------------------------------
/assets/images/tutorial/title.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/title.png
--------------------------------------------------------------------------------
/assets/images/tutorial/triplet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/triplet.png
--------------------------------------------------------------------------------
/assets/images/player/ABC-editor.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/player/ABC-editor.jpg
--------------------------------------------------------------------------------
/assets/images/tutorial/hornpipes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/hornpipes.png
--------------------------------------------------------------------------------
/assets/images/tutorial/naturals.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/naturals.png
--------------------------------------------------------------------------------
/assets/images/tutorial/unitnote.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/unitnote.png
--------------------------------------------------------------------------------
/assets/images/tutorial/accidentals.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/accidentals.png
--------------------------------------------------------------------------------
/assets/images/tutorial/information.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/information.png
--------------------------------------------------------------------------------
/assets/images/tutorial/keychanges.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/keychanges.png
--------------------------------------------------------------------------------
/assets/images/tutorial/keysignature.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/keysignature.png
--------------------------------------------------------------------------------
/assets/images/tutorial/quadruplet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/quadruplet.png
--------------------------------------------------------------------------------
/assets/images/tutorial/strathspeys.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/strathspeys.png
--------------------------------------------------------------------------------
/assets/images/tutorial/complextriplet.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/complextriplet.png
--------------------------------------------------------------------------------
/assets/images/tutorial/repeatvariants.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/repeatvariants.png
--------------------------------------------------------------------------------
/assets/images/tutorial/fractionalnotes.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/fractionalnotes.png
--------------------------------------------------------------------------------
/assets/images/tutorial/longnotesandbars.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/longnotesandbars.png
--------------------------------------------------------------------------------
/assets/images/tutorial/sharpandflatkeys.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/sharpandflatkeys.png
--------------------------------------------------------------------------------
/assets/images/tutorial/keychangetransient.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/newlandsvalley/elm-abc-player/HEAD/assets/images/tutorial/keychangetransient.png
--------------------------------------------------------------------------------
/tests/Main.elm:
--------------------------------------------------------------------------------
1 | port module Main exposing (..)
2 |
3 | import Tests
4 | import Test.Runner.Node exposing (run, TestProgram)
5 | import Json.Encode exposing (Value)
6 |
7 |
8 | main : TestProgram
9 | main =
10 | run emit Tests.all
11 |
12 |
13 | port emit : ( String, Value ) -> Cmd msg
14 |
--------------------------------------------------------------------------------
/tests/Tests.elm:
--------------------------------------------------------------------------------
1 | module Tests exposing (..)
2 |
3 | import Test exposing (..)
4 | import Test.AbcPerformance exposing (tests)
5 | import Test.MidiPerformance exposing (tests)
6 |
7 |
8 | all : Test
9 | all =
10 | concat
11 | [
12 | Test.AbcPerformance.tests
13 | , Test.MidiPerformance.tests
14 | ]
15 |
--------------------------------------------------------------------------------
/src/examples/editor-controller/VexTab/Config.elm:
--------------------------------------------------------------------------------
1 | module VexTab.Config exposing (Config)
2 |
3 | {-| configuration of VexTab
4 |
5 | @docs Config
6 | -}
7 |
8 |
9 | {-| the configuration of the VexTab Canvas
10 | -}
11 | type alias Config =
12 | { canvasDivId : String
13 | , canvasX : Int
14 | , canvasY : Int
15 | , canvasWidth : Int
16 | , scale : Float
17 | }
18 |
--------------------------------------------------------------------------------
/assets/css/abcplayer.css:
--------------------------------------------------------------------------------
1 | .hoverable {
2 | border: none;
3 | border-radius: 6px;
4 | padding: 2px 10px;
5 | margin: 10px 0px 10px 25px;
6 | font-size: 1.2em;
7 | background-color: #67d665; /* lighter green */
8 | color: black;
9 | box-shadow: 3px 3px #669966; /* darker green */
10 | transition: 500ms;
11 | }
12 |
13 | .hoverable:hover {
14 | box-shadow: 0 0;
15 | background-color: #669966; /* darker green */
16 | }
17 |
--------------------------------------------------------------------------------
/abc/lillasystern.abc:
--------------------------------------------------------------------------------
1 | X:1
2 | T:Lillasystern
3 | S:Boot (Soot)
4 | Z:John Watson 26/10/2014
5 | M:9/8
6 | L:1/8
7 | O:Sweden
8 | Q:3/8=120 "allegro"
9 | R:Polska
10 | K:Dmaj
11 | |: D2F Ad2 f2a | g2e ed2 c2c | efg (2Bc (2df | ece (2dA (2FA |
12 | DF2 (2Ad (2fa | g2e ed2 c2c | efg (2Bc (2df |1 ec2 d3-d3 :|
13 | |2 ece d3-d2 g |: f3 def def | g2 e2 c2 e2f | gb2 (2gf (2ed |
14 | c2 e2 c2 A2g | f3 def def | g2 e2 c2 e2g | afa d'2a f2g | e^c2 d3-d3 :|
15 |
16 |
--------------------------------------------------------------------------------
/src/examples/SoundFont/Msg.elm:
--------------------------------------------------------------------------------
1 | module SoundFont.Msg exposing (..)
2 |
3 | import SoundFont.Types exposing (..)
4 |
5 |
6 | type Msg
7 | = InitialiseAudioContext
8 | | ResponseAudioContext AudioContext
9 | | RequestOggEnabled
10 | | ResponseOggEnabled Bool
11 | | RequestLoadPianoFonts String
12 | | RequestLoadRemoteFonts String
13 | | ResponseFontsLoaded Bool
14 | | RequestPlayNote MidiNote
15 | | ResponsePlayedNote Bool
16 | | RequestPlayNoteSequence MidiNotes
17 | | ResponsePlaySequenceStarted Bool
18 | | NoOp
19 |
--------------------------------------------------------------------------------
/src/examples/editor-controller/FileIO/Ports.elm:
--------------------------------------------------------------------------------
1 | port module FileIO.Ports exposing (..)
2 |
3 | type alias Filespec =
4 | {
5 | contents : String
6 | , name : String
7 | }
8 |
9 | -- outgoing ports (for commands to javascript)
10 |
11 | port requestLoadFile : () -> Cmd msg
12 |
13 | port requestSaveFile : Filespec -> Cmd msg
14 |
15 | -- incoming ports (for subscriptions from javascript)
16 |
17 | {-| Has the file been loaded OK? -}
18 | port fileLoaded : (Maybe Filespec -> msg) -> Sub msg
19 |
20 | {-| Has the file been saved OK? -}
21 | port fileSaved : (Bool -> msg) -> Sub msg
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/examples/editor-controller/VexTab/Ports.elm:
--------------------------------------------------------------------------------
1 | port module VexTab.Ports exposing (..)
2 |
3 | -- outgoing ports (for commands to javascript)
4 |
5 | import VexTab.Config exposing (Config)
6 |
7 |
8 | port initialise : Config -> Cmd msg
9 |
10 |
11 | port requestRender : String -> Cmd msg
12 |
13 |
14 |
15 | -- incoming ports (for subscriptions from javascript)
16 |
17 |
18 | {-| is VexTab initialised - maybe there's an error message?
19 | -}
20 | port initialised : (Maybe String -> msg) -> Sub msg
21 |
22 |
23 | {-| Have we rendered the score - maybe there's an error message?
24 | -}
25 | port rendered : (Maybe String -> msg) -> Sub msg
26 |
--------------------------------------------------------------------------------
/assets/css/vextab.css:
--------------------------------------------------------------------------------
1 | div.vex-tabdiv {
2 | font-family: Arial, sans-serif;
3 | font-size: 18px;
4 | color: #554;
5 | white-space: pre;
6 | }
7 |
8 | div.vex-tabdiv .editor {
9 | background: #dfd;
10 | border: 0 solid 0;
11 | border-left: 6px solid #afa;
12 | font-family: "Lucida Console", Monaco, monospace;
13 | font-size: 12px;
14 | }
15 |
16 | div.vex-tabdiv .editor-error .text {
17 | font-family: "Lucida Console", Monaco, monospace;
18 | font-size: 12px;
19 | color: red;
20 | padding: 3px;
21 | }
22 |
23 | div.vex-tabdiv .title {
24 | font-family: Arial, sans-serif;
25 | font-size: 18px;
26 | padding: 10px;
27 | color: #554;
28 | }
29 |
--------------------------------------------------------------------------------
/src/examples/SoundFont/Subscriptions.elm:
--------------------------------------------------------------------------------
1 | module SoundFont.Subscriptions exposing (..)
2 |
3 | import SoundFont.Ports exposing (..)
4 | import SoundFont.Types exposing (..)
5 | import SoundFont.Msg exposing (..)
6 |
7 | -- SUBSCRIPTIONS
8 |
9 | audioContextSub : Sub Msg
10 | audioContextSub =
11 | getAudioContext ResponseAudioContext
12 |
13 | oggEnabledSub : Sub Msg
14 | oggEnabledSub =
15 | oggEnabled ResponseOggEnabled
16 |
17 | fontsLoadedSub : Sub Msg
18 | fontsLoadedSub =
19 | fontsLoaded ResponseFontsLoaded
20 |
21 | playedNoteSub : Sub Msg
22 | playedNoteSub =
23 | playedNote ResponsePlayedNote
24 |
25 | playSequenceStartedSub : Sub Msg
26 | playSequenceStartedSub =
27 | playSequenceStarted ResponsePlaySequenceStarted
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/tests/elm-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0.0",
3 | "summary": "Sample Elm Test",
4 | "repository": "https://github.com/user/project.git",
5 | "license": "BSD-3-Clause",
6 | "source-directories": [
7 | ".",
8 | "../src"
9 | ],
10 | "exposed-modules": [],
11 | "dependencies": {
12 | "elm-lang/core": "5.0.0 <= v < 6.0.0",
13 | "elm-community/maybe-extra": "3.0.0 <= v < 4.0.0",
14 | "elm-community/list-extra": "4.0.0 <= v < 5.0.0",
15 | "elm-community/ratio": "1.1.0 <= v < 2.0.0",
16 | "elm-community/elm-test": "3.0.0 <= v < 4.0.0",
17 | "rtfeldman/node-test-runner": "3.0.0 <= v < 4.0.0",
18 | "newlandsvalley/elm-abc-parser": "1.1.0 <= v < 2.0.0"
19 | },
20 | "elm-version": "0.18.0 <= v < 0.19.0"
21 | }
22 |
--------------------------------------------------------------------------------
/simpleplayer.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Elm 0.17 ports sample
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/abceditor.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Elm 0.17 ports sample
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/abctutorial.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Elm 0.17 ports sample
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/elm-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0.0",
3 | "summary": "Play ABC directly in the browser",
4 | "repository": "https://github.com/newlandsvalley/elm-abc-player.git",
5 | "license": "BSD3",
6 | "source-directories": [
7 | "src",
8 | "src/examples",
9 | "src/examples/editor",
10 | "src/examples/editor-controller",
11 | "src/examples/simpleplayer",
12 | "src/examples/tutorial"
13 | ],
14 | "exposed-modules": [
15 | ],
16 | "native-modules": false,
17 | "dependencies": {
18 | "elm-lang/core": "5.1.1 <= v < 6.0.0",
19 | "elm-lang/html": "2.0.0 <= v < 3.0.0",
20 | "elm-lang/http": "1.0.0 <= v < 2.0.0",
21 | "elm-community/maybe-extra": "3.0.0 <= v < 4.0.0",
22 | "elm-community/list-extra": "4.0.0 <= v < 5.0.0",
23 | "elm-community/ratio": "1.1.0 <= v < 2.0.0",
24 | "newlandsvalley/elm-abc-parser": "1.1.0 <= v < 2.0.0"
25 | },
26 | "elm-version": "0.18.0 <= v < 0.19.0"
27 | }
28 |
--------------------------------------------------------------------------------
/src/examples/SoundFont/Types.elm:
--------------------------------------------------------------------------------
1 | module SoundFont.Types exposing (..)
2 |
3 | {-
4 | Types that are shared between elm and javascript.
5 |
6 | This is partly experimental - to see what elm's custom and borders protection actually does.
7 | It seems that if you mention an object field's name in an elm type, it will be accepted as long
8 | as its type is supported. If you don't mention a name, then it will be ignored.
9 |
10 | This means that javascript types with binary fields are effectively useless if you want to store them
11 | in elm and pass them back to javascript later via another port. Notice that AudioNode is the empty tuple.
12 | -}
13 |
14 | {-| Audio Node -}
15 | type alias AudioNode =
16 | {
17 | }
18 |
19 | {-| Audio Context -}
20 | type alias AudioContext =
21 | {
22 | currentTime : Float
23 | , destination : AudioNode
24 | , sampleRate : Int
25 | }
26 |
27 | {-| Midi Note -}
28 | type alias MidiNote =
29 | { id : Int
30 | , timeOffset : Float
31 | , gain : Float
32 | }
33 |
34 | {-| Midi Notes -}
35 | type alias MidiNotes =
36 | List MidiNote
37 |
38 |
--------------------------------------------------------------------------------
/src/examples/editor-controller/Midi/Track.elm:
--------------------------------------------------------------------------------
1 | module Midi.Track
2 | exposing
3 | ( MidiTrack
4 | , fromRecording
5 | )
6 |
7 | {-| conversion of a MIDI recording to a performance of just Track 0
8 |
9 | # Definition
10 |
11 | # Data Types
12 | @docs MidiTrack
13 |
14 | # Functions
15 | @docs fromRecording
16 |
17 | -}
18 |
19 | import MidiTypes exposing (..)
20 | import Array exposing (Array, fromList)
21 | import Maybe exposing (withDefault)
22 | import Tuple exposing (first, second)
23 |
24 |
25 | {-| Midi Track
26 | -}
27 | type alias MidiTrack =
28 | { ticksPerBeat : Int
29 | , messages : Array MidiMessage
30 | }
31 |
32 |
33 | {-| translate a MIDI recording of track 0 to a MidiTrack0 description
34 | -}
35 | fromRecording : MidiRecording -> MidiTrack
36 | fromRecording mr =
37 | let
38 | header =
39 | first mr
40 |
41 | tracks =
42 | second mr
43 |
44 | track0 =
45 | List.head tracks
46 | |> withDefault []
47 | |> Array.fromList
48 | in
49 | { ticksPerBeat = header.ticksPerBeat, messages = track0 }
50 |
--------------------------------------------------------------------------------
/src/MidiNotes.elm:
--------------------------------------------------------------------------------
1 | module MidiNotes exposing (..)
2 |
3 | import SoundFont.Types exposing (MidiNote, MidiNotes)
4 | import Notable exposing (..)
5 |
6 | {- module that transforms an AbcPerformance into MidiNotes -}
7 |
8 | {- make the next MIDI note -}
9 | makeMIDINote : (Float, Notable) -> MidiNote
10 | makeMIDINote ne =
11 | let
12 | (time, notable) = ne
13 | in
14 | case notable of
15 | -- we've hit a Note
16 | Note pitch velocity ->
17 | MidiNote pitch time velocity
18 |
19 | {- make the MIDI notes - if we have a performance result from parsing the midi file, convert
20 | the performance into a list of MidiNote
21 | -}
22 | makeMIDINotes : Result error Performance -> MidiNotes
23 | makeMIDINotes perfResult =
24 | case perfResult of
25 | Ok perf ->
26 | List.map makeMIDINote perf
27 | Err err ->
28 | []
29 |
30 | {- calculate the phrase duration in seconds from the reversed MidiNotes sequence -}
31 | reversedPhraseDuration : MidiNotes -> Float
32 | reversedPhraseDuration notes =
33 | let
34 | maybeLastNote = List.head notes
35 | in
36 | case maybeLastNote of
37 | Nothing -> 0.0
38 | Just n -> n.timeOffset -- the accumulated time
39 |
40 |
--------------------------------------------------------------------------------
/js/nativeVexTab.js:
--------------------------------------------------------------------------------
1 | myapp.ports.initialise.subscribe(initialise);
2 |
3 | function initialise(vexDivName) {
4 | init(vexDivName);
5 | }
6 |
7 | myapp.ports.requestRender.subscribe(render);
8 |
9 |
10 | // IMPLEMENTATION
11 |
12 | // Load VexTab module.
13 | vextab = VexTabDiv;
14 | vexDiv = null;
15 |
16 | function init(config) {
17 | console.log(config.canvasDivId);
18 |
19 | VexTab = vextab.VexTab;
20 | Artist = vextab.Artist;
21 | Renderer = Vex.Flow.Renderer;
22 |
23 | Artist.DEBUG = true;
24 | VexTab.DEBUG = false;
25 |
26 | try {
27 | vexDiv = $(config.canvasDivId)[0];
28 | // Create VexFlow Renderer from canvas element with id vexDiv
29 | renderer = new Renderer(vexDiv, Renderer.Backends.CANVAS);
30 |
31 | // Initialize VexTab artist and parser.
32 | artist = new Artist(config.canvasX, config.canvasY, config.canvasWidth, {scale: config.scale});
33 | vextab = new VexTab(artist);
34 | myapp.ports.initialised.send(null);
35 | } catch (e) {
36 | myapp.ports.rendered.send(e.message);
37 | }
38 | }
39 |
40 | function render(text) {
41 | try {
42 | vextab.reset();
43 | artist.reset();
44 | vextab.parse(text);
45 | artist.render(renderer);
46 | myapp.ports.rendered.send(null);
47 | } catch (e) {
48 | myapp.ports.rendered.send(e.message);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/abceditorcontroller.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | ABC Editor with MIDI audio controller
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015, newlandsvalley
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | * Neither the name of the {organization} nor the names of its
15 | contributors may be used to endorse or promote products derived from
16 | this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
29 |
--------------------------------------------------------------------------------
/js/nativeFileIO.js:
--------------------------------------------------------------------------------
1 | myapp.ports.requestLoadFile.subscribe(loadFile);
2 |
3 | function loadFile() {
4 | var selectedFile = document.getElementById('fileinput').files[0];
5 | // console.log("selected file: " + selectedFile);
6 | var reader = new FileReader();
7 | reader.onload = function(event) {
8 | var contents = event.target.result;
9 | var filespec = {contents:contents, name:selectedFile.name};
10 | // console.log("File contents: " + contents);
11 | // console.log("File name: " + selectedFile.name);
12 | myapp.ports.fileLoaded.send(filespec);
13 | };
14 |
15 | reader.onerror = function(event) {
16 | // console.error("File could not be read! Code " + event.target.error.code);
17 | myapp.ports.fileLoaded.send(null);
18 | };
19 |
20 | if (selectedFile == undefined) {
21 | myapp.ports.fileLoaded.send(null);
22 | } else {
23 | reader.readAsText(selectedFile);
24 | }
25 | }
26 |
27 | myapp.ports.requestSaveFile.subscribe(saveFile);
28 |
29 | function saveFile(filespec) {
30 | var a = document.createElement("a");
31 | // console.log("File contents: " + filespec.contents);
32 | var file = new Blob([filespec.contents], {type: "text/plain;charset=utf-8"});
33 | url = URL.createObjectURL(file);
34 | a.href = url
35 | a.download = filespec.name;
36 | document.body.appendChild(a);
37 | a.click();
38 | setTimeout(function(){
39 | document.body.removeChild(a);
40 | window.URL.revokeObjectURL(url);
41 | }, 100);
42 | myapp.ports.fileSaved.send(true);
43 | }
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/examples/editor-controller/VexScore/Score.elm:
--------------------------------------------------------------------------------
1 | module VexScore.Score exposing (..)
2 |
3 | import Abc.ParseTree
4 | exposing
5 | ( KeySignature
6 | , MeterSignature
7 | , PitchClass
8 | , Accidental
9 | , AbcNote
10 | , Bar
11 | )
12 |
13 |
14 | type alias Score =
15 | List VexBodyPart
16 |
17 |
18 | type VexBodyPart
19 | = VLine VexLine
20 | | VContextChange
21 | | VEmptyLine
22 |
23 |
24 | type alias VexLine =
25 | { stave : Maybe VexStave
26 | , items : List VexItem
27 | }
28 |
29 |
30 | type VexItem
31 | = VNote VexNote
32 | | VRest VexDuration
33 | | VBar Bar
34 | | VTuplet Int (List VexNote)
35 | | VChord VexDuration (List VexNote)
36 | | VNotePair VexNote VexNote
37 | | VIgnore
38 |
39 |
40 | type alias VexStave =
41 | { clef : Clef
42 | , mKey : Maybe KeySignature
43 | , mMeter : Maybe MeterSignature
44 | }
45 |
46 |
47 | type Clef
48 | = Treble
49 | | Bass
50 |
51 |
52 | type VexDuration
53 | = Whole
54 | | Half
55 | | Quarter
56 | | Eighth
57 | | Sixteenth
58 | | ThirtySecond
59 | | SixtyFourth
60 | | HalfDotted
61 | | QuarterDotted
62 | | EighthDotted
63 | | SixteenthDotted
64 | | ThirtySecondDotted
65 | | SixtyFourthDotted
66 |
67 |
68 | type alias VexNote =
69 | { pitchClass : PitchClass
70 | , accidental : Maybe Accidental
71 | , octave : Int
72 | , duration : VexDuration
73 | , tied :
74 | Bool
75 | -- to the next note
76 | , decoration :
77 | Maybe String
78 | -- is the note decorated (staccato etc)
79 | }
80 |
--------------------------------------------------------------------------------
/src/RepeatTypes.elm:
--------------------------------------------------------------------------------
1 | module RepeatTypes
2 | exposing
3 | ( Section
4 | , Repeats
5 | , RepeatState
6 | , GeneralisedBar
7 | )
8 |
9 | {-| Data types that handle repeated melody sections. Thesse use a structural data type representing a bar of music
10 | where the type of notes in the bar can vary with use
11 |
12 | # Definition
13 |
14 | # Data Types
15 | @docs Section
16 | , Repeats
17 | , RepeatState
18 | , GeneralisedBar
19 |
20 | -}
21 |
22 | import Music.Accidentals exposing (Accidentals)
23 | import Abc.ParseTree exposing (Repeat(..))
24 |
25 |
26 | {-| a repeated section
27 | -}
28 | type alias Section =
29 | { start : Maybe Int
30 | , firstEnding : Maybe Int
31 | , secondEnding : Maybe Int
32 | , end : Maybe Int
33 | , isRepeated : Bool
34 | }
35 |
36 |
37 |
38 | {- ! a set of repeats -}
39 |
40 |
41 | type alias Repeats =
42 | List Section
43 |
44 |
45 | {-| the current repeat state
46 | -}
47 | type alias RepeatState =
48 | { current : Section
49 | , repeats : Repeats
50 | }
51 |
52 |
53 | {-| a parameterised type representing a bar of music where the type of note in the bar varies
54 | -}
55 | type alias GeneralisedBar n =
56 | { number :
57 | Int
58 | -- sequential from zero
59 | , repeat :
60 | Maybe Repeat
61 | -- the bar owns a repeat of some kind
62 | , iteration :
63 | Maybe Int
64 | -- the bar has an iteration marker (|1 or |2 etc)
65 | , accidentals :
66 | Accidentals
67 | -- any notes marked explicitly as accidentals in the bar (updated in sequence)
68 | , notes :
69 | List n
70 | -- the notes in the bar
71 | }
72 |
--------------------------------------------------------------------------------
/src/Melody.elm:
--------------------------------------------------------------------------------
1 | module Melody
2 | exposing
3 | ( NoteEvent(..)
4 | , MelodyLine
5 | , SingleNote
6 | , ABar
7 | )
8 |
9 | {-| Data structures for describing an ABC melody (i.e just a succession of notes and durations)
10 |
11 | # Definition
12 |
13 | # Data Types
14 | @docs NoteEvent, MelodyLine, SingleNote, ABar
15 |
16 | -}
17 |
18 | import Abc.ParseTree exposing (PitchClass, Accidental, KeySet, Repeat)
19 | import Music.Notation exposing (NoteTime, MidiPitch)
20 | import Music.Accidentals exposing (Accidentals)
21 | import RepeatTypes exposing (GeneralisedBar)
22 |
23 |
24 | {-| an individual Note (no pitch class implies a rest)
25 | -}
26 | type alias SingleNote =
27 | { time : NoteTime
28 | , pitch : MidiPitch
29 | , pc : Maybe PitchClass
30 | , accidental : Maybe Accidental
31 | }
32 |
33 |
34 | {-| a Note Event (note or chord)
35 | -}
36 | type NoteEvent
37 | = ANote SingleNote Bool
38 | -- Bool indicates whether note is tied
39 | | AChord (List SingleNote)
40 |
41 |
42 |
43 | {- A Bar -}
44 |
45 |
46 | type alias ABar =
47 | GeneralisedBar NoteEvent
48 |
49 |
50 |
51 | {- GeneralisedBar is a parameterised type defined in RepeatTypes.elm
52 | which makes ABar (when expanded) look like this:
53 | type alias ABar =
54 | { number : Int -- sequential from zero
55 | , repeat : Maybe Repeat -- the bar owns a repeat of some kind
56 | , iteration : Maybe Int -- the bar has an iteration marker (|1 or |2 etc)
57 | , accidentals : Accidentals -- any notes marked explicitly as accidentals in the bar (updated in sequence)
58 | , notes : List NoteEvent -- the notes in the bar
59 | }
60 | -}
61 | {- the overall melody -}
62 |
63 |
64 | type alias MelodyLine =
65 | List ABar
66 |
--------------------------------------------------------------------------------
/src/MidiTypes.elm:
--------------------------------------------------------------------------------
1 | module MidiTypes exposing
2 | ( Track
3 | , Header
4 | , MidiEvent(..)
5 | , MidiMessage
6 | , MidiRecording
7 | )
8 |
9 | {-| Type Definition of a MIDI recording
10 |
11 | # Definition
12 |
13 | # Data Types
14 | @docs Header, Track, MidiEvent, MidiMessage, MidiRecording
15 |
16 | -}
17 |
18 | type alias Ticks = Int
19 |
20 | {-| Midi Event -}
21 | type MidiEvent = -- meta messages
22 | SequenceNumber Int
23 | | Text String
24 | | Copyright String
25 | | TrackName String
26 | | InstrumentName String
27 | | Lyrics String
28 | | Marker String
29 | | CuePoint String
30 | | ChannelPrefix Int
31 | | Tempo Int
32 | | SMPTEOffset Int Int Int Int Int
33 | | TimeSignature Int Int Int Int
34 | | KeySignature Int Int
35 | | SequencerSpecific String
36 | | SysEx String
37 | | Unspecified Int (List Int)
38 | -- channel messages
39 | | NoteOn Int Int Int
40 | | NoteOff Int Int Int
41 | | NoteAfterTouch Int Int Int
42 | | ControlChange Int Int Int
43 | | ProgramChange Int Int
44 | | ChannelAfterTouch Int Int
45 | | PitchBend Int Int
46 | | RunningStatus Int Int
47 |
48 | {-| Midi Message -}
49 | type alias MidiMessage = (Ticks, MidiEvent)
50 |
51 | {-| Midi Track -}
52 | type alias Track = List MidiMessage
53 |
54 | {-| Midi Header -}
55 | type alias Header =
56 | { formatType : Int
57 | , trackCount : Int
58 | , ticksPerBeat : Int
59 | }
60 |
61 | {-| Midi Recording -}
62 | type alias MidiRecording = (Header, List Track)
63 |
64 |
65 |
--------------------------------------------------------------------------------
/src/MidiMelody.elm:
--------------------------------------------------------------------------------
1 | module MidiMelody
2 | exposing
3 | ( MidiInstruction(..)
4 | , MidiMelody
5 | , MidiNote
6 | , MidiBar
7 | )
8 |
9 | {-| Data structures for describing an ABC melody (i.e just a succession of notes and durations)
10 |
11 | # Definition
12 |
13 | # Data Types
14 | @docs MidiInstruction, MidiMelody, MidiNote, MidiBar
15 |
16 | -}
17 |
18 | import Abc.ParseTree exposing (PitchClass, Accidental, KeySet, Repeat)
19 | import Music.Notation exposing (NoteTime, MidiPitch, MidiTick)
20 | import Music.Accidentals exposing (Accidentals)
21 | import RepeatTypes exposing (GeneralisedBar)
22 |
23 |
24 | {-| an individual Note (no pitch class implies a rest)
25 | -}
26 | type alias MidiNote =
27 | { ticks : MidiTick
28 | , pitch : MidiPitch
29 | , pc : Maybe PitchClass
30 | , accidental : Maybe Accidental
31 | }
32 |
33 |
34 | {-| a Midi Instruction (not, chord or tempo)
35 | -}
36 | type MidiInstruction
37 | = MNote MidiNote Bool
38 | -- Bool indicates whether note is tied
39 | | MChord (List MidiNote)
40 | | MTempo Int
41 |
42 |
43 |
44 | -- Tempo in microseconds per unit beat - we wrap this up in a bar of its own
45 |
46 |
47 | {-| A Bar
48 | -}
49 | type alias MidiBar =
50 | GeneralisedBar MidiInstruction
51 |
52 |
53 |
54 | {- GeneralisedBar is a parameterised type defined in RepeatTypes.elm
55 | which makes MidiBar (when expanded) look like this:
56 | type alias MidiBar =
57 | { number : Int -- sequential from zero
58 | , repeat : Maybe Repeat -- the bar owns a repeat of some kind
59 | , iteration : Maybe Int -- the bar has an iteration marker (|1 or |2 etc)
60 | , accidentals : Accidentals -- any notes marked explicitly as accidentals in the bar (updated in sequence)
61 | , notes : List MidiNote -- the notes in the bar
62 | }
63 | -}
64 | {- the overall melody -}
65 |
66 |
67 | type alias MidiMelody =
68 | List MidiBar
69 |
--------------------------------------------------------------------------------
/src/examples/SoundFont/Ports.elm:
--------------------------------------------------------------------------------
1 | port module SoundFont.Ports exposing (..)
2 |
3 | import SoundFont.Types exposing (..)
4 |
5 |
6 | -- outgoing ports (for commands to javascript)
7 |
8 |
9 | {-| request the AudioContext
10 | -}
11 | port initialiseAudioContext : () -> Cmd msg
12 |
13 |
14 | {-| ask if the browser supports the OGG format
15 | -}
16 | port requestIsOggEnabled : () -> Cmd msg
17 |
18 |
19 | {-| request that the default piano fonts are loaded from the local resource
20 | under the given directory. By local we mean that the soundfonts are
21 | housed on the same server as the requesting code
22 | -}
23 | port requestLoadPianoFonts : String -> Cmd msg
24 |
25 |
26 | {-| request that the font for the named instrument is loaded from the
27 | remote gleitz github server (i.e. where they are maintained).
28 | This will take longer to load than the local resource above.
29 | -}
30 | port requestLoadRemoteFonts : String -> Cmd msg
31 |
32 |
33 | {-| request that the note is played through the soundfont via web-audio
34 | -}
35 | port requestPlayNote : MidiNote -> Cmd msg
36 |
37 |
38 | {-| request that the sequence of notes is played through the soundfont via web-audio
39 | -}
40 | port requestPlayNoteSequence : MidiNotes -> Cmd msg
41 |
42 |
43 |
44 | -- incoming ports (for subscriptions from javascript)
45 |
46 |
47 | {-| get the audio context.
48 | Probably not much use because it is incomplete and cannot be passed back to javascript
49 | -}
50 | port getAudioContext : (AudioContext -> msg) -> Sub msg
51 |
52 |
53 | {-| does the browser support the Ogg-Vorbis standard?
54 | -}
55 | port oggEnabled : (Bool -> msg) -> Sub msg
56 |
57 |
58 | {-| Have the soundfonts been loaded OK?
59 | -}
60 | port fontsLoaded : (Bool -> msg) -> Sub msg
61 |
62 |
63 | {-| Have we played the individual note?
64 | -}
65 | port playedNote : (Bool -> msg) -> Sub msg
66 |
67 |
68 | {-| Have we started to play the note sequence?
69 | -}
70 | port playSequenceStarted : (Bool -> msg) -> Sub msg
71 |
--------------------------------------------------------------------------------
/src/examples/editor-controller/VexTab.elm:
--------------------------------------------------------------------------------
1 | module VexTab
2 | exposing
3 | ( Model
4 | , Msg(RequestRenderScore)
5 | , init
6 | , update
7 | , subscriptions
8 | )
9 |
10 | {-|
11 | Interface to VexTab native functionality
12 |
13 | #types
14 |
15 | @docs Model, Msg
16 |
17 | #functions
18 |
19 | @docs init, update, subscriptions
20 |
21 |
22 | -}
23 |
24 | import VexTab.Ports as VexTab
25 |
26 |
27 | {-| in the model we only have a possible error message
28 | -}
29 | type alias Model =
30 | { text : Maybe String
31 | , error : Maybe String
32 | }
33 |
34 |
35 | {-| Initialise the model and initialise VexTab with the name of the canvas element
36 | -}
37 | init divName =
38 | ( Model Nothing Nothing
39 | , VexTab.initialise divName
40 | )
41 |
42 |
43 | {-| all messages are internal to the Module except for RequestRenderScore
44 | -}
45 | type Msg
46 | = InitialisedVexTab (Maybe String)
47 | | RequestRenderScore String
48 | | ResponseScoreRendered (Maybe String)
49 |
50 |
51 | {-| update the VexTab module
52 | -}
53 | update : Msg -> Model -> ( Model, Cmd Msg )
54 | update msg model =
55 | case msg of
56 | InitialisedVexTab maybeError ->
57 | let
58 | newModel =
59 | saveError maybeError model
60 | in
61 | ( newModel
62 | , Cmd.none
63 | )
64 |
65 | RequestRenderScore text ->
66 | ( { model | text = Just text, error = Nothing }
67 | , VexTab.requestRender text
68 | )
69 |
70 | ResponseScoreRendered maybeError ->
71 | let
72 | newModel =
73 | saveError maybeError model
74 | in
75 | ( newModel
76 | , Cmd.none
77 | )
78 |
79 |
80 |
81 | {- save any error from the VexFlow API in the model -}
82 |
83 |
84 | saveError : Maybe String -> Model -> Model
85 | saveError maybeError model =
86 | case maybeError of
87 | Just e ->
88 | { model | error = maybeError }
89 |
90 | _ ->
91 | model
92 |
93 |
94 | initialisedSub : Sub Msg
95 | initialisedSub =
96 | VexTab.initialised InitialisedVexTab
97 |
98 |
99 | renderedSub : Sub Msg
100 | renderedSub =
101 | VexTab.rendered ResponseScoreRendered
102 |
103 |
104 | {-| subscriptions that can be used with VexTab
105 | -}
106 | subscriptions : Model -> Sub Msg
107 | subscriptions model =
108 | Sub.batch
109 | [ initialisedSub
110 | , renderedSub
111 | ]
112 |
--------------------------------------------------------------------------------
/src/Notable.elm:
--------------------------------------------------------------------------------
1 | module Notable exposing
2 | ( Notable (..)
3 | , Performance
4 | , fromMelodyLine
5 | , toPerformance)
6 |
7 | import Melody exposing (..)
8 | import Maybe exposing (map, withDefault)
9 | import Debug exposing (..)
10 |
11 | type alias AccumulatedTime = Float
12 |
13 | {-| Note descriptions we need to keep-}
14 | type Notable = Note Int Float
15 |
16 | {-| Midi NoteEvent -}
17 | type alias NoteEvent = (AccumulatedTime, Notable)
18 |
19 | {-| AbcPerformance -}
20 | type alias Performance = List NoteEvent
21 |
22 | defaultGain = 1.0
23 |
24 | fromNote : SingleNote -> Bool -> (AccumulatedTime, Maybe NoteEvent, Performance) -> (AccumulatedTime, Maybe NoteEvent, Performance)
25 | fromNote n tied acc =
26 | let
27 | (t, mtie, p) = acc
28 | event = (t, Note n.pitch defaultGain)
29 | nextTie =
30 | if tied then
31 | Just event
32 | else
33 | Nothing
34 | in
35 | if (n.pitch == 0) then
36 | (t + n.time, Nothing, p)
37 | else
38 | case mtie of
39 | Nothing ->
40 | (t + n.time, nextTie, event :: p)
41 | Just (_, Note pitch g) ->
42 | if (n.pitch == pitch) then
43 | (t + n.time, nextTie, p)
44 | else
45 | (t + n.time, nextTie, event :: p)
46 |
47 |
48 | fromChord : List SingleNote -> (AccumulatedTime, Maybe NoteEvent, Performance) -> (AccumulatedTime, Maybe NoteEvent, Performance)
49 | fromChord ns acc =
50 | let
51 | (t, mtie, p) = acc
52 | in
53 | let
54 | f n = (t, Note n.pitch defaultGain)
55 | notes = List.map f ns
56 | -- increment the time just by that of the first note in the chord
57 | -- all the others should be the same
58 | noteTime = List.head ns
59 | |> Maybe.map (\n -> n.time)
60 | |> withDefault 0.0
61 | in
62 | -- we asssume we won't tie a chord
63 | (t + noteTime, Nothing, (List.append notes p))
64 |
65 | fromBar : ABar -> (AccumulatedTime, Maybe NoteEvent, Performance) -> (AccumulatedTime, Maybe NoteEvent, Performance)
66 | fromBar b acc =
67 | let
68 | (t, mtie, p) = acc
69 | f ne acc =
70 | case ne of
71 | ANote n tied ->
72 | fromNote n tied acc
73 | -- fromNote (log "note" n) tied acc
74 | AChord c ->
75 | fromChord c acc
76 | in
77 | List.foldl f acc b.notes
78 |
79 | fromMelodyLine : AccumulatedTime -> MelodyLine -> Performance
80 | fromMelodyLine t m =
81 | let
82 | (t, mtie, p) = List.foldl fromBar (0.025, Nothing, []) m
83 | in
84 | p
85 |
86 | {- Turn a melodyline result into a performance.
87 | note melody is reversed
88 | -}
89 | toPerformance : Result error MelodyLine -> Result error Performance
90 | toPerformance ml =
91 | let
92 | melody = {- log "melody" -} ml
93 | in
94 | Result.map (fromMelodyLine 0.0) melody
95 |
96 |
--------------------------------------------------------------------------------
/src/examples/simpleplayer/AbcPlayer.elm:
--------------------------------------------------------------------------------
1 | module AbcPlayer exposing (..)
2 |
3 | import Html exposing (..)
4 | import Html.Events exposing (onClick)
5 | import Http exposing (..)
6 |
7 |
8 | --import Task exposing (..)
9 |
10 | import List exposing (..)
11 | import Maybe exposing (..)
12 | import String exposing (..)
13 | import Result exposing (Result, mapError)
14 | import SoundFont.Ports exposing (..)
15 | import SoundFont.Types exposing (..)
16 | import Abc exposing (..)
17 | import Music.Notation exposing (..)
18 | import AbcPerformance exposing (..)
19 | import Melody exposing (..)
20 | import Notable exposing (..)
21 | import MidiNotes exposing (..)
22 |
23 |
24 | main =
25 | Html.program
26 | { init = ( init, Cmd.none ), update = update, view = view, subscriptions = \_ -> Sub.none }
27 |
28 |
29 |
30 | -- MODEL
31 |
32 |
33 | type alias Model =
34 | { fontsLoaded : Bool
35 | , performance : Result String Performance
36 | }
37 |
38 |
39 | init : Model
40 | init =
41 | { fontsLoaded = False
42 | , performance = Err "not started"
43 | }
44 |
45 |
46 |
47 | -- UPDATE
48 |
49 |
50 | type Msg
51 | = NoOp
52 | | RequestLoadFonts String
53 | | LoadFile String
54 | | FontsLoaded Bool
55 | | RawAbc (Result Error String)
56 | | Abc (Result String Performance)
57 | | Play
58 |
59 |
60 | update : Msg -> Model -> ( Model, Cmd Msg )
61 | update msg model =
62 | case msg of
63 | NoOp ->
64 | ( model, Cmd.none )
65 |
66 | RequestLoadFonts dir ->
67 | ( model
68 | , requestLoadPianoFonts dir
69 | )
70 |
71 | LoadFile name ->
72 | ( model
73 | , loadAbc name
74 | )
75 |
76 | FontsLoaded loaded ->
77 | ( { model | fontsLoaded = loaded }
78 | , Cmd.none
79 | )
80 |
81 | RawAbc abc ->
82 | update (Abc (parseLoadedFile abc)) model
83 |
84 | Abc result ->
85 | ( { model | performance = result }, Cmd.none )
86 |
87 | Play ->
88 | let
89 | notes =
90 | makeMIDINotes model.performance
91 | in
92 | ( model
93 | , requestPlayNoteSequence notes
94 | )
95 |
96 |
97 |
98 | {- load an ABC file -}
99 |
100 |
101 | loadAbc : String -> Cmd Msg
102 | loadAbc url =
103 | Http.send RawAbc (getString url)
104 |
105 |
106 | parseLoadedFile : Result Error String -> Result String Performance
107 | parseLoadedFile s =
108 | case s of
109 | Ok text ->
110 | text
111 | |> parse
112 | |> melodyFromAbcResult
113 | |> mapError parseError
114 | |> toPerformance
115 |
116 | Err e ->
117 | Err (toString e)
118 |
119 |
120 |
121 | -- VIEW
122 |
123 |
124 | viewPerformanceResult : Result String Performance -> String
125 | viewPerformanceResult mr =
126 | case mr of
127 | Ok res ->
128 | "OK: " ++ (toString res)
129 |
130 | Err errs ->
131 | "Fail: " ++ (toString errs)
132 |
133 |
134 | view : Model -> Html Msg
135 | view model =
136 | div []
137 | [ button [ onClick (RequestLoadFonts "assets/soundfonts") ] [ text "load fonts" ]
138 | , button [ onClick (LoadFile "abc/lillasystern.abc") ] [ text "load abc file" ]
139 | , div [] [ text ("parsed abc result: " ++ (viewPerformanceResult model.performance)) ]
140 | , button [ onClick Play ] [ text "play" ]
141 | ]
142 |
--------------------------------------------------------------------------------
/js/nativeSoundFont.js:
--------------------------------------------------------------------------------
1 | myapp.ports.initialiseAudioContext.subscribe(detectAudioContext);
2 |
3 | function detectAudioContext() {
4 | myapp.context = getAudioContext();
5 | myapp.ports.getAudioContext.send(myapp.context);
6 | }
7 |
8 | myapp.ports.requestIsOggEnabled.subscribe(detectOggEnabled);
9 |
10 | function detectOggEnabled() {
11 | enabled = canPlayOgg();
12 | myapp.ports.oggEnabled.send(enabled);
13 | }
14 |
15 | myapp.ports.requestLoadPianoFonts.subscribe(loadPianoSoundFonts);
16 |
17 | function loadPianoSoundFonts(dirname) {
18 | if (!myapp.context) {
19 | myapp.context = getAudioContext();
20 | }
21 | var name = 'acoustic_grand_piano'
22 | var dir = dirname + '/'
23 | if (canPlayOgg()) {
24 | extension = '-ogg.js'
25 | }
26 | else {
27 | extension = '-mp3.js'
28 | }
29 | Soundfont.nameToUrl = function (name) { return dir + name + extension }
30 | Soundfont.loadBuffers(myapp.context, name)
31 | .then(function (buffers) {
32 | console.log("buffers:", buffers)
33 | myapp.buffers = buffers;
34 | myapp.ports.fontsLoaded.send(true);
35 | })
36 | }
37 |
38 |
39 | myapp.ports.requestLoadRemoteFonts.subscribe(loadRemoteSoundFonts);
40 |
41 | function loadRemoteSoundFonts(instrumentname) {
42 | if (!myapp.context) {
43 | myapp.context = getAudioContext();
44 | }
45 | Soundfont.loadBuffers(myapp.context, instrumentname)
46 | .then(function (buffers) {
47 | console.log("buffers:", buffers)
48 | myapp.buffers = buffers;
49 | myapp.ports.fontsLoaded.send(true);
50 | })
51 | }
52 |
53 |
54 |
55 | myapp.ports.requestPlayNote.subscribe(playNote);
56 |
57 | /* play a midi note */
58 | function playNote(midiNote) {
59 | var res = playMidiNote(midiNote);
60 | myapp.ports.playedNote.send(res);
61 | }
62 |
63 |
64 | myapp.ports.requestPlayNoteSequence.subscribe(playMidiNoteSequence);
65 |
66 | /* play a sequence of midi notes */
67 | function playMidiNoteSequence(midiNotes) {
68 | /* console.log("play sequence"); */
69 | if (myapp.buffers) {
70 | midiNotes.map(playMidiNote);
71 | myapp.ports.playSequenceStarted.send(true);
72 | }
73 | else {
74 | myapp.ports.playSequenceStarted.send(false);
75 | }
76 | }
77 |
78 | /* IMPLEMENTATION */
79 |
80 | /* Get the audio context */
81 | getAudioContext = function() {
82 | return new (window.AudioContext || window.webkitAudioContext)();
83 | };
84 |
85 | /* can the browser play ogg format? */
86 | canPlayOgg = function() {
87 | var audioTester = document.createElement("audio");
88 | if (audioTester.canPlayType('audio/ogg')) {
89 | /* console.log("browser supports ogg"); */
90 | return true;
91 | }
92 | else {
93 | /* console.log("browser does not support ogg"); */
94 | return false;
95 | }
96 | }
97 |
98 | /* play a midi note */
99 | function playMidiNote(midiNote) {
100 | if (myapp.buffers) {
101 | // console.log("playing buffer at time: " + midiNote.timeOffset + " with gain: " + midiNote.gain + " for note: " + midiNote.id)
102 | var buffer = myapp.buffers[midiNote.id]
103 | var source = myapp.context.createBufferSource();
104 | var gainNode = myapp.context.createGain();
105 | var time = myapp.context.currentTime + midiNote.timeOffset;
106 | gainNode.gain.value = midiNote.gain;
107 | source.buffer = buffer;
108 | source.connect(gainNode);
109 | gainNode.connect(myapp.context.destination)
110 | source.start(time);
111 | return true;
112 | }
113 | else {
114 | // console.log("no buffers");
115 | return false;
116 | }
117 | };
118 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | elm-abc-player
2 | ==============
3 |
4 | 
5 |
6 | These projects explore the possibilities of playing [ABC notation](http://abcnotation.com/) directly in the browser within an Elm (0.18) application. They use the following Elm audio libraries:
7 |
8 | * Elm-abc-parser. This is a parser for ABC notation.
9 |
10 | * Soundfont-ports. This is the soundfont wrapper from [elm-soundfont-ports](https://github.com/newlandsvalley/elm-soundfont-ports). It is more a psuedo-library because it relies on elm ports rather than a packaged library.
11 |
12 | * Midi-player. This is a player for MIDI recordings packaged as an autonomous module. It, in turn, relies on soundfont-ports and is used in projects where the ABC is first converted to MIDI.
13 |
14 | * Elm-vextab. This is a wrapper around the [VexTab](https://github.com/0xfe/vextab) API into [VexFlow](https://github.com/0xfe/vexflow) which is a JavaScript library for rendering music notation.
15 |
16 | The idea is to take ABC input and play it directly in the browser (i.e. without there being any need for server-side rendering or generation of intermediate MIDI files).
17 |
18 | Projects
19 | --------
20 |
21 | In each of these projects, playback is attempted as simply as possible in order to give a clear, unornamented rendition of the tune. The various notes and chords are played, but chord symbols, grace notes and other ornamentation forms are all ignored.
22 |
23 | #### Simple Player
24 |
25 | [Simpleplayer](https://github.com/newlandsvalley/elm-abc-player/tree/master/src/examples/simpleplayer) is a simple ABC file player (it plays a Swedish tune called 'Lillasystern'). It first loads the acoustic grand piano soundfont, loads and parses the ABC file and converts this into a performance by accumulating the elapsed times of each 'Note' event. It then converts each of these to a playable 'sound bite' attached to the appropriate soundfont and plays them as a single uninterruptable Task.
26 |
27 | to build:
28 |
29 | ./compilep.sh
30 |
31 | to run, use:
32 |
33 | simpleplayer.html
34 |
35 | #### ABC Editor
36 |
37 | [Editor](https://github.com/newlandsvalley/elm-abc-player/tree/master/src/examples/editor) is an editor for ABC Scores. It allows you to edit ABC text, play the score or transpose it to another key.
38 |
39 | to build:
40 |
41 | ./compilee.sh
42 |
43 | to run, use:
44 |
45 | abceditor.html
46 |
47 | #### Interactive ABC Tutorial
48 |
49 | In [tutorial](https://github.com/newlandsvalley/elm-abc-player/tree/master/src/examples/tutorial), the idea is to use the simple player but to take ABC input directly from the user. She is taken through a succession of exercises, each of which gives a short tutorial on an aspect of ABC together with a tiny ABC sample which illustrates it. She can play the sample and then tinker with it to see the effect that her changes make. Samples start with just a few notes and end with a fully-fledged traditional tune illustrating key signatures, tempi, notes of different pitch and duration, triplets, chords, articulation and so on. You can try the tutorial [here](http://www.tradtunedb.org.uk/abctutorial).
50 |
51 | to build:
52 |
53 | ./compilet.sh
54 |
55 | to run, use:
56 |
57 | abctutorial.html
58 |
59 | #### ABC Editor with embedded MIDI player
60 |
61 | [Editor-controller](https://github.com/newlandsvalley/elm-abc-player/tree/master/src/examples/editor-controller) is another version of the editor above. However, this one translates the ABC into a [MidiRecording](https://github.com/newlandsvalley/elm-comidi/blob/master/src/MidiTypes.elm) and uses the [midi-player](https://github.com/newlandsvalley/midi-player) module to play the recording (which you can stop and start). It includes buttons that allow you to load and save the ABC file. It now also includes an experimental feature which attempts to display the growing score as the ABC is being edited using a renderer based on an alpha release of [VexTab](http://www.vexflow.com/vextab/). You can try the editor [here](http://www.tradtunedb.org.uk/abceditor).
62 |
63 | to build:
64 |
65 | ./compileec.sh
66 |
67 | to run, use:
68 |
69 | abceditorcontroller.html
70 |
71 | Sources of native javascript code
72 | ---------------------------------
73 |
74 | These projects (particularly the editor controller) integrate a number of modules together which use native javascript by means of ports. This javascript is maintained in the js directory. Sources are as follows:
75 |
76 |
77 | | Name | Description | Github Repo | Maintainer |
78 | |------|-------------|-------------|------------|
79 | | soundfont-player | Play soundfonts using web-audio | https://github.com/danigb/soundfont-player | danigb@gmail.com |
80 | | vextab-div | Display scores | https://github.com/0xfe/vextab | http://0xfe.muthanna.com |
81 | | nativeFileIO | Port for text file IO | https://github.com/newlandsvalley/elm-file-io | john.watson@gmx.co.uk |
82 | | nativeSoundFont | Port wrapper round soundfont-player | https://github.com/newlandsvalley/elm-soundfont-ports | john.watson@gmx.co.uk |
83 | | nativeVexTab | Port wrapper round vextab-div | https://github.com/newlandsvalley/elm-vextab | john.watson@gmx.co.uk |
84 |
85 |
--------------------------------------------------------------------------------
/tests/Test/AbcPerformance.elm:
--------------------------------------------------------------------------------
1 | module Test.AbcPerformance exposing
2 | (tests)
3 |
4 | import Test exposing (..)
5 | import Expect exposing (..)
6 | import Abc exposing (ParseError, parse, parseKeySignature, parseError)
7 | import Abc.ParseTree exposing (..)
8 | import AbcPerformance exposing (..)
9 | import Music.Notation exposing (NoteTime, MidiPitch)
10 | import Melody exposing (..)
11 | import RepeatTypes exposing (..)
12 |
13 | import Debug exposing (..)
14 |
15 |
16 |
17 | {- get the melody line -}
18 | getMelodyLine : String -> Result ParseError MelodyLine
19 | getMelodyLine s =
20 | parse s
21 | |> melodyFromAbcResult
22 |
23 | {- get notes from the first bar -}
24 | getFirstBarNotes : String -> List NoteEvent
25 | getFirstBarNotes s =
26 | let
27 | melodyResult = getMelodyLine s
28 | in
29 | case melodyResult of
30 | Ok res ->
31 | let
32 | maybeFirstBar = List.head res
33 | in
34 | case maybeFirstBar of
35 | Just abar -> abar.notes
36 | _ -> []
37 | Err errs ->
38 | []
39 |
40 |
41 | {- show bar 1 and assert we have some notes -}
42 | showBar1Notes : String -> Expectation
43 | showBar1Notes s =
44 | let
45 | bar1Notes = getFirstBarNotes s
46 | in
47 | Expect.false "non empty bar" (List.isEmpty bar1Notes)
48 |
49 |
50 | {- assert the notes match the target from bar 1 in the melody line -}
51 | assertBar1Notes : String -> List NoteEvent -> Expectation
52 | assertBar1Notes s target =
53 | Expect.equal target (getFirstBarNotes s)
54 |
55 |
56 | tests : Test
57 | tests =
58 | describe "ABC Performance melody line"
59 | [ test "sequence" <|
60 | \() -> (assertBar1Notes sequence sequenceM)
61 | , test "chord" <|
62 | \() -> (assertBar1Notes chord chordM)
63 | , test "triplet" <|
64 | \() -> (assertBar1Notes triplet tripletM)
65 | , test "broken rhythm up" <|
66 | \() -> (assertBar1Notes brokenRhythmUp brokenRhythmUpM)
67 | , test "broken rhythm down" <|
68 | \() -> (assertBar1Notes brokenRhythmDown brokenRhythmDownM)
69 | , test "tied sequence" <|
70 | \() -> (assertBar1Notes sequenceTied sequenceTiedM)
71 | , test "sequence with accidental" <|
72 | \() -> (assertBar1Notes sequenceAccidental sequenceAccidentalM)
73 | , test "triplet with accidental" <|
74 | \() -> (assertBar1Notes tripletAccidental tripletAccidentalM)
75 | , test "broken rhythm with accidental" <|
76 | \() -> (assertBar1Notes brokenRhythmAccidental brokenRhythmAccidentalM)
77 | , test "broken rhythm with internal accidental" <|
78 | \() -> (assertBar1Notes brokenRhythmInternalAccidental brokenRhythmInternalAccidentalM)
79 | ]
80 |
81 | -- each test should just investigate a single bar
82 |
83 | -- music sources
84 | sequence = "K: D\r\n| def |\r\n"
85 | chord = "K: D\r\n| [def]2 |\r\n"
86 | triplet = "K: D\r\n| (3def |\r\n"
87 | brokenRhythmUp = "K: D\r\n| d Result ParseError MidiMelody
18 | getMidiMelody s =
19 | parse s
20 | |> melodyFromAbcResult
21 |
22 | {- get notes from the second bar
23 | (the first bar just holds the MIDI tempo)
24 | -}
25 | getSecondBarNotes : String -> List MidiInstruction
26 | getSecondBarNotes s =
27 | let
28 | melodyResult = getMidiMelody s
29 | in
30 | case melodyResult of
31 | Ok res ->
32 | let
33 | tail = List.tail res
34 | |> withDefault []
35 | maybeSecondBar = List.head tail
36 | in
37 | case maybeSecondBar of
38 | Just abar -> abar.notes
39 | _ -> []
40 | Err errs ->
41 | []
42 |
43 |
44 | {- show bar 2 and assert we have some notes -}
45 | showBar2Notes : String -> Expectation
46 | showBar2Notes s =
47 | let
48 | bar2Notes = getSecondBarNotes s
49 | in
50 | Expect.false "non empty bar" (List.isEmpty bar2Notes)
51 |
52 | {- assert the notes match the target from bar 2 in the melody line -}
53 | assertBar2Notes : String -> List MidiInstruction -> Expectation
54 | assertBar2Notes s target =
55 | Expect.equal target (getSecondBarNotes s)
56 |
57 |
58 | tests : Test
59 | tests =
60 | describe "MIDI Performance melody line"
61 | [ test "sequence" <|
62 | \() -> (assertBar2Notes sequence sequenceM)
63 | , test "chord" <|
64 | \() -> (assertBar2Notes chord chordM)
65 | , test "triplet" <|
66 | \() -> (assertBar2Notes triplet tripletM)
67 | , test "broken rhythm up" <|
68 | \() -> (assertBar2Notes brokenRhythmUp brokenRhythmUpM)
69 | , test "broken rhythm down" <|
70 | \() -> (assertBar2Notes brokenRhythmDown brokenRhythmDownM)
71 | , test "tied sequence" <|
72 | \() -> (assertBar2Notes sequenceTied sequenceTiedM)
73 | , test "sequence with accidental" <|
74 | \() -> (assertBar2Notes sequenceAccidental sequenceAccidentalM)
75 | , test "triplet with accidental" <|
76 | \() -> (assertBar2Notes tripletAccidental tripletAccidentalM)
77 | , test "broken rhythm with accidental" <|
78 | \() -> (assertBar2Notes brokenRhythmAccidental brokenRhythmAccidentalM)
79 | , test "broken rhythm with internal accidental" <|
80 | \() -> (assertBar2Notes brokenRhythmInternalAccidental brokenRhythmInternalAccidentalM)
81 | ]
82 |
83 | -- each test should just investigate a single bar
84 |
85 | -- music sources
86 | sequence = "K: D\r\n| def |\r\n"
87 | chord = "K: D\r\n| [def]2 |\r\n"
88 | triplet = "K: D\r\n| (3def |\r\n"
89 | brokenRhythmUp = "K: D\r\n| d Section ( start .. end), section ( start .. end)
15 |
16 | or
17 | |: .... |1 ... :|2 .... ||
18 | -> Section ( start 1 2 end)
19 |
20 | # Definition
21 |
22 | # Functions
23 | @docs indexBar
24 | , defaultRepeatState
25 | , finalise
26 | , buildRepeatedMelody
27 |
28 | -}
29 |
30 | -- import Melody exposing (ABar, MelodyLine)
31 | import Abc.ParseTree exposing (Repeat (..))
32 | import Maybe exposing (withDefault)
33 | import Maybe.Extra exposing (isJust)
34 | import List.Extra exposing (takeWhile, dropWhile)
35 | import RepeatTypes exposing (..)
36 |
37 | {-| default repeats i.e. no repeats yet -}
38 | defaultRepeatState : RepeatState
39 | defaultRepeatState =
40 | { current = nullSection, repeats = [] }
41 |
42 |
43 | {-| index a bar by identifying any repeat markings and saving the marking against the bar number -}
44 | indexBar : GeneralisedBar n -> RepeatState -> RepeatState
45 | indexBar b r =
46 | case (b.iteration, b.repeat) of
47 | -- |1
48 | (Just 1, _) ->
49 | {r | current = firstRepeat b.number r.current}
50 | -- |2 or :|2
51 | (Just 2, _) ->
52 | {r | current = secondRepeat b.number r.current}
53 | -- |:
54 | (_, Just Begin) ->
55 | startSection b.number r
56 | -- :|
57 | (_, Just End) ->
58 | endSection b.number True r
59 | -- :|: or ::
60 | (_, Just BeginAndEnd) ->
61 | endAndStartSection b.number True True r
62 | _ ->
63 | r
64 |
65 | {-| accumulate any residual current state from the final bar in the tune -}
66 | finalise : GeneralisedBar n -> RepeatState -> RepeatState
67 | finalise lastBar r =
68 | let
69 | -- _ = log "last bar" lastBar
70 | -- end the current section with the last bar number
71 | current = endCurrentSection lastBar.number r.current
72 | -- fix a degenerate case where we have a repeat indicated by end markers and no begin markers
73 | -- i.e. this must be the first and also the last (repeated) phrase in the tune
74 | current1 =
75 | if (isEmptyRepeatEndBar lastBar) then
76 | setRepeated current
77 | else
78 | current
79 | newr =
80 | { r | current = current1 }
81 | in
82 | accumulateSection newr
83 |
84 | {-| build any repeated section into an extended melody with all repeats realised -}
85 | buildRepeatedMelody : (List (GeneralisedBar n), Repeats) -> List (GeneralisedBar n)
86 | buildRepeatedMelody (ml, repeats) =
87 | if (List.isEmpty repeats) then
88 | ml
89 | else
90 | List.foldr (repeatedSection ml) [] repeats
91 |
92 | {- a 'null' section -}
93 | nullSection : Section
94 | nullSection =
95 | { start = Just 0, firstEnding = Nothing, secondEnding = Nothing, end = Just 0, isRepeated = False }
96 |
97 | {- accumulate the last section and start a new section -}
98 | startSection : Int -> RepeatState -> RepeatState
99 | startSection pos r =
100 | -- a start implies an end of the last section
101 | endAndStartSection pos False True r
102 |
103 | {- end the section. If there is a first repeat, keep it open, else accumulate it
104 | pos : the bar number marking the end of section
105 | isRepeatEnd : True if invoked with a known Repeat End marker in the bar line
106 | -}
107 | endSection : Int -> Bool -> RepeatState -> RepeatState
108 | endSection pos isRepeatEnd r =
109 | if (hasFirstEnding r.current) then
110 | let
111 | current = endCurrentSection pos r.current
112 | in
113 | { r | current = current }
114 | else
115 | endAndStartSection pos isRepeatEnd False r
116 |
117 | {- end the current section, accumulate it and start a new section -}
118 | endAndStartSection : Int -> Bool -> Bool -> RepeatState -> RepeatState
119 | endAndStartSection pos isRepeatEnd isRepeatStart r =
120 | let
121 | -- cater for the situation where the ABC marks the first section of the tune as repeated solely by use
122 | -- of the End Repeat marker with no such explicit marker at the start of the section - it is implied as the tune start
123 | current =
124 | if (isRepeatEnd && r.current.start == Just 0) then
125 | setRepeated r.current
126 | else
127 | r.current
128 | -- now set the end position from the bar number position
129 | endCurrent = { current | end = Just pos }
130 | -- set the entire state and accumulate
131 | endState = { r | current = endCurrent }
132 | newState = accumulateSection endState
133 | newCurrent = { nullSection | start = Just pos, isRepeated = isRepeatStart }
134 | in
135 | { newState | current = newCurrent }
136 |
137 | {- set the end of the current section -}
138 | endCurrentSection : Int -> Section -> Section
139 | endCurrentSection pos s =
140 | { s | end = Just pos }
141 |
142 | {- set the first repeat of a section -}
143 | firstRepeat : Int -> Section -> Section
144 | firstRepeat pos s =
145 | { s | firstEnding = Just pos }
146 |
147 | {- set the second repeat of a section -}
148 | secondRepeat : Int -> Section -> Section
149 | secondRepeat pos s =
150 | { s | secondEnding = Just pos }
151 |
152 | {- set the isRepeated status of a section -}
153 | setRepeated : Section -> Section
154 | setRepeated s =
155 | { s | isRepeated = True }
156 |
157 | {- return True if the section is devoid of any useful content -}
158 | isNullSection : Section -> Bool
159 | isNullSection s =
160 | s == nullSection
161 |
162 | {- return True if the first (variant) ending is set -}
163 | hasFirstEnding : Section -> Bool
164 | hasFirstEnding s =
165 | isJust s.firstEnding
166 |
167 | {- recognise a solitary bar indicating an end repeat and nothing more - used in finalise -}
168 | isEmptyRepeatEndBar : GeneralisedBar n -> Bool
169 | isEmptyRepeatEndBar b =
170 | List.length b.notes == 0 && b.repeat == Just End
171 |
172 | {- accumulate the current section into the full score and re-initialise it -}
173 | accumulateSection : RepeatState -> RepeatState
174 | accumulateSection r =
175 | if not (isNullSection r.current) then
176 | {r | repeats = r.current :: r.repeats, current = nullSection }
177 | else
178 | r
179 |
180 | {-| take a slice of a melody line between start and finish -}
181 | slice : Int -> Int -> List (GeneralisedBar n) -> List (GeneralisedBar n)
182 | slice start end =
183 | dropWhile (\bar -> bar.number < start)
184 | >> takeWhile (\bar -> bar.number < end)
185 |
186 | {-| take two variant slices of a melody line between start and finish
187 | taking account of first repeat and second repeat sections
188 | -}
189 | variantSlice : Int -> Int -> Int -> Int -> List (GeneralisedBar n) -> List (GeneralisedBar n)
190 | variantSlice start firstRepeat secondRepeat end ml =
191 | let
192 | section = slice start end ml
193 | firstSection = slice start secondRepeat section
194 | secondSection = slice start firstRepeat section ++ slice secondRepeat end section
195 | in
196 | firstSection ++ secondSection
197 |
198 | {- build the complete melody with repeated sections in place -}
199 | repeatedSection : List (GeneralisedBar n) -> Section -> List (GeneralisedBar n) -> List (GeneralisedBar n)
200 | repeatedSection ml s acc =
201 | let
202 | section { start, firstEnding, secondEnding, end, isRepeated } = (start, firstEnding, secondEnding, end, isRepeated)
203 | in
204 | case (section s) of
205 | -- variant ending repeat
206 | (Just a, Just b, Just c, Just d, _ ) ->
207 | (variantSlice a b c d ml) ++ acc
208 | -- simple phrase - no repeats
209 | ( Just a, _, _, Just d, False ) ->
210 | slice a d ml ++ acc
211 | -- standard repeat
212 | ( Just a, _, _, Just d, True ) ->
213 | (slice a d ml ++ slice a d ml) ++ acc
214 | _ -> []
215 |
216 |
217 |
218 |
219 |
--------------------------------------------------------------------------------
/src/examples/editor-controller/VexScore/Canonical.elm:
--------------------------------------------------------------------------------
1 | module VexScore.Canonical exposing (toScoreText)
2 |
3 | {-|
4 |
5 | @docs toString
6 |
7 | -}
8 |
9 | import VexScore.Score exposing (..)
10 | import String exposing (concat)
11 | import Tuple exposing (first, second)
12 | import Maybe exposing (withDefault)
13 | import Abc.ParseTree
14 | exposing
15 | ( Accidental(..)
16 | , Mode(..)
17 | , AbcNote
18 | , PitchClass(..)
19 | , Bar
20 | , Thickness(..)
21 | , Repeat(..)
22 | )
23 |
24 |
25 | type NoteContext
26 | = Staved
27 | | Tupleted
28 | | Chordal
29 |
30 |
31 | eol : String
32 | eol =
33 | "\x0D\n"
34 |
35 |
36 |
37 | {- concatenate strings and space them simply -}
38 |
39 |
40 | nicelySpace : List String -> String
41 | nicelySpace xs =
42 | List.intersperse " " xs
43 | |> String.concat
44 |
45 |
46 | options : String
47 | options =
48 | "options beam-rests=false\x0D\n"
49 |
50 |
51 | toScoreText : Score -> String
52 | toScoreText score =
53 | let
54 | f vl acc =
55 | acc ++ vexBodyPart vl
56 | in
57 | options
58 | ++ List.foldl f "" score
59 |
60 |
61 | vexBodyPart : VexBodyPart -> String
62 | vexBodyPart bp =
63 | case bp of
64 | VLine line ->
65 | vexLine line
66 |
67 | _ ->
68 | -- VContextChange or VEmptyLine
69 | ""
70 |
71 |
72 | vexLine : VexLine -> String
73 | vexLine vl =
74 | vexStave vl.stave ++ (vexItems vl.items) ++ "\x0D\n"
75 |
76 |
77 | vexStave : Maybe VexStave -> String
78 | vexStave mvs =
79 | case mvs of
80 | Just vs ->
81 | let
82 | clef =
83 | "clef=" ++ ((String.toLower << toString) (vs.clef))
84 |
85 | time =
86 | case vs.mMeter of
87 | Just m ->
88 | "time=" ++ toString (first m) ++ "/" ++ toString (second m)
89 |
90 | _ ->
91 | ""
92 |
93 | key =
94 | case vs.mKey of
95 | Just k ->
96 | let
97 | accidental =
98 | headerAccidental k.accidental
99 |
100 | md =
101 | mode k.mode
102 | in
103 | "key=" ++ toString k.pitchClass ++ accidental ++ md
104 |
105 | _ ->
106 | ""
107 | in
108 | (nicelySpace [ "stave notation=true", clef, key, time, eol, "notes" ])
109 |
110 | Nothing ->
111 | " notes"
112 |
113 |
114 | vexItems : List VexItem -> String
115 | vexItems vis =
116 | List.map vexItem vis
117 | |> String.concat
118 |
119 |
120 | vexItem : VexItem -> String
121 | vexItem vi =
122 | case vi of
123 | VBar bar ->
124 | vexBar bar
125 |
126 | VNote vnote ->
127 | vexNote Staved vnote
128 |
129 | VRest duration ->
130 | let
131 | dur =
132 | noteDur duration
133 |
134 | rest =
135 | "##"
136 | in
137 | nicelySpace [ "", dur, rest ]
138 |
139 | VTuplet size vnotes ->
140 | " "
141 | ++ (List.map (vexNote Tupleted) vnotes
142 | |> List.intersperse " "
143 | |> String.concat
144 | )
145 | ++ " ^"
146 | ++ toString size
147 | ++ ","
148 | ++ toString (List.length vnotes)
149 | ++ "^"
150 |
151 | VChord dur vnotes ->
152 | let
153 | chordDur =
154 | noteDur dur
155 | in
156 | " "
157 | ++ chordDur
158 | ++ " ( "
159 | ++ (List.map (vexNote Chordal) vnotes
160 | |> List.intersperse "."
161 | |> String.concat
162 | )
163 | ++ " )"
164 |
165 | VNotePair vnote1 vnote2 ->
166 | vexNote Staved vnote1
167 | ++ vexNote Staved vnote2
168 |
169 | VIgnore ->
170 | ""
171 |
172 |
173 | vexNote : NoteContext -> VexNote -> String
174 | vexNote ctx vnote =
175 | let
176 | accident =
177 | Maybe.map accidental vnote.accidental
178 | |> withDefault ""
179 |
180 | pitch =
181 | toString vnote.pitchClass
182 | ++ accident
183 | ++ "/"
184 | ++ toString vnote.octave
185 |
186 | dur =
187 | noteDur vnote.duration
188 |
189 | tie =
190 | if vnote.tied then
191 | "T"
192 | else
193 | ""
194 |
195 | decor =
196 | vexDecoration vnote
197 | in
198 | case ctx of
199 | Chordal ->
200 | pitch
201 |
202 | Tupleted ->
203 | nicelySpace [ dur, pitch ]
204 |
205 | _ ->
206 | if vnote.tied then
207 | nicelySpace [ "", dur, tie, pitch ] ++ decor
208 | else
209 | nicelySpace [ "", dur, pitch ] ++ decor
210 |
211 |
212 | noteDur : VexDuration -> String
213 | noteDur nd =
214 | case nd of
215 | Whole ->
216 | ":w"
217 |
218 | Half ->
219 | ":h"
220 |
221 | Quarter ->
222 | ":q"
223 |
224 | Eighth ->
225 | ":8"
226 |
227 | Sixteenth ->
228 | ":16"
229 |
230 | ThirtySecond ->
231 | ":32"
232 |
233 | SixtyFourth ->
234 | ":64"
235 |
236 | HalfDotted ->
237 | ":hd"
238 |
239 | QuarterDotted ->
240 | ":qd"
241 |
242 | EighthDotted ->
243 | ":8d"
244 |
245 | SixteenthDotted ->
246 | ":16d"
247 |
248 | ThirtySecondDotted ->
249 | ":32d"
250 |
251 | SixtyFourthDotted ->
252 | ":64d"
253 |
254 |
255 | accidental : Accidental -> String
256 | accidental a =
257 | case a of
258 | Sharp ->
259 | "#"
260 |
261 | Flat ->
262 | "@"
263 |
264 | DoubleSharp ->
265 | "##"
266 |
267 | DoubleFlat ->
268 | "@@"
269 |
270 | Natural ->
271 | "n"
272 |
273 |
274 | vexBar : Bar -> String
275 | vexBar b =
276 | case b.repeat of
277 | Just Begin ->
278 | " =|:"
279 |
280 | Just End ->
281 | " =:|"
282 |
283 | Just BeginAndEnd ->
284 | " =::"
285 |
286 | Nothing ->
287 | case b.thickness of
288 | Thin ->
289 | " |"
290 |
291 | _ ->
292 | " =||"
293 |
294 |
295 | headerAccidental : Maybe Accidental -> String
296 | headerAccidental ma =
297 | case ma of
298 | Just Sharp ->
299 | "#"
300 |
301 | Just Flat ->
302 | "b"
303 |
304 | _ ->
305 | ""
306 |
307 |
308 | mode : Mode -> String
309 | mode m =
310 | case m of
311 | Major ->
312 | ""
313 |
314 | Minor ->
315 | "m"
316 |
317 | Ionian ->
318 | ""
319 |
320 | Aeolian ->
321 | "m"
322 |
323 | -- we need to trap this in translate - probably by converting modes to canonical forms
324 | _ ->
325 | "error not supported"
326 |
327 |
328 | vexDecoration : VexNote -> String
329 | vexDecoration v =
330 | let
331 | formatDecoration : Bool -> String -> String
332 | formatDecoration isTop vexCode =
333 | let
334 | position =
335 | if isTop then
336 | "/top"
337 | else
338 | "/bottom"
339 | in
340 | " $.a" ++ vexCode ++ position ++ ".$"
341 |
342 | isTopPosition =
343 | if v.octave > 4 then
344 | True
345 | else if v.octave < 4 then
346 | False
347 | --else if (B == v.pitchClass) then
348 | -- true
349 | else
350 | False
351 | in
352 | case v.decoration of
353 | -- staccato
354 | Just "." ->
355 | formatDecoration isTopPosition "."
356 |
357 | -- fermata
358 | Just "H" ->
359 | formatDecoration True "@a"
360 |
361 | -- accent
362 | Just "L" ->
363 | formatDecoration True ">"
364 |
365 | -- up bow
366 | Just "u" ->
367 | formatDecoration True "|"
368 |
369 | -- down bow
370 | Just "v" ->
371 | formatDecoration True "m"
372 |
373 | -- gross hack for 1st and 2nd repeats
374 | Just "1" ->
375 | " $.top.$ $1───$"
376 |
377 | Just "2" ->
378 | " $.top.$ $2───$"
379 |
380 | _ ->
381 | ""
382 |
--------------------------------------------------------------------------------
/src/examples/tutorial/AbcTutorial.elm:
--------------------------------------------------------------------------------
1 | module AbcTutorial exposing (..)
2 |
3 | import Html exposing (..)
4 | import Html.Attributes exposing (..)
5 | import Html.Events exposing (on, targetValue, onClick, onInput)
6 | import Task exposing (Task, andThen, succeed, sequence, onError)
7 | import Process exposing (sleep)
8 | import List exposing (reverse)
9 | import Maybe exposing (Maybe, withDefault)
10 | import String exposing (toInt)
11 | import Result exposing (Result)
12 | import Array exposing (Array, get)
13 | import Abc exposing (..)
14 | import AbcPerformance exposing (melodyFromAbcResult)
15 | import Melody exposing (..)
16 | import Notable exposing (..)
17 | import MidiNotes exposing (..)
18 | import Lessons exposing (..)
19 | import Json.Encode as Json
20 | import SoundFont.Ports exposing (..)
21 | import SoundFont.Types exposing (..)
22 | import Debug exposing (..)
23 |
24 |
25 | main =
26 | Html.program
27 | { init = ( init, requestLoadPianoFonts "assets/soundfonts" ), update = update, view = view, subscriptions = subscriptions }
28 |
29 |
30 |
31 | -- MODEL
32 |
33 |
34 | type alias Model =
35 | { fontsLoaded : Bool
36 | , abc : String
37 | , playing : Bool
38 | , lessonIndex : Int
39 | , duration :
40 | Float
41 | -- the tune duration in seconds
42 | , error : Maybe ParseError
43 | }
44 |
45 |
46 | init : Model
47 | init =
48 | { fontsLoaded = False
49 | , abc = example 0
50 | , playing = False
51 | , lessonIndex = 0
52 | , duration = 0.0
53 | , error = Nothing
54 | }
55 |
56 |
57 |
58 | -- UPDATE
59 |
60 |
61 | type Msg
62 | = NoOp
63 | | FontsLoaded Bool
64 | | Abc String
65 | | Play
66 | | PlayStarted Bool
67 | -- response from the player that it's started
68 | | PlayCompleted
69 | -- the play has completed (we compute the time ourselves)
70 | | ShowButtons
71 | -- immediately after play has ended
72 | | Move Bool
73 | | MoveToEnd Bool
74 | | Error ParseError
75 |
76 |
77 | update : Msg -> Model -> ( Model, Cmd Msg )
78 | update msg model =
79 | case msg of
80 | NoOp ->
81 | ( model, Cmd.none )
82 |
83 | ShowButtons ->
84 | ( { model | playing = False }, Cmd.none )
85 |
86 | FontsLoaded loaded ->
87 | ( { model | fontsLoaded = loaded }
88 | , Cmd.none
89 | )
90 |
91 | Abc s ->
92 | ( { model | abc = s }, Cmd.none )
93 |
94 | Play ->
95 | playAbc model
96 |
97 | PlayStarted _ ->
98 | ( model, (suspend model.duration) )
99 |
100 | PlayCompleted ->
101 | ( { model | playing = False }, Cmd.none )
102 |
103 | Move b ->
104 | let
105 | next =
106 | case b of
107 | True ->
108 | Basics.min (model.lessonIndex + 1) (Array.length lessons - 1)
109 |
110 | False ->
111 | Basics.max (model.lessonIndex - 1) 0
112 | in
113 | ( { model
114 | | lessonIndex = next
115 | , abc = (example next)
116 | , error = Nothing
117 | }
118 | , Cmd.none
119 | )
120 |
121 | MoveToEnd b ->
122 | let
123 | next =
124 | case b of
125 | True ->
126 | (Array.length lessons - 1)
127 |
128 | False ->
129 | 0
130 | in
131 | ( { model
132 | | lessonIndex = next
133 | , abc = (example next)
134 | , error = Nothing
135 | }
136 | , Cmd.none
137 | )
138 |
139 | Error pe ->
140 | ( { model | error = Just pe }, showButtonsAction )
141 |
142 |
143 |
144 | -- SUBSCRIPTIONS
145 |
146 |
147 | fontsLoadedSub : Sub Msg
148 | fontsLoadedSub =
149 | fontsLoaded FontsLoaded
150 |
151 |
152 | playSequenceStartedSub : Sub Msg
153 | playSequenceStartedSub =
154 | playSequenceStarted PlayStarted
155 |
156 |
157 | subscriptions : Model -> Sub Msg
158 | subscriptions m =
159 | Sub.batch [ fontsLoadedSub, playSequenceStartedSub ]
160 |
161 |
162 |
163 | -- COMMANDS
164 | {- sleep for a number of seconds -}
165 |
166 |
167 | suspend : Float -> Cmd Msg
168 | suspend secs =
169 | let
170 | _ =
171 | log "suspend time" secs
172 |
173 | time =
174 | secs * 1000
175 | in
176 | Process.sleep time
177 | |> Task.perform (\_ -> PlayCompleted)
178 |
179 |
180 |
181 | -- |> Task.perform (\_ -> NoOp) (\_ -> PlayCompleted)
182 | {- just the ShowButton action wrapped in a Task -}
183 |
184 |
185 | showButtons : Task Never Msg
186 | showButtons =
187 | succeed (ShowButtons)
188 |
189 |
190 |
191 | {- and as an effect -}
192 |
193 |
194 | showButtonsAction : Cmd Msg
195 | showButtonsAction =
196 | Task.perform (\_ -> NoOp) showButtons
197 |
198 |
199 |
200 | {- calculate the performance duration in seconds -}
201 |
202 |
203 | performanceDuration : MidiNotes -> Float
204 | performanceDuration notes =
205 | let
206 | maybeLastNote =
207 | List.head (List.reverse notes)
208 | in
209 | case maybeLastNote of
210 | Nothing ->
211 | 0.0
212 |
213 | Just n ->
214 | n.timeOffset
215 |
216 |
217 |
218 | -- the accumulated time
219 |
220 |
221 | returnError : ParseError -> Cmd Msg
222 | returnError e =
223 | Task.succeed (Error e)
224 | |> Task.perform (\_ -> NoOp)
225 |
226 |
227 | terminateLine : String -> String
228 | terminateLine s =
229 | s ++ "|\x0D\n"
230 |
231 |
232 |
233 | {- cast a String to an Int -}
234 |
235 |
236 | toInt : String -> Int
237 | toInt =
238 | String.toInt >> Result.toMaybe >> Maybe.withDefault 0
239 |
240 |
241 |
242 | {- play the ABC and return the duration in the amended model -}
243 |
244 |
245 | playAbc : Model -> ( Model, Cmd Msg )
246 | playAbc m =
247 | let
248 | abcTuneResult =
249 | m.abc
250 | |> terminateLine
251 | |> parse
252 | in
253 | case abcTuneResult of
254 | Ok _ ->
255 | let
256 | notesReversed =
257 | abcTuneResult
258 | |> melodyFromAbcResult
259 | |> toPerformance
260 | |> makeMIDINotes
261 |
262 | -- _ = log "notes reversed" notesReversed
263 | duration =
264 | reversedPhraseDuration notesReversed
265 | in
266 | ( { m
267 | | playing = True
268 | , duration = duration
269 | }
270 | , requestPlayNoteSequence (List.reverse notesReversed)
271 | )
272 |
273 | Err error ->
274 | ( { m | error = Just error }, returnError error )
275 |
276 |
277 |
278 | -- VIEW
279 |
280 |
281 | viewError : Maybe ParseError -> String
282 | viewError me =
283 | case me of
284 | Nothing ->
285 | ""
286 |
287 | Just pe ->
288 | "parse error: " ++ pe.input ++ " at position " ++ toString (pe.position)
289 |
290 |
291 | view : Model -> Html Msg
292 | view model =
293 | if (model.fontsLoaded) then
294 | div []
295 | [ h2 [ centreStyle ] [ text (title model.lessonIndex) ]
296 | , textarea
297 | [ centreStyle
298 | , value (instruction model.lessonIndex)
299 | , instructionStyle
300 | , readonly True
301 | , cols 96
302 | , rows 6
303 | ]
304 | []
305 | , div []
306 | [ fieldset [ fieldsetStyle ]
307 | [ legend [ legendStyle ] [ text "you can edit the text inside the box and then hit play" ]
308 | , textarea
309 | ([ placeholder "abc"
310 | , value model.abc
311 | , onInput Abc
312 | , taStyle
313 | , cols 70
314 | , rows 15
315 | , autocomplete False
316 | , spellcheck False
317 | , autofocus True
318 | ]
319 | ++ highlights model
320 | )
321 | []
322 | ]
323 | , img
324 | [ src (scoreUrl model.lessonIndex)
325 | , rightImageStyle
326 | ]
327 | []
328 | ]
329 | , div
330 | [ leftPaneCentreStyle ]
331 | [ button (buttonAttributes (not model.playing) (MoveToEnd False))
332 | [ text "first" ]
333 | , button (buttonAttributes (not model.playing) (Move False))
334 | [ text "previous" ]
335 | , button (buttonAttributes (not model.playing) Play)
336 | [ text "play" ]
337 | , button (buttonAttributes (not model.playing) (Move True))
338 | [ text "next" ]
339 | , button (buttonAttributes (not model.playing) (MoveToEnd True))
340 | [ text "last" ]
341 | ]
342 | , div
343 | [ leftPaneCentreStyle ]
344 | [ p [] [ text (hint model.lessonIndex) ]
345 | , p [] [ text (viewError model.error) ]
346 | ]
347 | ]
348 | else
349 | div [ centreStyle ]
350 | [ p [] [ text "It seems as if your browser does not support web-audio. Perhaps try Chrome" ]
351 | ]
352 |
353 |
354 | title : Int -> String
355 | title i =
356 | let
357 | mlesson =
358 | Array.get i lessons
359 | in
360 | case mlesson of
361 | Nothing ->
362 | "error"
363 |
364 | Just l ->
365 | "ABC Tutorial: lesson " ++ (toString (i + 1) ++ " - " ++ l.title)
366 |
367 |
368 | instruction : Int -> String
369 | instruction i =
370 | let
371 | mlesson =
372 | Array.get i lessons
373 | in
374 | case mlesson of
375 | Nothing ->
376 | "error"
377 |
378 | Just l ->
379 | l.instruction
380 |
381 |
382 | example : Int -> String
383 | example i =
384 | let
385 | mlesson =
386 | Array.get i lessons
387 | in
388 | case mlesson of
389 | Nothing ->
390 | "error"
391 |
392 | Just l ->
393 | l.example
394 |
395 |
396 | hint : Int -> String
397 | hint i =
398 | let
399 | mlesson =
400 | Array.get i lessons
401 | in
402 | case mlesson of
403 | Nothing ->
404 | ""
405 |
406 | Just l ->
407 | l.hint
408 |
409 |
410 | scoreUrl : Int -> String
411 | scoreUrl i =
412 | let
413 | mlesson =
414 | Array.get i lessons
415 | in
416 | case mlesson of
417 | Nothing ->
418 | ""
419 |
420 | Just l ->
421 | "assets/images/tutorial/" ++ l.id ++ ".png"
422 |
423 |
424 |
425 | {- style a textarea -}
426 |
427 |
428 | taStyle : Attribute Msg
429 | taStyle =
430 | style
431 | [ ( "padding", "10px 0" )
432 | , ( "font-size", "1.5em" )
433 | , ( "text-align", "left" )
434 | , ( "align", "center" )
435 | , ( "display", "block" )
436 | , ( "margin-left", "auto" )
437 | , ( "margin-right", "auto" )
438 | , ( "background-color", "#f3f6c6" )
439 | , ( "font-family", "monospace" )
440 | ]
441 |
442 |
443 |
444 | {- style the instructions section -}
445 |
446 |
447 | instructionStyle : Attribute Msg
448 | instructionStyle =
449 | style
450 | [ ( "padding", "10px 0" )
451 | , ( "border", "none" )
452 | , ( "text-align", "left" )
453 | , ( "align", "center" )
454 | , ( "display", "block" )
455 | , ( "margin-left", "auto" )
456 | , ( "margin-right", "auto" )
457 | , ( "font", "100% \"Trebuchet MS\", Verdana, sans-serif" )
458 | ]
459 |
460 |
461 |
462 | {- style a centered component -}
463 |
464 |
465 | centreStyle : Attribute Msg
466 | centreStyle =
467 | style
468 | [ ( "text-align", "center" )
469 | , ( "margin", "auto" )
470 | ]
471 |
472 |
473 | leftPaneStyle : Attribute msg
474 | leftPaneStyle =
475 | style
476 | [ ( "float", "left" )
477 | , ( "width", "800px" )
478 | ]
479 |
480 |
481 | leftPaneCentreStyle : Attribute msg
482 | leftPaneCentreStyle =
483 | style
484 | [ ( "float", "left" )
485 | , ( "margin-left", "200px" )
486 | ]
487 |
488 |
489 | rightImageStyle : Attribute msg
490 | rightImageStyle =
491 | style
492 | [ ( "position", "absolute" )
493 | ]
494 |
495 |
496 |
497 | {- gather together all the button attributes -}
498 |
499 |
500 | buttonAttributes : Bool -> Msg -> List (Attribute Msg)
501 | buttonAttributes isEnabled msg =
502 | [ class "hoverable"
503 | , bStyle isEnabled
504 | , onClick msg
505 | , disabled (not isEnabled)
506 | ]
507 |
508 |
509 |
510 | {- style a button
511 | Note: all button styling is deferred to the external css (which implements hover)
512 | except for when the button is greyed out when it is disabled
513 | -}
514 |
515 |
516 | bStyle : Bool -> Attribute msg
517 | bStyle enabled =
518 | let
519 | colour =
520 | if enabled then
521 | []
522 | else
523 | [ ( "background-color", "lightgray" )
524 | , ( "color", "darkgrey" )
525 | ]
526 | in
527 | style (colour)
528 |
529 |
530 |
531 | {- style a fieldset -}
532 |
533 |
534 | fieldsetStyle : Attribute Msg
535 | fieldsetStyle =
536 | style
537 | [ ( "background-color", "#f1f1f1" )
538 | , ( "border", "none" )
539 | , ( "border-radius", "2px" )
540 | , ( "margin-bottom", "12px" )
541 | , ( "margin-left", "12px" )
542 | , ( "margin-right", "12px" )
543 | , ( "padding", "10px 10px 20px 10px" )
544 | , ( "display", "inline-block" )
545 | ]
546 |
547 |
548 |
549 | {- style a fieldset legend -}
550 |
551 |
552 | legendStyle : Attribute Msg
553 | legendStyle =
554 | style
555 | [ ( "background-color", "#67d665" )
556 | , ( "border-top", "1px solid #d4d4d4" )
557 | , ( "border-bottom", "1px solid #d4d4d4" )
558 | , ( "-moz-box-shadow", "3px 3px 3px #ccc" )
559 | , ( "-webkit-box-shadow", "3px 3px 3px #ccc" )
560 | , ( "box-shadow", "3px 3px 3px #ccc" )
561 | , ( "font-size", "1em" )
562 | , ( "padding", "0.3em 1em" )
563 | ]
564 |
565 |
566 | highlights : Model -> List (Attribute Msg)
567 | highlights model =
568 | let
569 | mpe =
570 | model.error
571 | in
572 | case mpe of
573 | Nothing ->
574 | []
575 |
576 | Just pe ->
577 | if (String.length model.abc > pe.position) then
578 | [ property "selectionStart" (Json.string (toString pe.position))
579 | , property "selectionEnd" (Json.string (toString (pe.position + 1)))
580 | , property "focus" (Json.null)
581 | ]
582 | else
583 | []
584 |
--------------------------------------------------------------------------------
/js/soundfont-player.js:
--------------------------------------------------------------------------------
1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 64 && nChr < 91 ? nChr - 65
6 | : nChr > 96 && nChr < 123 ? nChr - 71
7 | : nChr > 47 && nChr < 58 ? nChr + 4
8 | : nChr === 43 ? 62
9 | : nChr === 47 ? 63
10 | : 0
11 | }
12 |
13 | // Decode Base64 to Uint8Array
14 | // ---------------------------
15 | function base64DecodeToArray (sBase64, nBlocksSize) {
16 | var sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, '')
17 | var nInLen = sB64Enc.length
18 | var nOutLen = nBlocksSize
19 | ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize
20 | : nInLen * 3 + 1 >> 2
21 | var taBytes = new Uint8Array(nOutLen)
22 |
23 | for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) {
24 | nMod4 = nInIdx & 3
25 | nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4
26 | if (nMod4 === 3 || nInLen - nInIdx === 1) {
27 | for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) {
28 | taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255
29 | }
30 | nUint24 = 0
31 | }
32 | }
33 | return taBytes
34 | }
35 |
36 | module.exports = base64DecodeToArray
37 |
38 | },{}],2:[function(require,module,exports){
39 | 'use strict'
40 |
41 | var midi = require('note-midi')
42 |
43 | /**
44 | * Create a soundfont bank player
45 | *
46 | * @param {AudioContext} ac - the audio context
47 | * @param {Hash} bank - a midi number to audio buffer hash map
48 | * @param {Hash} defaultOptions - (Optional) a hash of options:
49 | * - gain: the output gain (default: 2)
50 | * - destination: the destination of the player (default: `ac.destination`)
51 | */
52 | module.exports = function (ctx, bank, defaultOptions) {
53 | defaultOptions = defaultOptions || {}
54 | return function (note, time, duration, options) {
55 | var m = note > 0 && note < 128 ? note : midi(note)
56 | var buffer = bank[m]
57 | if (!buffer) return
58 |
59 | options = options || {}
60 | var gain = options.gain || defaultOptions.gain || 2
61 | var destination = options.destination || defaultOptions.destination || ctx.destination
62 |
63 | var source = ctx.createBufferSource()
64 | source.buffer = buffer
65 |
66 | /* VCA */
67 | var vca = ctx.createGain()
68 | vca.gain.value = gain
69 | source.connect(vca)
70 | vca.connect(destination)
71 | if (duration > 0) {
72 | source.start(time, 0, duration)
73 | } else {
74 | source.start(time)
75 | }
76 | return source
77 | }
78 | }
79 |
80 | },{"note-midi":8}],3:[function(require,module,exports){
81 | 'use strict'
82 |
83 | var base64DecodeToArray = require('./b64decode.js')
84 |
85 | /**
86 | * Given a base64 encoded audio data, return a prmomise with an audio buffer
87 | *
88 | * @param {AudioContext} context - the [audio context](https://developer.mozilla.org/en/docs/Web/API/AudioContext)
89 | * @param {String} data - the base64 encoded audio data
90 | * @return {Promise} a promise that resolves to an [audio buffer](https://developer.mozilla.org/en-US/docs/Web/API/AudioBuffer)
91 | * @api private
92 | */
93 | module.exports = function (context, data) {
94 | return new Promise(function (resolve, reject) {
95 | var decodedData = base64DecodeToArray(data.split(',')[1]).buffer
96 | context.decodeAudioData(decodedData, function (buffer) {
97 | resolve(buffer)
98 | }, function (e) {
99 | reject('DecodeAudioData error', e)
100 | })
101 | })
102 | }
103 |
104 | },{"./b64decode.js":1}],4:[function(require,module,exports){
105 | 'use strict'
106 |
107 | var loadBank = require('./load-bank')
108 | var oscillatorPlayer = require('./oscillator-player')
109 | var bankPlayer = require('./bank-player')
110 |
111 | /**
112 | * Create a Soundfont object
113 | *
114 | * @param {AudioContext} context - the [audio context](https://developer.mozilla.org/en/docs/Web/API/AudioContext)
115 | * @param {Function} nameToUrl - (Optional) a function that maps the sound font name to the url
116 | * @return {Soundfont} a soundfont object
117 | */
118 | function Soundfont (ctx, nameToUrl) {
119 | if (!(this instanceof Soundfont)) return new Soundfont(ctx)
120 |
121 | this.nameToUrl = nameToUrl || Soundfont.nameToUrl || gleitzUrl
122 | this.ctx = ctx
123 | this.instruments = {}
124 | this.promises = []
125 | }
126 |
127 | Soundfont.prototype.onready = function (callback) {
128 | Promise.all(this.promises).then(callback)
129 | }
130 |
131 | Soundfont.prototype.instrument = function (name, options) {
132 | var ctx = this.ctx
133 | name = name || 'default'
134 | if (name in this.instruments) return this.instruments[name]
135 | var inst = {name: name, play: oscillatorPlayer(ctx, options)}
136 | this.instruments[name] = inst
137 | var promise = loadBank(ctx, this.nameToUrl(name), options).then(function (bank) {
138 | inst.play = bankPlayer(ctx, bank, options)
139 | return inst
140 | })
141 | this.promises.push(promise)
142 | inst.onready = function (cb) {
143 | promise.then(cb)
144 | }
145 | return inst
146 | }
147 |
148 | Soundfont.loadBuffers = function (ctx, name) {
149 | var nameToUrl = Soundfont.nameToUrl || gleitzUrl
150 | return loadBank(ctx, nameToUrl(name))
151 | }
152 |
153 | /*
154 | * Given an instrument name returns a URL to to the Benjamin Gleitzman's
155 | * package of [pre-rendered sound fonts](https://github.com/gleitz/midi-js-soundfonts)
156 | *
157 | * @param {String} name - instrument name
158 | * @returns {String} the Soundfont file url
159 | */
160 | function gleitzUrl (name) {
161 | // return 'https://cdn.rawgit.com/gleitz/midi-js-Soundfonts/master/FluidR3_GM/' + name + '-ogg.js'
162 | return 'https://rawgit.com/gleitz/midi-js-soundfonts/gh-pages/FluidR3_GM/' + name + '-ogg.js'
163 | }
164 |
165 | if (typeof module === 'object' && module.exports) module.exports = Soundfont
166 | if (typeof window !== 'undefined') window.Soundfont = Soundfont
167 |
168 | },{"./bank-player":2,"./load-bank":5,"./oscillator-player":6}],5:[function(require,module,exports){
169 | 'use strict'
170 |
171 | var midi = require('note-midi')
172 | var decodeBuffer = require('./decode-buffer')
173 |
174 | /**
175 | * Load a soundfont bank
176 | *
177 | * @param {AudioContext} ctx - the audio context object
178 | * @param {String} url - the url of the js file
179 | * @param {Function} get - (Optional) given a url return a promise with the contents
180 | * @param {Function} parse - (Optinal) given a js file return JSON object
181 | */
182 | function loadBank (ctx, url, options) {
183 | var notes = options ? options.notes : undefined
184 | return Promise.resolve(url)
185 | .then(loadBank.fetchUrl)
186 | .then(loadBank.parseJS)
187 | .then(function (data) {
188 | return {ctx: ctx, data: data, buffers: {}}
189 | })
190 | .then(function (data) {
191 | return decodeBank(data, notes)
192 | })
193 | .then(function (bank) {
194 | return bank.buffers
195 | })
196 | }
197 |
198 | loadBank.fetchUrl = function (url) {
199 | return new Promise(function (resolve, reject) {
200 | var req = new window.XMLHttpRequest()
201 | req.open('GET', url)
202 |
203 | req.onload = function () {
204 | if (req.status === 200) {
205 | resolve(req.response)
206 | } else {
207 | reject(Error(req.statusText))
208 | }
209 | }
210 | req.onerror = function () {
211 | reject(Error('Network Error'))
212 | }
213 | req.send()
214 | })
215 | }
216 |
217 | /**
218 | * Parse the SoundFont data and return a JSON object
219 | * (SoundFont data are .js files wrapping json data)
220 | *
221 | * @param {String} data - the SoundFont js file content
222 | * @return {JSON} the parsed data as JSON object
223 | */
224 | loadBank.parseJS = function (data) {
225 | var begin = data.indexOf('MIDI.Soundfont.')
226 | begin = data.indexOf('=', begin) + 2
227 | var end = data.lastIndexOf(',')
228 | return JSON.parse(data.slice(begin, end) + '}')
229 | }
230 |
231 | /*
232 | * Decode a bank
233 | * @param {Object} bank - the bank object
234 | * @param {Array} notes - an array of required notes
235 | * @return {Promise} a promise that resolves to the bank with the buffers decoded
236 | * @api private
237 | */
238 |
239 | function decodeBank (bank, notes) {
240 | var promises = Object.keys(bank.data).map(function (note) {
241 | // First check is notes are passed by as param
242 | if (typeof notes !== 'undefined') {
243 | // convert the notes to midi number
244 | var notesMidi = notes.map(midi)
245 | // if the current notes is needed for the instrument.
246 | if (notesMidi.indexOf(midi(note)) !== -1) {
247 | return decodeBuffer(bank.ctx, bank.data[note])
248 | .then(function (buffer) {
249 | bank.buffers[midi(note)] = buffer
250 | })
251 | }
252 | } else {
253 | return decodeBuffer(bank.ctx, bank.data[note])
254 | .then(function (buffer) {
255 | bank.buffers[midi(note)] = buffer
256 | })
257 | }
258 | })
259 |
260 | return Promise.all(promises).then(function () {
261 | return bank
262 | })
263 | }
264 |
265 | module.exports = loadBank
266 |
267 | },{"./decode-buffer":3,"note-midi":8}],6:[function(require,module,exports){
268 | 'use strict'
269 |
270 | var freq = require('midi-freq')(440)
271 | var midi = require('note-midi')
272 |
273 | /**
274 | * Returns a function that plays an oscillator
275 | *
276 | * @param {AudioContext} ac - the audio context
277 | * @param {Hash} defaultOptions - (Optional) a hash of options:
278 | * - vcoType: the oscillator type (default: 'sine')
279 | * - gain: the output gain value (default: 0.4)
280 | * - destination: the player destination (default: ac.destination)
281 | */
282 | module.exports = function (ctx, defaultOptions) {
283 | defaultOptions = defaultOptions || {}
284 | return function (note, time, duration, options) {
285 | var f = freq(midi(note))
286 | if (!f) return
287 |
288 | duration = duration || 0.2
289 |
290 | options = options || {}
291 | var destination = options.destination || defaultOptions.destination || ctx.destination
292 | var vcoType = options.vcoType || defaultOptions.vcoType || 'sine'
293 | var gain = options.gain || defaultOptions.gain || 0.4
294 |
295 | var vco = ctx.createOscillator()
296 | vco.type = vcoType
297 | vco.frequency.value = f
298 |
299 | /* VCA */
300 | var vca = ctx.createGain()
301 | vca.gain.value = gain
302 |
303 | /* Connections */
304 | vco.connect(vca)
305 | vca.connect(destination)
306 |
307 | vco.start(time)
308 | if (duration > 0) vco.stop(time + duration)
309 | return vco
310 | }
311 | }
312 |
313 | },{"midi-freq":7,"note-midi":8}],7:[function(require,module,exports){
314 | /**
315 | * Get the pitch frequency in herzs (with custom concert tuning) from a midi number
316 | *
317 | * This function is currified so it can be partially applied (see examples)
318 | *
319 | * @name midi.freq
320 | * @function
321 | * @param {Float} tuning - the frequency of A4 (null means 440)
322 | * @param {Integer} midi - the midi number
323 | * @return {Float} the frequency of the note
324 | *
325 | * @example
326 | * var freq = require('midi-freq')
327 | * // 69 midi is A4
328 | * freq(null, 69) // => 440
329 | * freq(444, 69) // => 444
330 | *
331 | * @example
332 | * // partially applied
333 | * var freq = require('midi-freq')(440)
334 | * freq(69) // => 440
335 | */
336 | module.exports = function freq (tuning, midi) {
337 | tuning = tuning || 440
338 | if (arguments.length > 1) return freq(tuning)(midi)
339 |
340 | return function (m) {
341 | return m > 0 && m < 128 ? Math.pow(2, (m - 69) / 12) * tuning : null
342 | }
343 | }
344 |
345 | },{}],8:[function(require,module,exports){
346 | 'use strict'
347 |
348 | var parse = require('music-notation/note/parse')
349 |
350 | /**
351 | * Get the midi number of a note
352 | *
353 | * If the argument passed to this function is a valid midi number, it returns it
354 | *
355 | * The note can be an string in scientific notation or
356 | * [array pitch notation](https://github.com/danigb/music.array.notation)
357 | *
358 | * @name midi
359 | * @function
360 | * @param {String|Array|Integer} note - the note in string or array notation.
361 | * If the parameter is a valid midi number it return it as it.
362 | * @return {Integer} the midi number
363 | *
364 | * @example
365 | * var midi = require('note-midi')
366 | * midi('A4') // => 69
367 | * midi('a3') // => 57
368 | * midi([0, 2]) // => 36 (C2 in array notation)
369 | * midi(60) // => 60
370 | * midi('C') // => null (pitch classes don't have midi number)
371 | */
372 | function midi (note) {
373 | if ((typeof note === 'number' || typeof note === 'string') &&
374 | note > 0 && note < 128) return +note
375 | var p = Array.isArray(note) ? note : parse(note)
376 | if (!p || p.length < 2) return null
377 | return p[0] * 7 + p[1] * 12 + 12
378 | }
379 |
380 | if (typeof module === 'object' && module.exports) module.exports = midi
381 | if (typeof window !== 'undefined') window.midi = midi
382 |
383 | },{"music-notation/note/parse":10}],9:[function(require,module,exports){
384 | 'use strict'
385 |
386 | /**
387 | * A simple and fast memoization function
388 | *
389 | * It helps creating functions that convert from string to pitch in array format.
390 | * Basically it does two things:
391 | * - ensure the function only receives strings
392 | * - memoize the result
393 | *
394 | * @name memoize
395 | * @function
396 | * @private
397 | */
398 | module.exports = function (fn) {
399 | var cache = {}
400 | return function (str) {
401 | if (typeof str !== 'string') return null
402 | return (str in cache) ? cache[str] : cache[str] = fn(str)
403 | }
404 | }
405 |
406 | },{}],10:[function(require,module,exports){
407 | 'use strict'
408 |
409 | var memoize = require('../memoize')
410 | var R = require('./regex')
411 | var BASES = { C: [0, 0], D: [2, -1], E: [4, -2], F: [-1, 1], G: [1, 0], A: [3, -1], B: [5, -2] }
412 |
413 | /**
414 | * Get a pitch in [array notation]()
415 | * from a string in [scientific pitch notation](https://en.wikipedia.org/wiki/Scientific_pitch_notation)
416 | *
417 | * The string to parse must be in the form of: `letter[accidentals][octave]`
418 | * The accidentals can be up to four # (sharp) or b (flat) or two x (double sharps)
419 | *
420 | * This function is cached for better performance.
421 | *
422 | * @name note.parse
423 | * @function
424 | * @param {String} str - the string to parse
425 | * @return {Array} the note in array notation or null if not valid note
426 | *
427 | * @example
428 | * var parse = require('music-notation/note/parse')
429 | * parse('C') // => [ 0 ]
430 | * parse('c#') // => [ 8 ]
431 | * parse('c##') // => [ 16 ]
432 | * parse('Cx') // => [ 16 ] (double sharp)
433 | * parse('Cb') // => [ -6 ]
434 | * parse('db') // => [ -4 ]
435 | * parse('G4') // => [ 2, 3, null ]
436 | * parse('c#3') // => [ 8, -1, null ]
437 | */
438 | module.exports = memoize(function (str) {
439 | var m = R.exec(str)
440 | if (!m || m[5]) return null
441 |
442 | var base = BASES[m[1].toUpperCase()]
443 | var alt = m[2].replace(/x/g, '##').length
444 | if (m[2][0] === 'b') alt *= -1
445 | var fifths = base[0] + 7 * alt
446 | if (!m[3]) return [fifths]
447 | var oct = +m[3] + base[1] - 4 * alt
448 | var dur = m[4] ? +(m[4].substring(1)) : null
449 | return [fifths, oct, dur]
450 | })
451 |
452 | },{"../memoize":9,"./regex":11}],11:[function(require,module,exports){
453 | 'use strict'
454 |
455 | /**
456 | * A regex for matching note strings in scientific notation.
457 | *
458 | * The note string should have the form `letter[accidentals][octave][/duration]`
459 | * where:
460 | *
461 | * - letter: (Required) is a letter from A to G either upper or lower case
462 | * - accidentals: (Optional) can be one or more `b` (flats), `#` (sharps) or `x` (double sharps).
463 | * They can NOT be mixed.
464 | * - octave: (Optional) a positive or negative integer
465 | * - duration: (Optional) anything follows a slash `/` is considered to be the duration
466 | * - element: (Optional) additionally anything after the duration is considered to
467 | * be the element name (for example: 'C2 dorian')
468 | *
469 | * @name note.regex
470 | * @example
471 | * var R = require('music-notation/note/regex')
472 | * R.exec('c#4') // => ['c#4', 'c', '#', '4', '', '']
473 | */
474 | module.exports = /^([a-gA-G])(#{1,}|b{1,}|x{1,}|)(-?\d*)(\/\d+|)\s*(.*)\s*$/
475 |
476 | },{}]},{},[4]);
477 |
--------------------------------------------------------------------------------
/src/AbcPerformance.elm:
--------------------------------------------------------------------------------
1 | module AbcPerformance
2 | exposing
3 | ( fromAbc
4 | , fromAbcResult
5 | , melodyFromAbc
6 | , melodyFromAbcResult
7 | )
8 |
9 | {-| conversion of a ABC Tune parse tree to a performance
10 |
11 | # Definition
12 |
13 | # Functions
14 | @docs fromAbc
15 | , fromAbcResult
16 | , melodyFromAbc
17 | , melodyFromAbcResult
18 |
19 | -}
20 |
21 | {- note on implementation
22 |
23 | I originally folded from the right, which is conceptually much simpler and more efficient.
24 | However, I found that, because of the fact that an explicitly marked accidental influences
25 | notes of the same pitch class later on in the same bar, I was forced to fold from the left.
26 | This is more inefficient, as I have to reverse everything at the end.
27 | -}
28 |
29 | import Abc.ParseTree exposing (..)
30 | import Abc exposing (ParseError)
31 | import Music.Notation exposing (..)
32 | import Music.Accidentals exposing (..)
33 | import Melody exposing (..)
34 | import Repeats exposing (..)
35 | import RepeatTypes exposing (..)
36 | import String exposing (fromChar, toUpper)
37 | import Ratio exposing (Rational, over, fromInt, toFloat, add)
38 | import Maybe exposing (withDefault)
39 | import Tuple exposing (first, second)
40 |
41 |
42 | type alias TranslationState =
43 | { modifiedKeySignature : ModifiedKeySignature
44 | , tempo : AbcTempo
45 | , tempoModifier : Float
46 | , nextBarNumber : Int
47 | , thisBar : ABar
48 | , repeatState : RepeatState
49 | }
50 |
51 |
52 |
53 | -- default to 1/4=120
54 |
55 |
56 | defaultTempo : AbcTempo
57 | defaultTempo =
58 | { tempoNoteLength = over 1 4
59 | , bpm = 120
60 | , unitNoteLength = over 1 8
61 | }
62 |
63 |
64 |
65 | -- default to C Major (i.e. no accidental modifiers)
66 |
67 |
68 | defaultKey : ModifiedKeySignature
69 | defaultKey =
70 | ( { pitchClass = C
71 | , accidental = Nothing
72 | , mode = Major
73 | }
74 | , []
75 | )
76 |
77 |
78 | defaultBar : Int -> ABar
79 | defaultBar i =
80 | { number = i
81 | , repeat = Nothing
82 | , iteration = Nothing
83 | , accidentals = Music.Accidentals.empty
84 | , notes = []
85 | }
86 |
87 |
88 | isEmptyBar : ABar -> Bool
89 | isEmptyBar b =
90 | List.length b.notes == 0
91 |
92 |
93 |
94 | {- update the state of the player when we come across a header (either at the start or inline)
95 | which affects the tune tempo or the pitch of a note (i.e. they key)
96 | -}
97 |
98 |
99 | updateState : Header -> ( MelodyLine, TranslationState ) -> ( MelodyLine, TranslationState )
100 | updateState h acc =
101 | let
102 | ( melody, state ) =
103 | acc
104 |
105 | tempo =
106 | state.tempo
107 | in
108 | case h of
109 | UnitNoteLength d ->
110 | ( melody, { state | tempo = { tempo | unitNoteLength = d } } )
111 |
112 | Tempo t ->
113 | let
114 | tnl =
115 | List.foldl Ratio.add (fromInt 0) t.noteLengths
116 | in
117 | ( melody, { state | tempo = { tempo | tempoNoteLength = tnl, bpm = t.bpm } } )
118 |
119 | -- ignore accidental note modifiers in key signatures for the moment - they're little used
120 | Key mk ->
121 | ( melody, { state | modifiedKeySignature = mk } )
122 |
123 | _ ->
124 | acc
125 |
126 |
127 |
128 | {- we need to take note of any accidentals so far in the bar because these may influence
129 | later notes in that bar. Build the KeyAccidental for the accidental of the pitch class in question
130 | and add it to the list
131 | -}
132 |
133 |
134 | addNoteToBarAccidentals : SingleNote -> Accidentals -> Accidentals
135 | addNoteToBarAccidentals n accs =
136 | case ( n.pc, n.accidental ) of
137 | ( Just pitchClass, Just acc ) ->
138 | Music.Accidentals.add pitchClass acc accs
139 |
140 | _ ->
141 | accs
142 |
143 |
144 |
145 | {- ditto for note events (single notes or chords) -}
146 |
147 |
148 | addNoteEventToBarAccidentals : NoteEvent -> Accidentals -> Accidentals
149 | addNoteEventToBarAccidentals ne accs =
150 | case ne of
151 | ANote note _ ->
152 | addNoteToBarAccidentals note accs
153 |
154 | AChord ns ->
155 | List.foldl (addNoteToBarAccidentals) accs ns
156 |
157 |
158 |
159 | {- ditto for lists of note events -}
160 |
161 |
162 | addNoteEventsToBarAccidentals : List NoteEvent -> Accidentals -> Accidentals
163 | addNoteEventsToBarAccidentals nes accs =
164 | List.foldl (addNoteEventToBarAccidentals) accs nes
165 |
166 |
167 |
168 | {- add a note event to the state - add the note to the growing list of notes in the current bar
169 | and if the note has an explicit accidental marker, add it to the list of accidentals
170 | -}
171 |
172 |
173 | addNoteToState : NoteEvent -> TranslationState -> TranslationState
174 | addNoteToState n state =
175 | let
176 | line =
177 | state.thisBar.notes
178 |
179 | thisBar =
180 | state.thisBar
181 |
182 | accidentals =
183 | addNoteEventToBarAccidentals n thisBar.accidentals
184 | in
185 | { state | thisBar = { thisBar | notes = n :: line, accidentals = accidentals } }
186 |
187 |
188 |
189 | {- ditto for a list of notes -}
190 |
191 |
192 | addNotesToState : List NoteEvent -> TranslationState -> TranslationState
193 | addNotesToState ns state =
194 | let
195 | line =
196 | state.thisBar.notes
197 |
198 | thisBar =
199 | state.thisBar
200 |
201 | accidentals =
202 | addNoteEventsToBarAccidentals ns thisBar.accidentals
203 | in
204 | { state | thisBar = { thisBar | notes = List.append ns line, accidentals = accidentals } }
205 |
206 |
207 |
208 | {- build a new bar from the bar number and the next ABC bar that we recognise.
209 | If the last bar was empty, retain its repeat markings, because otherwise we drop this bar
210 | -}
211 |
212 |
213 | buildNewBar : Int -> Bar -> ABar -> ABar
214 | buildNewBar nextBarNumber abcBar lastBar =
215 | let
216 | nextBar =
217 | defaultBar nextBarNumber
218 | in
219 | if (isEmptyBar lastBar) then
220 | case ( lastBar.repeat, abcBar.repeat ) of
221 | ( Just End, Just Begin ) ->
222 | { nextBar | repeat = Just BeginAndEnd, iteration = abcBar.iteration }
223 |
224 | ( Just x, _ ) ->
225 | { nextBar | repeat = Just x, iteration = abcBar.iteration }
226 |
227 | _ ->
228 | { nextBar | repeat = abcBar.repeat, iteration = abcBar.iteration }
229 | else
230 | { nextBar | repeat = abcBar.repeat, iteration = abcBar.iteration }
231 |
232 |
233 |
234 | {- translate a chord (parallel sequence) -}
235 |
236 |
237 | translateChord : TranslationState -> List AbcNote -> Maybe NoteDuration -> List NoteEvent
238 | translateChord state notes maybeChordDur =
239 | let
240 | -- a chord can have a duration over and above that of any individual note in the chord
241 | chordDuration =
242 | case maybeChordDur of
243 | Nothing ->
244 | fromInt 1
245 |
246 | Just chordDur ->
247 | chordDur
248 |
249 | f abc =
250 | let
251 | duration =
252 | (chordalNoteDuration state.tempo abc.duration chordDuration) * state.tempoModifier
253 |
254 | barAccidentals =
255 | state.thisBar.accidentals
256 | in
257 | { time = duration, pitch = toMidiPitch abc state.modifiedKeySignature barAccidentals, pc = Just abc.pitchClass, accidental = abc.accidental }
258 | in
259 | [ AChord (List.map f notes) ]
260 |
261 |
262 |
263 | {- translate a sequence of notes as found in tuplets (sequential) -}
264 |
265 |
266 | translateNoteSequence : List AbcNote -> TranslationState -> TranslationState
267 | translateNoteSequence notes state =
268 | List.foldl translateNote state notes
269 |
270 |
271 |
272 | {- translate a single note and embed it into the state -}
273 |
274 |
275 | translateNote : AbcNote -> TranslationState -> TranslationState
276 | translateNote abc state =
277 | let
278 | duration =
279 | (noteDuration state.tempo abc.duration) * state.tempoModifier
280 |
281 | barAccidentals =
282 | state.thisBar.accidentals
283 |
284 | note =
285 | ANote { time = duration, pitch = toMidiPitch abc state.modifiedKeySignature barAccidentals, pc = Just abc.pitchClass, accidental = abc.accidental } abc.tied
286 | in
287 | addNoteToState note state
288 |
289 |
290 |
291 | {- translate Music items from the parse tree to a melody line - a sequence
292 | of bars containing notes, rests and chords where notes are in a MIDI-friendly format
293 | -}
294 |
295 |
296 | translateMusic : Music -> ( MelodyLine, TranslationState ) -> ( MelodyLine, TranslationState )
297 | translateMusic m acc =
298 | let
299 | ( melodyLine, state ) =
300 | acc
301 | in
302 | case m of
303 | Note abc ->
304 | let
305 | newState =
306 | translateNote abc state
307 | in
308 | ( melodyLine, newState )
309 |
310 | Rest r ->
311 | let
312 | duration =
313 | (noteDuration state.tempo r) * state.tempoModifier
314 |
315 | note =
316 | ANote { time = duration, pitch = 0, pc = Nothing, accidental = Nothing } False
317 |
318 | newState =
319 | addNoteToState note state
320 | in
321 | ( melodyLine, newState )
322 |
323 | Tuplet signature tnotes ->
324 | let
325 | ( p, q, r ) =
326 | signature
327 |
328 | tupletStateStart =
329 | { state | tempoModifier = (Basics.toFloat q / Basics.toFloat p) }
330 |
331 | tupletStateEnd =
332 | translateNoteSequence tnotes tupletStateStart
333 |
334 | -- recover the original tempo in the state but we save any accidentals we've come across
335 | newState =
336 | { tupletStateEnd | tempoModifier = 1 }
337 | in
338 | ( melodyLine, newState )
339 |
340 | BrokenRhythmPair n1 b n2 ->
341 | case b of
342 | LeftArrow i ->
343 | let
344 | leftStateStart =
345 | { state | tempoModifier = (1 - Ratio.toFloat (dotFactor i)) }
346 |
347 | leftStateEnd =
348 | translateNote n1 leftStateStart
349 |
350 | rightStateStart =
351 | { leftStateEnd | tempoModifier = (1 + Ratio.toFloat (dotFactor i)) }
352 |
353 | rightStateEnd =
354 | translateNote n2 rightStateStart
355 |
356 | -- recover the original tempo in the state but save any accidentals we've come across
357 | newState =
358 | { rightStateEnd | tempoModifier = 1 }
359 | in
360 | ( melodyLine, newState )
361 |
362 | RightArrow i ->
363 | let
364 | leftStateStart =
365 | { state | tempoModifier = (1 + Ratio.toFloat (dotFactor i)) }
366 |
367 | leftStateEnd =
368 | translateNote n1 leftStateStart
369 |
370 | rightStateStart =
371 | { leftStateEnd | tempoModifier = (1 - Ratio.toFloat (dotFactor i)) }
372 |
373 | rightStateEnd =
374 | translateNote n2 rightStateStart
375 |
376 | -- recover the original tempo in the state but save any accidentals we've come across
377 | newState =
378 | { rightStateEnd | tempoModifier = 1 }
379 | in
380 | ( melodyLine, newState )
381 |
382 | Chord abcChord ->
383 | let
384 | chord =
385 | translateChord state abcChord.notes (Just abcChord.duration)
386 |
387 | newState =
388 | addNotesToState chord state
389 | in
390 | ( melodyLine, newState )
391 |
392 | Barline b ->
393 | let
394 | -- don't add to the melody the existing bar accumulated by the state if it's empty
395 | newMelody =
396 | if (isEmptyBar state.thisBar) then
397 | melodyLine
398 | else
399 | let
400 | rb =
401 | state.thisBar
402 | in
403 | state.thisBar :: melodyLine
404 |
405 | -- don't increment the bar number if it's an empty bar
406 | nextBarNumber =
407 | if (isEmptyBar state.thisBar) then
408 | state.nextBarNumber
409 | else
410 | state.nextBarNumber + 1
411 |
412 | {-
413 | nextBar = defaultBar nextBarNumber
414 | newBar = { nextBar | repeat = b.repeat, iteration = b.iteration }
415 | -}
416 | -- build a new Bar from the incoming AbcBar, retaining any unused state from the last bar if it was empty (and hence to be dropped)
417 | newBar =
418 | buildNewBar nextBarNumber b state.thisBar
419 |
420 | -- index the last bar if it was not empty
421 | repeatState =
422 | if (isEmptyBar state.thisBar) then
423 | state.repeatState
424 | else
425 | indexBar state.thisBar state.repeatState
426 |
427 | newState =
428 | { state | thisBar = newBar, nextBarNumber = nextBarNumber, repeatState = repeatState }
429 | in
430 | ( newMelody, newState )
431 |
432 | Inline header ->
433 | updateState header acc
434 |
435 | _ ->
436 | acc
437 |
438 |
439 |
440 | -- translate an entire melody line from the tune body (up to an end of line)
441 |
442 |
443 | toMelodyLine : MusicLine -> ( MelodyLine, TranslationState ) -> ( MelodyLine, TranslationState )
444 | toMelodyLine ml state =
445 | let
446 | ( melody, s ) =
447 | List.foldl translateMusic state ml
448 | in
449 | ( melody, s )
450 |
451 |
452 | reverseMelody : MelodyLine -> MelodyLine
453 | reverseMelody =
454 | let
455 | reverseBar b =
456 | { b | notes = List.reverse b.notes }
457 | in
458 | List.map reverseBar
459 | >> List.reverse
460 |
461 |
462 |
463 | {- translate an AbcTune to a more playable melody line
464 | which is a list of notes (or rests) and their durations
465 | -}
466 |
467 |
468 | fromAbc : AbcTune -> ( MelodyLine, Repeats )
469 | fromAbc tune =
470 | let
471 | -- set a default state for case where there are no tune headers
472 | defaultState =
473 | ( []
474 | , { modifiedKeySignature = defaultKey
475 | , tempo = defaultTempo
476 | , tempoModifier = 1.0
477 | , nextBarNumber = 0
478 | , thisBar = defaultBar 0
479 | , repeatState = defaultRepeatState
480 | }
481 | )
482 |
483 | -- update this from the header state if we have any headers
484 | headerState =
485 | List.foldl updateState defaultState (first tune)
486 |
487 | f bp acc =
488 | case bp of
489 | -- process a line from the melody using the current state
490 | Score musicLine ->
491 | let
492 | ( existingLine, state ) =
493 | acc
494 |
495 | ( newLine, newState ) =
496 | toMelodyLine musicLine acc
497 | in
498 | ( newLine, newState )
499 |
500 | -- update the state if we have an inline header
501 | BodyInfo header ->
502 | updateState header acc
503 | in
504 | let
505 | ( music, state ) =
506 | List.foldl f headerState (second tune)
507 |
508 | -- ensure we don't forget the residual closing bar (still kept in the state) which may yet contain music
509 | fullMusic =
510 | reverseMelody (state.thisBar :: music)
511 |
512 | -- finalise the repeat state with the last bar
513 | repeatState =
514 | finalise state.thisBar state.repeatState
515 |
516 | -- _ = log "repeats" (List.reverse repeatState.repeats)
517 | in
518 | ( fullMusic, (List.reverse repeatState.repeats) )
519 |
520 |
521 | melodyFromAbc : Bool -> AbcTune -> ( MelodyLine, Repeats )
522 | melodyFromAbc expandRepeats tune =
523 | let
524 | mr =
525 | fromAbc tune
526 | in
527 | if (expandRepeats) then
528 | ( buildRepeatedMelody mr, [] )
529 | else
530 | mr
531 |
532 |
533 | fromAbcResult : Result ParseError AbcTune -> Result ParseError ( MelodyLine, Repeats )
534 | fromAbcResult r =
535 | Result.map fromAbc r
536 |
537 |
538 | melodyFromAbcResult : Result ParseError AbcTune -> Result ParseError MelodyLine
539 | melodyFromAbcResult r =
540 | -- Result.map (fromAbc >> first) r
541 | Result.map (fromAbc >> buildRepeatedMelody) r
542 |
--------------------------------------------------------------------------------
/src/examples/editor-controller/VexScore/Translate.elm:
--------------------------------------------------------------------------------
1 | module VexScore.Translate exposing (translate, translateText)
2 |
3 | {-|
4 |
5 | @docs translate, translateText
6 | -}
7 |
8 | import Abc.ParseTree exposing (..)
9 | import Abc.Canonical as AbcText
10 | import Abc exposing (parse, parseError)
11 | import Music.Notation exposing (getHeaderMap, dotFactor, normaliseModalKey)
12 | import VexScore.Score exposing (..)
13 | import Dict exposing (Dict, get)
14 | import Result exposing (Result)
15 | import Maybe exposing (withDefault)
16 | import Tuple exposing (first, second)
17 | import Ratio exposing (Rational, over, numerator, denominator, multiply)
18 | import Debug exposing (log)
19 |
20 |
21 | type alias Context =
22 | { modifiedKeySig : ModifiedKeySignature
23 | , meter : Maybe MeterSignature
24 | , unitNoteLength : NoteDuration
25 | , tied :
26 | Bool
27 | -- tie the next note
28 | , decoration :
29 | Maybe String
30 | -- decorate the next note (staccato etc)
31 | , continuation :
32 | Bool
33 | -- end of line continuation
34 | }
35 |
36 |
37 | {-| translate an ABC tune to a VexTab Score representation
38 | -}
39 | translate : AbcTune -> Result String Score
40 | translate t =
41 | let
42 | ctx =
43 | initialContext t
44 |
45 | ksmod =
46 | second ctx.modifiedKeySig
47 | in
48 | let
49 | result =
50 | tuneBody ctx (second t)
51 | in
52 | if (List.isEmpty ksmod) then
53 | case result of
54 | Ok sc ->
55 | Ok (first sc)
56 |
57 | Err e ->
58 | Err e
59 | else
60 | Err "modified key signatures not supported"
61 |
62 |
63 | {-| translate ABC text to a VexTab Score representation
64 | -}
65 | translateText : String -> Result String Score
66 | translateText s =
67 | let
68 | parseResult =
69 | parse s
70 | in
71 | case parseResult of
72 | Ok tune ->
73 | translate tune
74 |
75 | Err e ->
76 | Err ("parse error: " ++ (parseError e))
77 |
78 |
79 |
80 | {- translate the tune body -}
81 |
82 |
83 | tuneBody : Context -> TuneBody -> Result String ( Score, Context )
84 | tuneBody ctx tb =
85 | foldOverResult ctx tb bodyPart
86 |
87 |
88 | bodyPart : Context -> BodyPart -> Result String ( VexBodyPart, Context )
89 | bodyPart ctx bp =
90 | case bp of
91 | Score musicline ->
92 | if emptyLine musicline then
93 | Ok ( VEmptyLine, ctx )
94 | else
95 | vexLine ctx musicline
96 |
97 | BodyInfo h ->
98 | let
99 | newCtx =
100 | header ctx h
101 | in
102 | Ok ( VContextChange, newCtx )
103 |
104 |
105 | vexLine : Context -> MusicLine -> Result String ( VexBodyPart, Context )
106 | vexLine ctx line =
107 | let
108 | mKey =
109 | (first ctx.modifiedKeySig)
110 | |> normaliseMode
111 | |> Just
112 |
113 | vexStave =
114 | if (ctx.continuation) then
115 | Nothing
116 | else
117 | Just { clef = Treble, mKey = mKey, mMeter = ctx.meter }
118 |
119 | {- now we've processed the stave, remove the key signature from the context#
120 | which we don't need to generate any longer unless there's a key changes
121 | and now we've processed any possible end-of-line continuation then
122 | remove it from the comtext
123 | -}
124 | staveCtx =
125 | { ctx | meter = Nothing, continuation = False }
126 |
127 | itemsRes =
128 | musicLine staveCtx line
129 | in
130 | case itemsRes of
131 | Ok ( items, newCtx ) ->
132 | Ok ( VLine { stave = vexStave, items = items }, newCtx )
133 |
134 | Err e ->
135 | Err e
136 |
137 |
138 | musicLine : Context -> MusicLine -> Result String ( List VexItem, Context )
139 | musicLine ctx ml =
140 | foldOverResult ctx ml music
141 |
142 |
143 | music : Context -> Music -> Result String ( VexItem, Context )
144 | music ctx m =
145 | case m of
146 | Barline bar ->
147 | let
148 | newCtx =
149 | case bar.iteration of
150 | Just 1 ->
151 | { ctx | decoration = Just "1" }
152 |
153 | Just 2 ->
154 | { ctx | decoration = Just "2" }
155 |
156 | _ ->
157 | ctx
158 | in
159 | Ok ( VBar bar, newCtx )
160 |
161 | Note abcNote ->
162 | note ctx abcNote
163 | |> Result.map (\( vn, c ) -> ( VNote vn, c ))
164 |
165 | Rest duration ->
166 | let
167 | noteDurResult =
168 | noteDur ctx duration
169 | in
170 | case noteDurResult of
171 | Ok d ->
172 | Ok ( VRest d, ctx )
173 |
174 | Err e ->
175 | Err ("Rest " ++ e ++ ": " ++ ("rest"))
176 |
177 | Tuplet tupletSignature notes ->
178 | let
179 | ( size, _, noteCount ) =
180 | tupletSignature
181 |
182 | notesResult =
183 | noteList ctx notes
184 | in
185 | case notesResult of
186 | Ok ( vnotes, _ ) ->
187 | Ok ( VTuplet size vnotes, ctx )
188 |
189 | Err e ->
190 | Err e
191 |
192 | Chord abcChord ->
193 | let
194 | notesResult =
195 | noteList ctx abcChord.notes
196 |
197 | nDur =
198 | firstNoteDuration abcChord.notes
199 |
200 | overallDur =
201 | multiply (abcChord.duration) nDur
202 |
203 | chordDurResult =
204 | noteDur ctx overallDur
205 | in
206 | case ( notesResult, chordDurResult ) of
207 | ( Ok ( vnotes, _ ), Ok vexd ) ->
208 | Ok ( VChord vexd vnotes, ctx )
209 |
210 | ( Err e, _ ) ->
211 | Err e
212 |
213 | ( _, Err e ) ->
214 | Err ("Chord " ++ e ++ ": " ++ (AbcText.abcChord abcChord))
215 |
216 | BrokenRhythmPair abcNote1 broken abcNote2 ->
217 | let
218 | ( bNote1, bNote2 ) =
219 | makeBroken broken abcNote1 abcNote2
220 |
221 | note1Result =
222 | note ctx bNote1
223 |
224 | -- pass the context fron note1 to note 2
225 | ctx1 =
226 | case note1Result of
227 | Ok ( _, n1ctx ) ->
228 | n1ctx
229 |
230 | _ ->
231 | ctx
232 |
233 | note2Result =
234 | note ctx1 bNote2
235 | in
236 | case ( note1Result, note2Result ) of
237 | ( Ok ( vnote1, _ ), Ok ( vnote2, ctx2 ) ) ->
238 | Ok ( VNotePair vnote1 vnote2, ctx2 )
239 |
240 | ( Err e, _ ) ->
241 | Err ("Note " ++ e ++ ": " ++ (AbcText.abcNote abcNote1))
242 |
243 | ( _, Err e ) ->
244 | Err ("Note " ++ e ++ ": " ++ (AbcText.abcNote abcNote2))
245 |
246 | Decoration decor ->
247 | Ok ( VIgnore, { ctx | decoration = Just decor } )
248 |
249 | -- Inline headers not properly supported yet in VexTab
250 | Inline header ->
251 | inlineHeader ctx header
252 |
253 | Continuation ->
254 | Ok ( VIgnore, { ctx | continuation = True } )
255 |
256 | _ ->
257 | Ok ( VIgnore, ctx )
258 |
259 |
260 | note : Context -> AbcNote -> Result String ( VexNote, Context )
261 | note ctx abcNote =
262 | let
263 | noteDurResult =
264 | noteDur ctx abcNote.duration
265 | in
266 | case noteDurResult of
267 | Ok d ->
268 | let
269 | vexNote =
270 | { pitchClass = abcNote.pitchClass
271 | , accidental = abcNote.accidental
272 | , octave = abcNote.octave - 1
273 | , duration = d
274 | , tied =
275 | ctx.tied
276 | {- in ABC, ties attach to the first note in the pair
277 | but in VexTab, the second
278 | -}
279 | , decoration = ctx.decoration
280 | }
281 |
282 | {- pass the tie to the next note via the context
283 | and remove any note decoration (which would otherwise
284 | apply to the next note...
285 | -}
286 | newCtx =
287 | { ctx | tied = abcNote.tied, decoration = Nothing }
288 | in
289 | -- Ok ( VNote vexNote, ctx )
290 | Ok ( vexNote, newCtx )
291 |
292 | Err e ->
293 | Err ("Note " ++ e ++ ": " ++ (AbcText.abcNote abcNote))
294 |
295 |
296 |
297 | {- translate a note or rest duration, wrapping in a Result which indicates an
298 | unsupported duration. This rounds values of 'short enough' note durations
299 | to the nearest supported value
300 | -}
301 |
302 |
303 | noteDur : Context -> NoteDuration -> Result String VexDuration
304 | noteDur ctx d =
305 | let
306 | numer =
307 | numerator ctx.unitNoteLength
308 | * (numerator d)
309 | * 128
310 |
311 | denom =
312 | denominator ctx.unitNoteLength
313 | * (denominator d)
314 |
315 | -- replace this with precise arithmetic?
316 | durn =
317 | numer // denom
318 | in
319 | case durn of
320 | 128 ->
321 | Ok Whole
322 |
323 | 96 ->
324 | Ok HalfDotted
325 |
326 | 64 ->
327 | Ok Half
328 |
329 | 48 ->
330 | Ok QuarterDotted
331 |
332 | 32 ->
333 | Ok Quarter
334 |
335 | 24 ->
336 | Ok EighthDotted
337 |
338 | 16 ->
339 | Ok Eighth
340 |
341 | 12 ->
342 | Ok SixteenthDotted
343 |
344 | 8 ->
345 | Ok Sixteenth
346 |
347 | 6 ->
348 | Ok ThirtySecondDotted
349 |
350 | 4 ->
351 | Ok ThirtySecond
352 |
353 | 3 ->
354 | Ok SixtyFourthDotted
355 |
356 | 2 ->
357 | Ok SixtyFourth
358 |
359 | _ ->
360 | Err "too long or too dotted"
361 |
362 |
363 |
364 | {- apply the specified broken rhythm to each note in the note pair (presented individually)
365 | and return the broken note pair
366 | -}
367 |
368 |
369 | makeBroken : Broken -> AbcNote -> AbcNote -> ( AbcNote, AbcNote )
370 | makeBroken broken n1 n2 =
371 | let
372 | down i =
373 | Ratio.add (over 1 1) (Ratio.negate (dotFactor i))
374 |
375 | up i =
376 | Ratio.add (over 1 1) (dotFactor i)
377 | in
378 | case broken of
379 | LeftArrow i ->
380 | let
381 | left =
382 | { n1 | duration = multiply n1.duration (down i) }
383 |
384 | right =
385 | { n2 | duration = multiply n2.duration (up i) }
386 | in
387 | ( left, right )
388 |
389 | RightArrow i ->
390 | let
391 | left =
392 | { n1 | duration = multiply n1.duration (up i) }
393 |
394 | right =
395 | { n2 | duration = multiply n2.duration (down i) }
396 | in
397 | ( left, right )
398 |
399 |
400 | noteList : Context -> List AbcNote -> Result String ( List VexNote, Context )
401 | noteList ctx notes =
402 | foldOverResult ctx notes note
403 |
404 |
405 |
406 | {- cater for a new header inside the tune body after a line has completed
407 | we need to cater for changes in key signature, meter or unit note length
408 | which all alter the translation context. All other headers may be ignored
409 |
410 | These are headers within the tune body occupying a line of their own
411 | -}
412 |
413 |
414 | header : Context -> Header -> Context
415 | header ctx h =
416 | case h of
417 | Key mks ->
418 | { ctx | modifiedKeySig = mks }
419 |
420 | UnitNoteLength dur ->
421 | { ctx | unitNoteLength = dur }
422 |
423 | Meter meter ->
424 | { ctx | meter = meter }
425 |
426 | _ ->
427 | ctx
428 |
429 |
430 |
431 | {- Cater for inline headers (embedded within the growing stave)
432 | These are not properly supported yet by VexTab and so
433 | changes in key or time signature raise errors
434 | -}
435 |
436 |
437 | inlineHeader : Context -> Header -> Result String ( VexItem, Context )
438 | inlineHeader ctx h =
439 | case h of
440 | Key mks ->
441 | Err "inline key signature changes not supported"
442 |
443 | Meter meter ->
444 | Err "inline time signature changes not supported"
445 |
446 | UnitNoteLength dur ->
447 | Ok ( VIgnore, { ctx | unitNoteLength = dur } )
448 |
449 | _ ->
450 | Ok ( VIgnore, ctx )
451 |
452 |
453 |
454 | {- get the key signature defaulted to C Major -}
455 |
456 |
457 | getKeySig : Maybe Header -> ModifiedKeySignature
458 | getKeySig mkh =
459 | let
460 | cMajor : ModifiedKeySignature
461 | cMajor =
462 | ( { pitchClass = C, accidental = Nothing, mode = Major }, [] )
463 | in
464 | case mkh of
465 | Just kh ->
466 | case kh of
467 | Key mks ->
468 | mks
469 |
470 | _ ->
471 | cMajor
472 |
473 | _ ->
474 | cMajor
475 |
476 |
477 |
478 | {- get the meter defaulted to 4 4 -}
479 |
480 |
481 | getMeter : Maybe Header -> Maybe MeterSignature
482 | getMeter mmh =
483 | case mmh of
484 | Just mh ->
485 | case mh of
486 | Meter (Just ms) ->
487 | Just ms
488 |
489 | _ ->
490 | Just ( 4, 4 )
491 |
492 | _ ->
493 | Just ( 4, 4 )
494 |
495 |
496 |
497 | {- get the unit note length defaulted to 1/8 -}
498 |
499 |
500 | unitNoteLen : Maybe Header -> NoteDuration
501 | unitNoteLen muh =
502 | case muh of
503 | Just uh ->
504 | case uh of
505 | UnitNoteLength l ->
506 | l
507 |
508 | _ ->
509 | over 1 8
510 |
511 | _ ->
512 | over 1 8
513 |
514 |
515 |
516 | {- get the initial translation context from the tune headers -}
517 |
518 |
519 | initialContext : AbcTune -> Context
520 | initialContext t =
521 | let
522 | headerMap =
523 | getHeaderMap t
524 |
525 | keySig =
526 | Dict.get 'K' headerMap
527 | |> getKeySig
528 |
529 | meter =
530 | Dict.get 'M' headerMap
531 | |> getMeter
532 |
533 | unl =
534 | Dict.get 'L' headerMap
535 | |> unitNoteLen
536 | in
537 | { modifiedKeySig = keySig
538 | , meter = meter
539 | , unitNoteLength = unl
540 | , tied = False
541 | , decoration = Nothing
542 | , continuation = False
543 | }
544 |
545 |
546 |
547 | {- get the duration of the first note in a sequence -}
548 |
549 |
550 | firstNoteDuration : List AbcNote -> NoteDuration
551 | firstNoteDuration ns =
552 | List.map (\a -> a.duration) ns
553 | |> List.head
554 | |> withDefault (over 1 1)
555 |
556 |
557 |
558 | -- Helper Functions
559 | {- This is a generic function that operates where we start with a list in ABC and need to end up with the
560 | equivalent list in VexTab Score. It performs a left fold over the list using the next function in the tree
561 | that we need to use in the fold. It threads the context through the fold. Because it's a left fold
562 | then we need to reverse the list in the result when we finish
563 |
564 | -}
565 |
566 |
567 | foldOverResult : Context -> List a -> (Context -> a -> Result String ( b, Context )) -> Result String ( List b, Context )
568 | foldOverResult ctx aline fmus =
569 | let
570 | -- append via the pair through the result (we really need a monad here.....)
571 | apnd : Result String ( b, Context ) -> Result String ( List b, Context ) -> Result String ( List b, Context )
572 | apnd rvic rvics =
573 | case ( rvic, rvics ) of
574 | ( Ok vic, Ok vics ) ->
575 | let
576 | newvis =
577 | first vic :: first vics
578 | in
579 | Ok ( newvis, second vic )
580 |
581 | ( _, Err acc ) ->
582 | Err acc
583 |
584 | ( Err next, _ ) ->
585 | Err next
586 |
587 | -- thread the context through the fold
588 | f mus acc =
589 | let
590 | applicableCtx =
591 | case acc of
592 | Ok ( _, accCtx ) ->
593 | accCtx
594 |
595 | _ ->
596 | ctx
597 | in
598 | -- fmus is the next function in the tree to apply in the fold
599 | apnd (fmus applicableCtx mus) acc
600 | in
601 | let
602 | result =
603 | List.foldl f (Ok ( [], ctx )) aline
604 | in
605 | -- we have done a left fold so we need to reverse the result
606 | case result of
607 | Ok ( vis, ctx ) ->
608 | Ok ( List.reverse vis, ctx )
609 |
610 | _ ->
611 | result
612 |
613 |
614 | normaliseMode : KeySignature -> KeySignature
615 | normaliseMode ks =
616 | case ks.mode of
617 | Ionian ->
618 | ks
619 |
620 | Major ->
621 | ks
622 |
623 | Minor ->
624 | ks
625 |
626 | _ ->
627 | normaliseModalKey ks
628 |
629 |
630 |
631 | {- check if a line of music is effectively empty -}
632 |
633 |
634 | emptyLine : MusicLine -> Bool
635 | emptyLine musicLine =
636 | let
637 | f music =
638 | case music of
639 | Spacer _ ->
640 | True
641 |
642 | Ignore ->
643 | True
644 |
645 | Continuation ->
646 | True
647 |
648 | _ ->
649 | False
650 | in
651 | List.all f musicLine
652 |
--------------------------------------------------------------------------------
/src/examples/tutorial/Lessons.elm:
--------------------------------------------------------------------------------
1 | module Lessons exposing
2 | ( Lesson
3 | , lessons
4 | )
5 |
6 | {-| Lessons in learning ABC
7 |
8 | # Definition
9 |
10 | # Data Types
11 | @docs Lesson
12 |
13 |
14 | # Functions
15 | @docs lessons
16 |
17 | -}
18 |
19 | import Array exposing (Array, fromList)
20 |
21 | type alias Lesson =
22 | { id : String
23 | , title : String
24 | , instruction : String
25 | , example : String
26 | , hint : String
27 | }
28 |
29 | instNotes =
30 | "Use the upper-case characters A-G for the notes of the octave starting from middle C and lower-case a-g for the octave above that." ++
31 | " In this example, each note has the same length - let's call it the 'unit length' for the moment." ++
32 | " You can place notes next to each other or separate them with spaces - it won't make much difference to" ++
33 | " the sound. However, in a score, if they're adjacent then notes with tails will have them joined together."
34 |
35 | xmplNotes =
36 | "A B c def"
37 |
38 | hintNotes =
39 | "Try altering some of the notes."
40 |
41 | instLongNotesAndBars =
42 | "You can make a note last longer by putting a number after the note name." ++
43 | " So, for example, c2 represents the note C in the octave immediately above the one that starts with middle C, having a duration of two units." ++
44 | " Use a vertical bar to introduce a bar line."
45 |
46 | xmplLongNotesAndBars =
47 | "| c2 cG c2 e2 | g4"
48 |
49 | hintLongNotesAndBars =
50 | "Try experimenting with different note lengths."
51 |
52 | instRests =
53 | "Use the character z to represent a rest. In exactly the same manner as for notes, you can set the length of a rest by adding a number after it." ++
54 | " For example z3 will make the rest last for three units." ++
55 | " You can spread out the tune into multiple lines if you like by hitting carriage return."
56 |
57 | xmplRests =
58 | "| c2 c2 z cBA |\r\n" ++
59 | "| E2 A2 z3 A |"
60 |
61 | hintRests =
62 | "Try adding another bar which contains both notes and rests."
63 |
64 | instOctaves =
65 | "You can reach the octave below middle C by adding a comma immediately after the note name." ++
66 | " Each time you add a comma, you drop a further octave. " ++
67 | " Similarly higher octaves can be reached using apostrophes." ++
68 | " If you want a longer note, you must put the duration after the comma or apostrophe."
69 |
70 | xmplOctaves =
71 | "| C,, G,, C, G, | C G c g | c' g' c''4 |"
72 |
73 | hintOctaves =
74 | "Experiment by adding some more high or low notes."
75 |
76 | instFractionalNotes =
77 | "You can shorten a note by placing a fraction after the note. This could be, for example," ++
78 | " 1/2 or 1/3. A shorthand for 1/2 is simply / and a shorthand for 1/3 is simply /3." ++
79 | " You can also have longer notes if you use a fraction greater than 1. Rests are treated the same way." ++
80 | " If you make notes too short, they may not be heard."
81 |
82 | xmplFractionalNotes =
83 | "| C3/2G1/2 E3/2G1/2 C3/2G/ E3/2G/ |"
84 |
85 | hintFractionalNotes =
86 | "Try experimenting with a succession of notes of different pitch and with different fractional values."
87 |
88 | instHornpipe =
89 | "The last example was in a hornpipe-like rhythm. Because this is so common, there is a shorthand for it" ++
90 | " where you separate each pair of notes with the > character. This extends the first note by half its length" ++
91 | " and reduces the second by the same amount."
92 |
93 | xmplHornpipe =
94 | "| C>GE>G C>GE>G |\r\n| c>de>d c>BA>G |"
95 |
96 | hintHornpipe =
97 | "If you know it, can you finish off the 'A' part of the tune?"
98 |
99 | instStrathspey =
100 | "Conversely, you can shorten the first note of a pair and lengthen the second by means of the < character." ++
101 | " This rhythm is found in strathspeys."
102 |
103 | xmplStrathspey =
104 | "| G | c2 e>c Gg |\r\n| c'2 b>c' ae |"
105 |
106 | instChords =
107 | "You can play a chord by placing a group of notes, adjacent to each other, inside square brackets - for example [acE]." ++
108 | " To set the duration of the chord, you can either set the length of each note individually or else for the entire chord." ++
109 | " If you do both, the durations are multiplied together"
110 |
111 | xmplChords =
112 | "| [acE]3 B A2G2 | [eBGE]4 |"
113 |
114 | hintChords =
115 | "Try adding another phrase that ends in a chord."
116 |
117 | instKeySig =
118 | "ABC lets you add information that determines how the tune is to be played." ++
119 | " So far, we have only used the white notes on the piano - i.e. the tune snippets have tended to be in the keys of either" ++
120 | " C Major or A Minor. If we want tunes in a different key, we can add what's called a 'K header' where K represents the key signature. " ++
121 | " A header is usually placed on a line of its own before the melody starts." ++
122 | " In the key of A, every C, F and G in the melody is implicitly sharpened - this will give a 'major' feel to the chord example."
123 |
124 | xmplKeySig =
125 | "K: AMajor \r\n| [acE]3 B A2G2 | [eBGE]4 |"
126 |
127 | instFlatKeySig =
128 | "If your key is a major key, you can, if you want, leave out the word 'Major'. If it is a flat key, you use 'b' and if a sharp key, '#'. " ++
129 | " You can also choose to shorten the mode name to just three letters - i.e. BbMaj, BbMajor and Bb are equivalent to each other."
130 |
131 |
132 | xmplFlatKeySig =
133 | "K: Bb\r\n| BfdB AecA | FdBF D4 |"
134 |
135 | instNaturals =
136 | "If your key means that certain notes are sharpened or flattened, but you need to play the 'natural' " ++
137 | " (unsharpened or unflattened) note, then you can override the key by using an equals symbol immediately before the note." ++
138 | " Remember that, as in a score, you only need to mark as natural the first occurrence of the note in any given bar." ++
139 | " For example, this reintroduces the minor feel although the key is still a major one. Each C in the bar is natural."
140 |
141 | xmplNaturals =
142 | "K: AMajor \r\n| A2 B=c dcBc [CEA] |"
143 |
144 | instAccidentals =
145 | "Similarly, you can sharpen a note by placing a caret symbol (^) immediately before it and flatten it using an underscore" ++
146 | " symbol (_). If you need a double sharp or double flat, then just double the appropriate symbol." ++
147 | " This example brings back the major feel although the key is now A Minor. Each C is sharpened."
148 |
149 | xmplAccidentals =
150 | "K: AMinor \r\n| A2 B^c dcBc [CEA] |"
151 |
152 | instUnitNote =
153 | "You may have noticed when we first introduced notes that we talked about their duration in 'units'. But how long is a unit?" ++
154 | " So far, we have used, by default, a convention that it represents an eighth note (a quaver)." ++
155 | " We can change the unit to be a sixteenth note (a semiquaver) if we use the L (unit note length) header." ++
156 | " This will have the effect of doubling the speed."
157 |
158 | xmplUnitNote =
159 | "L: 1/16 \r\nA B c def"
160 |
161 | instTempo =
162 | "An accurate tempo is defined by means of the Q (tempo) header. Up till now, we've used a default where we have 120 quarter notes per minute" ++
163 | " i.e 1/4=120. We can, for example, slow down our tune firstly by reverting to a unit note length of 1/8 and secondly by explicitly reducing the " ++
164 | " tempo with the Q header."
165 |
166 | xmplTempo =
167 | "L: 1/8 \r\nQ: 1/4=60\r\nA B c def"
168 |
169 | instMeter =
170 | "The meter is defined with the M header. For example, a waltz would normally have the meter 3/4 and a march 4/4." ++
171 | " 3/4 means that each complete bar should have a total duration equal to that of three quarter notes." ++
172 | " The presence of a meter actually makes little difference to how the tune sounds, but will show up in a score." ++
173 | " But it is important to make sure that the duration of each complete bar agrees with the meter you designate." ++
174 | " This example is a slip-jig in 9/8."
175 |
176 | xmplMeter =
177 | "Q:3/8=120\r\nM:9/8\r\nK:D\r\n" ++
178 | "ABA A2G F2G | ABA AGF G2E |\r\n" ++
179 | "ABA A2G F2G | A2d d2c d3 |\r\n" ++
180 | "A2g f2d e2c | A2B =c2B c2B |\r\n" ++
181 | "A2g f2d e2^c | A2d d2c d3 |\r\n" ++
182 | "A2g f2d e2c | A2B =c2B c2^c |\r\n" ++
183 | "d2A A2G F2G | A2d d2c d3 |\r\n"
184 |
185 | instTie =
186 | "A tie joins together two notes of the same pitch. It is indicated by placing a hyphen directly after the first note of the pair." ++
187 | " The second note may follow immediately, but it can be separated by spaces or even a bar line. The effect is to play one long note" ++
188 | " with the combined duration of the pair. If the notes are of different pitches, the tie will simply be ignored."
189 |
190 | xmplTie =
191 | "| G2 | c2c2 A2Ac | B2B2- B2AB |"
192 |
193 | instTriplet =
194 | "A triplet is usually used if you want to play three notes in the time normally taken by two." ++
195 | " You introduce three notes of the same length placed together with the symbol (3" ++
196 | " This is extremely common in Swedish polskas - for example the start of the Grind Hans Jässpôdspolska."
197 |
198 | xmplTriplet =
199 | "K:Dmaj\r\n| A2 | d2 e>f (3g2f2d2 | B4 |"
200 |
201 | instComplexTriplet =
202 | "If your triplet has notes of different lengths, you have to use the complex triplet notation." ++
203 | " For example (3:2:4d2d2Bd means play the rhythm of three notes in the time of two over the following group" ++
204 | " of four notes."
205 |
206 | xmplComplexTriplet =
207 | "K:Gmaj\r\n| D2 G>A B>c| (3:2:4d2d2Bd g2|"
208 |
209 | instQuadruplet =
210 | "Quadruplets are used if you want to play four notes in the time usually taken by three." ++
211 | " In a similar fashion to triplets, introduce four notes of the same length placed together" ++
212 | " with the symbol (4. This example contains triplets, a tie and a quadruplet."
213 |
214 | xmplQuadruplet =
215 | "K:Amaj\r\n| (3efg a2 a>b | (3agf e2-e>e |\r\n| (4f2d2e2c2 | d>f (3f2e2c2 |"
216 |
217 | instRepeat =
218 | "You can indicate that a section should be repeated by sandwiching it between bars which use the colon as a repeat marker - |: and :|" ++
219 | " The initial repeat marker at the first bar is optional."
220 |
221 | xmplRepeat =
222 | "| C2 D2 E2 C2 :|: E2 F2 G4 :|\r\n|: GAGF E2 C2 :|: C2 G,2 C4 :|"
223 |
224 | instRepeatVariants =
225 | "In some tunes, the two repeats may differ in their endings. You can indicate that using |1 and |2 for the two variant endings"
226 |
227 | xmplRepeatVariants =
228 | "L: 1/16\r\nK:Dmaj\r\n|: A4 a4 a2f2 | gfga b3a g2f2 |\r\n| e3f g2b2 a2g2 | f3e d2c2 d2B2 |\r\n" ++
229 | "|1 B2A^G A8 :|2 B2AG F2EF A2A,2 | A,2D2 D8 |"
230 |
231 | instTitle =
232 | "Very many of our previous examples have had no headers - only the melody line. But, in fact a legitimate ABC tune always" ++
233 | " requires some headers. The first is largely irrelevant - a reference number denoted by X. Any number will do" ++
234 | " in most cases. The second header must be the tune title - T. You should also include the L (note length) and M (meter) headers" ++
235 | " introduced earlier. Finally, the K (key) header should always be the last one."
236 |
237 | xmplTitle =
238 | "X:1\r\nT:Camptown Races\r\nM:4/4\r\nL:1/8\r\nK:D\r\n|AAFA|BAF2|FE2z|FE2z|AAFA|BAF2|E2FE|D2-D2|\r\n|D>DFA|d4|B>BdB|A3F|\r\nAA F/2F/2 A/2A/2|BAF2|EF/2-G/2FE|D4 |\r\n"
239 |
240 | instRhythm =
241 | "You can use the R (rhythm) header to indicate the type of tune (jig, reel, polska etc.). In most ABC collections, this field is optional." ++
242 | " However, if you want to save your tune to tradtunedb, it requires a rhythm header to be present so that you can search" ++
243 | " easily for tunes of the same type"
244 |
245 | xmplRhythm =
246 | "X: 1\r\nT: Kapten Lindholms Engelska\r\nR: engelska\r\nM: 4/4\r\nL: 1/8\r\nK:Amaj\r\n" ++
247 | "|: ed | cAce dcdf | ecAF E2 ed |\r\n| cABc defg | aece agfe | cAce dcdf |\r\n| ecAF E2 ed | cABc defg | a2 ag a2 :|\r\n" ++
248 | "|: e2 | aac'e aac'e | bbd'e bbd'e | aac'e aac'e |\r\n| efed cB A2| fdfa ecea | fdfa ecea |\r\n| fdfa gegb | baag a2 :|\r\n"
249 |
250 | instInformation =
251 | "There are various other headers that you can use to add information about the tune as free text. The most important are these: " ++
252 | " C (composer), O (geographical origin), S (source - where or how the tune was collected) and Z (the tune transcriber)."
253 |
254 | xmplInformation =
255 | "X: 1\r\nT: Gubbdansen\r\nS: from 12 låtar för 2 eller 3 fioler med Gärdebylåten i Hjort Anders Olssons originalsättning\r\n" ++
256 | "Z: John Batchellor\r\nR: polska\r\nM: 3/4\r\nL: 1/16\r\nK:Dmin\r\n" ++
257 | "|: f3g f4 a4 | a2ba g2ag f2e2 | d3e f2g2 a2f2 | f3e e2^c2 A4 :|\r\n" ++
258 | "|: ^c2c2 d2d2 e2e2 | f2f2 gfed e4 | ^c2c2 d2d2 e2e2 | f2f2 gfed e4 |\r\n" ++
259 | "a4 b2a2 g2f2 | f2ef g2f2 e2d2 | fed^c c4 d4 :|\r\n"
260 |
261 | instChangeKey =
262 | "If a tune changes key, you can indicate this simply by placing the K (key) header inside the score at the point where the key changes." ++
263 | " In this example, the first part of the tune is in B Minor and the second part in F# Minor." ++
264 | " Various other headers can be used within the score in this way - in particular, the M (meter) and L (unit note length) headers."
265 |
266 | xmplChangeKey =
267 | "T:Polska från Småland \r\nM:3/4\r\nL:1/16\r\nR:polska\r\nK:Bmin\r\n" ++
268 | "|: B4 A4 B4 | d2f2 e2dc c2d2 | B2B2 A2A2 B2B2 |d2f2 e2dc d4 |\r\n" ++
269 | "F2GA B2AB c2Bc |d2cd edcB A2F2 | F2GA B2AB c2Bc |d2cd edcB A2F2 |\r\n" ++
270 | "F2GA B2c2 d3B | B2A2 B8 :|\r\n" ++
271 | "K:F#Min\r\n" ++
272 | "|: f4 e4 f4 |g2a2 b2ag g2a2 |f2f2 e2e2 f2f2 |g2a2 b2ag a4 |\r\n" ++
273 | "c2de f2ef g2fg |a2ga bagf e2c2 | c2de f2ef g2fg |a2ga bagf e2c2 |\r\n" ++
274 | "c2de f2g2 a3f |f2e2 f8 :|\r\n"
275 |
276 | instChangeKeyTransient =
277 | "You can also mark a transient key change by placing the K (key) header in the body of the tune score, but enclosed within square brackets."
278 |
279 | xmplChangeKeyTransient =
280 | "X:1\r\nQ:1/4=80\r\nM:2/4\r\nK:C\r\n| C,E,G,C |[K:A] A,CEA |\r\n|[K:B] B,DFB |[K:C] CEGc |\r\n"
281 |
282 | instMixolydian =
283 | "If you come across a modal tune, rather than marking its key signature as straightforward major or minor," ++
284 | " you can instead use the mode name. For example, the following tune is in D Mixolydian. But remember, the classical" ++
285 | " modes all use the standard diatonic scale - they just start at different places along the scale. So for this tune " ++
286 | " the printed score would look, to all intents and purposes, identical to that for E Minor. Feel free to use either signature."
287 |
288 | xmplMixolydian =
289 | "X: 1\r\nT: The Yellow Wattle\r\nR: jig\r\nM: 6/8\r\nL: 1/8\r\nK: Dmix\r\n" ++
290 | "|:dcA AGE|ABA ABc|dcA ABc|dcA AGE|\r\n" ++
291 | "dcA AGE|ABA AGE|EDD cde|dcA GED:|\r\n" ++
292 | "|:DED c3|ded c3|DED cde|dcA GED|\r\n" ++
293 | "DED c3|ded d2c|ABA ABc|dcA GED:|\r\n"
294 |
295 | instKlezmer =
296 | "Klezmer tends to use modes that are not diatonic scales - some intervals are more than two semitones." ++
297 | " Suppose you have a tune that would be in a 'standard' mode except that one note in the scale is sharpened." ++
298 | " You can either use the name of the mode in the key signature and then explicitly sharpen this note each time it occurs in the score" ++
299 | " or you can modify the key signature itself, adding as many (sharpened or flattened) accidentals as are needed." ++
300 | " The following tune is in D Dorian, but with every G sharpened."
301 |
302 | xmplKlezmer =
303 | "X: 1\r\nT: Der Badchen Freylach \r\nM: 2/4\r\nL: 1/16\r\nK: Ddor^G\r\n" ++
304 | "|: DA,DF GAGF | A2A2 FED2 | DA,DF GAGF | A4 A4- |\r\n" ++
305 | "| AA,DF GAFD | A2A2 FED2 | EFGF EDEF | D8 :|\r\n" ++
306 | "|: ABcB dcBA | GABc A4 | dcBA GABc | A4 A4 |\r\n" ++
307 | "| ABcB dcBA | GABc A4 |1 ABcB AB (3FED | EFD2- D4 |\r\n" ++
308 | ":|2 GABA GAFE | D8 :||\r\n"
309 |
310 | instBalkan =
311 | "Balkan music also tends to have unusual modes and time signatures. This tune is in A Minor with a sharpened G; the meter is 11/16." ++
312 | " The '~' symbol indicates a particular decoration - a roll - but this player does not attempt it."
313 |
314 | xmplBalkan =
315 | "X: 1\r\nT: Acano mlada nevesto\r\nO: Macedonia\r\nS: R.B.Iverson\r\nM: 11/16\r\nL: 1/16\r\nK: AMin^G\r\n" ++
316 | "|: E3 e2e2 d2c2 | ~B2A ~A2GA B2-B2 :: A2A dccB BAAG |\r\n" ++
317 | "| G2F ~F2EF ~G2FE | A2A dccB BAAG | ~G2F ~FGFE E2E2 :|\r\n" ++
318 | "|: EFD EFGA BcBA | Bcd cBcA BEeE |\r\n" ++
319 | "| EFD EFGA BcBA | GAB AGFE E2-E2 :|\r\n"
320 |
321 |
322 |
323 | lessons : Array Lesson
324 | lessons =
325 | [
326 | { id = "notes", title = "the notes", instruction = instNotes, example = xmplNotes, hint = hintNotes }
327 | , { id = "longnotesandbars", title = "long notes and bars", instruction = instLongNotesAndBars, example = xmplLongNotesAndBars, hint = hintLongNotesAndBars }
328 | , { id = "rests", title = "rests", instruction = instRests, example = xmplRests, hint = hintRests }
329 | , { id = "octaves", title = "octaves", instruction = instOctaves, example = xmplOctaves, hint = hintOctaves }
330 | , { id = "fractionalnotes", title = "fractional notes", instruction = instFractionalNotes, example = xmplFractionalNotes, hint = hintFractionalNotes }
331 | , { id = "hornpipes", title = "hornpipes", instruction = instHornpipe, example = xmplHornpipe, hint = hintHornpipe }
332 | , { id = "strathspeys", title = "strathspeys", instruction = instStrathspey, example = xmplStrathspey, hint = "" }
333 | , { id = "chords", title = "chords", instruction = instChords, example = xmplChords, hint = hintChords }
334 | , { id = "keysignature", title = "key signature", instruction = instKeySig, example = xmplKeySig, hint = "" }
335 | , { id = "sharpandflatkeys", title = "sharp and flat key signatures", instruction = instFlatKeySig, example = xmplFlatKeySig, hint = "" }
336 | , { id = "naturals", title = "naturals", instruction = instNaturals, example = xmplNaturals, hint = "" }
337 | , { id = "accidentals", title = "sharps and flats", instruction = instAccidentals, example = xmplAccidentals, hint = "" }
338 | , { id = "unitnote", title = "how long is a unit note?", instruction = instUnitNote, example = xmplUnitNote, hint = "" }
339 | , { id = "tempo", title = "tempo", instruction = instTempo, example = xmplTempo, hint = "" }
340 | , { id = "meter", title = "meter", instruction = instMeter, example = xmplMeter, hint = "" }
341 | , { id = "tie", title = "tie", instruction = instTie, example = xmplTie, hint = "" }
342 | , { id = "triplet", title = "triplet", instruction = instTriplet, example = xmplTriplet, hint = "" }
343 | , { id = "complextriplet", title = "triplet with differing note lengths", instruction = instComplexTriplet, example = xmplComplexTriplet, hint = "" }
344 | , { id = "quadruplet", title = "quadruplet", instruction = instQuadruplet, example = xmplQuadruplet, hint = "" }
345 | , { id = "repeats", title = "repeats", instruction = instRepeat, example = xmplRepeat, hint = "" }
346 | , { id = "repeatvariants", title = "repeats with variant endings", instruction = instRepeatVariants, example = xmplRepeatVariants, hint = "" }
347 | , { id = "title", title = "tune title", instruction = instTitle, example = xmplTitle, hint = "" }
348 | , { id = "rhythm", title = "rhythm", instruction = instRhythm, example = xmplRhythm, hint = "" }
349 | , { id = "information", title = "information headers", instruction = instInformation, example = xmplInformation, hint = "" }
350 | , { id = "keychanges", title = "key changes", instruction = instChangeKey, example = xmplChangeKey, hint = "" }
351 | , { id = "keychangetransient", title = "transient key changes", instruction = instChangeKeyTransient, example = xmplChangeKeyTransient, hint = "" }
352 | , { id = "modes", title = "other modes", instruction = instMixolydian, example = xmplMixolydian, hint = "" }
353 | , { id = "klezmer", title = "klezmer", instruction = instKlezmer, example = xmplKlezmer, hint = "" }
354 | , { id = "balkan", title = "Balkan", instruction = instBalkan, example = xmplBalkan, hint = "" }
355 | ] |> Array.fromList
356 |
357 |
358 |
359 |
--------------------------------------------------------------------------------
/src/examples/editor/AbcEditor.elm:
--------------------------------------------------------------------------------
1 | module AbcEditor exposing (..)
2 |
3 | import Html exposing (..)
4 | import Html.Attributes exposing (..)
5 | import Html.Events exposing (on, targetValue, onClick, onInput)
6 | import Task exposing (Task, andThen, succeed, sequence, onError)
7 | import Process exposing (sleep)
8 | import List exposing (reverse, isEmpty)
9 | import Maybe exposing (Maybe, withDefault)
10 | import String exposing (toInt, slice)
11 | import Result exposing (Result, mapError)
12 | import Array exposing (Array, get)
13 | import Tuple exposing (first, second)
14 | import SoundFont.Ports exposing (..)
15 | import SoundFont.Types exposing (..)
16 | import Abc exposing (..)
17 | import AbcPerformance exposing (melodyFromAbcResult)
18 | import Abc.ParseTree exposing (AbcTune, PitchClass(..), Mode(..), Accidental(..), ModifiedKeySignature, KeySignature)
19 | import Abc.Canonical exposing (fromResult, fromTune)
20 | import Music.Notation exposing (getKeySig)
21 | import Music.Transposition exposing (transposeTo)
22 | import Music.Octave exposing (up, down)
23 | import Melody exposing (..)
24 | import Notable exposing (..)
25 | import MidiNotes exposing (..)
26 | import Json.Decode as Json exposing (succeed)
27 | import Debug exposing (..)
28 |
29 |
30 | {-| An ABC editor. It continually parses the ABC as it is entered and flags up errors.
31 | If the (checked) tune contains a key signature, then transposition options will be shown.
32 |
33 | -}
34 | main =
35 | Html.program
36 | { init = ( init, requestLoadPianoFonts "assets/soundfonts" ), update = update, view = view, subscriptions = subscriptions }
37 |
38 |
39 |
40 | -- MODEL
41 |
42 |
43 | type alias Model =
44 | { fontsLoaded : Bool
45 | , playing : Bool
46 | , abc : String
47 | , tuneResult : Result ParseError AbcTune
48 | , duration :
49 | Float
50 | -- the tune duration in seconds
51 | }
52 |
53 |
54 | dummyError : ParseError
55 | dummyError =
56 | { msgs = []
57 | , input = ""
58 | , position = 0
59 | }
60 |
61 |
62 | emptyTune : AbcTune
63 | emptyTune =
64 | ( [], [] )
65 |
66 |
67 | init : Model
68 | init =
69 | { fontsLoaded = False
70 | , playing = False
71 | , abc = ""
72 | , tuneResult = Ok emptyTune
73 | , duration = 0.0
74 | }
75 |
76 |
77 |
78 | -- UPDATE
79 |
80 |
81 | type Msg
82 | = NoOp
83 | | FontsLoaded Bool
84 | | Abc String
85 | | Play
86 | -- request that a tune plays
87 | | PlayStarted Bool
88 | -- response from the player that it's started
89 | | PlayCompleted
90 | -- the play has completed (we compute the time ourselves)
91 | | Transpose String
92 | | MoveOctave Bool
93 | | TuneResult (Result ParseError AbcTune)
94 |
95 |
96 | update : Msg -> Model -> ( Model, Cmd Msg )
97 | update msg model =
98 | case msg of
99 | NoOp ->
100 | ( model, Cmd.none )
101 |
102 | FontsLoaded loaded ->
103 | ( { model | fontsLoaded = loaded }
104 | , Cmd.none
105 | )
106 |
107 | Abc s ->
108 | ( { model | abc = s }, checkAbc s )
109 |
110 | Play ->
111 | playAbc model
112 |
113 | PlayStarted _ ->
114 | ( model, (suspend model.duration) )
115 |
116 | PlayCompleted ->
117 | ( { model | playing = False }, Cmd.none )
118 |
119 | Transpose s ->
120 | ( transpose s model, Cmd.none )
121 |
122 | MoveOctave isUp ->
123 | if isUp then
124 | ( moveOctave up model, Cmd.none )
125 | else
126 | ( moveOctave down model, Cmd.none )
127 |
128 | TuneResult tr ->
129 | ( { model | tuneResult = tr }, Cmd.none )
130 |
131 |
132 |
133 | {- a different attempt at checking if buttons are enabled -}
134 |
135 |
136 | areButtonsEnabled : Model -> Bool
137 | areButtonsEnabled m =
138 | case m.tuneResult of
139 | Ok _ ->
140 | not (m.playing)
141 |
142 | Err _ ->
143 | False
144 |
145 |
146 |
147 | {- sleep for a number of seconds -}
148 |
149 |
150 | suspend : Float -> Cmd Msg
151 | suspend secs =
152 | let
153 | _ =
154 | log "suspend time" secs
155 |
156 | time =
157 | secs * 1000
158 | in
159 | Process.sleep time
160 | |> Task.perform (\_ -> PlayCompleted)
161 |
162 |
163 | returnTuneResult : Result ParseError AbcTune -> Cmd Msg
164 | returnTuneResult r =
165 | -- Task.perform (\_ -> NoOp) TuneResult (Task.succeed r)
166 | Task.perform TuneResult (Task.succeed r)
167 |
168 |
169 | terminateLine : String -> String
170 | terminateLine s =
171 | s ++ "\x0D\n"
172 |
173 |
174 |
175 | {- cast a String to an Int -}
176 |
177 |
178 | toInt : String -> Int
179 | toInt =
180 | String.toInt >> Result.toMaybe >> Maybe.withDefault 0
181 |
182 |
183 |
184 | {- continually parse the ABC after every key stroke -}
185 |
186 |
187 | checkAbc : String -> Cmd Msg
188 | checkAbc abc =
189 | let
190 | terminatedAbc =
191 | terminateLine abc
192 |
193 | -- _ = "checking" terminatedAbc
194 | pr =
195 | parse terminatedAbc
196 | in
197 | returnTuneResult (pr)
198 |
199 |
200 |
201 | {- play the ABC and return the duration in the amended model -}
202 |
203 |
204 | playAbc : Model -> ( Model, Cmd Msg )
205 | playAbc m =
206 | let
207 | notesReversed =
208 | m.abc
209 | |> terminateLine
210 | |> parse
211 | |> melodyFromAbcResult
212 | |> toPerformance
213 | |> makeMIDINotes
214 |
215 | -- _ = log "notes reversed" notesReversed
216 | duration =
217 | reversedPhraseDuration notesReversed
218 | in
219 | ( { m
220 | | playing = True
221 | , duration = duration
222 | }
223 | , requestPlayNoteSequence (List.reverse notesReversed)
224 | )
225 |
226 |
227 |
228 | {- transpose the tune to a new key -}
229 |
230 |
231 | transpose : String -> Model -> Model
232 | transpose kstr model =
233 | let
234 | mksr =
235 | parseKeySignature kstr
236 | in
237 | case ( mksr, model.tuneResult ) of
238 | ( Ok mks, Ok tune ) ->
239 | let
240 | newTuneResult =
241 | transposeTo mks tune
242 |
243 | -- this is awkward in elm's Result - in this instance we're guaranteed not to have errors
244 | -- in transposition because our modes always match. Just convert the notional String error to a notional empty parser error
245 | newTRCorrectedErr =
246 | newTuneResult
247 | |> mapError (\_ -> dummyError)
248 |
249 | -- and collect the new ABC wrapped in a Result
250 | newAbcResult =
251 | fromResult newTuneResult
252 | in
253 | -- if we're OK, we have both a new ABC Tune and a new ABC source of that tune
254 | case newAbcResult of
255 | Ok newAbc ->
256 | { model | abc = newAbc, tuneResult = newTRCorrectedErr }
257 |
258 | _ ->
259 | model
260 |
261 | _ ->
262 | model
263 |
264 |
265 |
266 | {- move the tune up or down an octave -}
267 |
268 |
269 | moveOctave : (AbcTune -> AbcTune) -> Model -> Model
270 | moveOctave movefn model =
271 | case model.tuneResult of
272 | Ok tune ->
273 | let
274 | newTune =
275 | movefn tune
276 |
277 | newAbc =
278 | fromTune newTune
279 | in
280 | { model | abc = newAbc, tuneResult = (Ok newTune) }
281 |
282 | _ ->
283 | model
284 |
285 |
286 |
287 | -- SUBSCRIPTIONS
288 |
289 |
290 | fontsLoadedSub : Sub Msg
291 | fontsLoadedSub =
292 | fontsLoaded FontsLoaded
293 |
294 |
295 | playSequenceStartedSub : Sub Msg
296 | playSequenceStartedSub =
297 | playSequenceStarted PlayStarted
298 |
299 |
300 | subscriptions : Model -> Sub Msg
301 | subscriptions m =
302 | Sub.batch [ fontsLoadedSub, playSequenceStartedSub ]
303 |
304 |
305 |
306 | -- VIEW
307 |
308 |
309 | viewError : Model -> Html Msg
310 | viewError m =
311 | let
312 | tuneResult =
313 | m.tuneResult
314 | in
315 | case tuneResult of
316 | Err e ->
317 | -- we start off with a dummy error message which is empty
318 | if (isEmpty e.msgs) then
319 | text ""
320 | else
321 | let
322 | -- display a prefix of 5 characters before the error (if they're there) and a suffix of 5 after
323 | startPhrase =
324 | Basics.max (e.position - 5) 0
325 |
326 | errorPrefix =
327 | "error: " ++ slice startPhrase e.position m.abc
328 |
329 | startSuffix =
330 | Basics.min (e.position + 1) (String.length m.abc)
331 |
332 | endSuffix =
333 | Basics.min (e.position + 6) (String.length m.abc)
334 |
335 | errorSuffix =
336 | slice startSuffix endSuffix m.abc
337 |
338 | errorChar =
339 | slice e.position (e.position + 1) m.abc
340 | in
341 | p []
342 | [ text errorPrefix
343 | , span [ errorHighlightStyle ]
344 | [ text errorChar ]
345 | , text errorSuffix
346 | ]
347 |
348 | _ ->
349 | text ""
350 |
351 |
352 | view : Model -> Html Msg
353 | view model =
354 | if (model.fontsLoaded) then
355 | div []
356 | [ h1 [ centreStyle ] [ text "ABC Editor" ]
357 | , div [ leftPaneStyle ]
358 | [ span [ leftPanelWidgetStyle ] [ text "Transpose to:" ]
359 | , transpositionMenu model
360 | , span [ leftPanelWidgetStyle ] [ text "Move octave:" ]
361 | , button (buttonAttributes (areButtonsEnabled model) (MoveOctave True))
362 | [ text "up" ]
363 | , button (buttonAttributes (areButtonsEnabled model) (MoveOctave False))
364 | [ text "down" ]
365 | ]
366 | , div [ rightPaneStyle ]
367 | [ fieldset [ fieldsetStyle ]
368 | [ textarea
369 | [ placeholder "abc"
370 | , value model.abc
371 | , onInput Abc
372 | , taStyle
373 | , cols 70
374 | , rows 16
375 | , autocomplete False
376 | , spellcheck False
377 | , autofocus True
378 | ]
379 | []
380 | ]
381 | , div
382 | []
383 | [ button (buttonAttributes (areButtonsEnabled model) Play)
384 | [ text "play" ]
385 | ]
386 | , div
387 | []
388 | [ p [] [ viewError model ]
389 | ]
390 | ]
391 | ]
392 | else
393 | div [ centreStyle ]
394 | [ p [] [ text "It seems as if your browser does not support web-audio. Perhaps try Chrome." ]
395 | ]
396 |
397 |
398 |
399 | {- an active menu of transposition options -}
400 |
401 |
402 | transpositionMenu : Model -> Html Msg
403 | transpositionMenu m =
404 | let
405 | mKeySig =
406 | case
407 | m.tuneResult
408 | of
409 | Ok tune ->
410 | defaultToC (getKeySig tune)
411 |
412 | _ ->
413 | Nothing
414 | in
415 | case mKeySig of
416 | Just mks ->
417 | select
418 | [ leftPanelWidgetStyle
419 | , (disabled m.playing)
420 | , on "change" (Json.map Transpose targetValue)
421 | ]
422 | (transpositionOptions mks)
423 |
424 | Nothing ->
425 | select
426 | [ leftPanelWidgetStyle
427 | , (disabled True)
428 | ]
429 | [ option [] [ text "not available" ]
430 | ]
431 |
432 |
433 |
434 | {- offer a menu of transposition options, appropriate to the
435 | current key (if such a key has been entered in the ABC).
436 | The mode of each option always matches the current mode and
437 | the selected option matches the current key
438 | -}
439 |
440 |
441 | transpositionOptions : ModifiedKeySignature -> List (Html Msg)
442 | transpositionOptions mks =
443 | let
444 | ks =
445 | first mks
446 |
447 | mode =
448 | ks.mode
449 |
450 | allModes =
451 | [ option [ selectedKey ks (key C mode) ]
452 | [ displayKeySig (key C mode) ]
453 | , option [ selectedKey ks (key D mode) ]
454 | [ displayKeySig (key D mode) ]
455 | , option [ selectedKey ks (key E mode) ]
456 | [ displayKeySig (key E mode) ]
457 | , option [ selectedKey ks (key F mode) ]
458 | [ displayKeySig (key F mode) ]
459 | , option [ selectedKey ks (key G mode) ]
460 | [ displayKeySig (key G mode) ]
461 | , option [ selectedKey ks (key A mode) ]
462 | [ displayKeySig (key A mode) ]
463 | , option [ selectedKey ks (key B mode) ]
464 | [ displayKeySig (key B mode) ]
465 | ]
466 |
467 | majorMode =
468 | [ option [ selectedKey ks (flatKey B Major) ]
469 | [ displayKeySig (flatKey B Major) ]
470 | , option [ selectedKey ks (flatKey A Major) ]
471 | [ displayKeySig (flatKey A Major) ]
472 | , option [ selectedKey ks (flatKey E Major) ]
473 | [ displayKeySig (flatKey E Major) ]
474 | ]
475 |
476 | minorMode =
477 | [ option [ selectedKey ks (sharpKey F Minor) ]
478 | [ displayKeySig (sharpKey F Minor) ]
479 | , option [ selectedKey ks (sharpKey C Minor) ]
480 | [ displayKeySig (sharpKey C Minor) ]
481 | , option [ selectedKey ks (sharpKey G Minor) ]
482 | [ displayKeySig (sharpKey G Minor) ]
483 | ]
484 | in
485 | case mode of
486 | Major ->
487 | allModes ++ majorMode
488 |
489 | Minor ->
490 | allModes ++ minorMode
491 |
492 | _ ->
493 | allModes
494 |
495 |
496 |
497 | {- return a (selected true) attribute if the pattern key signature matches the target -}
498 |
499 |
500 | selectedKey : KeySignature -> KeySignature -> Attribute Msg
501 | selectedKey target pattern =
502 | let
503 | isMatched =
504 | (target.pitchClass == pattern.pitchClass) && (target.accidental == pattern.accidental)
505 | in
506 | selected isMatched
507 |
508 |
509 |
510 | {- display a key signature as text -}
511 |
512 |
513 | displayKeySig : KeySignature -> Html Msg
514 | displayKeySig ks =
515 | let
516 | accidental =
517 | case ks.accidental of
518 | Just Sharp ->
519 | "#"
520 |
521 | Just Flat ->
522 | "b"
523 |
524 | _ ->
525 | ""
526 | in
527 | text (toString ks.pitchClass ++ accidental ++ " " ++ toString ks.mode)
528 |
529 |
530 |
531 | {- style a textarea -}
532 |
533 |
534 | taStyle : Attribute Msg
535 | taStyle =
536 | style
537 | [ ( "padding", "10px 0" )
538 | , ( "font-size", "1.5em" )
539 | , ( "text-align", "left" )
540 | , ( "align", "center" )
541 | , ( "display", "block" )
542 | , ( "margin-left", "auto" )
543 | , ( "margin-right", "auto" )
544 | , ( "background-color", "#f3f6c6" )
545 | , ( "font-family", "monospace" )
546 | ]
547 |
548 |
549 |
550 | {- style the instructions section -}
551 |
552 |
553 | instructionStyle : Attribute msg
554 | instructionStyle =
555 | style
556 | [ ( "padding", "10px 0" )
557 | , ( "border", "none" )
558 | , ( "text-align", "left" )
559 | , ( "align", "center" )
560 | , ( "display", "block" )
561 | , ( "margin-left", "auto" )
562 | , ( "margin-right", "auto" )
563 | , ( "font", "100% \"Trebuchet MS\", Verdana, sans-serif" )
564 | ]
565 |
566 |
567 | leftPanelWidgetStyle : Attribute msg
568 | leftPanelWidgetStyle =
569 | style
570 | [ ( "margin-left", "40px" )
571 | , ( "margin-top", "40px" )
572 | , ( "font-size", "1.2em" )
573 | ]
574 |
575 |
576 |
577 | {- style a centered component -}
578 |
579 |
580 | centreStyle : Attribute msg
581 | centreStyle =
582 | style
583 | [ ( "text-align", "center" )
584 | , ( "margin", "auto" )
585 | ]
586 |
587 |
588 | leftPaneStyle : Attribute msg
589 | leftPaneStyle =
590 | style
591 | [ ( "float", "left" )
592 | , ( "width", "350px" )
593 | ]
594 |
595 |
596 | rightPaneStyle : Attribute msg
597 | rightPaneStyle =
598 | style
599 | [ ( "float", "left" )
600 | ]
601 |
602 |
603 |
604 | {- gather together all the button attributes -}
605 |
606 |
607 | buttonAttributes : Bool -> Msg -> List (Attribute Msg)
608 | buttonAttributes isEnabled msg =
609 | [ class "hoverable"
610 | , bStyle isEnabled
611 | , onClick msg
612 | , disabled (not isEnabled)
613 | ]
614 |
615 |
616 |
617 | {- style a button
618 | Note: all button styling is deferred to the external css (which implements hover)
619 | except for when the button is greyed out when it is disabled
620 | -}
621 |
622 |
623 | bStyle : Bool -> Attribute msg
624 | bStyle enabled =
625 | let
626 | colour =
627 | if enabled then
628 | []
629 | else
630 | [ ( "background-color", "lightgray" )
631 | , ( "color", "darkgrey" )
632 | ]
633 | in
634 | style (colour)
635 |
636 |
637 |
638 | {- style a fieldset -}
639 |
640 |
641 | fieldsetStyle : Attribute msg
642 | fieldsetStyle =
643 | style
644 | [ ( "background-color", "#f1f1f1" )
645 | , ( "border", "none" )
646 | , ( "border-radius", "2px" )
647 | , ( "margin-bottom", "12px" )
648 | , ( "padding", "10px 10px 20px 10px" )
649 | , ( "display", "inline-block" )
650 | ]
651 |
652 |
653 | errorHighlightStyle : Attribute msg
654 | errorHighlightStyle =
655 | style
656 | [ ( "color", "red" )
657 | ]
658 |
659 |
660 |
661 | -- key signatures
662 |
663 |
664 | key : PitchClass -> Mode -> KeySignature
665 | key pc m =
666 | { pitchClass = pc, accidental = Nothing, mode = m }
667 |
668 |
669 | sharpKey : PitchClass -> Mode -> KeySignature
670 | sharpKey pc m =
671 | { pitchClass = pc, accidental = Just Sharp, mode = m }
672 |
673 |
674 | flatKey : PitchClass -> Mode -> KeySignature
675 | flatKey pc m =
676 | { pitchClass = pc, accidental = Just Flat, mode = m }
677 |
678 |
679 | cMajor : ModifiedKeySignature
680 | cMajor =
681 | ( { pitchClass = C, accidental = Nothing, mode = Major }, [] )
682 |
683 |
684 |
685 | {- if there's no key signature in a properly parsed tune then default to C -}
686 |
687 |
688 | defaultToC : Maybe ModifiedKeySignature -> Maybe ModifiedKeySignature
689 | defaultToC mks =
690 | case mks of
691 | Just ks ->
692 | mks
693 |
694 | _ ->
695 | Just cMajor
696 |
--------------------------------------------------------------------------------
/src/examples/editor-controller/Midi/Player.elm:
--------------------------------------------------------------------------------
1 | module Midi.Player exposing (Model, Msg(SetRecording), init, update, view, subscriptions)
2 |
3 | {-
4 | A Midi Player module
5 |
6 | This allows buttons of start/pause/continue/reset
7 |
8 | in order to contol the playing of the MIDI file
9 | (again played by means of soundfonts and Web-Audio through elm ports)
10 |
11 | -}
12 |
13 | import Html exposing (Html, div, button, input, text, progress)
14 | import Html.Events exposing (onClick)
15 | import Html.Attributes exposing (src, type_, style, value, max)
16 | import Http exposing (..)
17 | import Task exposing (..)
18 | import Array exposing (get)
19 | import String exposing (..)
20 | import Result exposing (Result)
21 | import Process exposing (sleep)
22 | import Tuple exposing (first, second)
23 | import MidiTypes exposing (MidiEvent(..), MidiRecording)
24 | import SoundFont.Ports exposing (..)
25 | import SoundFont.Types exposing (..)
26 | import Midi.Track exposing (..)
27 | import Debug exposing (..)
28 |
29 |
30 | main =
31 | Html.program
32 | { init = init (Err "not started"), update = update, view = view, subscriptions = subscriptions }
33 |
34 |
35 |
36 | -- MODEL
37 | -- a delta time measured in milliseconds and a MIDI event
38 |
39 |
40 | type alias SoundEvent =
41 | { deltaTime : Float
42 | , event : MidiEvent
43 | }
44 |
45 |
46 | {-| the current state of the playback
47 | -}
48 | type alias PlaybackState =
49 | { index :
50 | Int
51 | -- index into the MidiMessage Array
52 | , microsecondsPerBeat :
53 | Float
54 | -- current Tempo
55 | , playing :
56 | Bool
57 | -- are we currently playing?
58 | , noteOnSequence :
59 | Bool
60 | -- are we in the midst of a NoteOn sequence
61 | , noteOnChannel :
62 | Int
63 | -- if so, what's its channel
64 | , notes :
65 | MidiNotes
66 | -- accumulated notes to play until we see the next NoteOff message
67 | , delay :
68 | Float
69 | -- the next delay between notes
70 | }
71 |
72 |
73 | {-| the model of the player
74 | -}
75 | type alias Model =
76 | { fontsLoaded :
77 | Bool
78 | -- are the fonts loaded
79 | , track :
80 | Result String MidiTrack
81 | -- the midi recording to play
82 | , playbackState :
83 | PlaybackState
84 | -- the state of the playback
85 | }
86 |
87 |
88 |
89 | {- the slowdown in the player brought about by using elm's Tasks -}
90 |
91 |
92 | elmPlayerOverhead : Float
93 | elmPlayerOverhead =
94 | 0.872
95 |
96 |
97 |
98 | -- elmPlayerOverhead = 1.0
99 | -- let's use this to mark the end of a track or a track in error we can't play
100 |
101 |
102 | endOfTrack : MidiTypes.MidiEvent
103 | endOfTrack =
104 | MidiTypes.Text "EndOfTrack"
105 |
106 |
107 | {-| initialise the model and issue a command to load the sound fonts
108 | -}
109 | init : Result String MidiTrack -> ( Model, Cmd Msg )
110 | init track =
111 | { fontsLoaded = False
112 | , track = track
113 | , playbackState =
114 | { index = 0
115 | , microsecondsPerBeat = Basics.toFloat 500000
116 | , playing = False
117 | , noteOnSequence = False
118 | , noteOnChannel = -1
119 | , notes = []
120 | , delay = 0.0
121 | }
122 | }
123 | ! [ requestLoadPianoFonts "assets/soundfonts" ]
124 |
125 |
126 |
127 | -- UPDATE
128 |
129 |
130 | {-| the messages used by the player
131 | -}
132 | type Msg
133 | = NoOp
134 | | FontsLoaded Bool
135 | -- response that soundfonts have been loaded
136 | | SetRecording (Result String MidiRecording)
137 | -- an external command to set the recording that is to be played
138 | | Step
139 | -- step to the next event in the MIDI recording and play it if possible
140 | | PlayedNote Bool
141 | -- response that the note has been played
142 | | PlaySequenceStarted Bool
143 | -- response that a sequence of notes (a chord) has started to be played
144 | -- controller actions
145 | | Start
146 | -- start / restart
147 | | Pause
148 | -- pause
149 | | MoveTo Int
150 |
151 |
152 |
153 | -- move to index (usually invoked as move to start)
154 |
155 |
156 | {-| update the player
157 | -}
158 | update : Msg -> Model -> ( Model, Cmd Msg )
159 | update msg model =
160 | case msg of
161 | NoOp ->
162 | ( model, Cmd.none )
163 |
164 | FontsLoaded loaded ->
165 | ( { model | fontsLoaded = loaded }
166 | , Cmd.none
167 | )
168 |
169 | SetRecording r ->
170 | let
171 | state =
172 | model.playbackState
173 |
174 | newState =
175 | { state
176 | | playing = False
177 | , noteOnSequence = False
178 | , noteOnChannel = -1
179 | }
180 |
181 | newModel =
182 | { model
183 | | playbackState = newState
184 | , track = toTrack0 r
185 | }
186 | in
187 | ( newModel, stop )
188 |
189 | Start ->
190 | -- chaining the next action which is step
191 | let
192 | state =
193 | model.playbackState
194 |
195 | newState =
196 | { state | playing = True }
197 |
198 | newModel =
199 | { model | playbackState = newState }
200 |
201 | cmd =
202 | step 0.0
203 | in
204 | ( newModel, cmd )
205 |
206 | Pause ->
207 | let
208 | state =
209 | model.playbackState
210 |
211 | newState =
212 | { state | playing = False }
213 |
214 | newModel =
215 | { model | playbackState = newState }
216 | in
217 | ( newModel, Cmd.none )
218 |
219 | MoveTo index ->
220 | let
221 | state =
222 | model.playbackState
223 |
224 | newState =
225 | { state | playing = False, index = index }
226 |
227 | newModel =
228 | { model | playbackState = newState }
229 | in
230 | ( newModel, Cmd.none )
231 |
232 | Step ->
233 | let
234 | _ =
235 | log "step state" model.playbackState
236 |
237 | soundEvent =
238 | nextEvent model.playbackState model.track
239 |
240 | ( newState, midiNotes ) =
241 | stepState soundEvent model.playbackState
242 |
243 | -- next action is either suspendAndPlay or step
244 | nextAction =
245 | interpretSoundEvent soundEvent midiNotes newState
246 |
247 | newModel =
248 | { model | playbackState = newState }
249 | in
250 | ( newModel, nextAction )
251 |
252 | -- these two messages are responses from SoundFont subscriptions and we must delay for the NoteOff time
253 | -- before stepping to the next MIDI message
254 | PlayedNote played ->
255 | ( model, step model.playbackState.delay )
256 |
257 | PlaySequenceStarted played ->
258 | ( model, step model.playbackState.delay )
259 |
260 |
261 |
262 | {- extract track zero from the midi recording -}
263 |
264 |
265 | toTrack0 : Result String MidiRecording -> Result String MidiTrack
266 | toTrack0 r =
267 | Result.map fromRecording r
268 |
269 |
270 |
271 | -- SUBSCRIPTIONS
272 |
273 |
274 | fontsLoadedSub : Sub Msg
275 | fontsLoadedSub =
276 | fontsLoaded FontsLoaded
277 |
278 |
279 | playedNoteSub : Sub Msg
280 | playedNoteSub =
281 | playedNote PlayedNote
282 |
283 |
284 | playSequenceStartedSub : Sub Msg
285 | playSequenceStartedSub =
286 | playSequenceStarted PlaySequenceStarted
287 |
288 |
289 | subscriptions : Model -> Sub Msg
290 | subscriptions m =
291 | Sub.batch [ fontsLoadedSub, playedNoteSub, playSequenceStartedSub ]
292 |
293 |
294 |
295 | -- EFFECTS
296 | {- get the next event - if we have a recording result from parsing the midi file, convert
297 | the next indexed midi event to a delayed action (perhaps a NoteOn sound)
298 | -}
299 |
300 |
301 | nextEvent : PlaybackState -> Result String MidiTrack -> SoundEvent
302 | nextEvent state trackResult =
303 | case trackResult of
304 | Ok track ->
305 | let
306 | maybeNextMessage =
307 | track.messages
308 | |> Array.get state.index
309 |
310 | nextMessage =
311 | Maybe.withDefault ( 0, endOfTrack ) maybeNextMessage
312 |
313 | nextEvent =
314 | second nextMessage
315 |
316 | -- work out the interval to the next note in milliseconds
317 | deltaTime =
318 | Basics.toFloat (first nextMessage) * state.microsecondsPerBeat / (Basics.toFloat track.ticksPerBeat * 1000)
319 |
320 | {-
321 | _ = log "midi note delay" (first nextMessage)
322 | _ = log "delta time" deltaTime
323 | -}
324 | in
325 | { deltaTime = deltaTime, event = nextEvent }
326 |
327 | Err err ->
328 | { deltaTime = 0.0, event = endOfTrack }
329 |
330 |
331 |
332 | {- interpret the sound event - play the note if it's a single NoteOn event,
333 | play a chord if there's more than one note otherwise just step to the next MIDI event
334 | -}
335 |
336 |
337 | interpretSoundEvent : SoundEvent -> MidiNotes -> PlaybackState -> Cmd Msg
338 | interpretSoundEvent soundEvent notes state =
339 | if (state.playing) then
340 | case (List.length notes) of
341 | 0 ->
342 | step (soundEvent.deltaTime * elmPlayerOverhead)
343 |
344 | 1 ->
345 | case (List.head notes) of
346 | Just note ->
347 | play note
348 |
349 | _ ->
350 | -- can't happen
351 | step (soundEvent.deltaTime * elmPlayerOverhead)
352 |
353 | _ ->
354 | playChord notes
355 | else
356 | Cmd.none
357 |
358 |
359 |
360 | {- a non-note is processed by sleeping for the time delay and then
361 | stepping to the next MIDI event
362 | -}
363 |
364 |
365 | step : Float -> Cmd Msg
366 | step delay =
367 | let
368 | task =
369 | Process.sleep (delay * elmPlayerOverhead)
370 | |> andThen (\_ -> Task.succeed (\_ -> Step))
371 | in
372 | Task.perform (\_ -> Step) task
373 |
374 |
375 |
376 | -- Task.perform (\_ -> NoOp) (\_ -> Step) task
377 | {- play a note -}
378 |
379 |
380 | play : MidiNote -> Cmd Msg
381 | play note =
382 | let
383 | note1 =
384 | { note | timeOffset = 0.0 }
385 | in
386 | requestPlayNote note1
387 |
388 |
389 |
390 | {- play a chord -}
391 |
392 |
393 | playChord : MidiNotes -> Cmd Msg
394 | playChord chord =
395 | let
396 | f n =
397 | { n | timeOffset = 0.0 }
398 |
399 | notes =
400 | List.map f chord
401 | in
402 | requestPlayNoteSequence notes
403 |
404 |
405 |
406 | {- issue a stop asynchronously to what the player thinks it's doing - this is issued
407 | externally whenever the player gets a new MIDI recording to play and must interrupt
408 | what it's doing.
409 | -}
410 |
411 |
412 | stop : Cmd Msg
413 | stop =
414 | Task.perform (\_ -> MoveTo 0) (Task.succeed NoOp)
415 |
416 |
417 |
418 | -- Task.perform (\_ -> NoOp) (\_ -> MoveTo 0) (Task.succeed NoOp)
419 | {- step through the state, accumulating notes in the state if any NoteOn message is encountered.
420 | If we see a NoteOff then return the note sequence (if any) so that it can be played
421 | outside of chords, the note sequence will usually either be empty of contain a single note
422 | -}
423 |
424 |
425 | stepState : SoundEvent -> PlaybackState -> ( PlaybackState, MidiNotes )
426 | stepState soundEvent state =
427 | if state.playing then
428 | let
429 | _ =
430 | log "sound event" soundEvent.event
431 | in
432 | case soundEvent.event of
433 | MidiTypes.Text t ->
434 | if (t == "EndOfTrack") then
435 | ( { state | playing = False, noteOnSequence = False }, [] )
436 | else
437 | ( { state | index = state.index + 1, noteOnSequence = False }, [] )
438 |
439 | Tempo t ->
440 | ( { state | microsecondsPerBeat = Basics.toFloat t, index = state.index + 1, noteOnSequence = False }, [] )
441 |
442 | {- Running Status inherits the channel from the last event but only (in our case)
443 | if the state shows we're in the midst of a NoteOn sequence (i.e. a NoteOn followed
444 | immediately by 0 or more RunningStatus) then we generate a new NoteOn
445 | -}
446 | RunningStatus p1 p2 ->
447 | if state.noteOnSequence then
448 | let
449 | newEvent =
450 | { deltaTime = soundEvent.deltaTime, event = NoteOn state.noteOnChannel p1 p2 }
451 | in
452 | stepState newEvent state
453 | else
454 | -- ignore anything else and reset the sequence state
455 | ( { state | index = state.index + 1, noteOnSequence = False }, [] )
456 |
457 | NoteOn channel pitch velocity ->
458 | let
459 | midiNote =
460 | (MidiNote pitch soundEvent.deltaTime gain)
461 |
462 | midiNotes =
463 | midiNote :: state.notes
464 |
465 | newstate =
466 | { state
467 | | index = state.index + 1
468 | , noteOnSequence = True
469 | , noteOnChannel = channel
470 | , notes = midiNotes
471 | }
472 |
473 | maxVelocity =
474 | 0x7F
475 |
476 | gain =
477 | Basics.toFloat velocity / maxVelocity
478 | in
479 | ( newstate, [] )
480 |
481 | {- NoteOff messages will be used actually to request that the buffered note(s) will be played -}
482 | NoteOff _ _ _ ->
483 | let
484 | midiNotes =
485 | state.notes
486 | in
487 | -- save the note delay in the state. This will be used when control passes back to the player after the request to
488 | -- play the note has been issued (which happens immediately) and then the next action is to delay before the next step
489 | ( { state
490 | | index = state.index + 1
491 | , noteOnSequence = False
492 | , notes = []
493 | , delay = soundEvent.deltaTime
494 | }
495 | , midiNotes
496 | )
497 |
498 | _ ->
499 | ( { state | index = state.index + 1, noteOnSequence = False }, [] )
500 | else
501 | ( state, [] )
502 |
503 |
504 |
505 | -- VIEW
506 | {- view the result - just for debug purposes -}
507 |
508 |
509 | viewRecordingResult : Result String MidiTrack -> String
510 | viewRecordingResult mr =
511 | case mr of
512 | Ok res ->
513 | "OK: " ++ (toString res)
514 |
515 | Err errs ->
516 | "Fail: " ++ (toString errs)
517 |
518 |
519 | {-| view the player widget
520 | -}
521 | view : Model -> Html Msg
522 | view model =
523 | div []
524 | [ player model
525 | ]
526 |
527 |
528 | player : Model -> Html Msg
529 | player model =
530 | let
531 | _ =
532 | log "Midi Player view" model.track
533 |
534 | start =
535 | "assets/images/play.png"
536 |
537 | stop =
538 | "assets/images/stop.png"
539 |
540 | pause =
541 | "assets/images/pause.png"
542 |
543 | maxRange =
544 | case model.track of
545 | Ok track ->
546 | Array.length track.messages |> toString
547 |
548 | _ ->
549 | "0"
550 |
551 | sliderPos =
552 | model.playbackState.index |> toString
553 |
554 | playButton =
555 | case model.playbackState.playing of
556 | True ->
557 | pause
558 |
559 | False ->
560 | start
561 |
562 | playAction =
563 | case model.playbackState.playing of
564 | True ->
565 | Pause
566 |
567 | False ->
568 | Start
569 | in
570 | case model.track of
571 | Ok _ ->
572 | div [ style playerBlock ]
573 | [ div [ style (playerBase ++ playerStyle) ]
574 | [ progress
575 | [ Html.Attributes.max maxRange
576 | , value sliderPos
577 | , style capsuleStyle
578 | ]
579 | []
580 | , div [ style buttonStyle ]
581 | [ input
582 | [ type_ "image"
583 | , src playButton
584 | , onClick (playAction)
585 | ]
586 | []
587 | , input
588 | [ type_ "image"
589 | , src stop
590 | , onClick (MoveTo 0)
591 | ]
592 | []
593 | ]
594 | ]
595 | ]
596 |
597 | Err _ ->
598 | div [] []
599 |
600 |
601 |
602 | {- the player buttons -}
603 |
604 |
605 | buttons : Model -> Html Msg
606 | buttons model =
607 | case model.playbackState.playing of
608 | True ->
609 | div []
610 | [ button [ onClick (Pause) ] [ text "pause" ]
611 | , button [ onClick (MoveTo 0) ] [ text "stop" ]
612 | ]
613 |
614 | False ->
615 | div []
616 | [ button [ onClick (Start) ] [ text "play" ]
617 | , button [ onClick (MoveTo 0) ] [ text "stop" ]
618 | ]
619 |
620 |
621 |
622 | -- CSS
623 | {- Only half-successful attempt to reuse the styling of the MIDI.js player on which this project is based
624 | I've lost access to identicalsnowflake/elm-dynamic-style for effects like hover which is no longer
625 | compatible with Elm 0.16 and my gradient effects don't seem to work. Not sure what the future
626 | holds for libraries such as elm-style or elm-css.
627 | -}
628 |
629 |
630 | playerBlock : List ( String, String )
631 | playerBlock =
632 | [ ( "border", "1px solid #000" )
633 | --, ("background", "#000")
634 | , ( "border-radius", "10px" )
635 | , ( "width", "360px" )
636 | , ( "position", "relative; z-index: 2" )
637 | -- , ("margin-bottom", "15px")
638 | ]
639 |
640 |
641 | playerStyle : List ( String, String )
642 | playerStyle =
643 | [ ( "height", "30px" )
644 | , ( "box-shadow", "-1px #000" )
645 | , ( "border-bottom-right-radius", "10" )
646 | , ( "border-bottom-left-radius", "10" )
647 | --, ("margin-bottom", "0" )
648 | ]
649 |
650 |
651 | playerBase : List ( String, String )
652 | playerBase =
653 | [ ( "background", "rgba(0,0,0,0.7)" )
654 | -- ("background", "#000")
655 | , ( "background-image", "-webkit-gradient(linear,left top,left bottom,from(rgba(66,66,66,1)),to(rgba(22,22,22,1)))" )
656 | , ( "background-image", "-webkit-linear-gradient(top, rgba(66, 66, 66, 1) 0%, rgba(22, 22, 22, 1) 100%)" )
657 | , ( "background-image", "-moz-linear-gradient(top, rgba(66, 66, 66, 1) 0%, rgba(22, 22, 22, 1) 100%)" )
658 | , ( "background-image", "-ms-gradient(top, rgba(66, 66, 66, 1) 0%, rgba(22, 22, 22, 1) 100%)" )
659 | , ( "background-image", "-o-gradient(top, rgba(66, 66, 66, 1) 0%, rgba(22, 22, 22, 1) 100%)" )
660 | , ( "background-image", "linear-gradient(top, rgba(66, 66, 66, 1) 0%, rgba(22, 22, 22, 1) 100%)" )
661 | , ( "padding", "15px 20px" )
662 | , ( "border", "1px solid #000" )
663 | , ( "box-shadow", "0 0 10px #fff" )
664 | , ( "-moz-box-shadow", "0 0 10px #fff" )
665 | , ( "-webkit-box-shadow", "0 0 10px #fff" )
666 | , ( "border-radius", "10px" )
667 | , ( "-moz-border-radius", "10px" )
668 | , ( "-webkit-border-radius", "10px" )
669 | , ( "color", "#FFFFFF" )
670 | , ( "color", "rgba(255, 255, 255, 0.8)" )
671 | , ( "text-shadow", "1px 1px 2px #000" )
672 | , ( "-moz-text-shadow", "1px 1px 2px #000" )
673 | -- , ("margin-bottom", "15px")
674 | ]
675 |
676 |
677 | buttonStyle : List ( String, String )
678 | buttonStyle =
679 | [ ( "margin", "0 auto" )
680 | , ( "width", "80px" )
681 | , ( "float", "right" )
682 | , ( "opacity", "0.7" )
683 | ]
684 |
685 |
686 | capsuleStyle : List ( String, String )
687 | capsuleStyle =
688 | [ ( "border", "1px solid #000" )
689 | , ( "box-shadow", "0 0 10px #555" )
690 | , ( "-moz-box-shadow", "0 0 10px #555" )
691 | , ( "-webkit-box-shadow", "0 0 10px #555" )
692 | , ( "background", "#000" )
693 | , ( "background-image", "-webkit-gradient(linear, left top, left bottom, color-stop(1, rgba(0,0,0,0.5)), color-stop(0, #333))" )
694 | , ( "background-image", "-webkit-linear-gradient(top, rgba(0, 0, 0, 0.5) 1, #333 0)" )
695 | , ( "background-image", "-moz-linear-gradient(top, rgba(0, 0, 0, 0.5) 1, #333 0)" )
696 | , ( "background-image", "-ms-gradient(top, rgba(0, 0, 0, 0.5) 1, #333 0)" )
697 | , ( "background-image", "-o-gradient(top, rgba(0, 0, 0, 0.5) 1, #333 0)" )
698 | , ( "background-image", "linear-gradient(top, rgba(0, 0, 0, 0.5) 1, #333 0)" )
699 | , ( "overflow", "hidden" )
700 | , ( "border-radius", "5px" )
701 | , ( "-moz-border-radius", "5px" )
702 | , ( "-webkit-border-radius", "5px" )
703 | , ( "width", "220px" )
704 | , ( "display", "inline-block" )
705 | , ( "height", "30px" )
706 | ]
707 |
--------------------------------------------------------------------------------