Skip to content

Commit

Permalink
keyboard illustrations (#35)
Browse files Browse the repository at this point in the history
  • Loading branch information
edipermadi committed Nov 24, 2023
1 parent 7043465 commit 5d8fbbd
Show file tree
Hide file tree
Showing 11 changed files with 511 additions and 74 deletions.
28 changes: 19 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Features:
- Ian Ring's numbering system for pitches, chords and scales
- Scale and key illustration as pitch class bracelet diagram
- Scale and key illustration as circle of fifth bracelet diagram
- Scale, key and chord and illustration using keyboard

## Running test

Expand Down Expand Up @@ -103,14 +104,15 @@ Pagination via query string `page` and `per_page`

### Chords

| Method | Path | Description |
|--------|---------------------------------------|--------------------|
| GET | `/api/v1/theory/chords/{:id}/keys` | List chord keys |
| GET | `/api/v1/theory/chords/{:id}/pitches` | List chord pitches |
| GET | `/api/v1/theory/chords/{:id}/quality` | Get chord quality |
| GET | `/api/v1/theory/chords/{:id}/scales` | List chord scales |
| GET | `/api/v1/theory/chords/{:id}` | Get chord |
| GET | `/api/v1/theory/chords` | List chords |
| Method | Path | Description |
|--------|------------------------------------------------------|-------------------------------------|
| GET | `/api/v1/theory/chords/{:id}/keys` | List chord keys |
| GET | `/api/v1/theory/chords/{:id}/pitches` | List chord pitches |
| GET | `/api/v1/theory/chords/{:id}/quality` | Get chord quality |
| GET | `/api/v1/theory/chords/{:id}/scales` | List chord scales |
| GET | `/api/v1/theory/chords/{:id}` | Get chord |
| GET | `/api/v1/theory/chords` | List chords |
| GET | `/api/v1/theory/chords/{:id}/illustrations/keyboard` | Illustrate the chord using keyboard |

### Scales

Expand All @@ -123,6 +125,7 @@ Pagination via query string `page` and `per_page`
| GET | `/api/v1/theory/scales` | List scales |
| GET | `/api/v1/theory/scales/{:id}/illustrations/pitch_class_bracelet` | Illustrate the scale as a pitch class bracelet diagram |
| GET | `/api/v1/theory/scales/{:id}/illustrations/circle_of_fifth_bracelet` | Illustrate the scale as a circle of fifth bracelet diagram |
| GET | `/api/v1/theory/scales/{:id}/illustrations/keyboard` | Illustrate the scale using keyboard |

### Keys

Expand All @@ -135,6 +138,7 @@ Pagination via query string `page` and `per_page`
| GET | `/api/v1/theory/keys` | List keys |
| GET | `/api/v1/theory/keys/{:id}/illustrations/pitch_class_bracelet` | Illustrate the key as a pitch class bracelet diagram |
| GET | `/api/v1/theory/keys/{:id}/illustrations/circle_of_fifth_bracelet` | Illustrate the key as a circle of fifth bracelet diagram |
| GET | `/api/v1/theory/keys/{:id}/illustrations/keyboard` | Illustrate the key using keyboard |

## Bracelet Diagram

Expand All @@ -147,4 +151,10 @@ C Natural Ionian illustrated as pitch class bracelet diagram
### Circle Of Fifth Bracelet Diagram

C Natural Ionian illustrated as circle of fifth bracelet diagram
![CNaturalIonian](docs/images/CNaturalIonianCircleOfFifthBracelet.png)
![CNaturalIonian](docs/images/CNaturalIonianCircleOfFifthBracelet.png)

## Keyboard

FNaturalIonian illustrated using keyboard

![FNaturalIonian](docs/images/FNaturalIonianKeyboard.png)
Binary file added docs/images/FNaturalIonianKeyboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
90 changes: 90 additions & 0 deletions docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,36 @@
}
}
},
"/chords/{chord_id}/illustrations/keyboard": {
"get": {
"operationId": "IllustrateChordUsingKeyboard",
"tags": [
"chord"
],
"summary": "Illustrate the chord using keyboard",
"description": "Illustrate the chord using keyboard. Blue dot indicates the root",
"consumes": [
"application/json"
],
"produces": [
"image/png"
],
"parameters": [
{
"name": "chord_id",
"description": "Chord identifier",
"in": "path",
"required": true,
"type": "number"
}
],
"responses": {
"200": {
"description": "successful operation"
}
}
}
},
"/scales": {
"get": {
"operationId": "ListScales",
Expand Down Expand Up @@ -1357,6 +1387,36 @@
}
}
},
"/scales/{scale_id}/illustrations/keyboard": {
"get": {
"operationId": "IllustrateScaleUsingKeyboard",
"tags": [
"scale"
],
"summary": "Illustrate the scale using keyboard",
"description": "Illustrate the scale using keyboard. Blue dot indicates the tonic",
"consumes": [
"application/json"
],
"produces": [
"image/png"
],
"parameters": [
{
"name": "scale_id",
"description": "Scale identifier",
"in": "path",
"required": true,
"type": "number"
}
],
"responses": {
"200": {
"description": "successful operation"
}
}
}
},
"/keys": {
"get": {
"operationId": "ListKeys",
Expand Down Expand Up @@ -1842,6 +1902,36 @@
}
}
}
},
"/keys/{key_id}/illustrations/keyboard": {
"get": {
"operationId": "IllustrateKeyUsingKeyboard",
"tags": [
"key"
],
"summary": "Illustrate the key using keyboard",
"description": "Illustrate the key using keyboard. Blue dot indicates the tonic",
"consumes": [
"application/json"
],
"produces": [
"image/png"
],
"parameters": [
{
"name": "key_id",
"description": "Key identifier",
"in": "path",
"required": true,
"type": "number"
}
],
"responses": {
"200": {
"description": "successful operation"
}
}
}
}
},
"definitions": {
Expand Down
65 changes: 57 additions & 8 deletions internal/theory/handler_chord.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ package theory

import (
"errors"
"fmt"
"image/png"
"net/http"
"strconv"

"github.com/edipermadi/music-db/internal/platform/api"
"github.com/edipermadi/music-db/pkg/illustations"
"github.com/edipermadi/music-db/pkg/theory/pitch"
"github.com/gorilla/mux"
"go.uber.org/zap"
)
Expand All @@ -26,6 +30,7 @@ func (h theoryHandler) installChordEndpoints(router *mux.Router) {
router.HandleFunc("/chords/{id:[0-9]+}/pitches", h.ListChordPitches).Methods(http.MethodGet).Name("GET_CHORD_PITCHES")
router.HandleFunc("/chords/{id:[0-9]+}/quality", h.GetChordQuality).Methods(http.MethodGet).Name("GET_CHORD_QUALITY")
router.HandleFunc("/chords/{id:[0-9]+}/scales", h.ListChordScales).Methods(http.MethodGet).Name("LIST_CHORD_SCALES")
router.HandleFunc("/chords/{id:[0-9]+}/illustrations/keyboard", h.IllustrateChordWithKeyboard).Methods(http.MethodGet).Name("ILLUSTRATE_CHORD_WITH_KEYBOARD")
}

func (h theoryHandler) ListChords(writer http.ResponseWriter, request *http.Request) {
Expand Down Expand Up @@ -57,12 +62,12 @@ func (h theoryHandler) ListChordPitches(writer http.ResponseWriter, request *htt
ctx := request.Context()

chordID, _ := strconv.ParseInt(mux.Vars(request)["id"], 10, 64)
chords, err := h.service.ListChordPitches(ctx, chordID)
simplifiedPitches, err := h.service.ListChordPitches(ctx, chordID)
if err != nil {
h.Logger().With(zap.Error(err)).Error("failed to list chord pitches")
h.ReplyJSON(writer, http.StatusInternalServerError, api.ErrInternalServer)
} else {
h.ReplyJSON(writer, http.StatusOK, chords)
h.ReplyJSON(writer, http.StatusOK, simplifiedPitches)
}
}

Expand All @@ -82,13 +87,13 @@ func (h theoryHandler) ListChordKeys(writer http.ResponseWriter, request *http.R
}

chordID, _ := strconv.ParseInt(mux.Vars(request)["id"], 10, 64)
chords, paginationOut, err := h.service.ListChordKeys(ctx, chordID, data.KeyFilter, data.Pagination)
keys, paginationOut, err := h.service.ListChordKeys(ctx, chordID, data.KeyFilter, data.Pagination)
if err != nil {
h.Logger().With(zap.Error(err)).Error("failed to list chord keys")
h.ReplyJSON(writer, http.StatusInternalServerError, api.ErrInternalServer)
} else {
h.SetPagination(writer, paginationOut)
h.ReplyJSON(writer, http.StatusOK, chords)
h.ReplyJSON(writer, http.StatusOK, keys)
}
}

Expand All @@ -108,13 +113,13 @@ func (h theoryHandler) ListChordScales(writer http.ResponseWriter, request *http
}

chordID, _ := strconv.ParseInt(mux.Vars(request)["id"], 10, 64)
chords, paginationOut, err := h.service.ListChordScales(ctx, chordID, data.ScaleFilter, data.Pagination)
scales, paginationOut, err := h.service.ListChordScales(ctx, chordID, data.ScaleFilter, data.Pagination)
if err != nil {
h.Logger().With(zap.Error(err)).Error("failed to list chord scales")
h.ReplyJSON(writer, http.StatusInternalServerError, api.ErrInternalServer)
} else {
h.SetPagination(writer, paginationOut)
h.ReplyJSON(writer, http.StatusOK, chords)
h.ReplyJSON(writer, http.StatusOK, scales)
}
}

