Skip to content
a simple game for practicing pitch perception
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Type Name Latest commit message Commit time
Failed to load latest commit information.

Ear Sharpener

Ear Sharpener is a simple game for practicing pitch perception. While ear training probably won't give you absolute or perfect pitch, it may improve your relative pitch. Try it for yourself!

Tech notes

  • Made with TypeScript, React, Redux, Immutable.js, React Router, PostCSS, Mocha, Chai, Sinon, Enzyme, Webpack, and the Redux DevTools Extension.
  • I wrote about my experience with TypeScript on this game here on Reddit.
  • Redux actions are type safe in reducers using the discriminated union types available in the pre-2.0 TypeScript nightly. The actions are defined in src/types.ts. The old ununsed hacky action implementation is in src/utils/actions.
  • Friction between Redux and the needs of this game:
    • There's a lot of complexity in the gameActions to get the desired UX. The four main sources of complexity include time-sequenced actions and side effects, disabling input when appropriate, canceling async audio that should no longer be played due to user input, and sequencing actions in the combo game differently than in standalone games.
    • The Redux actions and reducers are tested together, not in isolation. This is not the recommended best practice, but this game has thunk action creators like gameActions.guess and gameActions.present that sequence multiple dispatched actions and side effects based on changing store state. Mocking the store with something like redux-mock-store is possible but impractical because these action creators read the state changes to behave correctly, so mocking the store would require mocking all state changes after each dispatch, which duplicates reducer logic and adds significant complexity. Given this dependency of actions on reducers, I wrote the action tests to include the entire action/reducer flow and assert the state changes rather than the dispatched actions, and the reducers have simple smoke tests.
    • Redux reducers should be pure, so presenting a game (playing its audio) is not performed in reducers. However presenting also requires data transformations, so the PresentingAction and PresentedAction create the new states. Doing this unfortunately fragments some of the logic. For example, it would follow the rest of the app's conventions that present(game) increments the presentCount and plays the audio, but now they are separate code paths. This design is more verbose and error prone than it feels like it should be.
  • There's a boundary between Immutable.js and plain JS objects/arrays in the models before the heavier data transformations. The app state is represented fully with Immutable.js data structures, but many supporting model functions use plain JS arrays/objects because Immutable.js collections are unwieldy.
  • The React idea of the UI being a pure projection of state is somewhat at odds with event-triggered time-dependent effects like the feedback animations after guesses. This lead to some strange workarounds like tracking action counters in the game state that get used in React component keys to get the desired animations.
  • All non-container React components implement a shallow compare for shouldComponentUpdate, made possible by Redux and immutable app state.


npm install
npm install -g tsd
tsd install
npm test
npm start
# browse to http://localhost:8080

Possible future enhancements

  • more sounds than just piano, like other instruments and generated synths
  • expose game variables in the ui so the player can configure things to their liking
  • let the player lock the current difficulty level
  • more game types? variations of existing games?
  • better support for mobile and keyboard controls
  • display audio load failures to the player instead of an unending loading animation
  • ...? share your ideas!

Piano audio samples

Uploaded by user beskhu at (public domain license) Transformed with Audacity into 3 second mp3s that fade out.



You can’t perform that action at this time.