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
2 changes: 1 addition & 1 deletion src/components/CloseButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Colors from "@/colors"
import Colors from "@/Colors"
import { useTranslation } from "@/configHooks"
import { HeaderButton } from "@react-navigation/elements"
import { useNavigation } from "@react-navigation/native"
Expand Down
2 changes: 1 addition & 1 deletion src/components/ConfigButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react"
import { Pressable } from "react-native-gesture-handler"
import Colors from "@/colors"
import Colors from "@/Colors"
import { Ionicons } from "@expo/vector-icons"
import { useNavigation } from "@react-navigation/native"
import { View } from "react-native"
Expand Down
2 changes: 1 addition & 1 deletion src/components/FormPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Colors from "@/colors"
import Colors from "@/Colors"
import { MenuAction } from "@react-native-menu/menu"
import { StyleSheet, Text, View } from "react-native"
import { Picker } from "./Picker"
Expand Down
2 changes: 1 addition & 1 deletion src/components/MainNote.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Colors from "@/colors"
import Colors from "@/Colors"
import { InstrumentString } from "@/instruments"
import { useParagraphBuilder } from "@/paragraphs"
import { useTranslation } from "@/configHooks"
Expand Down
2 changes: 1 addition & 1 deletion src/components/MovingGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useSharedValue, useDerivedValue, Easing, cancelAnimation } from "react-
import { withTiming, withRepeat } from "react-native-reanimated"
import { Rect, Line, LinearGradient, Group, vec, Points, Mask } from "@shopify/react-native-skia"
import { useWindowDimensions } from "react-native"
import Colors from "@/colors"
import Colors from "@/Colors"
import { useConfigStore } from "@/stores/configStore"

const GRID_COLOR = Colors.bgInactive
Expand Down
2 changes: 1 addition & 1 deletion src/components/Picker.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Colors from "@/colors"
import Colors from "@/Colors"
import { MenuAction, MenuView } from "@react-native-menu/menu"
import { ReactNode } from "react"
import { Appearance, Platform } from "react-native"
Expand Down
2 changes: 1 addition & 1 deletion src/components/RequireMicAccess.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Colors from "@/colors"
import Colors from "@/Colors"
import { useTranslation } from "@/configHooks"
import React from "react"
import { View, Text, StyleSheet, TouchableOpacity, Linking, Platform } from "react-native"
Expand Down
94 changes: 91 additions & 3 deletions src/components/RightButtons.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import Colors from "@/colors"
import Colors from "@/Colors"
import { Instrument } from "@/instruments"
import { getTuningFreq, InstrumentType, TuningType, useConfigStore } from "@/stores/configStore"
import { getTuningFreq, InstrumentType, TuningType, StretchTuningType, useConfigStore } from "@/stores/configStore"
import { Feather, FontAwesome5, Ionicons } from "@expo/vector-icons"
import { Linking, Platform, Pressable, Text, useWindowDimensions, View } from "react-native"
import { Picker } from "./Picker"
import { MenuAction } from "@react-native-menu/menu"
import { useMemo } from "react"
import { useTranslation } from "@/configHooks"
import { useTranslation, useSettingsOptions } from "@/configHooks"

export const RightButtons = ({
positionY,
Expand All @@ -20,8 +20,11 @@ export const RightButtons = ({
const setManual = useConfigStore((state) => state.setManual)
const setInstrument = useConfigStore((state) => state.setInstrument)
const setTuning = useConfigStore((state) => state.setTuning)
const setStretchTuning = useConfigStore((state) => state.setStretchTuning)
const tuning = useConfigStore((state) => state.tuning)
const stretchTuning = useConfigStore((state) => state.stretchTuning)
const t = useTranslation()
const options = useSettingsOptions()
const btnW = width / 7
const fontHeight = height / 70
const fontSize = fontHeight / 1.3
Expand Down Expand Up @@ -85,6 +88,52 @@ export const RightButtons = ({
]
}, [t])

const stretchTunings: MenuAction[] = useMemo(() => {
const subactions = [
{
id: "none" as StretchTuningType,
title: t("stretch_none"),
displayInline: true,
},
{
id: "upright" as StretchTuningType,
title: t("stretch_upright"),
displayInline: true,
},
{
id: "spinet" as StretchTuningType,
title: t("stretch_spinet"),
displayInline: true,
},
{
id: "console" as StretchTuningType,
title: t("stretch_console"),
displayInline: true,
},
{
id: "studio" as StretchTuningType,
title: t("stretch_studio"),
displayInline: true,
},
{
id: "baby_grand" as StretchTuningType,
title: t("stretch_baby_grand"),
displayInline: true,
},
]
// Avoid nested menus in android (collapsed by default)
return Platform.OS === "android"
? subactions
: [
{
id: "stretch-tuning-type",
title: t("stretch_tuning"),
displayInline: true,
subactions,
},
]
}, [t])

return (
<View
style={{
Expand Down Expand Up @@ -192,6 +241,45 @@ export const RightButtons = ({
</View>
</Picker>

{/* Stretch tuning picker - only show for chromatic instrument */}
{instrument.name === "chromatic" && (
<Picker actions={stretchTunings} onSelect={(value) => setStretchTuning(value as StretchTuningType)} value={stretchTuning}>
<View
style={{
marginLeft: btnSpacing,
width: btnW,
borderRadius: 10,
backgroundColor: Colors.bgActive,
borderColor: Colors.secondary,
borderWidth: btnBorder,
justifyContent: "center",
paddingVertical: 10,
gap: 3,
}}
>
<Text
style={{
color: Colors.primary,
fontSize: fontSize * 0.8,
textAlign: "center",
}}
>
{t("stretch_tuning")}
</Text>
<Text
style={{
color: stretchTuning === "none" ? Colors.ok : Colors.warn,
fontSize: fontSize,
textAlign: "center",
fontWeight: "600",
}}
>
{options.getStretchTuningName(stretchTuning)}
</Text>
</View>
</Picker>
)}

<Pressable
onPress={async () => {
await Linking.openURL(`https://www.instagram.com/tuneo.app/`)
Expand Down
2 changes: 1 addition & 1 deletion src/components/Strings.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Colors from "@/colors"
import Colors from "@/Colors"
import { Instrument } from "@/instruments"
import { getFreqFromNote } from "@/notes"
import { useConfigStore } from "@/stores/configStore"
Expand Down
2 changes: 1 addition & 1 deletion src/components/TuningGauge.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Colors from "@/colors"
import Colors from "@/Colors"
import { Circle, Group, Line, Paint } from "@shopify/react-native-skia"
import { useEffect } from "react"
import { useWindowDimensions } from "react-native"
Expand Down
2 changes: 1 addition & 1 deletion src/components/Waveform.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Colors from "@/colors"
import Colors from "@/Colors"
import { Group, Path, SkPath, usePathInterpolation } from "@shopify/react-native-skia"
import { useEffect, useMemo, useState } from "react"
import { useWindowDimensions } from "react-native"
Expand Down
22 changes: 22 additions & 0 deletions src/configHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
InstrumentType,
LANGUAGE_IDS,
LanguageType,
STRETCH_TUNING_IDS,
StretchTuningType,
THEME_IDS,
ThemeType,
TUNING_IDS,
Expand Down Expand Up @@ -75,6 +77,23 @@ export const useSettingsOptions = () => {
}
},

getStretchTuningName: (stretchTuning: StretchTuningType): string => {
switch (stretchTuning) {
case "none":
return t("stretch_none")
case "upright":
return t("stretch_upright")
case "spinet":
return t("stretch_spinet")
case "console":
return t("stretch_console")
case "studio":
return t("stretch_studio")
case "baby_grand":
return t("stretch_baby_grand")
}
},

getGraphicModeName: (graphics: GraphicsMode): string => {
switch (graphics) {
case "high":
Expand All @@ -96,6 +115,9 @@ export const useSettingsOptions = () => {
getTunings: function () {
return TUNING_IDS.map((id) => ({ id, title: this.getTuningName(id) } as MenuAction))
},
getStretchTunings: function () {
return STRETCH_TUNING_IDS.map((id) => ({ id, title: this.getStretchTuningName(id) } as MenuAction))
},
getGraphics: function () {
return GRAPHIC_MODES.map((id) => ({ id, title: this.getGraphicModeName(id) } as MenuAction))
},
Expand Down
16 changes: 9 additions & 7 deletions src/instruments.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { getFreqFromNote, getNoteFromFreq, Note } from "./notes"
import { InstrumentType, TuningType } from "./stores/configStore"
import { InstrumentType, TuningType, StretchTuningType } from "./stores/configStore"

export type InstrumentString = { note: Note; freq: number }

export abstract class Instrument {
tuning: TuningType
stretchTuning: StretchTuningType
abstract readonly name: InstrumentType
abstract readonly hasStrings: boolean

constructor(tuning: TuningType) {
constructor(tuning: TuningType, stretchTuning: StretchTuningType = "none") {
this.tuning = tuning
this.stretchTuning = stretchTuning
}

abstract getStrings(): Note[]
Expand Down Expand Up @@ -42,9 +44,9 @@ export class Guitar extends Instrument {
]
stringFreqs: number[] // depends on tuning type

constructor(tuning: TuningType) {
super(tuning)
this.stringFreqs = this.stringNotes.map((note) => getFreqFromNote(note, tuning))
constructor(tuning: TuningType, stretchTuning: StretchTuningType = "none") {
super(tuning, stretchTuning)
this.stringFreqs = this.stringNotes.map((note) => getFreqFromNote(note, tuning, stretchTuning))
}

get name(): InstrumentType {
Expand Down Expand Up @@ -85,8 +87,8 @@ export class Chromatic extends Instrument {
const note = getNoteFromFreq(freq, this.tuning)
if (!note) return undefined

// Find frequency of the nearest note
const noteFreq = getFreqFromNote(note, this.tuning)
// Find frequency of the nearest note with stretch tuning applied
const noteFreq = getFreqFromNote(note, this.tuning, this.stretchTuning)
return { note, freq: noteFreq }
}
}
2 changes: 1 addition & 1 deletion src/navigation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { StaticParamList } from "@react-navigation/native"
import { createNativeStackNavigator } from "@react-navigation/native-stack"
import { Settings } from "./screens/Settings"
import { Tuneo } from "./screens/Tuneo"
import Colors from "@/colors"
import Colors from "@/Colors"
import { CloseButton } from "@/components/CloseButton"
import { Platform } from "react-native"

Expand Down
2 changes: 1 addition & 1 deletion src/navigation/screens/Settings.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Colors from "@/colors"
import Colors from "@/Colors"
import { FormPicker } from "@/components/FormPicker"
import { useSettingsOptions, useTranslation } from "@/configHooks"
import { GraphicsMode, LanguageType, ThemeType, useConfigStore } from "@/stores/configStore"
Expand Down
8 changes: 4 additions & 4 deletions src/navigation/screens/Tuneo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Canvas } from "@shopify/react-native-skia"
import DSPModule from "@/../specs/NativeDSPModule"
import MicrophoneStreamModule, { AudioBuffer } from "@/../modules/microphone-stream"
import { AudioModule } from "expo-audio"
import Colors from "@/colors"
import Colors from "@/Colors"
import { getTestSignal } from "@/test"
import MovingGrid from "@/components/MovingGrid"
import ConfigButton from "@/components/ConfigButton"
Expand Down Expand Up @@ -184,11 +184,11 @@ export const Tuneo = () => {
const instrument: Instrument = useMemo(() => {
switch (config.instrument) {
case "guitar":
return new Guitar(config.tuning)
return new Guitar(config.tuning, config.stretchTuning)
case "chromatic":
return new Chromatic(config.tuning)
return new Chromatic(config.tuning, config.stretchTuning)
}
}, [config.instrument, config.tuning])
}, [config.instrument, config.tuning, config.stretchTuning])

// Disable manual mode if instrument doesn't support strings
useEffect(() => {
Expand Down
20 changes: 15 additions & 5 deletions src/notes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TuningType } from "./configHooks"
import { TuningType, StretchTuningType } from "./stores/configStore"
import { applyStretchTuning } from "./stretchTuning"

const NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] as const
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand Down Expand Up @@ -27,7 +28,7 @@ export function getNoteFromFreq(frequency: number, tuning: TuningType): Note | u

// Calculate the number of semitones from reference A4
const a4_frequency = getReferenceFrequency(tuning)
const semitonesFromA4 = 12 * Math.log2(frequency / a4_frequency)
const semitonesFromA4 = 12 * (Math.log(frequency / a4_frequency) / Math.LN2)

// Octaves start in C, calculate semitones from C4
const semitonesFromC4 = Math.round(semitonesFromA4 + 9)
Expand All @@ -42,9 +43,11 @@ export function getNoteFromFreq(frequency: number, tuning: TuningType): Note | u
/**
* Calculates the frequency of a note given its name and octave.
* @param note The name and octave of the note.
* @param tuning The tuning type
* @param stretchTuning The stretch tuning type (optional)
* @returns The frequency of the note in Hz.
*/
export function getFreqFromNote(note: Note | undefined, tuning: TuningType): number {
export function getFreqFromNote(note: Note | undefined, tuning: TuningType, stretchTuning?: StretchTuningType): number {
if (!note) return 0

const a4_frequency = getReferenceFrequency(tuning)
Expand All @@ -53,8 +56,15 @@ export function getFreqFromNote(note: Note | undefined, tuning: TuningType): num
const noteDistance = NOTE_NAMES.indexOf(note.name) - NOTE_NAMES.indexOf("A")
const semitonesFromA4 = (note.octave - 4) * 12 + noteDistance

// freq = ref^(semitones / 12)
return a4_frequency * Math.pow(2, semitonesFromA4 / 12)
// Calculate base frequency in equal temperament
const baseFreq = a4_frequency * Math.pow(2, semitonesFromA4 / 12)

// Apply stretch tuning if specified
if (stretchTuning) {
return applyStretchTuning(baseFreq, stretchTuning)
}

return baseFreq
}

/**
Expand Down
Loading