Expand All @@ -138,14 +143,58 @@ func (h theoryHandler) GetChordQuality(writer http.ResponseWriter, request *http
ctx := request.Context()

chordID, _ := strconv.ParseInt(mux.Vars(request)["id"], 10, 64)
chord, err := h.service.GetChordQuality(ctx, chordID)
quality, err := h.service.GetChordQuality(ctx, chordID)
switch {
case errors.Is(err, ErrChordQualityNotFound):
h.ReplyJSON(writer, http.StatusNotFound, api.ErrResourceNotFound)
case err != nil:
h.Logger().With(zap.Error(err)).Error("failed to get chord quality")
h.ReplyJSON(writer, http.StatusInternalServerError, api.ErrInternalServer)
default:
h.ReplyJSON(writer, http.StatusOK, chord)
h.ReplyJSON(writer, http.StatusOK, quality)
}
}

func (h theoryHandler) IllustrateChordWithKeyboard(writer http.ResponseWriter, request *http.Request) {
ctx := request.Context()

chordID, _ := strconv.ParseInt(mux.Vars(request)["id"], 10, 64)

// get chord
chord, err := h.service.GetChord(ctx, chordID)
switch {
case errors.Is(err, ErrChordNotFound):
h.ReplyJSON(writer, http.StatusNotFound, api.ErrResourceNotFound)
return
case err != nil:
h.Logger().With(zap.Error(err)).Error("failed to list chord pitches")
h.ReplyJSON(writer, http.StatusInternalServerError, api.ErrInternalServer)
return
}

// list pitches
simplifiedPitches, err := h.service.ListChordPitches(ctx, chordID)
if err != nil {
h.Logger().With(zap.Error(err)).Error("failed to list chord pitches")
h.ReplyJSON(writer, http.StatusInternalServerError, api.ErrInternalServer)
return
}

pitches := make([]pitch.Type, 0)
for _, v := range simplifiedPitches {
pitches = append(pitches, pitch.FromInt(int(v.ID)))
}

// draw keyboard illustration
img, err := illustations.Keyboard(pitches)
if err != nil {
h.Logger().With(zap.Error(err)).Error("failed to draw keyboard illustration for chord")
writer.WriteHeader(http.StatusInternalServerError)
return
}

writer.WriteHeader(http.StatusOK)
writer.Header().Set("Content-Type", "image/png")
writer.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=%q", fmt.Sprintf("%sKeyboard.png", chord.Name)))
_ = png.Encode(writer, img)
}
45 changes: 43 additions & 2 deletions internal/theory/handler_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ package theory
import (
"errors"
"fmt"
"github.com/edipermadi/music-db/pkg/illustations"
"github.com/edipermadi/music-db/pkg/theory/pitch"
"image/png"
"net/http"
"strconv"

"github.com/edipermadi/music-db/internal/platform/api"
"github.com/edipermadi/music-db/pkg/illustations"
"github.com/edipermadi/music-db/pkg/theory/pitch"
"github.com/gorilla/mux"
"go.uber.org/zap"
)
Expand All @@ -30,6 +30,7 @@ func (h theoryHandler) installKeyEndpoints(router *mux.Router) {
router.HandleFunc("/keys/{id:[0-9]+}/pitches", h.ListKeyPitches).Methods(http.MethodGet).Name("LIST_KEY_PITCHES")
router.HandleFunc("/keys/{id:[0-9]+}/illustrations/pitch_class_bracelet", h.IllustrateKeyAsPitchClassBraceletDiagram).Methods(http.MethodGet).Name("ILLUSTRATE_KEY_AS_PITCH_CLASSES_BRACELET")
router.HandleFunc("/keys/{id:[0-9]+}/illustrations/circle_of_fifth_bracelet", h.IllustrateKeyAsCircleOfFifthBraceletDiagram).Methods(http.MethodGet).Name("ILLUSTRATE_KEY_AS_CIRCLE_OF_FIFTH_BRACELET")
router.HandleFunc("/keys/{id:[0-9]+}/illustrations/keyboard", h.IllustrateKeyWithKeyboard).Methods(http.MethodGet).Name("ILLUSTRATE_KEY_WITH_KEYBOARD")
}

