Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Frequency module #51

Open
MartinSStewart opened this issue May 9, 2020 · 9 comments
Open

Add Frequency module #51

MartinSStewart opened this issue May 9, 2020 · 9 comments
Milestone

Comments

@MartinSStewart
Copy link

I'm working on adding the ability to play sine/square/sawtooth/triangle waves to elm-audio. Currently elm-audio has it's own type alias Frequency = Quantity Float (Rate Cycles Seconds) and a cyclesPerSecond : Float -> Frequency helper function. I started adding the ability to create frequencies from music notes when I realized that's a broad topic and not something I know enough about to properly implement. It would be useful then if both elm-audio and a potential future elm-music-theory elm package could share the same Frequency type so they don't need to have dependencies on each other.

The module could look something like this:

module Frequency exposing (..)

import Duration exposing (Seconds)

-- Maybe this could be called `Cycles`, `Oscillations`, or `Vibrations`?
type SoundWaves = SoundWaves

type alias Frequency = Quantity Float (Rate SoundWaves Seconds)

hertz: Float -> Frequency

kilohertz : Float -> Frequency

...

An alternate name for this module could be Pitch but it seems like there is a distinction between pitch and frequency where frequency is oscillations per second and pitch is the human perception of frequencies (at least, this is what wikipedia tells me https://en.wikipedia.org/wiki/Pitch_%28music%29).

Lastly, this issue is sort of related to this discussion but that discussed a generic frequency type and this is for a sound based frequency module.

@ianmackenzie
Copy link
Owner

I think my preference would be for a generic Frequency type, but I'm not sure what the best definition would be:

-- Most generic
type alias Frequency units =
    Quantity Float (Rate units seconds)

-- Simpler but less flexible
type alias Frequency =
    Quantity Float (Rate Unitless Seconds)

-- Simplest (and best error messages) but least flexible
type alias Frequency=
    Quantity Float Hertz

Can you think of what operations you might want to do on a Frequency value other than just pass it around? Having a type-safe way to pass around frequency values is absolutely a good enough reason by itself to have a Frequency type, but what operations we want to support would affect what type definition made the most sense.

For example, is it actually useful to multiply frequency by time to get number of occurrences? What would that look like? If it's not useful (and the only useful operations are just on frequency values themselves, like subtracting two frequencies to get a beat frequency) then Quantity Float Hertz would probably be simplest.

@MartinSStewart
Copy link
Author

MartinSStewart commented May 10, 2020

Can you think of what operations you might want to do on a Frequency value other than just pass it around?

If someone is creating a music theory package then they might want to take a frequency and raise it an octave. Or finding harmonics for a frequency. For elm-audio, maybe I could add a function that takes the sample rate of the user's audio system (currently I represent sample rate as just an int but maybe that's also a frequency?) and computes highest possible frequency supported when playing a sine wave.

@ianmackenzie
Copy link
Owner

Those all sound like things that don't involve changing units - e.g. raising an octave would be just Quantity.twice frequency, right? Basically all the operations I can think of are ones that manipulate one or two frequencies to produce another frequency, as opposed to operations which involve changing units (like multiplying a frequency by a duration to get a count). If that's true, then we might be able to reasonably get away with

type Hertz
    = Hertz

type alias Frequency =
    Quantity Float Hertz

instead of the more general but confusing

type alias Frequency events =
    Quantity Float (Rate events Seconds)

I guess a compromise might be

type Events
    = Events

type alias Frequency =
    Quantity Float (Rate Events Seconds)

which opens the door to some rate-related math but avoids needing a type variable and is somewhat self-documenting. You could even have something like

module Frequency exposing
    ( Frequency
    , Events
    , events
    , eventCount
    , hertz
    , inHertz
    )

type Events
    = Events 

type alias Frequency =
    Quantity Float (Rate Events Seconds)

events : Float -> Quantity Float Events
events numEvents =
    Quantity numEvents

eventCount : Quantity Float Events -> Float
eventCount (Quantity numEvents) =
    numEvents

hertz : Float -> Frequency
hertz numHertz =
    Quantity numHertz

inHertz : Frequency -> Hertz
inHertz (Quantity numHertz) =
    numHertz

which would allow for things like

heartRate =
    Frequency.events 55 |> Quantity.per Duration.minute

numHeartbeats =
    Duration.seconds 10
        |> Quantity.at heartRate
        |> Frequency.eventCount

