├── 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 | ![alt tag](https://cdn.rawgit.com/newlandsvalley/elm-abc-player/3fceda93/assets/images/player/ABC-editor.jpg) 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 | --------------------------------------------------------------------------------