diff --git a/api-server/src/common/models/user.json b/api-server/src/common/models/user.json index 137c679c72..8654fb98de 100644 --- a/api-server/src/common/models/user.json +++ b/api-server/src/common/models/user.json @@ -251,6 +251,10 @@ "type": "string", "default": "default" }, + "sound": { + "type": "boolean", + "default": false + }, "profileUI": { "type": "object", "default": { diff --git a/api-server/src/server/utils/publicUserProps.js b/api-server/src/server/utils/publicUserProps.js index cd8199bc07..58cf7c4784 100644 --- a/api-server/src/server/utils/publicUserProps.js +++ b/api-server/src/server/utils/publicUserProps.js @@ -51,6 +51,7 @@ export const userPropsForSession = [ 'id', 'sendQuincyEmail', 'theme', + 'sound', 'completedChallengeCount', 'completedProjectCount', 'completedCertCount', diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json index f667083c79..e609e3aa0d 100644 --- a/client/i18n/locales/english/translations.json +++ b/client/i18n/locales/english/translations.json @@ -133,7 +133,8 @@ "my-portfolio": "My portfolio", "my-timeline": "My timeline", "my-donations": "My donations", - "night-mode": "Night Mode" + "night-mode": "Night Mode", + "sound-mode": "Campfire Mode" }, "headings": { "certs": "Certifications", diff --git a/client/package-lock.json b/client/package-lock.json index 77a4fcc00c..d9c223aca0 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -6264,6 +6264,30 @@ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" }, + "automation-events": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/automation-events/-/automation-events-4.0.3.tgz", + "integrity": "sha512-FVIO4QTVi4u4ORRMszcXhoUj1U8rtgIVCbMKgw1i/zlcbEn1MICbhfquDJiA1QZgh0KysUGuxrn2JeloH04drQ==", + "requires": { + "@babel/runtime": "^7.14.6", + "tslib": "^2.3.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.14.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz", + "integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + } + } + }, "autoprefixer": { "version": "10.3.7", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.3.7.tgz", @@ -19344,6 +19368,31 @@ "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.0.tgz", "integrity": "sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA==" }, + "standardized-audio-context": { + "version": "25.2.9", + "resolved": "https://registry.npmjs.org/standardized-audio-context/-/standardized-audio-context-25.2.9.tgz", + "integrity": "sha512-8xRl8Cqv3j8UZdRvAYhgqSpVRntjxUBxute+9Zg8zeXTr+aRfDgbAu5FHzTLpUlu8uAJ9232CdOOMkTceSNAWA==", + "requires": { + "@babel/runtime": "^7.14.6", + "automation-events": "^4.0.3", + "tslib": "^2.3.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.14.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz", + "integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + } + } + }, "state-toggle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz", @@ -20027,6 +20076,22 @@ "ieee754": "^1.2.1" } }, + "tone": { + "version": "14.7.77", + "resolved": "https://registry.npmjs.org/tone/-/tone-14.7.77.tgz", + "integrity": "sha512-tCfK73IkLHyzoKUvGq47gyDyxiKLFvKiVCOobynGgBB9Dl0NkxTM2p+eRJXyCYrjJwy9Y0XCMqD3uOYsYt2Fdg==", + "requires": { + "standardized-audio-context": "^25.1.8", + "tslib": "^2.0.1" + }, + "dependencies": { + "tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + } + } + }, "totalist": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", diff --git a/client/package.json b/client/package.json index 1b0cfe2228..fa479a2c24 100644 --- a/client/package.json +++ b/client/package.json @@ -121,6 +121,7 @@ "sass.js": "0.11.1", "store": "2.0.12", "stream-browserify": "3.0.0", + "tone": "^14.7.77", "typescript": "4.4.4", "uuid": "8.3.2", "validator": "13.6.0" diff --git a/client/src/client-only-routes/show-settings.tsx b/client/src/client-only-routes/show-settings.tsx index 44e12fdc2b..7b01dc6943 100644 --- a/client/src/client-only-routes/show-settings.tsx +++ b/client/src/client-only-routes/show-settings.tsx @@ -35,6 +35,7 @@ interface IShowSettingsProps { showLoading: boolean; submitNewAbout: () => void; toggleNightMode: (theme: string) => void; + toggleSoundMode: (sound: boolean) => void; updateInternetSettings: () => void; updateIsHonest: () => void; updatePortfolio: () => void; @@ -60,6 +61,7 @@ const mapDispatchToProps = { navigate, submitNewAbout, toggleNightMode: (theme: string) => updateUserFlag({ theme }), + toggleSoundMode: (sound: boolean) => updateUserFlag({ sound }), updateInternetSettings: updateUserFlag, updateIsHonest: updateUserFlag, updatePortfolio: updateUserFlag, @@ -75,6 +77,7 @@ export function ShowSettings(props: IShowSettingsProps): JSX.Element { isSignedIn, submitNewAbout, toggleNightMode, + toggleSoundMode, user: { completedChallenges, email, @@ -101,6 +104,7 @@ export function ShowSettings(props: IShowSettingsProps): JSX.Element { picture, points, theme, + sound, location, name, githubProfile, @@ -143,8 +147,10 @@ export function ShowSettings(props: IShowSettingsProps): JSX.Element { name={name} picture={picture} points={points} + sound={sound} submitNewAbout={submitNewAbout} toggleNightMode={toggleNightMode} + toggleSoundMode={toggleSoundMode} username={username} /> diff --git a/client/src/components/Donation/DonationModal.tsx b/client/src/components/Donation/DonationModal.tsx index fa126f57ae..e97196469f 100644 --- a/client/src/components/Donation/DonationModal.tsx +++ b/client/src/components/Donation/DonationModal.tsx @@ -6,9 +6,11 @@ import { connect } from 'react-redux'; import { goToAnchor } from 'react-scrollable-anchor'; import { bindActionCreators, Dispatch, AnyAction } from 'redux'; import { createSelector } from 'reselect'; +import store from 'store'; import { modalDefaultDonation } from '../../../../config/donation-settings'; import Cup from '../../assets/icons/cup'; import Heart from '../../assets/icons/heart'; + import { closeDonationModal, isDonationModalOpenSelector, @@ -76,6 +78,16 @@ function DonateModal({ useEffect(() => { if (show) { + const playSound = store.get('fcc-sound') as boolean | undefined; + if (playSound) { + void import('tone').then(tone => { + const player = new tone.Player( + 'https://campfire-mode.freecodecamp.org/donate.mp3' + ).toDestination(); + if (tone.context.state !== 'running') void tone.context.resume(); + player.autostart = playSound; + }); + } executeGA({ type: 'modal', data: '/donation-modal' }); executeGA({ type: 'event', diff --git a/client/src/components/Flash/redux/index.ts b/client/src/components/Flash/redux/index.ts index 66ea341587..8dd13fe89b 100644 --- a/client/src/components/Flash/redux/index.ts +++ b/client/src/components/Flash/redux/index.ts @@ -1,4 +1,5 @@ import { nanoid } from 'nanoid'; +import store from 'store'; import { FlashState, State } from '../../../redux/types'; export const FlashApp = 'flash'; @@ -31,10 +32,32 @@ export type FlashMessageArg = { export const createFlashMessage = ( flash: FlashMessageArg -): ReducerPayload => ({ - type: FlashActionTypes.createFlashMessage, - payload: { ...flash, id: nanoid() } -}); +): ReducerPayload => { + const playSound = store.get('fcc-sound') as boolean | undefined; + if (playSound) { + void import('tone').then(tone => { + if (tone.context.state !== 'running') { + void tone.context.resume(); + } + if (flash.message === 'flash.incomplete-steps') { + const player = new tone.Player( + 'https://campfire-mode.freecodecamp.org/try-again.mp3' + ).toDestination(); + player.autostart = playSound; + } + if (flash.message === 'flash.cert-claim-success') { + const player = new tone.Player( + 'https://campfire-mode.freecodecamp.org/cert.mp3' + ).toDestination(); + player.autostart = playSound; + } + }); + } + return { + type: FlashActionTypes.createFlashMessage, + payload: { ...flash, id: nanoid() } + }; +}; export const removeFlashMessage = (): ReducerPayload => ({ diff --git a/client/src/components/settings/about.tsx b/client/src/components/settings/about.tsx index bfff042cfa..af02526c0f 100644 --- a/client/src/components/settings/about.tsx +++ b/client/src/components/settings/about.tsx @@ -10,6 +10,7 @@ import React, { Component } from 'react'; import { TFunction, withTranslation } from 'react-i18next'; import { FullWidthRow, Spacer } from '../helpers'; import BlockSaveButton from '../helpers/form/block-save-button'; +import SoundSettings from './sound'; import ThemeSettings from './theme'; import UsernameSettings from './username'; @@ -27,9 +28,11 @@ type AboutProps = { name: string; picture: string; points: number; + sound: boolean; submitNewAbout: (formValues: FormValues) => void; t: TFunction; toggleNightMode: (theme: string) => void; + toggleSoundMode: (sound: boolean) => void; username: string; }; @@ -184,7 +187,14 @@ class AboutSettings extends Component { const { formValues: { name, location, picture, about } } = this.state; - const { currentTheme, username, t, toggleNightMode } = this.props; + const { + currentTheme, + sound, + username, + t, + toggleNightMode, + toggleSoundMode + } = this.props; return (
@@ -241,6 +251,7 @@ class AboutSettings extends Component { currentTheme={currentTheme} toggleNightMode={toggleNightMode} /> +
); diff --git a/client/src/components/settings/sound.tsx b/client/src/components/settings/sound.tsx new file mode 100644 index 0000000000..67c9c29320 --- /dev/null +++ b/client/src/components/settings/sound.tsx @@ -0,0 +1,34 @@ +import { Form } from '@freecodecamp/react-bootstrap'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import ToggleSetting from './toggle-setting'; + +type SoundProps = { + sound: boolean; + toggleSoundMode: (sound: boolean) => void; +}; + +export default function SoundSettings({ + sound, + toggleSoundMode +}: SoundProps): JSX.Element { + const { t } = useTranslation(); + + return ( +
e.preventDefault()}> + { + toggleSoundMode(sound ? false : true); + }} + /> + + ); +} + +SoundSettings.displayName = 'SoundSettings'; diff --git a/client/src/components/settings/theme.tsx b/client/src/components/settings/theme.tsx index 56a1950133..49f8b635ba 100644 --- a/client/src/components/settings/theme.tsx +++ b/client/src/components/settings/theme.tsx @@ -1,6 +1,7 @@ import { Form } from '@freecodecamp/react-bootstrap'; import React from 'react'; import { useTranslation } from 'react-i18next'; +import store from 'store'; import ToggleSetting from './toggle-setting'; @@ -26,9 +27,33 @@ export default function ThemeSettings({ flagName='currentTheme' offLabel={t('buttons.off')} onLabel={t('buttons.on')} - toggleFlag={() => - toggleNightMode(currentTheme === 'night' ? 'default' : 'night') - } + toggleFlag={async () => { + const playSound = store.get('fcc-sound') as boolean | undefined; + if (playSound) { + const tone = await import('tone'); + const nightToDayPlayer = new tone.Player( + 'https://campfire-mode.freecodecamp.org/day.mp3' + ).toDestination(); + const dayToNightPlayer = new tone.Player( + 'https://campfire-mode.freecodecamp.org/night.mp3' + ).toDestination(); + if (tone.context.state !== 'running') await tone.context.resume(); + if (currentTheme === 'night') { + if (!nightToDayPlayer.loaded) + await nightToDayPlayer.load( + 'https://campfire-mode.freecodecamp.org/day.mp3' + ); + nightToDayPlayer.start(); + } else { + if (!dayToNightPlayer.loaded) + await dayToNightPlayer.load( + 'https://campfire-mode.freecodecamp.org/night.mp3' + ); + dayToNightPlayer.start(); + } + } + toggleNightMode(currentTheme === 'night' ? 'default' : 'night'); + }} /> ); diff --git a/client/src/redux/index.js b/client/src/redux/index.js index 25ef52d43c..905def7199 100644 --- a/client/src/redux/index.js +++ b/client/src/redux/index.js @@ -14,9 +14,9 @@ import { createGaSaga } from './ga-saga'; import hardGoToEpic from './hard-go-to-epic'; import { createReportUserSaga } from './report-user-saga'; - import { actionTypes as settingsTypes } from './settings/action-types'; import { createShowCertSaga } from './show-cert-saga'; +import { createSoundModeSaga } from './sound-mode-saga'; import updateCompleteEpic from './update-complete-epic'; export const MainApp = 'app'; @@ -74,7 +74,8 @@ export const sagas = [ ...createGaSaga(actionTypes), ...createFetchUserSaga(actionTypes), ...createShowCertSaga(actionTypes), - ...createReportUserSaga(actionTypes) + ...createReportUserSaga(actionTypes), + ...createSoundModeSaga({ ...actionTypes, ...settingsTypes }) ]; export const appMount = createAction(actionTypes.appMount); diff --git a/client/src/redux/prop-types.ts b/client/src/redux/prop-types.ts index f7ad3aee6c..cb5df32e60 100644 --- a/client/src/redux/prop-types.ts +++ b/client/src/redux/prop-types.ts @@ -118,6 +118,7 @@ export const User = PropTypes.shape({ }) ), sendQuincyEmail: PropTypes.bool, + sound: PropTypes.bool, theme: PropTypes.string, twitter: PropTypes.string, username: PropTypes.string, @@ -295,6 +296,7 @@ export type UserType = { }; progressTimestamps: Array; sendQuincyEmail: boolean; + sound: boolean; theme: string; twitter: string; username: string; diff --git a/client/src/redux/sound-mode-saga.js b/client/src/redux/sound-mode-saga.js new file mode 100644 index 0000000000..8fc1903a84 --- /dev/null +++ b/client/src/redux/sound-mode-saga.js @@ -0,0 +1,26 @@ +/* eslint-disable require-yield */ + +import { takeEvery } from 'redux-saga/effects'; +import store from 'store'; + +const soundKey = 'fcc-sound'; + +export function setSound(setting) { + store.set(soundKey, setting); +} + +function* updateLocalSoundSaga({ payload: { user, sound } }) { + if (user) { + const { sound = false } = user; + setSound(sound); + } else if (typeof sound !== 'undefined') { + setSound(sound); + } +} + +export function createSoundModeSaga(types) { + return [ + takeEvery(types.fetchUserComplete, updateLocalSoundSaga), + takeEvery(types.updateUserFlagComplete, updateLocalSoundSaga) + ]; +} diff --git a/client/src/templates/Challenges/classic/editor.tsx b/client/src/templates/Challenges/classic/editor.tsx index c4893806a1..b510ac0e7c 100644 --- a/client/src/templates/Challenges/classic/editor.tsx +++ b/client/src/templates/Challenges/classic/editor.tsx @@ -17,6 +17,7 @@ import React, { import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import store from 'store'; + import { Loader } from '../../../components/helpers'; import { userSelector, isDonationModalOpenSelector } from '../../../redux'; import { @@ -27,7 +28,8 @@ import { ResizePropsType, Test } from '../../../redux/prop-types'; - +import { editorToneOptions } from '../../../utils/tone/editor-config'; +import { editorNotes } from '../../../utils/tone/editor-notes'; import { canFocusEditorSelector, consoleOutputSelector, @@ -44,6 +46,7 @@ import { import './editor.css'; +// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call const MonacoEditor = Loadable(() => import('react-monaco-editor')); interface EditorProps { @@ -191,6 +194,17 @@ const Editor = (props: EditorProps): JSX.Element => { const monacoRef: MutableRefObject = useRef(null); const dataRef = useRef({ ...initialData }); + const player = useRef<{ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sampler: any; + noteIndex: number; + shouldPlay: boolean | undefined; + }>({ + // eslint-disable-next-line no-undefined + sampler: undefined, + noteIndex: 0, + shouldPlay: store.get('fcc-sound') as boolean | undefined + }); // since editorDidMount runs once with the initial props object, it keeps a // reference to *those* props. If we want it to use the latest props, we can @@ -265,6 +279,14 @@ const Editor = (props: EditorProps): JSX.Element => { ); dataRef.current.model = model; + if (player.current.shouldPlay && !player.current.sampler) { + void import('tone').then(tone => { + player.current.sampler = new tone.Sampler( + editorToneOptions + ).toDestination(); + }); + } + // TODO: do we need to return this? return { model }; }; @@ -535,6 +557,21 @@ const Editor = (props: EditorProps): JSX.Element => { coveringRange.startLineNumber - 1, coveringRange.endLineNumber + 1 ]; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (player.current.sampler?.loaded && player.current.shouldPlay) { + void import('tone').then(tone => { + if (tone.context.state !== 'running') void tone.context.resume(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call + player.current.sampler?.triggerAttack( + editorNotes[player.current.noteIndex] + ); + player.current.noteIndex++; + if (player.current.noteIndex >= editorNotes.length) { + player.current.noteIndex = 0; + } + }); + } updateFile({ fileKey, editorValue, editableRegionBoundaries }); }; diff --git a/client/src/templates/Challenges/redux/execute-challenge-saga.js b/client/src/templates/Challenges/redux/execute-challenge-saga.js index 6b2e1c1108..c6161a14ff 100644 --- a/client/src/templates/Challenges/redux/execute-challenge-saga.js +++ b/client/src/templates/Challenges/redux/execute-challenge-saga.js @@ -13,6 +13,7 @@ import { take, cancel } from 'redux-saga/effects'; +import store from 'store'; import { buildChallenge, @@ -97,10 +98,21 @@ export function* executeChallengeSaga({ payload }) { yield put(updateTests(testResults)); const challengeComplete = testResults.every(test => test.pass && !test.err); + const playSound = store.get('fcc-sound'); + let player; + if (playSound) { + void import('tone').then(tone => { + player = new tone.Player( + challengeComplete && payload?.showCompletionModal + ? 'https://campfire-mode.freecodecamp.org/chal-comp.mp3' + : 'https://campfire-mode.freecodecamp.org/try-again.mp3' + ).toDestination(); + player.autostart = true; + }); + } if (challengeComplete && payload?.showCompletionModal) { yield put(openModal('completion')); } - yield put(updateConsole(i18next.t('learn.tests-completed'))); yield put(logsToConsole(i18next.t('learn.console-output'))); } catch (e) { diff --git a/client/src/templates/Introduction/components/Block.js b/client/src/templates/Introduction/components/Block.js index 25eaf6a180..c36df06a63 100644 --- a/client/src/templates/Introduction/components/Block.js +++ b/client/src/templates/Introduction/components/Block.js @@ -5,6 +5,7 @@ import { connect } from 'react-redux'; import ScrollableAnchor from 'react-scrollable-anchor'; import { bindActionCreators } from 'redux'; import { createSelector } from 'reselect'; +import store from 'store'; import envData from '../../../../../config/env.json'; import { isAuditedCert } from '../../../../../utils/is-audited'; @@ -56,6 +57,16 @@ export class Block extends Component { handleBlockClick() { const { blockDashedName, toggleBlock, executeGA } = this.props; + const playSound = store.get('fcc-sound'); + if (playSound) { + void import('tone').then(tone => { + const player = new tone.Player( + 'https://tonejs.github.io/audio/berklee/guitar_chord1.mp3' + ).toDestination(); + if (tone.context.state !== 'running') tone.context.resume(); + player.autostart = playSound; + }); + } executeGA({ type: 'event', data: { diff --git a/client/src/utils/tone/editor-config.ts b/client/src/utils/tone/editor-config.ts new file mode 100644 index 0000000000..e9aeb25174 --- /dev/null +++ b/client/src/utils/tone/editor-config.ts @@ -0,0 +1,51 @@ +export const editorToneOptions = { + urls: { + C1: 'Guitar-Sample-01.mp3', + D1: 'Guitar-Sample-02.mp3', + E1: 'Guitar-Sample-03.mp3', + F1: 'Guitar-Sample-04.mp3', + G1: 'Guitar-Sample-05.mp3', + A1: 'Guitar-Sample-06.mp3', + B1: 'Guitar-Sample-07.mp3', + C2: 'Guitar-Sample-08.mp3', + D2: 'Guitar-Sample-09.mp3', + E2: 'Guitar-Sample-10.mp3', + F2: 'Guitar-Sample-11.mp3', + G2: 'Guitar-Sample-12.mp3', + A2: 'Guitar-Sample-13.mp3', + B2: 'Guitar-Sample-14.mp3', + C3: 'Guitar-Sample-15.mp3', + D3: 'Guitar-Sample-16.mp3', + E3: 'Guitar-Sample-17.mp3', + F3: 'Guitar-Sample-18.mp3', + G3: 'Guitar-Sample-19.mp3', + A3: 'Guitar-Sample-20.mp3', + B3: 'Guitar-Sample-21.mp3', + C4: 'Guitar-Sample-22.mp3', + D4: 'Guitar-Sample-23.mp3', + E4: 'Guitar-Sample-24.mp3', + F4: 'Guitar-Sample-25.mp3', + G4: 'Guitar-Sample-26.mp3', + A4: 'Guitar-Sample-27.mp3', + B4: 'Guitar-Sample-28.mp3', + C5: 'Guitar-Sample-29.mp3', + D5: 'Guitar-Sample-30.mp3', + E5: 'Guitar-Sample-31.mp3', + F5: 'Guitar-Sample-32.mp3', + G5: 'Guitar-Sample-33.mp3', + A5: 'Guitar-Sample-34.mp3', + B5: 'Guitar-Sample-35.mp3', + C6: 'Guitar-Sample-36.mp3', + D6: 'Guitar-Sample-37.mp3', + E6: 'Guitar-Sample-38.mp3', + F6: 'Guitar-Sample-39.mp3', + G6: 'Guitar-Sample-40.mp3', + A6: 'Guitar-Sample-41.mp3', + B6: 'Guitar-Sample-42.mp3', + C7: 'Guitar-Sample-43.mp3', + D7: 'Guitar-Sample-44.mp3', + E7: 'Guitar-Sample-45.mp3' + }, + release: 1, + baseUrl: 'https://campfire-mode.freecodecamp.org/mariachi/' +}; diff --git a/client/src/utils/tone/editor-notes.ts b/client/src/utils/tone/editor-notes.ts new file mode 100644 index 0000000000..0971787c77 --- /dev/null +++ b/client/src/utils/tone/editor-notes.ts @@ -0,0 +1,47 @@ +export const editorNotes = [ + 'C1', + 'D1', + 'E1', + 'F1', + 'G1', + 'A1', + 'B1', + 'C2', + 'D2', + 'E2', + 'F2', + 'G2', + 'A2', + 'B2', + 'C3', + 'D3', + 'E3', + 'F3', + 'G3', + 'A3', + 'B3', + 'C4', + 'D4', + 'E4', + 'F4', + 'G4', + 'A4', + 'B4', + 'C5', + 'D5', + 'E5', + 'F5', + 'G5', + 'A5', + 'B5', + 'C6', + 'D6', + 'E6', + 'F6', + 'G6', + 'A6', + 'B6', + 'C7', + 'D7', + 'E7' +];