(I'm not sure I'm wild about the eventCount name, but structurally this doesn't seem too bad...)

@MartinSStewart
Copy link
Author

MartinSStewart commented May 11, 2020

To me it feels like the problem with Frequency represented in these ways is that it's too general and too similar to Rate (I think that's the conclusion we came to in the previous discussion). A generic Frequency could represent heart rate, pitch, or how often someone buys groceries. So the risk I see is that seeing Frequency in a data structure, doesn't tell you much about what kind of data it is. And being similar to Rate means there's ambiguity in whether to use Frequency or Rate. In the case of elm-audio, I have a sampleRate field. Should that be a frequency since it's events over time? Seems unintuitive since it has rate in the name.

So the advantage with having Frequency only refer to acoustic waves is that it's clear when it use it and what it means (maybe acoustics is too specific and it could be a module for anything dealing with sine waves such as light wave frequencies).

Those all sound like things that don't involve changing units - e.g. raising an octave would be just Quantity.twice frequency, right?

Yeah, I think so. As mentioned, this is really not my area of expertise 😅

Edit:
To address "why not use this example and let the user pick an appropriate unit?"

type alias Frequency events =
Quantity Float (Rate events Seconds)

That comes back to the original problem I'm having which is having a common unit both elm-audio and some elm-music-theory package can both use to represent frequencies. If I need to define what events is then elm-music-theory will need to add elm-audio as a dependency (given that elm-audio deals with ports and JS that's not desirable) if it wants to make it easy for someone to create notes and then play them with elm-audio.

@ianmackenzie
Copy link
Owner

I guess what I like about having a built-in Events units type is that it does at least prevent you from doing nonsensical things like representing speed as a Frequency Meters, so cuts down on the ambiguity a bit. But it does seem slightly weird.

What do you think of the plain

type Hertz
    = Hertz

type alias Frequency =
    Quantity Float Hertz

idea? I think it addresses most use cases I can think of, is pretty simple and avoids weirdness/confusion. And for stuff like the heart rate example I came up with, it probably makes more sense to just have (in app code) something like

type Heartbeats
    = Heartbeats

heartbeats : Float -> Quantity Float Heartbeats

inHeartbeats : Quantity Float Heartbeats -> Float

heartRate =
    heartbeats 55 |> Quantity.per Duration.minute

numHeartbeats =
    Duration.seconds 10
        |> Quantity.at heartRate
        |> inHeartbeats

@MartinSStewart
Copy link
Author

I checked the definition of hertz and I think part of my argument that it's too similar to Rate was caused by thinking hertz represented a generic "events over time" unit. Wikipedia defines it as one cycle per second and uses examples of things that oscillate and have fixed repeating patterns. That's probably unique enough to not be mixed up with Rate.

type alias Frequency =
    Quantity Float Hertz

So this works for me.

@ianmackenzie ianmackenzie added this to the 2.6 milestone May 21, 2020
@ianmackenzie
Copy link
Owner

It occurred to me that you could add a couple functions that would help compensate for Hertz not actually being a Rate:

-- Count number of cycles in a particular time period,
-- similar to Quantity.for
Frequency.for : Duration -> Frequency -> Float 

-- Get the cycle period of a particular frequency
Frequency.period : Frequency -> Duration

Then you could do things like

numCycles : Float
numCycles =
    Frequency.kilohertz 3.5 |> Frequency.for (Duration.milliseconds 10)

to get a plain Float value, or

frequency : Frequency
frequency =
    Frequency.hertz 55

heartRate : Quantity Float (Rate Heartbeats Seconds)
heartRate =
    heartbeats 1 |> Quantity.per (Frequency.period frequency)

to convert a Frequency into a Quantity Float (Rate Heartbeats Seconds). You could also compress that a bit to something like

Frequency.perCycle : 
    Quantity Float units 
    -> Frequency 
    -> Quantity Float (Rate units Seconds)

which you could use as

heartRate : Quantity Float (Rate Heartbeats Seconds)
heartRate =
    Frequency.hertz 55 |> Frequency.perCycle (heartbeats 1)

@MartinSStewart
Copy link
Author

Frequency.for and Frequency.period seem like reasonable helper functions. Not sure about Frequency.perCycle as I had a harder time understanding what it does when looking at the name and type signature.

@ianmackenzie
Copy link
Owner

Yeah, Frequency.perCycle is definitely weirder and probably unnecessary. I've been trying to avoid getting distracted by other Elm projects as I'm finishing up elm-3d-scene, but this module seems pretty tractable now so maybe I'll take a bit of time this weekend and see if I can add it 🙂

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants