From cb38784f210b84574786f23f3bb3bb7f4e781714 Mon Sep 17 00:00:00 2001 From: AndyMac4321 <76612952+AndyMac4321@users.noreply.github.com> Date: Mon, 31 Mar 2025 03:54:46 -0700 Subject: [PATCH 1/6] Metronome Added metronome that plays sound Tick based on the BPM setting. --- .gitignore | 8 ++ public/audio/GHB/tick.mp3 | Bin 0 -> 1440 bytes public/audio/GHB/tick.wav | Bin 0 -> 1702 bytes public/audio/chanter/tick.mp3 | Bin 0 -> 1440 bytes public/audio/chanter/tick.wav | Bin 0 -> 1702 bytes public/images/beat-indicator-off.svg | 1 + public/images/beat-indicator-on.svg | 1 + public/images/play-fromselection.svg | 1 + public/images/play-loopedselection.svg | 1 + public/images/play-metronome.svg | 1 + public/images/stop-metronome.svg | 1 + src/PipeScore/Controller.ts | 4 + src/PipeScore/Events/Misc.ts | 1 + src/PipeScore/Events/Playback.ts | 22 +++- src/PipeScore/Note/index.ts | 11 +- src/PipeScore/Playback/impl.ts | 24 +++- src/PipeScore/Playback/resources.ts | 4 + src/PipeScore/Playback/sounds.ts | 43 ++++++- src/PipeScore/Playback/state.ts | 2 + src/PipeScore/Translations/English.ts | 4 + src/PipeScore/Translations/French.ts | 4 + src/PipeScore/Translations/index.ts | 4 + src/PipeScore/UI/view.ts | 159 ++++++++++++++++++------- src/styles/pipescore.scss | 24 ++++ todo.md | 4 +- 25 files changed, 271 insertions(+), 53 deletions(-) create mode 100644 public/audio/GHB/tick.mp3 create mode 100644 public/audio/GHB/tick.wav create mode 100644 public/audio/chanter/tick.mp3 create mode 100644 public/audio/chanter/tick.wav create mode 100644 public/images/beat-indicator-off.svg create mode 100644 public/images/beat-indicator-on.svg create mode 100644 public/images/play-fromselection.svg create mode 100644 public/images/play-loopedselection.svg create mode 100644 public/images/play-metronome.svg create mode 100644 public/images/stop-metronome.svg diff --git a/.gitignore b/.gitignore index 5410f6c2..7bbeff3e 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,11 @@ tsserver.log __pycache__ .DS_Store +.vscode +*.aup3-shm +*.aup3-wal +*.aup3 +public/audio/GHB/snare-roll-end.mp3 +public/audio/GHB/snare-roll-end.wav +public/audio/GHB/snare-roll-start.mp3 +public/audio/GHB/snare-roll-start.wav diff --git a/public/audio/GHB/tick.mp3 b/public/audio/GHB/tick.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..8672b0041a1ba8c93f98217f3a3623a8b64402ae GIT binary patch literal 1440 zcmezWdrAre0T7Xymkw0I55!Ci46F+nc99DHlMH}%1^77nx*F>l8W^zb1aiT^R6&Kg zxtD?Q0mvQ8_1nB5!b6w*|N9Cs{28Y*JowANz`?-4tFVB9Q9<@(`OY;=4Gat>%|NfU zrH2?XYTUAPZ)IfXZenOqWM^bJV)rEB;W_#LEV7>-{<^=y@Tc?4Kf8Y@JaGQce!uy_ zKV`wsE@ihj?6@?^;$Ht`X70(w4lQm?4Du2iQXI~_a8ho5*dy`evO$~Ai{KoCZ4N?- z1#GE1c|}7Oh&#wk@om17C@>{2Do#!Ae7dl1!mP`#ac2(xOXaRz?S4NrEGb~^e(#f$ z?M$}cI<1(uC+}VMpGU`*ZYVLe=Z#;`u(;sqOjql_x((N^wmo{VOKD-qwSIw-ag+93!tp6tM_GXEz zx;o3I{;5rx^k(ViYrCud&bfbQbI+?a%cV{8f@60&>n>UNe{+=XZp-wub@6BRBPXzr z*Z-z66tJ^2l?xZDufOp}nv+4HGb2kbIl|y3XQS=0-@Fo2SW+ho81Se5V^s5D(h^fp zEE0}b5YTLpdvWQK15M!x9#vbGix)=~EX?9M6YiZl-CFPTYc*NsHPbIF4QrFX!DY^C z9jGb%TC?GF(SxniLj76x_nn!f_qJ%;?!H^fR&8>nkNNj(+G+e+XQFd3$Z2YYvDdZY z3r{x4UT5Pgnx8)NW*2DT(OzkkIH0-*GN0GQ$#82`yIFerf1e**){u=6wt9s&RW C5)-ii literal 0 HcmV?d00001 diff --git a/public/audio/GHB/tick.wav b/public/audio/GHB/tick.wav new file mode 100644 index 0000000000000000000000000000000000000000..1a378d8cd3a917455625f7db29b99ef9016510c8 GIT binary patch literal 1702 zcmZ{k+fNf&7{I&xu-V6b^G!GL$!Np`qcMQQ%Ys~5u++-Xo255s%RoERGHs`~>6Es# zObZp-B9v>bg%Tt|A0!Ayj6qEdqDZ_y`e*F<7+T%U?(gB`oMdw5`+b)qZm6rv{w5II zyd?cg-$VZShd?0sMewV@w}v+Tu=`!0Rg-Eb`i7>RPKvVG9F9O>V8HKZ*(pa!bqg>lk+HmR5`71VK?8x4r%S`_9h({@1U&yB|Kh zeY>}p%bh-b^k_rF#fw6rQrXrflPQ%7g(V?|=LD^5yRC&d&Dst5?sT zZ*4t({NTazaxOPHsnZ=hcIs4hbwx#Wb!}};&9!S13CBHs`taf6;_7O#xUf(x-n*C2 z&(F`!PEU`IXEMp;$cWorRdxJ$P0fW1=gwWaR9jnDCla-_sZ=IYBC)aY?AfD7o10Ib ztgWrDKSC@nS}Y=wQmN6X)kdS)jN?`-Nje-f?eQ=S%Z5UcNGz5}q*C+qtE-!vTU+bv zOH1i=G|IAmzt89Oddj#F9*@_{FpuYPPt1BzT;@!JbQ|a``NHiJ< zxLgFG)n2)B;>5|56%}XCHa6ZuU|0vDy&XHCqeCW>%auyCy0cTO)#>yGovyk0%$dr{ zs;cVh`uf(^_I8MwBt4!`Xk=t!B9|)`mzP&o?%!WpT3jp??j2AlEG!fbfL!JCxg4Lb zY<6N|yi6vOPLGWxlkxcAAj^6@6ounzwOroWX*60aR;$HAk`x7hN+###mX->I+1YFs zGR|?~FvHMvf4{|IGU@f5oeBkpNu^?OON&t0+}zaE*x1m}h-hkRZWf84ZzK|_6vO0l zl}e-0BcL~0Tg76D1jAG+oz7&kTH$ZOU^JRYjE#+tPfpIv%+2L;`8-I04sVyw-$mqd zb8|B@)6?1PU8jVh8 zFm!jDOg%lly?uQ+Zbq0)W;2{E7J{(boldvg=Yx5Ku8PNphm*XCDmU#qoN13*O_02_MNn*HT=3mLW2TiwADu Q@g8IzZssXJa{s&e8w>qE7ytkO literal 0 HcmV?d00001 diff --git a/public/audio/chanter/tick.mp3 b/public/audio/chanter/tick.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..8672b0041a1ba8c93f98217f3a3623a8b64402ae GIT binary patch literal 1440 zcmezWdrAre0T7Xymkw0I55!Ci46F+nc99DHlMH}%1^77nx*F>l8W^zb1aiT^R6&Kg zxtD?Q0mvQ8_1nB5!b6w*|N9Cs{28Y*JowANz`?-4tFVB9Q9<@(`OY;=4Gat>%|NfU zrH2?XYTUAPZ)IfXZenOqWM^bJV)rEB;W_#LEV7>-{<^=y@Tc?4Kf8Y@JaGQce!uy_ zKV`wsE@ihj?6@?^;$Ht`X70(w4lQm?4Du2iQXI~_a8ho5*dy`evO$~Ai{KoCZ4N?- z1#GE1c|}7Oh&#wk@om17C@>{2Do#!Ae7dl1!mP`#ac2(xOXaRz?S4NrEGb~^e(#f$ z?M$}cI<1(uC+}VMpGU`*ZYVLe=Z#;`u(;sqOjql_x((N^wmo{VOKD-qwSIw-ag+93!tp6tM_GXEz zx;o3I{;5rx^k(ViYrCud&bfbQbI+?a%cV{8f@60&>n>UNe{+=XZp-wub@6BRBPXzr z*Z-z66tJ^2l?xZDufOp}nv+4HGb2kbIl|y3XQS=0-@Fo2SW+ho81Se5V^s5D(h^fp zEE0}b5YTLpdvWQK15M!x9#vbGix)=~EX?9M6YiZl-CFPTYc*NsHPbIF4QrFX!DY^C z9jGb%TC?GF(SxniLj76x_nn!f_qJ%;?!H^fR&8>nkNNj(+G+e+XQFd3$Z2YYvDdZY z3r{x4UT5Pgnx8)NW*2DT(OzkkIH0-*GN0GQ$#82`yIFerf1e**){u=6wt9s&RW C5)-ii literal 0 HcmV?d00001 diff --git a/public/audio/chanter/tick.wav b/public/audio/chanter/tick.wav new file mode 100644 index 0000000000000000000000000000000000000000..1a378d8cd3a917455625f7db29b99ef9016510c8 GIT binary patch literal 1702 zcmZ{k+fNf&7{I&xu-V6b^G!GL$!Np`qcMQQ%Ys~5u++-Xo255s%RoERGHs`~>6Es# zObZp-B9v>bg%Tt|A0!Ayj6qEdqDZ_y`e*F<7+T%U?(gB`oMdw5`+b)qZm6rv{w5II zyd?cg-$VZShd?0sMewV@w}v+Tu=`!0Rg-Eb`i7>RPKvVG9F9O>V8HKZ*(pa!bqg>lk+HmR5`71VK?8x4r%S`_9h({@1U&yB|Kh zeY>}p%bh-b^k_rF#fw6rQrXrflPQ%7g(V?|=LD^5yRC&d&Dst5?sT zZ*4t({NTazaxOPHsnZ=hcIs4hbwx#Wb!}};&9!S13CBHs`taf6;_7O#xUf(x-n*C2 z&(F`!PEU`IXEMp;$cWorRdxJ$P0fW1=gwWaR9jnDCla-_sZ=IYBC)aY?AfD7o10Ib ztgWrDKSC@nS}Y=wQmN6X)kdS)jN?`-Nje-f?eQ=S%Z5UcNGz5}q*C+qtE-!vTU+bv zOH1i=G|IAmzt89Oddj#F9*@_{FpuYPPt1BzT;@!JbQ|a``NHiJ< zxLgFG)n2)B;>5|56%}XCHa6ZuU|0vDy&XHCqeCW>%auyCy0cTO)#>yGovyk0%$dr{ zs;cVh`uf(^_I8MwBt4!`Xk=t!B9|)`mzP&o?%!WpT3jp??j2AlEG!fbfL!JCxg4Lb zY<6N|yi6vOPLGWxlkxcAAj^6@6ounzwOroWX*60aR;$HAk`x7hN+###mX->I+1YFs zGR|?~FvHMvf4{|IGU@f5oeBkpNu^?OON&t0+}zaE*x1m}h-hkRZWf84ZzK|_6vO0l zl}e-0BcL~0Tg76D1jAG+oz7&kTH$ZOU^JRYjE#+tPfpIv%+2L;`8-I04sVyw-$mqd zb8|B@)6?1PU8jVh8 zFm!jDOg%lly?uQ+Zbq0)W;2{E7J{(boldvg=Yx5Ku8PNphm*XCDmU#qoN13*O_02_MNn*HT=3mLW2TiwADu Q@g8IzZssXJa{s&e8w>qE7ytkO literal 0 HcmV?d00001 diff --git a/public/images/beat-indicator-off.svg b/public/images/beat-indicator-off.svg new file mode 100644 index 00000000..685a8830 --- /dev/null +++ b/public/images/beat-indicator-off.svg @@ -0,0 +1 @@ + diff --git a/public/images/beat-indicator-on.svg b/public/images/beat-indicator-on.svg new file mode 100644 index 00000000..8748686b --- /dev/null +++ b/public/images/beat-indicator-on.svg @@ -0,0 +1 @@ + diff --git a/public/images/play-fromselection.svg b/public/images/play-fromselection.svg new file mode 100644 index 00000000..7579301e --- /dev/null +++ b/public/images/play-fromselection.svg @@ -0,0 +1 @@ + diff --git a/public/images/play-loopedselection.svg b/public/images/play-loopedselection.svg new file mode 100644 index 00000000..e83ebc8f --- /dev/null +++ b/public/images/play-loopedselection.svg @@ -0,0 +1 @@ + diff --git a/public/images/play-metronome.svg b/public/images/play-metronome.svg new file mode 100644 index 00000000..0bf3e884 --- /dev/null +++ b/public/images/play-metronome.svg @@ -0,0 +1 @@ + diff --git a/public/images/stop-metronome.svg b/public/images/stop-metronome.svg new file mode 100644 index 00000000..03050377 --- /dev/null +++ b/public/images/stop-metronome.svg @@ -0,0 +1 @@ + diff --git a/src/PipeScore/Controller.ts b/src/PipeScore/Controller.ts index 8c0ca2f4..df4c717a 100644 --- a/src/PipeScore/Controller.ts +++ b/src/PipeScore/Controller.ts @@ -45,6 +45,8 @@ const state: State = { playback: { userPressedStop: false, playing: false, + playingMetronome:false, + beatIndicator:false, loading: true, cursor: null, }, @@ -136,6 +138,8 @@ function redraw() { loggedIn: state.isLoggedIn, loadingAudio: state.playback.loading, isPlaying: state.playback.playing, + isPlayingMetronome: state.playback.playingMetronome, + beatIndicator:state.playback.beatIndicator, zoomLevel: state.score.zoom, preview: state.preview, showingPageNumbers: state.score.showNumberOfPages, diff --git a/src/PipeScore/Events/Misc.ts b/src/PipeScore/Events/Misc.ts index d9ef61ef..44cb2ca6 100644 --- a/src/PipeScore/Events/Misc.ts +++ b/src/PipeScore/Events/Misc.ts @@ -179,6 +179,7 @@ export function exportPDF(): ScoreEvent { userPressedStop: false, loading: false, cursor: null, + playingMetronome:false, }, dispatch: async () => void 0, }; diff --git a/src/PipeScore/Events/Playback.ts b/src/PipeScore/Events/Playback.ts index c10623e6..f2bbb9a7 100644 --- a/src/PipeScore/Events/Playback.ts +++ b/src/PipeScore/Events/Playback.ts @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -import { playback } from '../Playback/impl'; +import { playback, playMetronome } from '../Playback/impl'; import { ScoreSelection } from '../Selection/score'; import type { State } from '../State'; import type { ID } from '../global/id'; @@ -68,7 +68,7 @@ export function playbackLoopingSelection(): ScoreEvent { export function stopPlayback(): ScoreEvent { return async (state: State) => { - if (state.playback.playing) { + if (state.playback.playing||state.playback.playingMetronome) { state.playback.userPressedStop = true; return Update.ViewChanged; } @@ -109,3 +109,21 @@ export function updateInstrument(instrument: Instrument): ScoreEvent { return Update.NoChange; }; } +export function startPlayMetronome(): ScoreEvent { + return async (state: State) => { + const playbackElements = state.score.play(); + await playMetronome( + state.playback, + ); + return Update.NoChange; + }; +} +export function updateBeatIndicator(beat:boolean): ScoreEvent { + return async (state: State) => { + if (beat !== state.playback.beatIndicator) { + state.playback.beatIndicator=beat; + return Update.ViewChanged; + } + return Update.NoChange; + }; +} \ No newline at end of file diff --git a/src/PipeScore/Note/index.ts b/src/PipeScore/Note/index.ts index ac533a11..a416f3b2 100644 --- a/src/PipeScore/Note/index.ts +++ b/src/PipeScore/Note/index.ts @@ -19,7 +19,11 @@ import type { IGracenote } from '../Gracenote'; import type { PlaybackItem } from '../Playback'; import type { Previews } from '../Preview/previews'; -import type { SavedNote, SavedNoteOrTriplet, SavedTriplet } from '../SavedModel'; +import type { + SavedNote, + SavedNoteOrTriplet, + SavedTriplet, +} from '../SavedModel'; import { Item } from '../global/id'; import type { Pitch } from '../global/pitch'; import { unreachable } from '../global/utils'; @@ -41,7 +45,10 @@ export abstract class INote abstract setLength(length: NoteLength): void; abstract hasPreview(): boolean; abstract makePreviewReal(): void; - abstract setPreview(gracenote: IGracenote | Pitch, noteBefore: INote | null): void; + abstract setPreview( + gracenote: IGracenote | Pitch, + noteBefore: INote | null + ): void; abstract removePreview(): void; abstract isPreview(): boolean; abstract makeUnPreview(): INote; diff --git a/src/PipeScore/Playback/impl.ts b/src/PipeScore/Playback/impl.ts index 4e2723c2..3316a418 100644 --- a/src/PipeScore/Playback/impl.ts +++ b/src/PipeScore/Playback/impl.ts @@ -38,7 +38,7 @@ import { sum, unreachable, } from '../global/utils'; -import { Drone, type SoundedMeasure, SoundedPitch, SoundedSilence } from './sounds'; +import { Drone, type SoundedMeasure, SoundedPitch, SoundedSilence, Tick } from './sounds'; import type { PlaybackState } from './state'; function shouldDeleteBecauseOfSecondTimings( @@ -407,3 +407,25 @@ async function playPitches( state.userPressedStop = false; dispatch(updateView()); } +export async function playMetronome( + state: PlaybackState, +) +{ + if (state.playing || state.loading) return; + + const context = new AudioContext(); + const tick = new Tick(context); + state.playingMetronome = true; + + tick.start(); + while(true) // Loop until user presses stop + { + await sleep(1000); + if(state.userPressedStop) + break; + } + tick.stop(); + state.userPressedStop = false; + state.playingMetronome = false; + dispatch(updateView()); +} diff --git a/src/PipeScore/Playback/resources.ts b/src/PipeScore/Playback/resources.ts index aa60a06c..01e6c101 100644 --- a/src/PipeScore/Playback/resources.ts +++ b/src/PipeScore/Playback/resources.ts @@ -38,6 +38,7 @@ type InstrumentResources = { highg: AudioResource; higha: AudioResource; drones: AudioResource | null; + tick:AudioResource; }; const ghb: InstrumentResources = { @@ -51,6 +52,7 @@ const ghb: InstrumentResources = { highg: new AudioResource('GHB/highg'), higha: new AudioResource('GHB/higha'), drones: new AudioResource('GHB/drones'), + tick:new AudioResource('GHB/tick'), }; const chanter: InstrumentResources = { @@ -64,6 +66,7 @@ const chanter: InstrumentResources = { highg: new AudioResource('chanter/highg'), higha: new AudioResource('chanter/higha'), drones: null, + tick: new AudioResource('chanter/tick'), }; /** @@ -87,6 +90,7 @@ function loadInstrumentResources( resources.highg.load(context), resources.higha.load(context), resources.drones?.load(context), + resources.tick.load(context), ]); } diff --git a/src/PipeScore/Playback/sounds.ts b/src/PipeScore/Playback/sounds.ts index 10dbf0e7..c93685fb 100644 --- a/src/PipeScore/Playback/sounds.ts +++ b/src/PipeScore/Playback/sounds.ts @@ -17,7 +17,7 @@ // Drone and SoundedPitch classes enable playback of drones and notes (including gracenotes). import { dispatch } from '../Controller'; -import { updatePlaybackCursor } from '../Events/Playback'; +import { updateBeatIndicator, updatePlaybackCursor } from '../Events/Playback'; import type { ID } from '../global/id'; import type { Pitch } from '../global/pitch'; import { settings } from '../global/settings'; @@ -59,6 +59,47 @@ export class Drone { } } +/** +/** + * Metronome tick playback. + */ +export class Tick { + private sample: Sample; + private stopped = false; + + constructor(context: AudioContext) { + const tick = getInstrumentResources().tick; + this.sample = tick && new Sample(tick, context); + } + + /** + * Start the metronome tick, looping forever until .stop() is called. + */ + async start() { + + const beatIndicatorDuration = 200; // duration of beat indicator on UI in ms + const tickLeadInDuration = 150 // Aligns the centre of the audio tick to the beat indicator in ms + while (!this.stopped) { + const duration = (1000 * 60) / settings.bpm; + this.sample.start(1); + await sleep(tickLeadInDuration); + dispatch(updateBeatIndicator(true)); + await sleep(beatIndicatorDuration); + dispatch(updateBeatIndicator(false)); + await sleep(duration - beatIndicatorDuration - tickLeadInDuration); + } + } + + /** + * Stop the tick. + */ + stop() { + if (this.sample) { + this.sample.stop(); + } + this.stopped = true; + } +} /** * Pitched note playback (used for notes and gracenotes). */ diff --git a/src/PipeScore/Playback/state.ts b/src/PipeScore/Playback/state.ts index 318dc799..581ec264 100644 --- a/src/PipeScore/Playback/state.ts +++ b/src/PipeScore/Playback/state.ts @@ -19,6 +19,8 @@ import type { ID } from '../global/id'; export type PlaybackState = { userPressedStop: boolean; playing: boolean; + playingMetronome: boolean; + beatIndicator:boolean; loading: boolean; // Location of playback cursor cursor: ID | null; diff --git a/src/PipeScore/Translations/English.ts b/src/PipeScore/Translations/English.ts index d6b2b691..fbb96c65 100644 --- a/src/PipeScore/Translations/English.ts +++ b/src/PipeScore/Translations/English.ts @@ -115,6 +115,9 @@ export const EnglishDocumentation: Documentation = { 'Play a preview of the score, starting at the currently selected note/bar. This will only work once the samples are downloaded (if the samples need to download, you will see a notice).', 'play-looping-selection': 'Play the currently selected part of the score, repeating forever.', + 'play-metronome':'Start the metronome', + 'stop-metronome':'Stop the metronome', + beatindicator:'Displays the beat of the metronome', stop: 'Stop the playback.', 'playback-speed': 'Control the playback speed (further right is faster).', 'harmony-volume': 'Control how loud the harmony plays (further right is louder).', @@ -203,6 +206,7 @@ export const EnglishTextItems: TextItems = { playFromBeginning: 'Play from Beginning', playFromSelection: 'Play from Selection', playLoopedSelection: 'Play looped Selection', + playMetronome: 'Play Metronome', stop: 'Stop', playbackOptions: 'Playback Options', beatsPerMinute: 'beats per minute', diff --git a/src/PipeScore/Translations/French.ts b/src/PipeScore/Translations/French.ts index ddbab4cc..264331f6 100644 --- a/src/PipeScore/Translations/French.ts +++ b/src/PipeScore/Translations/French.ts @@ -119,6 +119,9 @@ export const FrenchDocumentation: Documentation = { "Jouer un aperçu de la partition à partir de la note/mesure sélectionnée. Cela ne fonctionnera qu'une fois les échantillons téléchargés (si les échantillons doivent être téléchargés, vous verrez un avis).", 'play-looping-selection': 'Jouer la partie de la partition actuellement sélectionnée, en jouant en boucle.', + 'play-metronome': 'Démarrer le métronome', + 'stop-metronome': 'Arrêter le métronome', + beatindicator:'Affiche le battement du métronome', stop: 'Arrêter la lecture.', 'playback-speed': "Contrôler la vitesse de lecture (plus c'est à droite, plus c'est rapide).", @@ -209,6 +212,7 @@ export const FrenchTextItems: TextItems = { playFromBeginning: 'Jouer du Début', playFromSelection: 'Jouer de la Sélection', playLoopedSelection: 'Jouer Sélection en Boucle', + playMetronome: 'Jouer au métronome', stop: 'Arrêter', playbackOptions: 'Playback Options', beatsPerMinute: 'battements par minute', diff --git a/src/PipeScore/Translations/index.ts b/src/PipeScore/Translations/index.ts index c3fc20bc..f0a0ee5a 100644 --- a/src/PipeScore/Translations/index.ts +++ b/src/PipeScore/Translations/index.ts @@ -82,6 +82,8 @@ export type Documentation = { play: string; 'play-from-selection': string; 'play-looping-selection': string; + 'play-metronome': string; + 'stop-metronome': string; stop: string; 'playback-speed': string; 'harmony-volume': string; @@ -97,6 +99,7 @@ export type Documentation = { 'move-bar-to-previous-line': string; 'move-bar-to-next-line': string; 'nothing-hovered': string; + beatindicator: string; }; export type TextItems = { @@ -165,6 +168,7 @@ export type TextItems = { playFromBeginning: string; playFromSelection: string; playLoopedSelection: string; + playMetronome:string; stop: string; playbackOptions: string; beatsPerMinute: string; diff --git a/src/PipeScore/UI/view.ts b/src/PipeScore/UI/view.ts index 597f7a8a..5f1e96ef 100644 --- a/src/PipeScore/UI/view.ts +++ b/src/PipeScore/UI/view.ts @@ -59,6 +59,7 @@ import { setPlaybackBpm, startPlayback, startPlaybackAtSelection, + startPlayMetronome, stopPlayback, updateInstrument, } from '../Events/Playback'; @@ -108,6 +109,8 @@ export interface UIState { loggedIn: boolean; loadingAudio: boolean; isPlaying: boolean; + isPlayingMetronome: boolean; + beatIndicator:boolean, selectedGracenote: IGracenote | null; selectedStaves: IStave[]; selectedMeasures: IMeasure[]; @@ -921,36 +924,62 @@ export default function render(state: UIState): m.Children { help( 'play', m( - 'button.double-width.text', - { - disabled: state.isPlaying, + 'button', + { + disabled: state.isPlaying || state.isPlayingMetronome, onclick: () => state.dispatch(startPlayback()), - }, - text('playFromBeginning') + class: 'play-button', + } ), state.dispatch ), help( 'play-from-selection', m( - 'button.double-width.text', - { - disabled: state.isPlaying || !barsSelected, + 'button', + { + disabled: state.isPlaying || !barsSelected || state.isPlayingMetronome, onclick: () => state.dispatch(startPlaybackAtSelection()), + class: 'play-fromselection', }, - text('playFromSelection') ), state.dispatch ), help( 'play-looping-selection', m( - 'button.double-width.text', - { - disabled: state.isPlaying || state.selectedNotes.length === 0, + 'button', + { + disabled: state.isPlaying || state.selectedNotes.length === 0 || state.isPlayingMetronome, onclick: () => state.dispatch(playbackLoopingSelection()), + class: 'play-loopedselection', + }, + ), + state.dispatch + ), + help( + state.isPlayingMetronome ? 'stop' : 'play-metronome', + m( + 'button', + { + disabled: state.isPlaying || state.isPlayingMetronome, + onclick: () => + state.dispatch( + startPlayMetronome() + ), + class: 'play-metronome', + } + ), + state.dispatch + ), + help( + 'beatindicator', + m( + 'button', + { + disabled: !state.isPlayingMetronome, + class: state.beatIndicator ? 'beat-indicator-on' : 'beat-indicator-off', }, - text('playLoopedSelection') ), state.dispatch ), @@ -959,10 +988,10 @@ export default function render(state: UIState): m.Children { m( 'button', { - disabled: !state.isPlaying, + disabled: !state.isPlaying && !state.isPlayingMetronome, onclick: () => state.dispatch(stopPlayback()), + class: 'stop-button', }, - text('stop') ), state.dispatch ), @@ -1023,33 +1052,33 @@ export default function render(state: UIState): m.Children { ), ]), ]), - m('section', [ - m('h2', text('instrument')), + m('section',[ + m('div.section-content.vertical', [ + m('h2', text('instrument')), - m( - 'label', - m('input', { - type: 'radio', - name: 'instrument', - disabled: state.isPlaying, - checked: settings.instrument === Instrument.GHB, - onchange: () => state.dispatch(updateInstrument(Instrument.GHB)), - value: '', - }), - text('instrumentPipes') - ), - m( - 'label', - m('input', { - type: 'radio', - name: 'instrument', - disabled: state.isPlaying, - checked: settings.instrument === Instrument.Chanter, - onchange: () => state.dispatch(updateInstrument(Instrument.Chanter)), - value: 'pc', - }), - text('instrumentPC') - ), + m('label', + m('input', { + type: 'radio', + name: 'instrument', + disabled: state.isPlaying, + checked: settings.instrument === Instrument.GHB, + onchange: () => state.dispatch(updateInstrument(Instrument.GHB)), + value: '', + }), + text('instrumentPipes') + ), + m('label', + m('input', { + type: 'radio', + name: 'instrument', + disabled: state.isPlaying, + checked: settings.instrument === Instrument.Chanter, + onchange: () => state.dispatch(updateInstrument(Instrument.Chanter)), + value: 'pc', + }), + text('instrumentPC') + ), + ]), ]), ]; @@ -1369,11 +1398,12 @@ function mobileView(state: UIState): m.Children { m('section', [ m( 'div.section-content', - { class: state.isPlaying ? 'play-button' : 'stop-button' }, + { class: state.isPlaying ? 'stop-button':'play-button' }, [ help( state.isPlaying ? 'stop' : 'play', m('button', { + disabled: state.isPlayingMetronome, onclick: () => state.dispatch( state.isPlaying @@ -1389,6 +1419,43 @@ function mobileView(state: UIState): m.Children { ] ), ]), + m( + 'div.section-content', + { class: state.isPlayingMetronome ? 'stop-metronome' :'play-metronome'}, + [ + help( + state.isPlaying ? 'stop-metronome' : 'play-metronome', + m('button', { + disabled: state.isPlaying, + onclick: () => + state.dispatch( + state.isPlayingMetronome + ? stopPlayback() + : startPlayMetronome() + ), + class: state.isPlayingMetronome ? 'stop-metronome' : 'play-metronome', + }), + state.dispatch + ), + ] + ), + m( + 'div.section-content', + { class: state.beatIndicator ? 'beat-indicator-on' : 'beat-indicator-off'}, + [ + help( + 'beatindicator', + m( + 'button', + { + disabled: !state.isPlayingMetronome, + class: state.beatIndicator ? 'beat-indicator-on' : 'beat-indicator-off', + }, + ), + state.dispatch + ), + ] + ), m('div.section-content', [ m('input', { type: 'range', @@ -1420,9 +1487,9 @@ function mobileView(state: UIState): m.Children { ]), state.dispatch ), - m('section', [ - m('h2', text('instrument')), - + ]), + m('div.section-content', [ + m('div.section-content.vertical', [ m( 'label', m('input', { @@ -1435,6 +1502,8 @@ function mobileView(state: UIState): m.Children { }), text('instrumentPipes') ), + ]), + m('div.section-content.vertical', [ m( 'label', m('input', { diff --git a/src/styles/pipescore.scss b/src/styles/pipescore.scss index f0213464..a7c74e6c 100644 --- a/src/styles/pipescore.scss +++ b/src/styles/pipescore.scss @@ -393,3 +393,27 @@ label.text-coord { .stop-button { background-image: url('../images/stop.svg'); } + +.play-metronome { + background-image: url('../images/play-metronome.svg'); +} + +.stop-metronome { + background-image: url('../images/stop-metronome.svg'); +} + +.play-fromselection { + background-image: url('../images/play-fromselection.svg'); +} + +.play-loopedselection { + background-image: url('../images/play-loopedselection.svg'); +} + +.beat-indicator-on { + background-image: url('../images/beat-indicator-on.svg'); +} + +.beat-indicator-off { + background-image: url('../images/beat-indicator-off.svg'); +} \ No newline at end of file diff --git a/todo.md b/todo.md index b918a7c5..477b8e65 100644 --- a/todo.md +++ b/todo.md @@ -14,10 +14,10 @@ - [ ] Note in docs about keyboard-based - [x] Fix harmony playback - [ ] Importing tunes into other scores -- [ ] Count ins +- [ ] Count ins / Quick march attack and Slow march attack - [ ] Password change - [ ] Chanter playback -- [ ] Metronome while playing +- [x] Metronome while playing ## Bugs to fix From b77acaa4e9cb0210bd94076a8a06adc943114313 Mon Sep 17 00:00:00 2001 From: AndyMac4321 <76612952+AndyMac4321@users.noreply.github.com> Date: Mon, 31 Mar 2025 04:08:54 -0700 Subject: [PATCH 2/6] Prettier Made prettier --- src/PipeScore/Controller.ts | 14 +- src/PipeScore/Events/Misc.ts | 8 +- src/PipeScore/Events/Playback.ts | 12 +- src/PipeScore/Playback/impl.ts | 43 ++++-- src/PipeScore/Playback/index.ts | 17 ++- src/PipeScore/Playback/resources.ts | 8 +- src/PipeScore/Playback/sounds.ts | 22 ++-- src/PipeScore/Playback/state.ts | 2 +- src/PipeScore/UI/view.ts | 195 ++++++++++++++++------------ 9 files changed, 193 insertions(+), 128 deletions(-) diff --git a/src/PipeScore/Controller.ts b/src/PipeScore/Controller.ts index df4c717a..c238648c 100644 --- a/src/PipeScore/Controller.ts +++ b/src/PipeScore/Controller.ts @@ -45,8 +45,8 @@ const state: State = { playback: { userPressedStop: false, playing: false, - playingMetronome:false, - beatIndicator:false, + playingMetronome: false, + beatIndicator: false, loading: true, cursor: null, }, @@ -139,7 +139,7 @@ function redraw() { loadingAudio: state.playback.loading, isPlaying: state.playback.playing, isPlayingMetronome: state.playback.playingMetronome, - beatIndicator:state.playback.beatIndicator, + beatIndicator: state.playback.beatIndicator, zoomLevel: state.score.zoom, preview: state.preview, showingPageNumbers: state.score.showNumberOfPages, @@ -154,9 +154,13 @@ function redraw() { state.selection.gracenote(state.score)) || null, selectedText: - state.selection instanceof TextSelection ? state.selection.text : null, + state.selection instanceof TextSelection + ? state.selection.text + : null, selectedTiming: - state.selection instanceof TimingSelection ? state.selection.timing : null, + state.selection instanceof TimingSelection + ? state.selection.timing + : null, isLandscape: state.score.landscape, selectedStaves: (state.selection instanceof ScoreSelection && diff --git a/src/PipeScore/Events/Misc.ts b/src/PipeScore/Events/Misc.ts index 44cb2ca6..d5f99ce6 100644 --- a/src/PipeScore/Events/Misc.ts +++ b/src/PipeScore/Events/Misc.ts @@ -179,7 +179,7 @@ export function exportPDF(): ScoreEvent { userPressedStop: false, loading: false, cursor: null, - playingMetronome:false, + playingMetronome: false, }, dispatch: async () => void 0, }; @@ -294,7 +294,11 @@ export function exportPDF(): ScoreEvent { export function download(): ScoreEvent { return async (state: State) => { const json = state.score.toJSON(); - saveFile(`${state.score.name()}.pipescore`, JSON.stringify(json), 'text/json'); + saveFile( + `${state.score.name()}.pipescore`, + JSON.stringify(json), + 'text/json' + ); return Update.NoChange; }; diff --git a/src/PipeScore/Events/Playback.ts b/src/PipeScore/Events/Playback.ts index f2bbb9a7..09a0c0ee 100644 --- a/src/PipeScore/Events/Playback.ts +++ b/src/PipeScore/Events/Playback.ts @@ -68,7 +68,7 @@ export function playbackLoopingSelection(): ScoreEvent { export function stopPlayback(): ScoreEvent { return async (state: State) => { - if (state.playback.playing||state.playback.playingMetronome) { + if (state.playback.playing || state.playback.playingMetronome) { state.playback.userPressedStop = true; return Update.ViewChanged; } @@ -112,18 +112,16 @@ export function updateInstrument(instrument: Instrument): ScoreEvent { export function startPlayMetronome(): ScoreEvent { return async (state: State) => { const playbackElements = state.score.play(); - await playMetronome( - state.playback, - ); + await playMetronome(state.playback); return Update.NoChange; }; } -export function updateBeatIndicator(beat:boolean): ScoreEvent { +export function updateBeatIndicator(beat: boolean): ScoreEvent { return async (state: State) => { if (beat !== state.playback.beatIndicator) { - state.playback.beatIndicator=beat; + state.playback.beatIndicator = beat; return Update.ViewChanged; } return Update.NoChange; }; -} \ No newline at end of file +} diff --git a/src/PipeScore/Playback/impl.ts b/src/PipeScore/Playback/impl.ts index 3316a418..2d42fcf4 100644 --- a/src/PipeScore/Playback/impl.ts +++ b/src/PipeScore/Playback/impl.ts @@ -38,7 +38,13 @@ import { sum, unreachable, } from '../global/utils'; -import { Drone, type SoundedMeasure, SoundedPitch, SoundedSilence, Tick } from './sounds'; +import { + Drone, + type SoundedMeasure, + SoundedPitch, + SoundedSilence, + Tick, +} from './sounds'; import type { PlaybackState } from './state'; function shouldDeleteBecauseOfSecondTimings( @@ -206,7 +212,9 @@ function expandRepeats( } // Append the item to the current measure/part in the output - if (!shouldDeleteBecauseOfSecondTimings(inputIndex, timings, repeating)) { + if ( + !shouldDeleteBecauseOfSecondTimings(inputIndex, timings, repeating) + ) { nlast(output).parts[partIndex].push(item); } @@ -236,7 +244,8 @@ function expandRepeats( repeatStartIndex = measureIndex; } else if (measure.repeatEnd && measureIndex > repeatEndIndex) { // If the measure has an end repeat, then set measureIndex back to repeatStartIndex - timingOverRepeat = timings.find((t) => t.in(inputIndexAfterMeasure)) || null; + timingOverRepeat = + timings.find((t) => t.in(inputIndexAfterMeasure)) || null; repeatEndIndex = measureIndex; // Go back to repeat measureIndex = repeatStartIndex - 1; @@ -300,7 +309,9 @@ function getSoundedPitches( switch (e.type) { case 'note': { const duration = e.duration - currentGracenoteDuration; - soundedPart.push(new SoundedPitch(e.pitch, duration, ctx, currentID)); + soundedPart.push( + new SoundedPitch(e.pitch, duration, ctx, currentID) + ); currentGracenoteDuration = 0; break; } @@ -371,10 +382,18 @@ async function playPitches( end: ID | null, loop: boolean ) { - const measuresToPlay = getSoundedPitches(measures, timings, context, start, end); + const measuresToPlay = getSoundedPitches( + measures, + timings, + context, + start, + end + ); const numberOfItems = sum( - measuresToPlay.flatMap((measure) => measure.parts.flatMap((part) => part.length)) + measuresToPlay.flatMap((measure) => + measure.parts.flatMap((part) => part.length) + ) ); if (numberOfItems === 0) { @@ -407,10 +426,7 @@ async function playPitches( state.userPressedStop = false; dispatch(updateView()); } -export async function playMetronome( - state: PlaybackState, -) -{ +export async function playMetronome(state: PlaybackState) { if (state.playing || state.loading) return; const context = new AudioContext(); @@ -418,11 +434,10 @@ export async function playMetronome( state.playingMetronome = true; tick.start(); - while(true) // Loop until user presses stop - { + while (true) { + // Loop until user presses stop await sleep(1000); - if(state.userPressedStop) - break; + if (state.userPressedStop) break; } tick.stop(); state.userPressedStop = false; diff --git a/src/PipeScore/Playback/index.ts b/src/PipeScore/Playback/index.ts index 87aebde6..27098a29 100644 --- a/src/PipeScore/Playback/index.ts +++ b/src/PipeScore/Playback/index.ts @@ -49,8 +49,15 @@ export type PlaybackGracenote = { * @param children playback items that go between the object start and object end * @returns updated list of PlaybackItems, including object */ -export function playbackObject(id: ID, children: PlaybackItem[]): PlaybackItem[] { - return [{ type: 'object-start', id }, ...children, { type: 'object-end', id }]; +export function playbackObject( + id: ID, + children: PlaybackItem[] +): PlaybackItem[] { + return [ + { type: 'object-start', id }, + ...children, + { type: 'object-end', id }, + ]; } /** @@ -100,7 +107,11 @@ export class PlaybackMeasure { public repeatStart: boolean; public repeatEnd: boolean; - constructor(items: PlaybackItem[][], repeatStart: boolean, repeatEnd: boolean) { + constructor( + items: PlaybackItem[][], + repeatStart: boolean, + repeatEnd: boolean + ) { this.parts = items; this.repeatStart = repeatStart; this.repeatEnd = repeatEnd; diff --git a/src/PipeScore/Playback/resources.ts b/src/PipeScore/Playback/resources.ts index 01e6c101..05ed8d17 100644 --- a/src/PipeScore/Playback/resources.ts +++ b/src/PipeScore/Playback/resources.ts @@ -38,7 +38,7 @@ type InstrumentResources = { highg: AudioResource; higha: AudioResource; drones: AudioResource | null; - tick:AudioResource; + tick: AudioResource; }; const ghb: InstrumentResources = { @@ -52,7 +52,7 @@ const ghb: InstrumentResources = { highg: new AudioResource('GHB/highg'), higha: new AudioResource('GHB/higha'), drones: new AudioResource('GHB/drones'), - tick:new AudioResource('GHB/tick'), + tick: new AudioResource('GHB/tick'), }; const chanter: InstrumentResources = { @@ -102,8 +102,8 @@ export function getInstrumentResources(): InstrumentResources { return settings.instrument === Instrument.Chanter ? chanter : settings.instrument === Instrument.GHB - ? ghb - : unreachable(settings.instrument); + ? ghb + : unreachable(settings.instrument); } // This is in a function (rather than at the top level) diff --git a/src/PipeScore/Playback/sounds.ts b/src/PipeScore/Playback/sounds.ts index c93685fb..4a06b2bb 100644 --- a/src/PipeScore/Playback/sounds.ts +++ b/src/PipeScore/Playback/sounds.ts @@ -76,17 +76,16 @@ export class Tick { * Start the metronome tick, looping forever until .stop() is called. */ async start() { - - const beatIndicatorDuration = 200; // duration of beat indicator on UI in ms - const tickLeadInDuration = 150 // Aligns the centre of the audio tick to the beat indicator in ms + const beatIndicatorDuration = 200; // duration of beat indicator on UI in ms + const tickLeadInDuration = 150; // Aligns the centre of the audio tick to the beat indicator in ms while (!this.stopped) { const duration = (1000 * 60) / settings.bpm; this.sample.start(1); - await sleep(tickLeadInDuration); - dispatch(updateBeatIndicator(true)); - await sleep(beatIndicatorDuration); - dispatch(updateBeatIndicator(false)); - await sleep(duration - beatIndicatorDuration - tickLeadInDuration); + await sleep(tickLeadInDuration); + dispatch(updateBeatIndicator(true)); + await sleep(beatIndicatorDuration); + dispatch(updateBeatIndicator(false)); + await sleep(duration - beatIndicatorDuration - tickLeadInDuration); } } @@ -118,7 +117,12 @@ export class SoundedPitch { // see SoundedSilence for details. public durationIncludingTies: number; - constructor(pitch: Pitch, duration: number, ctx: AudioContext, id: ID | null) { + constructor( + pitch: Pitch, + duration: number, + ctx: AudioContext, + id: ID | null + ) { this.sample = new Sample(pitchToAudioResource(pitch), ctx); this.pitch = pitch; this.duration = duration; diff --git a/src/PipeScore/Playback/state.ts b/src/PipeScore/Playback/state.ts index 581ec264..b4c1afa5 100644 --- a/src/PipeScore/Playback/state.ts +++ b/src/PipeScore/Playback/state.ts @@ -20,7 +20,7 @@ export type PlaybackState = { userPressedStop: boolean; playing: boolean; playingMetronome: boolean; - beatIndicator:boolean; + beatIndicator: boolean; loading: boolean; // Location of playback cursor cursor: ID | null; diff --git a/src/PipeScore/UI/view.ts b/src/PipeScore/UI/view.ts index 5f1e96ef..761da76a 100644 --- a/src/PipeScore/UI/view.ts +++ b/src/PipeScore/UI/view.ts @@ -73,8 +73,18 @@ import { resetStaveGap, setStaveGap, } from '../Events/Stave'; -import { addText, centreText, editText, setTextX, setTextY } from '../Events/Text'; -import { addSecondTiming, addSingleTiming, editTimingText } from '../Events/Timing'; +import { + addText, + centreText, + editText, + setTextX, + setTextY, +} from '../Events/Text'; +import { + addSecondTiming, + addSingleTiming, + editTimingText, +} from '../Events/Timing'; import { addTune, deleteTune, resetTuneGap, setTuneGap } from '../Events/Tune'; import type { IGracenote } from '../Gracenote'; import type { IMeasure } from '../Measure'; @@ -110,7 +120,7 @@ export interface UIState { loadingAudio: boolean; isPlaying: boolean; isPlayingMetronome: boolean; - beatIndicator:boolean, + beatIndicator: boolean; selectedGracenote: IGracenote | null; selectedStaves: IStave[]; selectedMeasures: IMeasure[]; @@ -442,7 +452,8 @@ export default function render(state: UIState): m.Children { disabled: !barsSelected, class: startBarClass(Barline.normal), style: 'margin-left: .5rem;', - onclick: () => state.dispatch(setBarline('start', Barline.normal)), + onclick: () => + state.dispatch(setBarline('start', Barline.normal)), }, text('normalBarline') ), @@ -455,7 +466,8 @@ export default function render(state: UIState): m.Children { { disabled: !barsSelected, class: startBarClass(Barline.repeat), - onclick: () => state.dispatch(setBarline('start', Barline.repeat)), + onclick: () => + state.dispatch(setBarline('start', Barline.repeat)), }, text('repeatBarline') ), @@ -468,7 +480,8 @@ export default function render(state: UIState): m.Children { { disabled: !barsSelected, class: startBarClass(Barline.part), - onclick: () => state.dispatch(setBarline('start', Barline.part)), + onclick: () => + state.dispatch(setBarline('start', Barline.part)), }, text('partBarline') ), @@ -485,7 +498,8 @@ export default function render(state: UIState): m.Children { disabled: !barsSelected, class: endBarClass(Barline.normal), style: 'margin-left: .5rem;', - onclick: () => state.dispatch(setBarline('end', Barline.normal)), + onclick: () => + state.dispatch(setBarline('end', Barline.normal)), }, text('normalBarline') ), @@ -498,7 +512,8 @@ export default function render(state: UIState): m.Children { { disabled: !barsSelected, class: endBarClass(Barline.repeat), - onclick: () => state.dispatch(setBarline('end', Barline.repeat)), + onclick: () => + state.dispatch(setBarline('end', Barline.repeat)), }, text('repeatBarline') ), @@ -855,7 +870,10 @@ export default function render(state: UIState): m.Children { 'edit-text', m( 'button.double-width.text', - { disabled: !textSelected, onclick: () => state.dispatch(editText()) }, + { + disabled: !textSelected, + onclick: () => state.dispatch(editText()), + }, text('editText') ), state.dispatch @@ -923,76 +941,61 @@ export default function render(state: UIState): m.Children { m('div.section-content', [ help( 'play', - m( - 'button', - { - disabled: state.isPlaying || state.isPlayingMetronome, - onclick: () => state.dispatch(startPlayback()), - class: 'play-button', - } - ), + m('button', { + disabled: state.isPlaying || state.isPlayingMetronome, + onclick: () => state.dispatch(startPlayback()), + class: 'play-button', + }), state.dispatch ), help( 'play-from-selection', - m( - 'button', - { - disabled: state.isPlaying || !barsSelected || state.isPlayingMetronome, - onclick: () => state.dispatch(startPlaybackAtSelection()), - class: 'play-fromselection', - }, - ), + m('button', { + disabled: + state.isPlaying || !barsSelected || state.isPlayingMetronome, + onclick: () => state.dispatch(startPlaybackAtSelection()), + class: 'play-fromselection', + }), state.dispatch ), help( 'play-looping-selection', - m( - 'button', - { - disabled: state.isPlaying || state.selectedNotes.length === 0 || state.isPlayingMetronome, - onclick: () => state.dispatch(playbackLoopingSelection()), - class: 'play-loopedselection', - }, - ), + m('button', { + disabled: + state.isPlaying || + state.selectedNotes.length === 0 || + state.isPlayingMetronome, + onclick: () => state.dispatch(playbackLoopingSelection()), + class: 'play-loopedselection', + }), state.dispatch ), help( state.isPlayingMetronome ? 'stop' : 'play-metronome', - m( - 'button', - { + m('button', { disabled: state.isPlaying || state.isPlayingMetronome, - onclick: () => - state.dispatch( - startPlayMetronome() - ), + onclick: () => state.dispatch(startPlayMetronome()), class: 'play-metronome', - } - ), + }), state.dispatch ), help( 'beatindicator', - m( - 'button', - { - disabled: !state.isPlayingMetronome, - class: state.beatIndicator ? 'beat-indicator-on' : 'beat-indicator-off', - }, - ), + m('button', { + disabled: !state.isPlayingMetronome, + class: state.beatIndicator + ? 'beat-indicator-on' + : 'beat-indicator-off', + }), state.dispatch ), help( 'stop', - m( - 'button', - { - disabled: !state.isPlaying && !state.isPlayingMetronome, - onclick: () => state.dispatch(stopPlayback()), - class: 'stop-button', - }, - ), + m('button', { + disabled: !state.isPlaying && !state.isPlayingMetronome, + onclick: () => state.dispatch(stopPlayback()), + class: 'stop-button', + }), state.dispatch ), ]), @@ -1052,11 +1055,12 @@ export default function render(state: UIState): m.Children { ), ]), ]), - m('section',[ + m('section', [ m('div.section-content.vertical', [ m('h2', text('instrument')), - m('label', + m( + 'label', m('input', { type: 'radio', name: 'instrument', @@ -1067,13 +1071,15 @@ export default function render(state: UIState): m.Children { }), text('instrumentPipes') ), - m('label', + m( + 'label', m('input', { type: 'radio', name: 'instrument', disabled: state.isPlaying, checked: settings.instrument === Instrument.Chanter, - onchange: () => state.dispatch(updateInstrument(Instrument.Chanter)), + onchange: () => + state.dispatch(updateInstrument(Instrument.Chanter)), value: 'pc', }), text('instrumentPC') @@ -1091,7 +1097,9 @@ export default function render(state: UIState): m.Children { m( 'button', { - class: `text double-width ${state.isLandscape ? ' highlighted' : ''}`, + class: `text double-width ${ + state.isLandscape ? ' highlighted' : '' + }`, onclick: () => state.dispatch(landscape()), }, text('landscape') @@ -1103,7 +1111,9 @@ export default function render(state: UIState): m.Children { m( 'button', { - class: `text double-width ${state.isLandscape ? '' : ' highlighted'}`, + class: `text double-width ${ + state.isLandscape ? '' : ' highlighted' + }`, onclick: () => state.dispatch(portrait()), }, text('portrait') @@ -1212,7 +1222,8 @@ export default function render(state: UIState): m.Children { document: documentMenu, }; - const menuClass = (s: Menu): string => (s === state.currentMenu ? 'selected' : ''); + const menuClass = (s: Menu): string => + s === state.currentMenu ? 'selected' : ''; const loginWarning = [ 'You are currently not logged in. Any changes you make will not be saved. ', @@ -1226,7 +1237,8 @@ export default function render(state: UIState): m.Children { ]; const showLoginWarning = state.canEdit && !state.loggedIn; const showOtherUsersScoreWarning = !state.canEdit; - const showAudioWarning = state.loadingAudio && state.currentMenu === 'playback'; + const showAudioWarning = + state.loadingAudio && state.currentMenu === 'playback'; const warning = [ ...(showLoginWarning ? loginWarning : []), ...(showOtherUsersScoreWarning ? otherUsersScoreWarning : []), @@ -1267,7 +1279,10 @@ export default function render(state: UIState): m.Children { menuHead('settings', text('settingsMenu')), help( 'help', - m('button', m('a[href=/help]', { target: '_blank' }, text('helpMenu'))), + m( + 'button', + m('a[href=/help]', { target: '_blank' }, text('helpMenu')) + ), state.dispatch ), m( @@ -1301,7 +1316,10 @@ export default function render(state: UIState): m.Children { 'save', m( 'button.save', - { disabled: state.saved, onclick: () => state.dispatch(save()) }, + { + disabled: state.saved, + onclick: () => state.dispatch(save()), + }, text('save') ), state.dispatch @@ -1398,7 +1416,7 @@ function mobileView(state: UIState): m.Children { m('section', [ m( 'div.section-content', - { class: state.isPlaying ? 'stop-button':'play-button' }, + { class: state.isPlaying ? 'stop-button' : 'play-button' }, [ help( state.isPlaying ? 'stop' : 'play', @@ -1409,8 +1427,8 @@ function mobileView(state: UIState): m.Children { state.isPlaying ? stopPlayback() : state.selectedTune === null - ? startPlayback() - : startPlaybackAtSelection() + ? startPlayback() + : startPlaybackAtSelection() ), class: state.isPlaying ? 'stop-button' : 'play-button', }), @@ -1421,19 +1439,25 @@ function mobileView(state: UIState): m.Children { ]), m( 'div.section-content', - { class: state.isPlayingMetronome ? 'stop-metronome' :'play-metronome'}, + { + class: state.isPlayingMetronome + ? 'stop-metronome' + : 'play-metronome', + }, [ help( state.isPlaying ? 'stop-metronome' : 'play-metronome', m('button', { - disabled: state.isPlaying, + disabled: state.isPlaying, onclick: () => state.dispatch( state.isPlayingMetronome ? stopPlayback() : startPlayMetronome() ), - class: state.isPlayingMetronome ? 'stop-metronome' : 'play-metronome', + class: state.isPlayingMetronome + ? 'stop-metronome' + : 'play-metronome', }), state.dispatch ), @@ -1441,17 +1465,20 @@ function mobileView(state: UIState): m.Children { ), m( 'div.section-content', - { class: state.beatIndicator ? 'beat-indicator-on' : 'beat-indicator-off'}, + { + class: state.beatIndicator + ? 'beat-indicator-on' + : 'beat-indicator-off', + }, [ help( 'beatindicator', - m( - 'button', - { - disabled: !state.isPlayingMetronome, - class: state.beatIndicator ? 'beat-indicator-on' : 'beat-indicator-off', - }, - ), + m('button', { + disabled: !state.isPlayingMetronome, + class: state.beatIndicator + ? 'beat-indicator-on' + : 'beat-indicator-off', + }), state.dispatch ), ] @@ -1497,7 +1524,8 @@ function mobileView(state: UIState): m.Children { name: 'instrument', disabled: state.isPlaying, checked: settings.instrument === Instrument.GHB, - onchange: () => state.dispatch(updateInstrument(Instrument.GHB)), + onchange: () => + state.dispatch(updateInstrument(Instrument.GHB)), value: '', }), text('instrumentPipes') @@ -1511,7 +1539,8 @@ function mobileView(state: UIState): m.Children { name: 'instrument', disabled: state.isPlaying, checked: settings.instrument === Instrument.Chanter, - onchange: () => state.dispatch(updateInstrument(Instrument.Chanter)), + onchange: () => + state.dispatch(updateInstrument(Instrument.Chanter)), value: 'pc', }), text('instrumentPC') From cedb914ad25600a19e7807c3cdbc982c41329f44 Mon Sep 17 00:00:00 2001 From: AndyMac4321 <76612952+AndyMac4321@users.noreply.github.com> Date: Mon, 31 Mar 2025 04:12:01 -0700 Subject: [PATCH 3/6] Prettier Made prettier --- src/PipeScore/Translations/English.ts | 9 +++++---- src/PipeScore/Translations/French.ts | 10 ++++++---- src/PipeScore/Translations/index.ts | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/PipeScore/Translations/English.ts b/src/PipeScore/Translations/English.ts index fbb96c65..c4581edc 100644 --- a/src/PipeScore/Translations/English.ts +++ b/src/PipeScore/Translations/English.ts @@ -115,12 +115,13 @@ export const EnglishDocumentation: Documentation = { 'Play a preview of the score, starting at the currently selected note/bar. This will only work once the samples are downloaded (if the samples need to download, you will see a notice).', 'play-looping-selection': 'Play the currently selected part of the score, repeating forever.', - 'play-metronome':'Start the metronome', - 'stop-metronome':'Stop the metronome', - beatindicator:'Displays the beat of the metronome', + 'play-metronome': 'Start the metronome', + 'stop-metronome': 'Stop the metronome', + beatindicator: 'Displays the beat of the metronome', stop: 'Stop the playback.', 'playback-speed': 'Control the playback speed (further right is faster).', - 'harmony-volume': 'Control how loud the harmony plays (further right is louder).', + 'harmony-volume': + 'Control how loud the harmony plays (further right is louder).', export: 'Export the score to a PDF file, that may then be shared or printed.', 'export-bww': "Export the score to a BWW file, that may be opened in other applications. This is currently very new, and won't work for most scores.", diff --git a/src/PipeScore/Translations/French.ts b/src/PipeScore/Translations/French.ts index 264331f6..4091af86 100644 --- a/src/PipeScore/Translations/French.ts +++ b/src/PipeScore/Translations/French.ts @@ -120,12 +120,13 @@ export const FrenchDocumentation: Documentation = { 'play-looping-selection': 'Jouer la partie de la partition actuellement sélectionnée, en jouant en boucle.', 'play-metronome': 'Démarrer le métronome', - 'stop-metronome': 'Arrêter le métronome', - beatindicator:'Affiche le battement du métronome', + 'stop-metronome': 'Arrêter le métronome', + beatindicator: 'Affiche le battement du métronome', stop: 'Arrêter la lecture.', 'playback-speed': "Contrôler la vitesse de lecture (plus c'est à droite, plus c'est rapide).", - 'harmony-volume': 'Control how loud the harmony plays (further right is louder).', + 'harmony-volume': + 'Control how loud the harmony plays (further right is louder).', export: 'Exporter la partition vers un fichier PDF, qui peut ensuite être partagé ou imprimé.', 'export-bww': @@ -143,7 +144,8 @@ export const FrenchDocumentation: Documentation = { "Déplacez la mesure sélectionnée à la fin de la portée précédente. Ceci ne s'applique que si vous êtes en train de sélectionner la première mesure d'une portée.", 'move-bar-to-next-line': "Déplacer la mesure sélectionnée au début de la portée suivante. Ceci ne s'applique que si vous êtes en train de sélectionner la dernière mesure d'une portée.", - 'nothing-hovered': "Survolez les différentes icônes pour afficher l'aide ici.", + 'nothing-hovered': + "Survolez les différentes icônes pour afficher l'aide ici.", }; export const FrenchTextItems: TextItems = { diff --git a/src/PipeScore/Translations/index.ts b/src/PipeScore/Translations/index.ts index f0a0ee5a..f3c378f2 100644 --- a/src/PipeScore/Translations/index.ts +++ b/src/PipeScore/Translations/index.ts @@ -168,7 +168,7 @@ export type TextItems = { playFromBeginning: string; playFromSelection: string; playLoopedSelection: string; - playMetronome:string; + playMetronome: string; stop: string; playbackOptions: string; beatsPerMinute: string; From 5ea60eaeb5c7dcf5d32cb614434a7bb764aa8bdf Mon Sep 17 00:00:00 2001 From: AndyMac4321 <76612952+AndyMac4321@users.noreply.github.com> Date: Mon, 31 Mar 2025 04:20:57 -0700 Subject: [PATCH 4/6] Update index.ts --- src/PipeScore/Note/index.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/PipeScore/Note/index.ts b/src/PipeScore/Note/index.ts index a416f3b2..ac533a11 100644 --- a/src/PipeScore/Note/index.ts +++ b/src/PipeScore/Note/index.ts @@ -19,11 +19,7 @@ import type { IGracenote } from '../Gracenote'; import type { PlaybackItem } from '../Playback'; import type { Previews } from '../Preview/previews'; -import type { - SavedNote, - SavedNoteOrTriplet, - SavedTriplet, -} from '../SavedModel'; +import type { SavedNote, SavedNoteOrTriplet, SavedTriplet } from '../SavedModel'; import { Item } from '../global/id'; import type { Pitch } from '../global/pitch'; import { unreachable } from '../global/utils'; @@ -45,10 +41,7 @@ export abstract class INote abstract setLength(length: NoteLength): void; abstract hasPreview(): boolean; abstract makePreviewReal(): void; - abstract setPreview( - gracenote: IGracenote | Pitch, - noteBefore: INote | null - ): void; + abstract setPreview(gracenote: IGracenote | Pitch, noteBefore: INote | null): void; abstract removePreview(): void; abstract isPreview(): boolean; abstract makeUnPreview(): INote; From 71e64eab3dcf0fb1fd94861b2d75a4a1531dd18c Mon Sep 17 00:00:00 2001 From: AndyMac4321 <76612952+AndyMac4321@users.noreply.github.com> Date: Mon, 31 Mar 2025 04:39:37 -0700 Subject: [PATCH 5/6] Run Lint and fix type check issues --- src/PipeScore/Events/Misc.ts | 1 + src/PipeScore/tests/events/common.ts | 2 ++ src/styles/pipescore.scss | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/PipeScore/Events/Misc.ts b/src/PipeScore/Events/Misc.ts index d5f99ce6..5059e13a 100644 --- a/src/PipeScore/Events/Misc.ts +++ b/src/PipeScore/Events/Misc.ts @@ -180,6 +180,7 @@ export function exportPDF(): ScoreEvent { loading: false, cursor: null, playingMetronome: false, + beatIndicator: false, }, dispatch: async () => void 0, }; diff --git a/src/PipeScore/tests/events/common.ts b/src/PipeScore/tests/events/common.ts index 8f98881f..b1fe5c5f 100644 --- a/src/PipeScore/tests/events/common.ts +++ b/src/PipeScore/tests/events/common.ts @@ -19,6 +19,8 @@ export function emptyState(score: IScore = Score.blank()): State { loading: false, userPressedStop: false, cursor: null, + playingMetronome: false, + beatIndicator: false, }, score, }; diff --git a/src/styles/pipescore.scss b/src/styles/pipescore.scss index a7c74e6c..f1ad9f77 100644 --- a/src/styles/pipescore.scss +++ b/src/styles/pipescore.scss @@ -416,4 +416,4 @@ label.text-coord { .beat-indicator-off { background-image: url('../images/beat-indicator-off.svg'); -} \ No newline at end of file +} From c1bb60ee340c3b79ead79ba88a8c1d7ebf1a93ff Mon Sep 17 00:00:00 2001 From: AndyMac4321 <76612952+AndyMac4321@users.noreply.github.com> Date: Mon, 31 Mar 2025 04:47:08 -0700 Subject: [PATCH 6/6] Update .gitignore --- .gitignore | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 7bbeff3e..1bfeb041 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,4 @@ __pycache__ *.aup3-shm *.aup3-wal *.aup3 -public/audio/GHB/snare-roll-end.mp3 -public/audio/GHB/snare-roll-end.wav -public/audio/GHB/snare-roll-start.mp3 -public/audio/GHB/snare-roll-start.wav +