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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ tsserver.log
__pycache__

.DS_Store
.vscode
*.aup3-shm
*.aup3-wal
*.aup3

Binary file added public/audio/GHB/tick.mp3
Binary file not shown.
Binary file added public/audio/GHB/tick.wav
Binary file not shown.
Binary file added public/audio/chanter/tick.mp3
Binary file not shown.
Binary file added public/audio/chanter/tick.wav
Binary file not shown.
1 change: 1 addition & 0 deletions public/images/beat-indicator-off.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/images/beat-indicator-on.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/images/play-fromselection.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/images/play-loopedselection.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/images/play-metronome.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions public/images/stop-metronome.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 10 additions & 2 deletions src/PipeScore/Controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ const state: State = {
playback: {
userPressedStop: false,
playing: false,
playingMetronome: false,
beatIndicator: false,
loading: true,
cursor: null,
},
Expand Down Expand Up @@ -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,
Expand All @@ -150,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 &&
Expand Down
8 changes: 7 additions & 1 deletion src/PipeScore/Events/Misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ export function exportPDF(): ScoreEvent {
userPressedStop: false,
loading: false,
cursor: null,
playingMetronome: false,
beatIndicator: false,
},
dispatch: async () => void 0,
};
Expand Down Expand Up @@ -293,7 +295,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;
};
Expand Down
20 changes: 18 additions & 2 deletions src/PipeScore/Events/Playback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

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';
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -109,3 +109,19 @@ 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;
};
}
49 changes: 43 additions & 6 deletions src/PipeScore/Playback/impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,13 @@ 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(
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -407,3 +426,21 @@ 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());
}
17 changes: 14 additions & 3 deletions src/PipeScore/Playback/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
];
}

/**
Expand Down Expand Up @@ -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;
Expand Down
8 changes: 6 additions & 2 deletions src/PipeScore/Playback/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type InstrumentResources = {
highg: AudioResource;
higha: AudioResource;
drones: AudioResource | null;
tick: AudioResource;
};

const ghb: InstrumentResources = {
Expand All @@ -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 = {
Expand All @@ -64,6 +66,7 @@ const chanter: InstrumentResources = {
highg: new AudioResource('chanter/highg'),
higha: new AudioResource('chanter/higha'),
drones: null,
tick: new AudioResource('chanter/tick'),
};

/**
Expand All @@ -87,6 +90,7 @@ function loadInstrumentResources(
resources.highg.load(context),
resources.higha.load(context),
resources.drones?.load(context),
resources.tick.load(context),
]);
}

Expand All @@ -98,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)
Expand Down
49 changes: 47 additions & 2 deletions src/PipeScore/Playback/sounds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -59,6 +59,46 @@ 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).
*/
Expand All @@ -77,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;
Expand Down
2 changes: 2 additions & 0 deletions src/PipeScore/Playback/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
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 @@ -115,9 +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',
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 Down Expand Up @@ -203,6 +207,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',
Expand Down
Loading