Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ tsserver.log
__pycache__

.DS_Store
.vscode
*.aup3-shm
*.aup3-wal
*.aup3
Binary file added public/audio/GHB/snare-roll-end.mp3
Binary file not shown.
Binary file added public/audio/GHB/snare-roll-end.wav
Binary file not shown.
Binary file added public/audio/GHB/snare-roll-start.mp3
Binary file not shown.
Binary file added public/audio/GHB/snare-roll-start.wav
Binary file not shown.
Binary file added public/audio/chanter/snare-roll-end.mp3
Binary file not shown.
Binary file added public/audio/chanter/snare-roll-end.wav
Binary file not shown.
Binary file added public/audio/chanter/snare-roll-start.mp3
Binary file not shown.
Binary file added public/audio/chanter/snare-roll-start.wav
Binary file not shown.
11 changes: 11 additions & 0 deletions src/PipeScore/Events/Playback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
};
}
168 changes: 158 additions & 10 deletions src/PipeScore/Playback/impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand All @@ -362,6 +385,123 @@ export async function playback(
state.playing = false;
}

async function playAttack(
state: PlaybackState,
drone: Drone | null,
measures: PlaybackMeasure[],
context: AudioContext,
): Promise<boolean> {
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<boolean> {
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<boolean> {
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[],
Expand All @@ -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) {
Expand Down
8 changes: 8 additions & 0 deletions src/PipeScore/Playback/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ type InstrumentResources = {
highg: AudioResource;
higha: AudioResource;
drones: AudioResource | null;
snareRoll: AudioResource;
snareTap: AudioResource;
};

const ghb: InstrumentResources = {
Expand All @@ -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 = {
Expand All @@ -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'),
};

/**
Expand All @@ -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),
]);
}

Expand Down
35 changes: 34 additions & 1 deletion src/PipeScore/Playback/sounds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*/
Expand All @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/PipeScore/SavedModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ export type SavedSettings = {
gapAfterGracenote: number;
harmonyVolume: number;
instrument: string;
attack: string;
};

export type DeprecatedSavedNoteOrTriplet =
Expand Down
7 changes: 6 additions & 1 deletion src/PipeScore/Translations/English.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand All @@ -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 = {
Expand Down Expand Up @@ -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',
};
Loading