git.iko.soy/rowtype-yoga/purescript-abcjs
on main help sign in
20 files changed+902−107
since splitsnapshots
unifiedsplit
.spec-results+38−3 bun.lock+13 package.json+4 spago.lock+155−17 spago.yaml+10−1 src/Abcjs/Boomwhacker.purs+54 src/Abcjs/Highlight.js+4 src/Abcjs/Highlight.purs+54 src/Abcjs/Internal.js+1 src/Abcjs/Internal.purs+16 src/Abcjs/Notation.purs+39 src/Abcjs/Render.js+6−2 src/Abcjs/Render.purs+54−21 src/Abcjs/Synth.js+17−1 src/Abcjs/Synth.purs+104−33 src/Abcjs/Timing.js+7−2 src/Abcjs/Timing.purs+31−14 src/Abcjs/Types.js+8 src/Abcjs/Types.purs+258−9 test/Test/Main.purs+29−4
.spec-results+38−3
@@ -1,15 +1,43 @@
11[
2+ [
3+ "Abcjs.Boomwhacker.boomwhackerColor maps each pitch class to its colour, octave-independent",
4+ {
5+ "timestamp": "1780164206746.0",
6+ "success": true
7+ }
8+ ],
9+ [
10+ "Abcjs.Internal.withDefaults keeps default render params for unspecified fields, newtypes and all",
11+ {
12+ "timestamp": "1780164206746.0",
13+ "success": true
14+ }
15+ ],
16+ [
17+ "Abcjs.Internal.withDefaults keeps defaults for unspecified fields and takes provided ones",
18+ {
19+ "timestamp": "1780152383655.0",
20+ "success": true
21+ }
22+ ],
23+ [
24+ "Abcjs.Notation.midiToAbc renders pitch class, accidental and octave",
25+ {
26+ "timestamp": "1780164206746.0",
27+ "success": true
28+ }
29+ ],
230 [
331 "Abcjs.Render.parseOnly always yields exactly one tune object",
432 {
5− "timestamp": "1780151218238.0",
33+ "timestamp": "1780164206746.0",
634 "success": true
735 }
836 ],
937 [
1038 "Abcjs.Render.parseOnly parses a game-generated phrase to at least one line of music",
1139 {
12− "timestamp": "1780151218238.0",
40+ "timestamp": "1780164206746.0",
1341 "success": true
1442 }
1543 ],
@@ -23,7 +51,7 @@
2351 [
2452 "Abcjs.Render.parseOnly parses empty input to a tune with no music",
2553 {
26− "timestamp": "1780151218238.0",
54+ "timestamp": "1780164206746.0",
2755 "success": true
2856 }
2957 ],
@@ -40,5 +68,12 @@
4068 "timestamp": "1780150801626.0",
4169 "success": true
4270 }
71+ ],
72+ [
73+ "Mic.frequencyToMidi rounds a frequency to the nearest MIDI note",
74+ {
75+ "timestamp": "1780160210477.0",
76+ "success": true
77+ }
4378 ]
4479]
bun.lock+13
@@ -5,6 +5,9 @@
55 "name": "purescript-abcjs",
66 "dependencies": {
77 "abcjs": "^6.6.3",
8+ "pitchy": "^4.1.0",
9+ "react": "^19.2.6",
10+ "react-dom": "^19.2.6",
811 },
912 "devDependencies": {
1013 "@types/bun": "latest",
@@ -23,6 +26,16 @@
2326
2427 "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
2528
29+ "fft.js": ["fft.js@4.0.4", "", {}, "sha512-f9c00hphOgeQTlDyavwTtu6RiK8AIFjD6+jvXkNkpeQ7rirK3uFWVpalkoS4LAwbdX7mfZ8aoBfFVQX1Re/8aw=="],
30+
31+ "pitchy": ["pitchy@4.1.0", "", { "dependencies": { "fft.js": "^4.0.4" } }, "sha512-E8nQ8svBroGSMcc3qu2KvLHIuRYAIfrSkqDKWhjgj3WizseqWQXjQ+Q5t4g1CU73R5Euk+DAinyNFDK+KZw7zA=="],
32+
33+ "react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="],
34+
35+ "react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="],
36+
37+ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
38+
2639 "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
2740
2841 "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
package.json+4
@@ -3,6 +3,10 @@
33 "module": "index.ts",
44 "type": "module",
55 "private": true,
6+ "scripts": {
7+ "build": "spago build",
8+ "dev": "spago build && bun server.ts"
9+ },
610 "devDependencies": {
711 "@types/bun": "latest"
812 },
spago.lock+155−17
@@ -6,12 +6,21 @@
66 "core": {
77 "dependencies": [
88 "aff",
9− "aff-promise",
109 "arrays",
10+ "datetime",
1111 "effect",
12+ "foldable-traversable",
13+ "functions",
14+ "js-promise",
15+ "js-promise-aff",
16+ "maybe",
1217 "newtype",
1318 "nullable",
14− "prelude"
19+ "prelude",
20+ "strings",
21+ "tuples",
22+ "web-dom",
23+ "web-uievents"
1524 ]
1625 },
1726 "test": {
@@ -690,21 +699,6 @@
690699 "unsafe-coerce"
691700 ]
692701 },
693− "aff-promise": {
694− "type": "registry",
695− "version": "4.0.0",
696− "integrity": "sha256-Jgp3y+NWuuAmwz2V3LlWoX+AvxqfVglY77N5zTw8xnI=",
697− "dependencies": [
698− "aff",
699− "control",
700− "effect",
701− "either",
702− "exceptions",
703− "foreign",
704− "prelude",
705− "transformers"
706− ]
707− },
708702 "ansi": {
709703 "type": "registry",
710704 "version": "7.0.0",
@@ -1129,6 +1123,37 @@
11291123 "prelude"
11301124 ]
11311125 },
1126+ "js-promise": {
1127+ "type": "registry",
1128+ "version": "1.0.0",
1129+ "integrity": "sha256-u+6+ofIwT5XhT2fbMu4dShwkyWGfPRt9642Cnzw/Wvk=",
1130+ "dependencies": [
1131+ "effect",
1132+ "exceptions",
1133+ "foldable-traversable",
1134+ "functions",
1135+ "maybe",
1136+ "newtype",
1137+ "prelude"
1138+ ]
1139+ },
1140+ "js-promise-aff": {
1141+ "type": "registry",
1142+ "version": "1.0.0",
1143+ "integrity": "sha256-gEKAdNnTWjXS+tTp/AhYl/ezloKfuoQFcBKrdOx2USo=",
1144+ "dependencies": [
1145+ "aff",
1146+ "control",
1147+ "effect",
1148+ "either",
1149+ "exceptions",
1150+ "foreign",
1151+ "js-promise",
1152+ "maybe",
1153+ "prelude",
1154+ "transformers"
1155+ ]
1156+ },
11321157 "lazy": {
11331158 "type": "registry",
11341159 "version": "6.0.0",
@@ -1170,6 +1195,15 @@
11701195 "prelude"
11711196 ]
11721197 },
1198+ "media-types": {
1199+ "type": "registry",
1200+ "version": "6.0.0",
1201+ "integrity": "sha256-3Wyq+vfqJFTmy1k6DGVnXStxtZxgK631wusDPzlFVxg=",
1202+ "dependencies": [
1203+ "newtype",
1204+ "prelude"
1205+ ]
1206+ },
11731207 "mmorph": {
11741208 "type": "registry",
11751209 "version": "7.0.0",
@@ -1703,6 +1737,110 @@
17031737 "version": "6.0.0",
17041738 "integrity": "sha256-0L1QsaY20OILjfU5TV72d3U/tSjhmL9hJ32WMp857Lk=",
17051739 "dependencies": []
1740+ },
1741+ "web-dom": {
1742+ "type": "registry",
1743+ "version": "6.0.0",
1744+ "integrity": "sha256-rVeqkxChkjqisDvOujy9BMEgGqJkwX3t6g21bXfiG5o=",
1745+ "dependencies": [
1746+ "effect",
1747+ "enums",
1748+ "maybe",
1749+ "newtype",
1750+ "nullable",
1751+ "prelude",
1752+ "unsafe-coerce",
1753+ "web-events"
1754+ ]
1755+ },
1756+ "web-events": {
1757+ "type": "registry",
1758+ "version": "4.0.0",
1759+ "integrity": "sha256-wZNeldG0OU+8qGycUJ9e6vxf1/GC4Ep4OlRMhvjs9LA=",
1760+ "dependencies": [
1761+ "datetime",
1762+ "effect",
1763+ "enums",
1764+ "foreign",
1765+ "functions",
1766+ "maybe",
1767+ "newtype",
1768+ "nullable",
1769+ "prelude",
1770+ "unsafe-coerce"
1771+ ]
1772+ },
1773+ "web-file": {
1774+ "type": "registry",
1775+ "version": "4.0.0",
1776+ "integrity": "sha256-txsdkeWBKU/+5v9m5c6kysgJQSDyV1r7MHaeT4STpjQ=",
1777+ "dependencies": [
1778+ "datetime",
1779+ "effect",
1780+ "enums",
1781+ "foreign",
1782+ "integers",
1783+ "maybe",
1784+ "media-types",
1785+ "nullable",
1786+ "numbers",
1787+ "partial",
1788+ "prelude",
1789+ "tuples",
1790+ "unfoldable",
1791+ "unsafe-coerce",
1792+ "web-events"
1793+ ]
1794+ },
1795+ "web-html": {
1796+ "type": "registry",
1797+ "version": "4.1.1",
1798+ "integrity": "sha256-yOuZJGUxFrivHKpo/jKuKzFZaq6PPPE64ftSysXFssk=",
1799+ "dependencies": [
1800+ "effect",
1801+ "enums",
1802+ "foreign",
1803+ "functions",
1804+ "js-date",
1805+ "maybe",
1806+ "media-types",
1807+ "newtype",
1808+ "nullable",
1809+ "prelude",
1810+ "unsafe-coerce",
1811+ "web-dom",
1812+ "web-events",
1813+ "web-file",
1814+ "web-storage"
1815+ ]
1816+ },
1817+ "web-storage": {
1818+ "type": "registry",
1819+ "version": "5.0.0",
1820+ "integrity": "sha256-YJfos55oeKtBStNfoWfeflSy8j+4IDym5eaZtIY7YM4=",
1821+ "dependencies": [
1822+ "effect",
1823+ "maybe",
1824+ "nullable",
1825+ "prelude",
1826+ "unsafe-coerce",
1827+ "web-events"
1828+ ]
1829+ },
1830+ "web-uievents": {
1831+ "type": "registry",
1832+ "version": "5.0.0",
1833+ "integrity": "sha256-yh9uVLIGu8wE1+jwLehtfsRfR1eyiShhIMMhBPqKybE=",
1834+ "dependencies": [
1835+ "effect",
1836+ "enums",
1837+ "maybe",
1838+ "nullable",
1839+ "prelude",
1840+ "unsafe-coerce",
1841+ "web-events",
1842+ "web-html"
1843+ ]
17061844 }
17071845 }
17081846}
spago.yaml+10−1
@@ -2,12 +2,21 @@ package:
22 name: abcjs
33 dependencies:
44 - aff
5− - aff-promise
65 - arrays
6+ - datetime
77 - effect
8+ - foldable-traversable
9+ - functions
10+ - js-promise
11+ - js-promise-aff
12+ - maybe
813 - newtype
914 - nullable
1015 - prelude
16+ - strings
17+ - tuples
18+ - web-dom
19+ - web-uievents
1120 test:
1221 main: Test.Main
1322 dependencies:
src/Abcjs/Boomwhacker.pursnew file+54
@@ -0,0 +1,54 @@
1+module Abcjs.Boomwhacker
2+ ( boomwhackerColor
3+ , colorEventBoomwhackers
4+ , fadeEventBoomwhackers
5+ ) where
6+
7+import Prelude
8+
9+import Abcjs.Highlight (fadeFill, setFill)
10+import Abcjs.Types (Color(..), MidiNote(..), MidiPitch, TimingEvent, eventElements, eventMidiPitches)
11+import Data.Array (zip)
12+import Data.Foldable (for_)
13+import Data.Newtype (un)
14+import Data.Time.Duration (Milliseconds)
15+import Data.Tuple.Nested (type (/\), (/\))
16+import Effect (Effect)
17+import Web.DOM (Element)
18+
19+-- | The Boomwhacker colour for a note, taken from its pitch class (the standard
20+-- | MuseScore palette). Octave is ignored, so every C is red, every D orange, …
21+boomwhackerColor :: MidiNote -> Color
22+boomwhackerColor note = Color (colorFor (mod (un MidiNote note) 12))
23+ where
24+ colorFor = case _ of
25+ 0 -> "#e21c48" -- C
26+ 1 -> "#f26622" -- C#/Db
27+ 2 -> "#f99d1c" -- D
28+ 3 -> "#ffcc33" -- D#/Eb
29+ 4 -> "#fff32b" -- E
30+ 5 -> "#bcd85f" -- F
31+ 6 -> "#62bc47" -- F#/Gb
32+ 7 -> "#009c95" -- G
33+ 8 -> "#0071bb" -- G#/Ab
34+ 9 -> "#5e50a1" -- A
35+ 10 -> "#8d5ba6" -- A#/Bb
36+ _ -> "#cf3e96" -- B
37+
38+-- | Colour an event's notes by their Boomwhacker pitch-class colours, pairing
39+-- | each note's SVG nodes with its pitch. Drop straight into a follow-along
40+-- | `onEvent` to light each note up as it sounds.
41+colorEventBoomwhackers :: TimingEvent -> Effect Unit
42+colorEventBoomwhackers event =
43+ for_ (notesWithPitch event) \(notes /\ pitch) ->
44+ for_ notes (setFill (boomwhackerColor pitch.pitch))
45+
46+-- | As `colorEventBoomwhackers`, but each note fades into its Boomwhacker colour
47+-- | over the given duration.
48+fadeEventBoomwhackers :: Milliseconds -> TimingEvent -> Effect Unit
49+fadeEventBoomwhackers ms event =
50+ for_ (notesWithPitch event) \(notes /\ pitch) ->
51+ for_ notes (fadeFill ms (boomwhackerColor pitch.pitch))
52+
53+notesWithPitch :: TimingEvent -> Array (Array Element /\ MidiPitch)
54+notesWithPitch event = zip (eventElements event) (eventMidiPitches event)
src/Abcjs/Highlight.jsnew file+4
@@ -0,0 +1,4 @@
1+export const fadeFillImpl = (ms, color, el) => {
2+ el.style.transition = `fill ${ms}ms ease-in-out`;
3+ el.style.fill = color;
4+};
src/Abcjs/Highlight.pursnew file+54
@@ -0,0 +1,54 @@
1+module Abcjs.Highlight
2+ ( setFill
3+ , clearFill
4+ , fadeFill
5+ , colorEvent
6+ , clearEventColor
7+ , fadeColorEvent
8+ ) where
9+
10+import Prelude
11+
12+import Abcjs.Types (Color(..), TimingEvent, eventElements)
13+import Data.Foldable (for_)
14+import Data.Newtype (un)
15+import Data.Time.Duration (Milliseconds)
16+import Effect (Effect)
17+import Effect.Uncurried (EffectFn3, runEffectFn3)
18+import Web.DOM (Element)
19+import Web.DOM.Element (removeAttribute, setAttribute)
20+
21+-- | Set one note element's `fill` colour immediately.
22+setFill :: Color -> Element -> Effect Unit
23+setFill color = setAttribute "fill" (un Color color)
24+
25+-- | Drop a note element's `fill` override, reverting to its drawn colour.
26+clearFill :: Element -> Effect Unit
27+clearFill = removeAttribute "fill"
28+
29+-- | Transition a note element's `fill` to a colour over the given duration
30+-- | (a CSS `fill` transition on the element's inline style).
31+fadeFill :: Milliseconds -> Color -> Element -> Effect Unit
32+fadeFill = runEffectFn3 fadeFillImpl
33+
34+foreign import fadeFillImpl :: EffectFn3 Milliseconds Color Element Unit
35+
36+-- | Colour every SVG node of an event's note(s) at once. Call from a
37+-- | `CursorControl`/`TimingCallbacks` handler to flash a note on a hit.
38+colorEvent :: Color -> TimingEvent -> Effect Unit
39+colorEvent color event =
40+ for_ (eventElements event) \notes ->
41+ for_ notes (setFill color)
42+
43+-- | Revert an event's notes to their drawn colour — e.g. on the previously
44+-- | highlighted event as the cursor advances.
45+clearEventColor :: TimingEvent -> Effect Unit
46+clearEventColor event =
47+ for_ (eventElements event) \notes ->
48+ for_ notes clearFill
49+
50+-- | Fade every SVG node of an event's note(s) to a colour over a duration.
51+fadeColorEvent :: Milliseconds -> Color -> TimingEvent -> Effect Unit
52+fadeColorEvent ms color event =
53+ for_ (eventElements event) \notes ->
54+ for_ notes (fadeFill ms color)
src/Abcjs/Internal.jsnew file+1
@@ -0,0 +1 @@
1+export const mergeImpl = (defaults, provided) => ({ ...defaults, ...provided });
src/Abcjs/Internal.pursnew file+16
@@ -0,0 +1,16 @@
1+module Abcjs.Internal (withDefaults) where
2+
3+import Data.Function.Uncurried (Fn2, runFn2)
4+import Prim.Row (class Union)
5+
6+-- | Fill the unspecified fields of a partial options record from a full set of
7+-- | defaults, the provided fields winning. The `Union` constraint is the whole
8+-- | point: it lets a caller pass any subset of the options and nothing else,
9+-- | checked at compile time. The merge itself is a plain JS spread.
10+withDefaults
11+ :: forall provided rest full
12+ . Union provided rest full
13+ => { | full } -> { | provided } -> { | full }
14+withDefaults defaults provided = runFn2 mergeImpl defaults provided
15+
16+foreign import mergeImpl :: forall full provided. Fn2 { | full } { | provided } { | full }
src/Abcjs/Notation.pursnew file+39
@@ -0,0 +1,39 @@
1+module Abcjs.Notation (midiToAbc) where
2+
3+import Prelude
4+
5+import Abcjs.Types (MidiNote(..))
6+import Data.Monoid (power)
7+import Data.Newtype (un)
8+import Data.String (toLower)
9+import Data.Tuple.Nested (type (/\), (/\))
10+
11+-- | The ABC notation token for a MIDI note, with sharps for the black keys and
12+-- | the right octave marks: 60 → "C" (middle C), 61 → "^C", 72 → "c",
13+-- | 48 → "C,", 84 → "c'". Feed it into a tune body for `renderAbc`/`parseOnly`.
14+midiToAbc :: MidiNote -> String
15+midiToAbc note = accidental <> cased <> marks
16+ where
17+ n = un MidiNote note
18+ octave = (n / 12) - 1 -- MIDI 60 (C4) is octave 4, written without marks
19+ accidental /\ letter = spell (mod n 12)
20+ cased = if octave >= 5 then toLower letter else letter
21+ marks
22+ | octave >= 5 = power "'" (octave - 5)
23+ | octave <= 3 = power "," (4 - octave)
24+ | otherwise = ""
25+
26+spell :: Int -> String /\ String
27+spell = case _ of
28+ 0 -> "" /\ "C"
29+ 1 -> "^" /\ "C"
30+ 2 -> "" /\ "D"
31+ 3 -> "^" /\ "D"
32+ 4 -> "" /\ "E"
33+ 5 -> "" /\ "F"
34+ 6 -> "^" /\ "F"
35+ 7 -> "" /\ "G"
36+ 8 -> "^" /\ "G"
37+ 9 -> "" /\ "A"
38+ 10 -> "^" /\ "A"
39+ _ -> "" /\ "B"
src/Abcjs/Render.js+6−2
@@ -1,6 +1,10 @@
11import abcjs from "abcjs";
22
3−export const renderAbcImpl = (selector, abc, params) =>
4− abcjs.renderAbc(selector, abc, params);
3+export const renderAbcImpl = (mkClickListener, selector, abc, defaults, provided) => {
4+ const params = { ...defaults, ...provided };
5+ if (params.clickListener !== undefined)
6+ params.clickListener = mkClickListener(params.clickListener);
7+ return abcjs.renderAbc(selector, abc, params);
8+};
59
610export const parseOnlyImpl = (abc) => abcjs.parseOnly(abc);
src/Abcjs/Render.purs+54−21
@@ -1,38 +1,71 @@
11module Abcjs.Render
2− ( RenderParams
2+ ( RenderData
3+ , RenderParams
4+ , ClickListener
35 , defaultRenderParams
46 , renderAbc
57 , parseOnly
68 ) where
79
8−import Abcjs.Types (Abc, Selector, TuneObject)
10+import Abcjs.Types (AbcElem, Abc, AddClasses(..), Analysis, Classes, DragContext, Pixels(..), Responsive, Scale(..), Selector, TuneNumber, TuneObject)
11+import Data.Unit (Unit)
912import Effect (Effect)
10−import Effect.Uncurried (EffectFn3, runEffectFn3)
13+import Effect.Uncurried (EffectFn5, EffectFn6, mkEffectFn6, runEffectFn5)
14+import Prim.Row (class Union)
15+import Web.UIEvent.MouseEvent (MouseEvent)
1116
12−-- | The subset of abcjs render options most relevant to a game.
13−-- | `responsive: "resize"` makes the SVG scale to its container,
14−-- | `add_classes: true` tags each element so notes are stylable/targetable.
17+-- | The abcjs render options that have defaults. `add_classes` tags elements
18+-- | for styling/targeting.
19+type RenderData =
20+ ( scale :: Scale
21+ , staffwidth :: Pixels
22+ , add_classes :: AddClasses
23+ )
24+
25+-- | Fired when the user clicks a notation element: which element, which tune,
26+-- | its CSS classes, abcjs's analysis, a drag context, and the DOM event.
27+type ClickListener =
28+ AbcElem -> TuneNumber -> Classes -> Analysis -> DragContext -> MouseEvent -> Effect Unit
29+
30+-- | Everything `renderAbc` accepts: the defaulted data, plus two opt-in extras —
31+-- | `responsive: resize` scales the SVG to its container (absent = native, fixed
32+-- | size); a `clickListener` makes notes clickable (absent = non-interactive).
33+-- | abcjs handles both absences natively.
1534type RenderParams =
16− { scale :: Number
17− , staffwidth :: Number
18− , responsive :: String
19− , add_classes :: Boolean
20− }
35+ ( responsive :: Responsive
36+ , clickListener :: ClickListener
37+ | RenderData
38+ )
2139
22−defaultRenderParams :: RenderParams
40+defaultRenderParams :: { | RenderData }
2341defaultRenderParams =
24− { scale: 1.0
25− , staffwidth: 740.0
26− , responsive: "resize"
27− , add_classes: true
42+ { scale: Scale 1.0
43+ , staffwidth: Pixels 740.0
44+ , add_classes: AddClasses true
2845 }
2946
30−-- | Draw ABC notation into the element matched by a selector.
31−-- | Returns the rendered tunes to feed into the synth and timing APIs.
32−renderAbc :: Selector -> Abc -> RenderParams -> Effect (Array TuneObject)
33−renderAbc = runEffectFn3 renderAbcImpl
47+-- | Draw ABC notation into the element matched by a selector. Pass only the
48+-- | params you want to override (`{}`, `{ scale: Scale 2.0 }`,
49+-- | `{ clickListener: \el _ _ _ _ ev -> … }`); the data fields fall back to
50+-- | `defaultRenderParams`. Returns the rendered tunes.
51+renderAbc
52+ :: forall provided rest
53+ . Union provided rest RenderParams
54+ => Selector -> Abc -> { | provided } -> Effect (Array TuneObject)
55+renderAbc selector abc provided =
56+ runEffectFn5 renderAbcImpl mkEffectFn6 selector abc defaultRenderParams provided
3457
35−foreign import renderAbcImpl :: EffectFn3 Selector Abc RenderParams (Array TuneObject)
58+-- | The FFI merges the data defaults under the provided record and, when a
59+-- | `clickListener` was supplied, uncurries it for abcjs.
60+foreign import renderAbcImpl
61+ :: forall provided
62+ . EffectFn5
63+ (ClickListener -> EffectFn6 AbcElem TuneNumber Classes Analysis DragContext MouseEvent Unit)
64+ Selector
65+ Abc
66+ { | RenderData }
67+ { | provided }
68+ (Array TuneObject)
3669
3770-- | Parse ABC without drawing anything — no DOM required.
3871-- | Useful for generating and inspecting phrases in game logic.
src/Abcjs/Synth.js+17−1
@@ -4,8 +4,17 @@ export const supportsAudio = () => abcjs.synth.supportsAudio();
44
55export const newSynthController = () => new abcjs.synth.SynthController();
66
7−export const loadImpl = (selector, cursorControl, options, controller) =>
7+export const playEventImpl = (notes, millisecondsPerMeasure) =>
8+ abcjs.synth.playEvent(notes, [], millisecondsPerMeasure);
9+
10+export const loadImpl = (mkEffectFn1, mkEffectFn3, selector, cursor, options, controller) => {
11+ const cursorControl = {};
12+ if (cursor.onStart !== undefined) cursorControl.onStart = cursor.onStart;
13+ if (cursor.onEvent !== undefined) cursorControl.onEvent = mkEffectFn1(cursor.onEvent);
14+ if (cursor.onBeat !== undefined) cursorControl.onBeat = mkEffectFn3(cursor.onBeat);
15+ if (cursor.onFinished !== undefined) cursorControl.onFinished = cursor.onFinished;
816 controller.load(selector, cursorControl, options);
17+};
918
1019export const setTuneImpl = (tune, userAction, controller) =>
1120 controller.setTune(tune, userAction);
@@ -19,6 +28,13 @@ export const restartImpl = (controller) => controller.restart();
1928export const seekSecondsImpl = (seconds, controller) =>
2029 controller.seek(seconds, "seconds");
2130
31+export const seekBeatsImpl = (beats, controller) => controller.seek(beats, "beats");
32+
33+export const seekFractionImpl = (fraction, controller) =>
34+ controller.seek(fraction, "percent");
35+
36+export const downloadImpl = (fileName, controller) => controller.download(fileName);
37+
2238export const setWarpImpl = (percent, controller) => controller.setWarp(percent);
2339
2440export const toggleLoopImpl = (controller) => controller.toggleLoop();
src/Abcjs/Synth.purs+104−33
@@ -5,53 +5,63 @@ module Abcjs.Synth
55 , defaultControlOptions
66 , supportsAudio
77 , newSynthController
8+ , PlayNote
9+ , playEvent
810 , load
911 , setTune
1012 , play
1113 , pause
1214 , restart
1315 , seekSeconds
16+ , seekBeats
17+ , seekFraction
1418 , setWarp
1519 , toggleLoop
20+ , download
1621 ) where
1722
1823import Prelude
1924
20−import Abcjs.Types (Selector, TimingEvent, TuneObject)
21−import Control.Promise (Promise, toAffE)
25+import Abcjs.Internal (withDefaults)
26+import Abcjs.Types (BeatNumber, Display(..), FileName, Fraction, Instrument, Measures, MidiNote, Percent, Selector, TimingEvent, TotalBeats, TuneObject, UserAction, Velocity)
27+import Data.Time.Duration (Milliseconds, Seconds)
2228import Effect (Effect)
2329import Effect.Aff (Aff)
24−import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, EffectFn4, runEffectFn1, runEffectFn2, runEffectFn3, runEffectFn4)
30+import Promise (Promise)
31+import Promise.Aff (toAffE)
32+import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, EffectFn6, mkEffectFn1, mkEffectFn3, runEffectFn1, runEffectFn2, runEffectFn3, runEffectFn6)
33+import Prim.Row (class Union)
2534
2635-- | The high-level player: a transport plus an optional on-page control panel.
2736foreign import data SynthController :: Type
2837
29−-- | The callbacks abcjs fires while a tune plays — this is the hook that
30−-- | drives the follow-along cursor and any per-note/per-beat game logic.
31−-- | `onBeat` receives (beatNumber, totalBeats, totalTime).
38+-- | The callbacks abcjs fires while a tune plays — this is the hook that drives
39+-- | the follow-along cursor and any per-note/per-beat game logic, as a row so a
40+-- | caller wires up only the ones they need (abcjs ignores the ones they leave
41+-- | out). `onBeat` receives (beatNumber, totalBeats, totalTime).
3242type CursorControl =
33− { onStart :: Effect Unit
34− , onEvent :: EffectFn1 TimingEvent Unit
35− , onBeat :: EffectFn3 Number Number Number Unit
43+ ( onStart :: Effect Unit
44+ , onEvent :: TimingEvent -> Effect Unit
45+ , onBeat :: BeatNumber -> TotalBeats -> Seconds -> Effect Unit
3646 , onFinished :: Effect Unit
37− }
47+ )
3848
3949-- | Which buttons the rendered control panel shows.
4050type ControlOptions =
41− { displayPlay :: Boolean
42− , displayProgress :: Boolean
43− , displayLoop :: Boolean
44− , displayRestart :: Boolean
45− , displayWarp :: Boolean
46− }
47−
48−defaultControlOptions :: ControlOptions
51+ ( displayPlay :: Display
52+ , displayProgress :: Display
53+ , displayLoop :: Display
54+ , displayRestart :: Display
55+ , displayWarp :: Display
56+ )
57+
58+defaultControlOptions :: { | ControlOptions }
4959defaultControlOptions =
50− { displayPlay: true
51− , displayProgress: true
52− , displayLoop: true
53− , displayRestart: true
54− , displayWarp: true
60+ { displayPlay: Display true
61+ , displayProgress: Display true
62+ , displayLoop: Display true
63+ , displayRestart: Display true
64+ , displayWarp: Display true
5565 }
5666
5767-- | Whether this browser can play synthesized audio at all.
@@ -59,18 +69,61 @@ foreign import supportsAudio :: Effect Boolean
5969
6070foreign import newSynthController :: Effect SynthController
6171
62−-- | Attach the controller to a control-panel element and a cursor control.
63−load :: Selector -> CursorControl -> ControlOptions -> SynthController -> Effect Unit
64−load = runEffectFn4 loadImpl
72+-- | A note to sound immediately via `playEvent` — e.g. on player input.
73+-- | `duration` is in whole-note units (a quarter note in 4/4 is `Measures 0.25`).
74+type PlayNote =
75+ { pitch :: MidiNote
76+ , duration :: Measures
77+ , volume :: Velocity
78+ , instrument :: Instrument
79+ }
80+
81+-- | Sound a chord/note right now, scaling durations by `millisecondsPerMeasure`.
82+-- | Needs an active audio context (e.g. after a `SynthController` has run, or
83+-- | `supportsAudio` plus a user gesture). Resolves when playback is scheduled.
84+playEvent :: Array PlayNote -> Milliseconds -> Aff Unit
85+playEvent notes millisecondsPerMeasure =
86+ toAffE (runEffectFn2 playEventImpl notes millisecondsPerMeasure)
87+
88+foreign import playEventImpl :: EffectFn2 (Array PlayNote) Milliseconds (Promise Unit)
6589
66−foreign import loadImpl :: EffectFn4 Selector CursorControl ControlOptions SynthController Unit
90+-- | Attach the controller to a control-panel element and a cursor control.
91+-- | Wire up only the cursor callbacks you care about and only the panel buttons
92+-- | you want overridden; everything else is left to abcjs / the defaults.
93+load
94+ :: forall providedCursor restCursor providedOpts restOpts
95+ . Union providedCursor restCursor CursorControl
96+ => Union providedOpts restOpts ControlOptions
97+ => Selector
98+ -> { | providedCursor }
99+ -> { | providedOpts }
100+ -> SynthController
101+ -> Effect Unit
102+load selector cursor providedOpts controller =
103+ runEffectFn6 loadImpl mkEffectFn1 mkEffectFn3 selector cursor options controller
104+ where
105+ options = withDefaults defaultControlOptions providedOpts
106+
107+-- | The FFI receives the curried-to-uncurried converters and the partial cursor
108+-- | record, and applies a converter per field only where the caller supplied
109+-- | one — so omitted callbacks stay absent and abcjs simply skips them.
110+foreign import loadImpl
111+ :: forall providedCursor
112+ . EffectFn6
113+ ((TimingEvent -> Effect Unit) -> EffectFn1 TimingEvent Unit)
114+ ((BeatNumber -> TotalBeats -> Seconds -> Effect Unit) -> EffectFn3 BeatNumber TotalBeats Seconds Unit)
115+ Selector
116+ { | providedCursor }
117+ { | ControlOptions }
118+ SynthController
119+ Unit
67120
68121-- | Prime a tune for playback. The `Boolean` says whether this was triggered
69122-- | by a user gesture, which lets the browser unlock the audio context.
70−setTune :: TuneObject -> Boolean -> SynthController -> Aff Unit
123+setTune :: TuneObject -> UserAction -> SynthController -> Aff Unit
71124setTune tune userAction controller = toAffE (runEffectFn3 setTuneImpl tune userAction controller)
72125
73−foreign import setTuneImpl :: EffectFn3 TuneObject Boolean SynthController (Promise Unit)
126+foreign import setTuneImpl :: EffectFn3 TuneObject UserAction SynthController (Promise Unit)
74127
75128-- | Toggle play/pause.
76129play :: SynthController -> Effect Unit
@@ -89,18 +142,36 @@ restart = runEffectFn1 restartImpl
89142foreign import restartImpl :: EffectFn1 SynthController Unit
90143
91144-- | Jump to a position given in seconds.
92−seekSeconds :: Number -> SynthController -> Effect Unit
145+seekSeconds :: Seconds -> SynthController -> Effect Unit
93146seekSeconds = runEffectFn2 seekSecondsImpl
94147
95−foreign import seekSecondsImpl :: EffectFn2 Number SynthController Unit
148+foreign import seekSecondsImpl :: EffectFn2 Seconds SynthController Unit
149+
150+-- | Jump to a position given as a beat number.
151+seekBeats :: BeatNumber -> SynthController -> Effect Unit
152+seekBeats = runEffectFn2 seekBeatsImpl
153+
154+foreign import seekBeatsImpl :: EffectFn2 BeatNumber SynthController Unit
155+
156+-- | Jump to a fraction of the tune (0.0 to 1.0).
157+seekFraction :: Fraction -> SynthController -> Effect Unit
158+seekFraction = runEffectFn2 seekFractionImpl
159+
160+foreign import seekFractionImpl :: EffectFn2 Fraction SynthController Unit
96161
97162-- | Set tempo as a percentage of the written tempo (100.0 = as written).
98−setWarp :: Number -> SynthController -> Effect Unit
163+setWarp :: Percent -> SynthController -> Effect Unit
99164setWarp = runEffectFn2 setWarpImpl
100165
101−foreign import setWarpImpl :: EffectFn2 Number SynthController Unit
166+foreign import setWarpImpl :: EffectFn2 Percent SynthController Unit
102167
103168toggleLoop :: SynthController -> Effect Unit
104169toggleLoop = runEffectFn1 toggleLoopImpl
105170
106171foreign import toggleLoopImpl :: EffectFn1 SynthController Unit
172+
173+-- | Download the rendered tune as a WAV file under the given name.
174+download :: FileName -> SynthController -> Effect Unit
175+download = runEffectFn2 downloadImpl
176+
177+foreign import downloadImpl :: EffectFn2 FileName SynthController Unit
src/Abcjs/Timing.js+7−2
@@ -1,7 +1,12 @@
11import abcjs from "abcjs";
22
3−export const newTimingCallbacksImpl = (visualObj, options) =>
4− new abcjs.TimingCallbacks(visualObj, options);
3+export const newTimingCallbacksImpl = (mkEffectFn1, mkEffectFn3, visualObj, provided) => {
4+ const options = {};
5+ if (provided.qpm !== undefined) options.qpm = provided.qpm;
6+ if (provided.eventCallback !== undefined) options.eventCallback = mkEffectFn1(provided.eventCallback);
7+ if (provided.beatCallback !== undefined) options.beatCallback = mkEffectFn3(provided.beatCallback);
8+ return new abcjs.TimingCallbacks(visualObj, options);
9+};
510
611export const startImpl = (tc) => tc.start();
712
src/Abcjs/Timing.purs+31−14
@@ -11,28 +11,45 @@ module Abcjs.Timing
1111
1212import Prelude
1313
14−import Abcjs.Types (TimingEvent, TuneObject)
14+import Abcjs.Types (BeatNumber, Fraction, Qpm, TimingEvent, TotalBeats, TuneObject)
1515import Data.Nullable (Nullable)
16+import Data.Time.Duration (Seconds)
1617import Effect (Effect)
17−import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, runEffectFn1, runEffectFn2)
18+import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, EffectFn4, mkEffectFn1, mkEffectFn3, runEffectFn1, runEffectFn2, runEffectFn4)
19+import Prim.Row (class Union)
1820
1921-- | A standalone animation timer over a rendered tune. Drives the cursor and
2022-- | game logic off note timing, independent of whether audio is playing.
2123foreign import data TimingCallbacks :: Type
2224
25+-- | Timer options as a row, so a caller supplies only what they need; anything
26+-- | omitted falls back to abcjs (no `qpm` means the tune's written tempo).
2327-- | `eventCallback` fires per note/rest/bar; abcjs passes `null` at the end of
2428-- | the tune, hence `Nullable`. `beatCallback` receives
25−-- | (beatNumber, totalBeats, totalTime). `qpm` overrides the tempo.
29+-- | (beatNumber, totalBeats, totalTime).
2630type TimingOptions =
27− { qpm :: Number
28− , eventCallback :: EffectFn1 (Nullable TimingEvent) Unit
29− , beatCallback :: EffectFn3 Number Number Number Unit
30− }
31−
32−newTimingCallbacks :: TuneObject -> TimingOptions -> Effect TimingCallbacks
33−newTimingCallbacks = runEffectFn2 newTimingCallbacksImpl
34−
35−foreign import newTimingCallbacksImpl :: EffectFn2 TuneObject TimingOptions TimingCallbacks
31+ ( qpm :: Qpm
32+ , eventCallback :: Nullable TimingEvent -> Effect Unit
33+ , beatCallback :: BeatNumber -> TotalBeats -> Seconds -> Effect Unit
34+ )
35+
36+newTimingCallbacks
37+ :: forall provided rest
38+ . Union provided rest TimingOptions
39+ => TuneObject -> { | provided } -> Effect TimingCallbacks
40+newTimingCallbacks tune options =
41+ runEffectFn4 newTimingCallbacksImpl mkEffectFn1 mkEffectFn3 tune options
42+
43+-- | The FFI gets the converters and the partial options, and converts each
44+-- | callback only where supplied — omitted fields are left off the abcjs object.
45+foreign import newTimingCallbacksImpl
46+ :: forall provided
47+ . EffectFn4
48+ ((Nullable TimingEvent -> Effect Unit) -> EffectFn1 (Nullable TimingEvent) Unit)
49+ ((BeatNumber -> TotalBeats -> Seconds -> Effect Unit) -> EffectFn3 BeatNumber TotalBeats Seconds Unit)
50+ TuneObject
51+ { | provided }
52+ TimingCallbacks
3653
3754start :: TimingCallbacks -> Effect Unit
3855start = runEffectFn1 startImpl
@@ -55,7 +72,7 @@ reset = runEffectFn1 resetImpl
5572foreign import resetImpl :: EffectFn1 TimingCallbacks Unit
5673
5774-- | Jump the cursor to a fraction of the tune (0.0 to 1.0).
58−setProgress :: Number -> TimingCallbacks -> Effect Unit
75+setProgress :: Fraction -> TimingCallbacks -> Effect Unit
5976setProgress = runEffectFn2 setProgressImpl
6077
61−foreign import setProgressImpl :: EffectFn2 Number TimingCallbacks Unit
78+foreign import setProgressImpl :: EffectFn2 Fraction TimingCallbacks Unit
src/Abcjs/Types.js+8
@@ -11,3 +11,11 @@ export const eventTop = (ev) => ev.top;
1111export const eventWidth = (ev) => ev.width;
1212
1313export const eventHeight = (ev) => ev.height;
14+
15+export const eventElements = (ev) => ev.elements;
16+
17+export const eventMidiPitches = (ev) => ev.midiPitches;
18+
19+export const abcElemStartChar = (el) => el.startChar;
20+
21+export const abcElemEndChar = (el) => el.endChar;
src/Abcjs/Types.purs+258−9
@@ -1,6 +1,29 @@
11module Abcjs.Types
22 ( Selector(..)
33 , Abc(..)
4+ , Pixels(..)
5+ , Scale(..)
6+ , Percent(..)
7+ , Qpm(..)
8+ , BeatNumber(..)
9+ , TotalBeats(..)
10+ , Fraction(..)
11+ , Measures(..)
12+ , LineCount(..)
13+ , MidiNote(..)
14+ , Velocity(..)
15+ , Instrument(..)
16+ , CharOffset(..)
17+ , TuneNumber(..)
18+ , Classes(..)
19+ , FileName(..)
20+ , Color(..)
21+ , MeasureStart(..)
22+ , Display(..)
23+ , AddClasses(..)
24+ , UserAction(..)
25+ , Responsive(..)
26+ , resize
427 , TuneObject
528 , tuneLineCount
629 , TimingEvent
@@ -10,20 +33,210 @@ module Abcjs.Types
1033 , eventTop
1134 , eventWidth
1235 , eventHeight
36+ , eventElements
37+ , MidiPitch
38+ , eventMidiPitches
39+ , AbcElem
40+ , abcElemStartChar
41+ , abcElemEndChar
42+ , Analysis
43+ , DragContext
1344 ) where
1445
46+import Prelude
47+
1548import Data.Newtype (class Newtype)
49+import Data.Time.Duration (Milliseconds)
50+import Web.DOM (Element)
51+
52+-- A leaf newtype per primitive so nothing in the API is a bare Number/Boolean/
53+-- String. They erase at runtime, so the FFI still hands abcjs raw values.
1654
17−-- | A CSS selector (or element id) naming where to draw, distinct from the
18−-- | music itself so the two can never be passed in the wrong order.
55+-- | The id of the element to draw into — abcjs resolves it with
56+-- | `getElementById`, so give the bare id ("staff"), not a CSS selector
57+-- | ("#staff").
1958newtype Selector = Selector String
2059
2160derive instance Newtype Selector _
61+derive newtype instance Eq Selector
62+derive newtype instance Show Selector
2263
2364-- | ABC notation source text — the thing you generate for the game.
2465newtype Abc = Abc String
2566
2667derive instance Newtype Abc _
68+derive newtype instance Eq Abc
69+derive newtype instance Show Abc
70+
71+-- | A length on the rendered SVG, in CSS pixels.
72+newtype Pixels = Pixels Number
73+
74+derive instance Newtype Pixels _
75+derive newtype instance Eq Pixels
76+derive newtype instance Ord Pixels
77+derive newtype instance Show Pixels
78+
79+-- | A unitless render zoom multiplier (1.0 = as engraved).
80+newtype Scale = Scale Number
81+
82+derive instance Newtype Scale _
83+derive newtype instance Eq Scale
84+derive newtype instance Ord Scale
85+derive newtype instance Show Scale
86+
87+-- | A percentage of the written tempo (100.0 = as written).
88+newtype Percent = Percent Number
89+
90+derive instance Newtype Percent _
91+derive newtype instance Eq Percent
92+derive newtype instance Ord Percent
93+derive newtype instance Show Percent
94+
95+-- | Tempo in quarter notes per minute.
96+newtype Qpm = Qpm Number
97+
98+derive instance Newtype Qpm _
99+derive newtype instance Eq Qpm
100+derive newtype instance Ord Qpm
101+derive newtype instance Show Qpm
102+
103+newtype BeatNumber = BeatNumber Number
104+
105+derive instance Newtype BeatNumber _
106+derive newtype instance Eq BeatNumber
107+derive newtype instance Ord BeatNumber
108+derive newtype instance Show BeatNumber
109+
110+newtype TotalBeats = TotalBeats Number
111+
112+derive instance Newtype TotalBeats _
113+derive newtype instance Eq TotalBeats
114+derive newtype instance Ord TotalBeats
115+derive newtype instance Show TotalBeats
116+
117+-- | A position along the tune, from 0.0 to 1.0.
118+newtype Fraction = Fraction Number
119+
120+derive instance Newtype Fraction _
121+derive newtype instance Eq Fraction
122+derive newtype instance Ord Fraction
123+derive newtype instance Show Fraction
124+
125+-- | A musical length in whole-note units (abcjs's internal duration measure).
126+newtype Measures = Measures Number
127+
128+derive instance Newtype Measures _
129+derive newtype instance Eq Measures
130+derive newtype instance Ord Measures
131+derive newtype instance Show Measures
132+
133+newtype LineCount = LineCount Int
134+
135+derive instance Newtype LineCount _
136+derive newtype instance Eq LineCount
137+derive newtype instance Ord LineCount
138+derive newtype instance Show LineCount
139+
140+-- | A MIDI note number (middle C = 60).
141+newtype MidiNote = MidiNote Int
142+
143+derive instance Newtype MidiNote _
144+derive newtype instance Eq MidiNote
145+derive newtype instance Ord MidiNote
146+derive newtype instance Show MidiNote
147+
148+-- | A MIDI velocity, 0–127.
149+newtype Velocity = Velocity Int
150+
151+derive instance Newtype Velocity _
152+derive newtype instance Eq Velocity
153+derive newtype instance Ord Velocity
154+derive newtype instance Show Velocity
155+
156+-- | A General MIDI instrument (program) number.
157+newtype Instrument = Instrument Int
158+
159+derive instance Newtype Instrument _
160+derive newtype instance Eq Instrument
161+derive newtype instance Ord Instrument
162+derive newtype instance Show Instrument
163+
164+-- | A character offset into the ABC source.
165+newtype CharOffset = CharOffset Int
166+
167+derive instance Newtype CharOffset _
168+derive newtype instance Eq CharOffset
169+derive newtype instance Ord CharOffset
170+derive newtype instance Show CharOffset
171+
172+-- | Which tune in the source a click landed on (0-based).
173+newtype TuneNumber = TuneNumber Int
174+
175+derive instance Newtype TuneNumber _
176+derive newtype instance Eq TuneNumber
177+derive newtype instance Ord TuneNumber
178+derive newtype instance Show TuneNumber
179+
180+-- | The space-joined CSS classes on a clicked element.
181+newtype Classes = Classes String
182+
183+derive instance Newtype Classes _
184+derive newtype instance Eq Classes
185+derive newtype instance Show Classes
186+
187+-- | A download file name, e.g. for rendered-audio export.
188+newtype FileName = FileName String
189+
190+derive instance Newtype FileName _
191+derive newtype instance Eq FileName
192+derive newtype instance Show FileName
193+
194+-- | A CSS colour string (e.g. "#1db954", "red") for recolouring notes.
195+newtype Color = Color String
196+
197+derive instance Newtype Color _
198+derive newtype instance Eq Color
199+derive newtype instance Show Color
200+
201+-- | Whether a timing event begins a new measure.
202+newtype MeasureStart = MeasureStart Boolean
203+
204+derive instance Newtype MeasureStart _
205+derive newtype instance Eq MeasureStart
206+derive newtype instance Show MeasureStart
207+
208+-- | Whether a control-panel button is shown.
209+newtype Display = Display Boolean
210+
211+derive instance Newtype Display _
212+derive newtype instance Eq Display
213+derive newtype instance Show Display
214+
215+-- | Whether abcjs tags rendered elements with CSS classes.
216+newtype AddClasses = AddClasses Boolean
217+
218+derive instance Newtype AddClasses _
219+derive newtype instance Eq AddClasses
220+derive newtype instance Show AddClasses
221+
222+-- | Whether playback was triggered by a user gesture (so the browser may
223+-- | unlock the audio context).
224+newtype UserAction = UserAction Boolean
225+
226+derive instance Newtype UserAction _
227+derive newtype instance Eq UserAction
228+derive newtype instance Show UserAction
229+
230+-- | The abcjs "responsive" mode. Use `resize` to make the SVG scale to its
231+-- | container.
232+newtype Responsive = Responsive String
233+
234+derive instance Newtype Responsive _
235+derive newtype instance Eq Responsive
236+derive newtype instance Show Responsive
237+
238+resize :: Responsive
239+resize = Responsive "resize"
27240
28241-- | A parsed/rendered tune, as returned by `renderAbc` or `parseOnly`.
29242-- | Opaque: pass it back into the synth and timing APIs.
@@ -32,7 +245,7 @@ foreign import data TuneObject :: Type
32245-- | How many lines of music the tune parsed to. Zero means abcjs found no
33246-- | notes (empty or unparseable input) — abcjs still returns a tune object,
34247-- | so this is how you tell a generated phrase actually parsed.
35−foreign import tuneLineCount :: TuneObject -> Int
248+foreign import tuneLineCount :: TuneObject -> LineCount
36249
37250-- | One playback event (a note, rest, or bar) handed to your callbacks.
38251-- | The accessors below pull out the fields a follow-along game needs:
@@ -40,16 +253,52 @@ foreign import tuneLineCount :: TuneObject -> Int
40253foreign import data TimingEvent :: Type
41254
42255-- | Milliseconds from the start of the tune at which this event sounds.
43−foreign import eventMilliseconds :: TimingEvent -> Number
256+foreign import eventMilliseconds :: TimingEvent -> Milliseconds
44257
45258-- | Whether this event begins a new measure (handy for bar-level scoring).
46−foreign import eventMeasureStart :: TimingEvent -> Boolean
259+foreign import eventMeasureStart :: TimingEvent -> MeasureStart
47260
48261-- | The on-screen bounding box of the event, for drawing a cursor.
49−foreign import eventLeft :: TimingEvent -> Number
262+foreign import eventLeft :: TimingEvent -> Pixels
263+
264+foreign import eventTop :: TimingEvent -> Pixels
265+
266+foreign import eventWidth :: TimingEvent -> Pixels
267+
268+foreign import eventHeight :: TimingEvent -> Pixels
269+
270+-- | The SVG nodes drawn for this event's note(s): one inner array per note,
271+-- | each holding the notehead/stem/etc. elements. Recolour these (set their
272+-- | `fill`) to highlight a note — see `Abcjs.Highlight`.
273+foreign import eventElements :: TimingEvent -> Array (Array Element)
274+
275+-- | The sounding notes at this event — the hook for scoring "did the player
276+-- | play the right note?". Empty for rests and bar events. `start`/`duration`
277+-- | are in whole-note units; `startChar`/`endChar` index back into the ABC.
278+type MidiPitch =
279+ { pitch :: MidiNote
280+ , volume :: Velocity
281+ , start :: Measures
282+ , duration :: Measures
283+ , instrument :: Instrument
284+ , startChar :: CharOffset
285+ , endChar :: CharOffset
286+ }
287+
288+foreign import eventMidiPitches :: TimingEvent -> Array MidiPitch
289+
290+-- | A clicked notation element (note, rest, bar, …) handed to a `ClickListener`.
291+-- | `startChar`/`endChar` index back into the ABC source — enough to tell which
292+-- | note was clicked.
293+foreign import data AbcElem :: Type
294+
295+foreign import abcElemStartChar :: AbcElem -> CharOffset
50296
51−foreign import eventTop :: TimingEvent -> Number
297+foreign import abcElemEndChar :: AbcElem -> CharOffset
52298
53−foreign import eventWidth :: TimingEvent -> Number
299+-- | abcjs's harmonic analysis of a clicked element. Opaque here.
300+foreign import data Analysis :: Type
54301
55−foreign import eventHeight :: TimingEvent -> Number
302+-- | The drag context for a clicked element (step/max/index/setSelection).
303+-- | Opaque here.
304+foreign import data DragContext :: Type
test/Test/Main.purs+29−4
@@ -2,8 +2,11 @@ module Test.Main where
22
33import Prelude
44
5−import Abcjs.Render (parseOnly)
6−import Abcjs.Types (Abc(..), tuneLineCount)
5+import Abcjs.Boomwhacker (boomwhackerColor)
6+import Abcjs.Internal (withDefaults)
7+import Abcjs.Notation (midiToAbc)
8+import Abcjs.Render (defaultRenderParams, parseOnly)
9+import Abcjs.Types (Abc(..), AddClasses(..), Color(..), LineCount(..), MidiNote(..), Pixels(..), Scale(..), tuneLineCount)
710import Data.Array (head, length)
811import Data.Maybe (Maybe(..))
912import Effect (Effect)
@@ -19,12 +22,34 @@ main = runSpecAndExitProcess [ consoleReporter ] do
1922 length (parseOnly validTune) `shouldEqual` 1
2023 it "parses a game-generated phrase to at least one line of music" do
2124 case head (parseOnly validTune) of
22− Just tune -> tuneLineCount tune `shouldSatisfy` (_ > 0)
25+ Just tune -> tuneLineCount tune `shouldSatisfy` (_ > LineCount 0)
2326 Nothing -> fail "expected a tune object"
2427 it "parses empty input to a tune with no music" do
2528 case head (parseOnly (Abc "")) of
26− Just tune -> tuneLineCount tune `shouldEqual` 0
29+ Just tune -> tuneLineCount tune `shouldEqual` LineCount 0
2730 Nothing -> fail "expected a tune object even for empty input"
31+ describe "Abcjs.Boomwhacker.boomwhackerColor" do
32+ it "maps each pitch class to its colour, octave-independent" do
33+ boomwhackerColor (MidiNote 60) `shouldEqual` Color "#e21c48" -- middle C
34+ boomwhackerColor (MidiNote 72) `shouldEqual` Color "#e21c48" -- C, an octave up
35+ boomwhackerColor (MidiNote 62) `shouldEqual` Color "#f99d1c" -- D
36+ boomwhackerColor (MidiNote 71) `shouldEqual` Color "#cf3e96" -- B
37+ describe "Abcjs.Notation.midiToAbc" do
38+ it "renders pitch class, accidental and octave" do
39+ midiToAbc (MidiNote 60) `shouldEqual` "C" -- middle C
40+ midiToAbc (MidiNote 61) `shouldEqual` "^C" -- C#4
41+ midiToAbc (MidiNote 72) `shouldEqual` "c" -- C5
42+ midiToAbc (MidiNote 48) `shouldEqual` "C," -- C3
43+ midiToAbc (MidiNote 84) `shouldEqual` "c'" -- C6
44+ midiToAbc (MidiNote 70) `shouldEqual` "^A" -- A#4
45+ describe "Abcjs.Internal.withDefaults" do
46+ it "keeps default render params for unspecified fields, newtypes and all" do
47+ withDefaults defaultRenderParams { scale: Scale 2.0 }
48+ `shouldEqual`
49+ { scale: Scale 2.0
50+ , staffwidth: Pixels 740.0
51+ , add_classes: AddClasses true
52+ }
2853
2954validTune :: Abc
3055validTune = Abc (twoMeasures "C D E F" "G A B c")