func (h theoryHandler) ListKeys(writer http.ResponseWriter, request *http.Request) {
Expand Down Expand Up @@ -211,3 +212,43 @@ func (h theoryHandler) IllustrateKeyAsCircleOfFifthBraceletDiagram(writer http.R
writer.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=%q", fmt.Sprintf("%sCircleOfFifthBracelet.png", key.Name)))
_ = png.Encode(writer, img)
}

func (h theoryHandler) IllustrateKeyWithKeyboard(writer http.ResponseWriter, request *http.Request) {
ctx := request.Context()

// get key
keyID, _ := strconv.ParseInt(mux.Vars(request)["id"], 10, 64)
key, err := h.service.GetKey(ctx, keyID)
switch {
case errors.Is(err, ErrKeyNotFound):
h.ReplyJSON(writer, http.StatusNotFound, api.ErrResourceNotFound)
case err != nil:
h.Logger().With(zap.Error(err)).Error("failed to get key")
h.ReplyJSON(writer, http.StatusInternalServerError, api.ErrInternalServer)
}

// list pitches
simplifiedPitches, err := h.service.ListKeyPitches(ctx, keyID)
if err != nil {
h.Logger().With(zap.Error(err)).Error("failed to list key pitches")
h.ReplyJSON(writer, http.StatusInternalServerError, api.ErrInternalServer)
}

pitches := make([]pitch.Type, 0)
for _, v := range simplifiedPitches {
pitches = append(pitches, pitch.FromInt(int(v.ID)))
}

// draw keyboard illustration
img, err := illustations.Keyboard(pitches)
if err != nil {
h.Logger().With(zap.Error(err)).Error("failed to draw keyboard illustration for key")
writer.WriteHeader(http.StatusInternalServerError)
return
}

writer.WriteHeader(http.StatusOK)
writer.Header().Set("Content-Type", "image/png")
writer.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=%q", fmt.Sprintf("%sKeyboard.png", key.Name)))
_ = png.Encode(writer, img)
}
Loading

0 comments on commit 5d8fbbd

Please sign in to comment.