diff --git a/.gitignore b/.gitignore index 5410f6c2..650904b8 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,7 @@ tsserver.log __pycache__ .DS_Store +.vscode +*.aup3-shm +*.aup3-wal +*.aup3 diff --git a/public/audio/GHB/snare-roll-end.mp3 b/public/audio/GHB/snare-roll-end.mp3 new file mode 100644 index 00000000..975325fb Binary files /dev/null and b/public/audio/GHB/snare-roll-end.mp3 differ diff --git a/public/audio/GHB/snare-roll-end.wav b/public/audio/GHB/snare-roll-end.wav new file mode 100644 index 00000000..7fe1c16c Binary files /dev/null and b/public/audio/GHB/snare-roll-end.wav differ diff --git a/public/audio/GHB/snare-roll-start.mp3 b/public/audio/GHB/snare-roll-start.mp3 new file mode 100644 index 00000000..fbae2b9d Binary files /dev/null and b/public/audio/GHB/snare-roll-start.mp3 differ diff --git a/public/audio/GHB/snare-roll-start.wav b/public/audio/GHB/snare-roll-start.wav new file mode 100644 index 00000000..b9e1a243 Binary files /dev/null and b/public/audio/GHB/snare-roll-start.wav differ diff --git a/public/audio/chanter/snare-roll-end.mp3 b/public/audio/chanter/snare-roll-end.mp3 new file mode 100644 index 00000000..975325fb Binary files /dev/null and b/public/audio/chanter/snare-roll-end.mp3 differ diff --git a/public/audio/chanter/snare-roll-end.wav b/public/audio/chanter/snare-roll-end.wav new file mode 100644 index 00000000..7fe1c16c Binary files /dev/null and b/public/audio/chanter/snare-roll-end.wav differ diff --git a/public/audio/chanter/snare-roll-start.mp3 b/public/audio/chanter/snare-roll-start.mp3 new file mode 100644 index 00000000..fbae2b9d Binary files /dev/null and b/public/audio/chanter/snare-roll-start.mp3 differ diff --git a/public/audio/chanter/snare-roll-start.wav b/public/audio/chanter/snare-roll-start.wav new file mode 100644 index 00000000..b9e1a243 Binary files /dev/null and b/public/audio/chanter/snare-roll-start.wav differ diff --git a/src/PipeScore/Events/Playback.ts b/src/PipeScore/Events/Playback.ts index c10623e6..eb50380b 100644 --- a/src/PipeScore/Events/Playback.ts +++ b/src/PipeScore/Events/Playback.ts @@ -17,6 +17,7 @@ import { playback } from '../Playback/impl'; import { ScoreSelection } from '../Selection/score'; import type { State } from '../State'; +import { Attack } from '../global/attack'; import type { ID } from '../global/id'; import type { Instrument } from '../global/instrument'; import { settings } from '../global/settings'; @@ -109,3 +110,13 @@ export function updateInstrument(instrument: Instrument): ScoreEvent { return Update.NoChange; }; } + +export function updateAttack(attack: Attack): ScoreEvent { + return async () => { + if (attack !== settings.attack) { + settings.attack = attack; + return Update.ShouldSave; + } + return Update.NoChange; + }; +} diff --git a/src/PipeScore/Playback/impl.ts b/src/PipeScore/Playback/impl.ts index 4e2723c2..4073db0d 100644 --- a/src/PipeScore/Playback/impl.ts +++ b/src/PipeScore/Playback/impl.ts @@ -27,7 +27,9 @@ import { } from '.'; import { dispatch } from '../Controller'; import { updateView } from '../Events/Misc'; +import { Attack } from '../global/attack'; import type { ID } from '../global/id'; +import { Pitch } from '../global/pitch'; import { settings } from '../global/settings'; import { isRoughlyZero, @@ -38,7 +40,13 @@ import { sum, unreachable, } from '../global/utils'; -import { Drone, type SoundedMeasure, SoundedPitch, SoundedSilence } from './sounds'; +import { + Drone, + Snare, + type SoundedMeasure, + SoundedPitch, + SoundedSilence, +} from './sounds'; import type { PlaybackState } from './state'; function shouldDeleteBecauseOfSecondTimings( @@ -206,7 +214,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 +246,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 +311,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; } @@ -349,10 +362,20 @@ export async function playback( document.body.classList.add('loading'); const drone = new Drone(context); - - drone.start(); - - await sleep(1000); + if (start != null) { + // Playback from selection or loop selection doesn't play attack + drone.start(); + } else { + if ( + await playAttack( + state, + drone, + measures, + context, + ) + ) + return; + } document.body.classList.remove('loading'); await playPitches(state, measures, timings, context, start, end, loop); @@ -362,6 +385,123 @@ export async function playback( state.playing = false; } +async function playAttack( + state: PlaybackState, + drone: Drone | null, + measures: PlaybackMeasure[], + context: AudioContext, +): Promise { + let stopAttack: boolean = false; + switch (settings.attack) { + case Attack.QuickMarchAttack: { + stopAttack = await quickAttack( + state, + drone, + measures, + context, + ); + break; + } + case Attack.SlowMarchAttack: { + stopAttack = await slowAttack( + state, + drone, + measures, + context, + ); + break; + } + case Attack.Off: { + if (drone != undefined) drone.start(); + const leadInDuration = measures[0].lengthOfMainPart(); + let silent2Beats = new SoundedSilence( + 2 - (leadInDuration > 1 ? 0 : leadInDuration), + null + ); + await silent2Beats.play(settings.bpm, true); + break; + } + } + if (stopAttack) { + if (drone != undefined) drone.stop(); + state.playing = false; + state.userPressedStop = false; + dispatch(updateView()); + } + return stopAttack; +} + +/** + * play quick march attack before tune starts + */ +async function quickAttack( + state: PlaybackState, + drone: Drone | null, + measures: PlaybackMeasure[], + context: AudioContext, +): Promise { + const snare = new Snare(context); + const leadInDuration = measures[0].lengthOfMainPart(); + const silent2Beats = new SoundedSilence(2, null); + //Pipe Major Calls 1,2 + await silent2Beats.play(settings.bpm, false); + if (state.userPressedStop) return true; + //1 ,2 - Drum Roll + await snare.Roll(2, true); + if (state.userPressedStop) return true; + //3, 4 - Right hand on bag + await silent2Beats.play(settings.bpm, false); + if (state.userPressedStop) return true; + //5 , 6 - 2nd Drum Roll + //5 - Strike in Drones + if (drone != undefined) drone.start(); + await snare.Roll(2, true); + if (state.userPressedStop) return true; + // 7 Start Chanter (intro E) + // 8 Start Tune if it has 1 beat of lead in + // 9 Start Tune (if no lead in) + const pitchEIntro = new SoundedPitch( + Pitch.E, + 2 - (leadInDuration > 1 ? 0 : leadInDuration), // assumption here is lead in is never more than 1 beat + context, + null + ); + await pitchEIntro.play(settings.bpm, false); + if (state.userPressedStop) return true; + + return false; +} +/** + * play slow march attack before tune starts + */ +async function slowAttack( + state: PlaybackState, + drone: Drone | null, + measures: PlaybackMeasure[], + context: AudioContext, +): Promise { + const snare = new Snare(context); + const leadInDuration = measures[0].lengthOfMainPart(); + const silent2Beats = new SoundedSilence(2, null); + //Pipe Major Calls 1,2 + await silent2Beats.play(settings.bpm, false); + if (state.userPressedStop) return true; + //1 , 2 - Drum Roll + //2 - Right hand on bag + await snare.Roll(2, true); + if (state.userPressedStop) return true; + //3 - Strike in Drones + //4 - Start Tune if it has 1 beat of lead in (No E intro) + //5 - Start Tune (if no lead in) + if (drone != undefined) drone.start(); + // assumption here is lead is never more than 1 beat + await sleep( + ((2 - (leadInDuration > 1 ? 0 : leadInDuration)) * 1000 * 60) / settings.bpm + ); + if (state.userPressedStop) return true; + return false; +} + async function playPitches( state: PlaybackState, measures: PlaybackMeasure[], @@ -371,10 +511,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) { diff --git a/src/PipeScore/Playback/resources.ts b/src/PipeScore/Playback/resources.ts index aa60a06c..556edfa3 100644 --- a/src/PipeScore/Playback/resources.ts +++ b/src/PipeScore/Playback/resources.ts @@ -38,6 +38,8 @@ type InstrumentResources = { highg: AudioResource; higha: AudioResource; drones: AudioResource | null; + snareRoll: AudioResource; + snareTap: AudioResource; }; const ghb: InstrumentResources = { @@ -51,6 +53,8 @@ const ghb: InstrumentResources = { highg: new AudioResource('GHB/highg'), higha: new AudioResource('GHB/higha'), drones: new AudioResource('GHB/drones'), + snareRoll:new AudioResource('GHB/snare-roll-start'), + snareTap:new AudioResource('GHB/snare-roll-end'), }; const chanter: InstrumentResources = { @@ -64,6 +68,8 @@ const chanter: InstrumentResources = { highg: new AudioResource('chanter/highg'), higha: new AudioResource('chanter/higha'), drones: null, + snareRoll:new AudioResource('chanter/snare-roll-start'), + snareTap:new AudioResource('chanter/snare-roll-end'), }; /** @@ -87,6 +93,8 @@ function loadInstrumentResources( resources.highg.load(context), resources.higha.load(context), resources.drones?.load(context), + resources.snareRoll.load(context), + resources.snareTap.load(context), ]); } diff --git a/src/PipeScore/Playback/sounds.ts b/src/PipeScore/Playback/sounds.ts index 10dbf0e7..cb5e411a 100644 --- a/src/PipeScore/Playback/sounds.ts +++ b/src/PipeScore/Playback/sounds.ts @@ -59,6 +59,34 @@ export class Drone { } } +/** + * Snare playback. + */ +export class Snare { + private sample: Sample; + private sampleTap: Sample; + private stopped = false; + + constructor(context: AudioContext) { + const snareRoll = getInstrumentResources().snareRoll; + const snareTap = getInstrumentResources().snareTap; + this.sample = new Sample(snareRoll, context); + this.sampleTap = new Sample(snareTap, context); + } + + /** + * Play the snare roll and tap for duration in ms. + */ + async Roll(count: number, hasEndTap: boolean) { + const tapDuration: number = 285; + const rollDuration: number = + (count * 1000 * 60) / settings.bpm - (hasEndTap ? tapDuration : 0); + this.sample.start(0.5); + await sleep(rollDuration); + this.sample.stop(); + if (hasEndTap) await this.sampleTap.start(0.5); + } +} /** * Pitched note playback (used for notes and gracenotes). */ @@ -77,7 +105,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/SavedModel.ts b/src/PipeScore/SavedModel.ts index 45caa7cf..0718d4f1 100644 --- a/src/PipeScore/SavedModel.ts +++ b/src/PipeScore/SavedModel.ts @@ -212,6 +212,7 @@ export type SavedSettings = { gapAfterGracenote: number; harmonyVolume: number; instrument: string; + attack: string; }; export type DeprecatedSavedNoteOrTriplet = diff --git a/src/PipeScore/Translations/English.ts b/src/PipeScore/Translations/English.ts index d6b2b691..e6a06637 100644 --- a/src/PipeScore/Translations/English.ts +++ b/src/PipeScore/Translations/English.ts @@ -117,7 +117,8 @@ export const EnglishDocumentation: Documentation = { 'Play the currently selected part of the score, repeating forever.', 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.", @@ -135,6 +136,7 @@ export const EnglishDocumentation: Documentation = { 'move-bar-to-next-line': 'Move the currently selected bar to the start of the next stave. This only applies if you are currently selecting the last bar of a stave.', 'nothing-hovered': 'Hover over different icons to view Help here.', + attackoptions: 'Select the method of attack for the beginning of a tune.', }; export const EnglishTextItems: TextItems = { @@ -231,4 +233,7 @@ export const EnglishTextItems: TextItems = { instrumentPC: 'Practice Chanter', instrumentPipes: 'Bagpipe', instrument: 'Instrument', + attackoff: 'Attack off', + attackquick: 'Quick march attack', + attackslow: 'Slow march attack', }; diff --git a/src/PipeScore/Translations/French.ts b/src/PipeScore/Translations/French.ts index ddbab4cc..aecf82bd 100644 --- a/src/PipeScore/Translations/French.ts +++ b/src/PipeScore/Translations/French.ts @@ -122,7 +122,8 @@ export const FrenchDocumentation: Documentation = { 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': @@ -140,7 +141,10 @@ 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.", + attackoptions: + 'Sélectionnez la méthode d’attaque pour le début d’un morceau.', }; export const FrenchTextItems: TextItems = { @@ -237,4 +241,7 @@ export const FrenchTextItems: TextItems = { instrumentPC: 'Instrument de pratique', instrumentPipes: 'Bagpipe', instrument: 'Instrument', + attackoff: 'Attaque désactivée', + attackquick: 'Attaque à marche rapide', + attackslow: 'Attaque à marche lente', }; diff --git a/src/PipeScore/Translations/index.ts b/src/PipeScore/Translations/index.ts index c3fc20bc..4f77e7b1 100644 --- a/src/PipeScore/Translations/index.ts +++ b/src/PipeScore/Translations/index.ts @@ -97,6 +97,7 @@ export type Documentation = { 'move-bar-to-previous-line': string; 'move-bar-to-next-line': string; 'nothing-hovered': string; + attackoptions :string; }; export type TextItems = { @@ -193,4 +194,7 @@ export type TextItems = { instrumentPC: string; instrumentPipes: string; instrument: string; + attackoff:string; + attackquick:string; + attackslow:string; }; diff --git a/src/PipeScore/UI/view.ts b/src/PipeScore/UI/view.ts index 597f7a8a..f3cfa031 100644 --- a/src/PipeScore/UI/view.ts +++ b/src/PipeScore/UI/view.ts @@ -60,6 +60,7 @@ import { startPlayback, startPlaybackAtSelection, stopPlayback, + updateAttack, updateInstrument, } from '../Events/Playback'; import { copy, deleteSelection, paste } from '../Events/Selection'; @@ -72,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'; @@ -98,6 +109,7 @@ import { Instrument } from '../global/instrument'; import { Relative } from '../global/relativeLocation'; import { type Settings, settings } from '../global/settings'; import type { Menu } from './model'; +import { Attack } from '../global/attack'; export interface UIState { saved: boolean; @@ -439,7 +451,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') ), @@ -452,7 +465,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') ), @@ -465,7 +479,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') ), @@ -482,7 +497,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') ), @@ -495,7 +511,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') ), @@ -852,7 +869,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 @@ -1021,6 +1041,50 @@ export default function render(state: UIState): m.Children { ]), state.dispatch ), + help( + 'attackoptions', + m('div.section-content.vertical', [ + m( + 'label', + m('input', { + type: 'radio', + name: 'attack', + disabled: state.isPlaying, + checked: settings.attack === Attack.Off, + onchange: () => state.dispatch(updateAttack(Attack.Off)), + value: '', + }), + text('attackoff') + ), + m( + 'label', + m('input', { + type: 'radio', + name: 'attack', + disabled: state.isPlaying, + checked: settings.attack === Attack.QuickMarchAttack, + onchange: () => + state.dispatch(updateAttack(Attack.QuickMarchAttack)), + value: '', + }), + text('attackquick') + ), + m( + 'label', + m('input', { + type: 'radio', + name: 'attack', + disabled: state.isPlaying, + checked: settings.attack === Attack.SlowMarchAttack, + onchange: () => + state.dispatch(updateAttack(Attack.SlowMarchAttack)), + value: 'pc', + }), + text('attackslow') + ), + ]), + state.dispatch + ), ]), ]), m('section', [ @@ -1062,7 +1126,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') @@ -1074,7 +1140,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') @@ -1183,7 +1251,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. ', @@ -1197,7 +1266,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 : []), @@ -1238,7 +1308,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( @@ -1272,7 +1345,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 @@ -1379,8 +1455,8 @@ function mobileView(state: UIState): m.Children { state.isPlaying ? stopPlayback() : state.selectedTune === null - ? startPlayback() - : startPlaybackAtSelection() + ? startPlayback() + : startPlaybackAtSelection() ), class: state.isPlaying ? 'stop-button' : 'play-button', }), @@ -1430,7 +1506,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') @@ -1442,7 +1519,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') diff --git a/src/PipeScore/global/attack.ts b/src/PipeScore/global/attack.ts new file mode 100644 index 00000000..f834abb7 --- /dev/null +++ b/src/PipeScore/global/attack.ts @@ -0,0 +1,49 @@ +// PipeScore - online bagpipe notation +// Copyright (C) macarc +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +import { unreachable } from './utils'; + +export enum Attack { + Off = 0, + QuickMarchAttack = 1, + SlowMarchAttack = 2, +} + +export function attackToString(attack: Attack): string { + switch (attack) { + case Attack.Off: + return 'none'; + case Attack.QuickMarchAttack: + return 'quick'; + case Attack.SlowMarchAttack: + return 'slow'; + default: + unreachable(attack); + } +} + +export function parseAttack(attack: string): Attack | null { + switch (attack) { + case 'none': + return Attack.Off; + case 'quick': + return Attack.QuickMarchAttack; + case 'slow': + return Attack.SlowMarchAttack; + default: + return null; + } +} diff --git a/src/PipeScore/global/settings.ts b/src/PipeScore/global/settings.ts index 100cda53..09ee4fc1 100644 --- a/src/PipeScore/global/settings.ts +++ b/src/PipeScore/global/settings.ts @@ -17,6 +17,7 @@ // Document settings singleton. import type { SavedSettings } from '../SavedModel'; +import { Attack, attackToString ,parseAttack} from './attack'; import { Instrument, instrumentToString, parseInstrument } from './instrument'; import { clamp } from './utils'; @@ -34,6 +35,7 @@ export class Settings { gapAfterGracenote = 7; bpm = 80; instrument = Instrument.GHB; + attack = Attack.Off; static defaultStaveGap = 65; static defaultHarmonyGap = 50; @@ -49,6 +51,7 @@ export class Settings { this.gapAfterGracenote = o.gapAfterGracenote || 7; this.harmonyVolume = o.harmonyVolume || Settings.defaultHarmonyVolume; this.instrument = parseInstrument(o.instrument) || Instrument.GHB; + this.attack = parseAttack(o.attack) || Attack.Off; } toJSON(): SavedSettings { return { @@ -60,6 +63,7 @@ export class Settings { gapAfterGracenote: this.gapAfterGracenote, harmonyVolume: this.harmonyVolume, instrument: instrumentToString(this.instrument), + attack: attackToString(this.attack), }; } validate(key: T, value: number) { diff --git a/todo.md b/todo.md index b918a7c5..ffdc82f9 100644 --- a/todo.md +++ b/todo.md @@ -14,7 +14,7 @@ - [ ] Note in docs about keyboard-based - [x] Fix harmony playback - [ ] Importing tunes into other scores -- [ ] Count ins +- [x] Count ins / Quick and Slow attack options - [ ] Password change - [ ] Chanter playback - [ ] Metronome while playing