Skip to content

Latest commit

 

History

History
631 lines (473 loc) · 20.5 KB

Technical.md

File metadata and controls

631 lines (473 loc) · 20.5 KB

Technical

Table of Contents

1. Introduction

A. Project description

The aim of this project is to create a sound synthesizer that can be used to create programmable music. This project was inspired by Sonic Pi and other live coding music packages. The ultimate goal of this project is to be able to play music from code. By synthesizer it means a simple library that could be used by other programs. This isn't a program specially designed to be used for a traditional piano keyboard.

B. How it should be

Need to be a library with all function callable from another program.

2. Development rules

A. Technologies used

a. Type of needs

What ? Which ? Why ?
Code editor Any ( Visual Studio Code adviced) /
Sound Playing SMFL Library MacOS + Windows Compatibility
Waves display xPlot Used by Google Chart
Math calculation Math net Support Linear Algebra
Github Repository Code
Trello Trello Project progression
Slack Slack Project Communication

b. Language used

Although F# is great for specialist areas such as scientific or data analysis, it is also an excellent choice for enterprise development. Since we need to process lot of data with lot of list of Float ( Result of Sin(x) ), F# seems to be a good choice for that task. It's also a Dotnet language, so it is available on Mac and Windows. And have a lot of library available because of Dotnet.

B. Formating

Type Format
Namespace Camelcase
Module Camelcase
Function Pascalcase
Variable Pascalcase

C. Compatibility

SFML, Visual Studio, xPlot, Math.Net are compatible on Windows and MacOS.

3. Possible issues

A. Compatibility

At the moment, all library used are compatible on Windows, Linux and MacOS. We shouldn't have any issue on those plateforms, but most of them aren't compatible on Android and IOS.

B. Team Leaving

If we have someone leaving the team, someone else will be able to take his work. In order to do that, there will be at least 2 people on each parts. Also we ask everyone to push they're work everyday so we don't loose any work done.

C. Knowledge

Nobody in the team knows about FSharp, that's the main problem. Here is an exhaustive list of website to use:

4. Team

We are currently a team of 6.

  • Clémentine Curel Team Lead
  • Guillaume Rivière Tech Lead
  • Laura-Lee Hollande Contributor
  • Salaheddine Namir Contributor
  • Gaël Le Brun Contributor
  • Victor Leroy Contributor

5. Regroup

A. Namespaces

namespace synthesizer Since the project doesn't seems to be a large thing, it got decided to use only 1 namespace for the whole project.

B. Modules

a. NoteToHz

module NoteToHz = will handle the conversion from readable notes ( A, B, Etc) to actual Frequency.

b. WaveGen

module WaveGen = will handle the creation of the base of a sound, with Sine, Square, Triangle and Saw waves.

c. Filters

module Filters = will handle the filters like Echo, Flange, LFO, etc.

d. save

module Save = will have all functions for saving to .wav and .mp3.

e. PlaySynth

module PlaySynth = will have the sound player ( using SFML library ).

6. Functions

A. NoteToHz

a. convert

let convert note octave = 
        let noteHz =
            match note with
            | "C" -> 16.35
            | "C#" -> 17.32
            | "D" -> 18.35
            | "D#" -> 19.45
            | "E" -> 20.60
            | "F" -> 21.83
            | "F#" -> 23.12
            | "G" -> 24.5
            | "G#" -> 25.96
            | "A" -> 27.5
            | "A#" -> 29.14
            | "B" -> 30.87
            | _ -> 0.

        let result = noteHz * (2. ** octave)
        result

First, we do a list of all notes, and match the given notes with existing ones. Then we multiply them with the octave. The formula for the octave is x * (2^octave), for example, a C4 which, is note C on octave 4 is, 16.35 *2 *2 *2 *2 that can also be writen 16.35 * 2^4.

b. noteListToFloatList

let noteListToFloatList (inputNote:(NOTE * OCTAVE * AMPLITUDE * PLAYTIME)[]) (sampleRate:float) =
        let listNormalWave = [
            for i = 0 to inputNote.Length-1 do
                let tmp = 
                    WaveGen.calcSin sampleRate PLAYTIME (convert NOTE OCTAVE) AMPLITUDE
                yield tmp
        ]
        let normalWave = List.concat listNormalWave
        normalWave

If you want to play mutiple notes in a row, you need something to do that. So this function take a list of notes as input in addition to the samplerate, and output a list of float, with multiple notes.

B. WaveGen

a. calcSin

let calcSin sampleRate time freq amp=

In order to calculate a wave designed for the sound you need at least 4 values. sampleRate = the quality of the sound time = how long the wave have to be freq = the pitch of the note, or commonly called Frequency amp = amplitude, of the volume, how high the wave will go

Based on that, we can use this formula to calculate our Sine wave:

$ f(y) = amp * sin(2 * π * freq * x) $

b. calcSquare

let calcSquare sampleRate time freq amp=

In order to calculate a wave designed for the sound you need at least 4 values. sampleRate = the quality of the sound time = how long the wave have to be freq = the pitch of the note, or commonly called Frequency amp = amplitude, of the volume, how high the wave will go

Based on that, we can use this formula to calculate our Square wave:

$ f(y) = amp * sign( sin (2 * π * freq * x)) $

sign() is a function that set a value to 1 if it's over 0 and set the value to -1 if it's under 0. So we need to generate a sine and apply a sign to it in order to have a square.

c. calc Tri

let calcTri sampleRate time freq amp=

In order to calculate a wave designed for the sound you need at least 4 values. sampleRate = the quality of the sound time = how long the wave have to be freq = the pitch of the note, or commonly called Frequency amp = amplitude, of the volume, how high the wave will go

Based on that, we can use this formula to calculate our Square wave:

$ f(y) = amp * 2. * asin (sin (2. * π * freq * x)) / π ) $

asin(sin(x)) is used to generate triangle wave and then we apply other parameters in order to be able to change the shape of the wave. We also have to mutiply the amplitude by 2 because we no longer have 1 trigonometrical call, but 2 ( asin and sin).

d. calcSaw

let calcSaw sampleRate time freq amp=

In order to calculate a wave designed for the sound you need at least 4 values. sampleRate = the quality of the sound time = how long the wave have to be freq = the pitch of the note, or commonly called Frequency amp = amplitude, of the volume, how high the wave will go

Based on that, we can use this formula to calculate our Square wave:

$ f(y) = amp * 2. * (x * freq - floor(0.5 + x * freq)) $

This one is a bit tricky and use something that is reserved to digital sound. All points of the wave have to be between -1 and 1. When a point is over 1 it automatically go to -1, and so on.

This function use that feature, (See below) to have a clear wave that looks like a saw.

Raw SawWave looks

C. Filters

All filters are here to bring modifications to the base wave generated by functions above. Everything that change the shape of the wave is considered as a filter. A sound is always compsed by only one wave, so even adding 2 sounds together is considered as a filter.

a. amplitude

let amplitude initialList amp =
  let returnList = List.map (fun x -> x*amp) initialList
  returnList

We need a function to change the amplitude without having to re-create a wave. It's mainly used by other filters, I think of Echo that will rely on this amplitude modification. On so doing that way, values over 1 will make bigger numbers, while value under 1 will lower values.

b. overdriven

overdriven FIlter explanation

let overdriven list amp =
  let lenght = list.Length
  let returnList = [
    for i in 0..lenght-1 do 
      if list.[i]>= amp then amp
      elif list.[i]<= (-amp) then (-amp) 
      else list.[i]
  ]
  returnList

Pretty simple function that limit value that are higher than a specified amplitude.

c. flange

let flange wave =
  let lenght = wave.Length
  let subWave = [for i in 0..lenght-1 do wave.[i] * -1.]
  subWave

A flange effect is the same sound but reverted upside down, so multiplying all value by -1 invert all the wave.

d. echo

let echo initialList delay amp repeat =
  let tmpList = WaveGen.calcSin 44100. delay 0. 0. 
  let returnFullList = [
      yield initialList
      for i = 1 to repeat do
          let returnList = tmpList + (amplitude initialList ( amp / i)) 
          yield returnList
  ]
  let returnFullList = List.concat returnFullList
  returnFullList

Echo example audacity

So the way we build echo is like an addition, Sound + Silence + Reduced sound + Silence + Etc That is why a for loop based on the number of repeat each iteration is Silence + Sound

e. reverb

 // creat some list who begin in different place and amplitude and return in list of averge of list
    let rev(list:float list) =
        let lenght = list.Length       
        let rev = [for i in 0..lenght-1 do list.[i]]       
        let rev1 = [for i in 0..lenght-1 do if i >= (lenght-1)/6 then  list.[i]/2. else 0.]       
        let rev2 = [for i in 0..lenght-1 do if i >= (lenght-1)/5 then  list.[i]/3. else 0.]       
        let rev3 = [for i in 0..lenght-1 do if i >= (lenght-1)/4 then  list.[i]/4. else 0.]       
        let rev4 = [for i in 0..lenght-1 do if i >=(lenght-1)/3 then  list.[i]/5. else 0.]       
        let rev5 = [for i in 0..lenght-1 do if i >=(lenght-1)/2  then  list.[i]/6. else 0.]       
        let mergeRev = [for i in 0..lenght-1 do (rev.[i]+rev1.[i]+rev2.[i]+rev3.[i]+rev4.[i]+rev5.[i])/6.]
        mergeRev
         
        // reverse list
    let reverse list=
        let rec loop acc = function
        | [] -> acc
        | head :: tail -> loop(head::acc) tail
        loop [] list
        // return reverb list
    let reverb list =
       let init =  rev list
       let reverse = reverse init
       reverse

Apply a reverb effect on the wave.

f. chords

let chords wave1 wave2 =
  let sumList = List.map2 (fun x y -> (x + y)/2.) wave1 wave2
  sumList

Chord filter goal is to be able to add two waves together, so it is kinda easy on how it works.

D. Spectroscope

    let spectroscope (list:float list) =
       
       let lenght = list.Length
       let mutable trig = 0
       let mutable t = 0.
       let mutable check = 0.
       
       let periode = // find period of list
           for i in 0..lenght-1 do
               if i < lenght-1 && i > 0 then
                if trig >= 1 && trig <= 2 then
                    t <- t + 0.0001
                if trig = 2 then
                    trig <- trig + 1
                   
                  elif list.[i] > list.[i+1] && list.[i] >= list.[i-1] then
                    
                    trig <-trig + 1
                    check <- check + list.[i] - check

                    if check > list.[i] then // trig work only  highest value
                     trig <- trig - 1
           t  
       let getFrequency =
        1./ periode
       getFrequency

Spectroscope goal is to find the frequency of the wave so it can then be used in low pass and high pass filters.

a. Low pass

 let lowPass (list: float list, fcut: float, order: int) = // return list with Lowpass Filter
        let fs = spectroscope(list)
        let lowPass =  OnlineFilter.CreateLowpass(ImpulseResponse.Finite,fs,fcut,order)
        let array = list |> List.toArray
        let filtered = array |> lowPass.ProcessSamples
        let reList = filtered |> Array.toList
       // printfn"list %A" list
       // printf"lowPass %A" reList
        reList 

Low pass filter goal is to cut-off all the sounds with a frequecy higher than 5 kHz.

b. High pass

let highPass (list: float list, fcut: float, order: int) = // return list with Highpass Filter
        let fs = spectroscope(list)
        let highPass =  OnlineFilter.CreateHighpass(ImpulseResponse.Finite,fs,fcut,order)
        let array = list |> List.toArray
        let filtered = array |> highPass.ProcessSamples
        let reList = filtered |> Array.toList
       // printfn"list %A" list
       // printf"highPass %A" reList
        reList

High pass filter goal is to cut-off all the sounds with a frequency lower than 500 Hz.

c. Both pass

let BothPass (list: float list, fcut: float, order: int) = // return both filters at the same time
        let pass = LowPass (list, fcut, order)
        let bothPass = HighPass (pass, fcut, order)
        bothPass

Both Pass function calls our two filters previously presented, it's goal is to have only sounds with a frequency between 500 Hz and 5 kHz.

E. Low Frequency Oscilliator

The LFO uses the shape of the waveform assigned to it to create the movement. Changing the type of waveform changes the shape of the movement.

  LFO = A * sin(2 * PI * highFrenquency * x + Intensity * highFrenquency * sin(2 * PI * lowFrequency * x) )

A = amplitude highFrequency = should be a parameter for the higher frequency lowFrequency = should be a parameter for the lower frequency

This is a mathematical formule to modulate the frequency and the amplitude of a wave.

F. save

The save module is made to regroup all saving related function.

a. sample

let sample x = (x + 1.)/2. * 255. |> byte

This function is quite simple, but achieve a lot of work. Since now we were working with list of float between -1 and 1, now we need to work with bytes. Bytes are value from 0 to 255, and this simple function do the calculation to convert a float between -1 and 1 to a Byte.

b. floatToByte

let floatToByte wave =
  let data = wave
              |> List.map sample 
              |> Microsoft.FSharp.Collections.List.toArray
  data

As we can't write a list on a file, we have to convert it to an Array. Before converting to an array, it call the function above to convert all data to bytes, then convert the list to an Array. And return the array generated.

c. write

let write stream data =

  let data = floatToByte data

  use writer = new BinaryWriter(stream)
  // RIFF
  writer.Write("RIFF"B)
  let size = 36 + data.Length in writer.Write(size)
  writer.Write("WAVE"B)
  // fmt
  writer.Write("fmt "B)
  let headerSize = 16 in writer.Write(headerSize)
  let pcmFormat = 1s in writer.Write(pcmFormat)
  let mono = 1s in writer.Write(mono)
  let sampleRate = 44100 in writer.Write(sampleRate)
  let byteRate = sampleRate in writer.Write(byteRate)
  let blockAlign = 1s in writer.Write(blockAlign)
  let bitsPerSample = 8s in writer.Write(bitsPerSample)

  writer.Write("data"B)
  writer.Write(data.Length)
  writer.Write(data)

Let's process parts by parts:

First, the Parameters, Stream and data. Stream is the name of the file we want. data is the list of numbers generated by other functions.

Second, The conversion: let data = floatToByte data We just call the function detailed a few lines above

Third, the Header

  use writer = new BinaryWriter(stream)
  // RIFF
  writer.Write("RIFF"B)
  let size = 36 + data.Length in writer.Write(size)
  writer.Write("WAVE"B)
  // fmt
  writer.Write("fmt "B)
  let headerSize = 16 in writer.Write(headerSize)
  let pcmFormat = 1s in writer.Write(pcmFormat)
  let mono = 1s in writer.Write(mono)
  let sampleRate = 44100 in writer.Write(sampleRate)
  let byteRate = sampleRate in writer.Write(byteRate)
  let blockAlign = 1s in writer.Write(blockAlign)
  let bitsPerSample = 8s in writer.Write(bitsPerSample)

In a .wav file there is a header that regroup all needed informations for being played, or imported after that. We create an object writer that will allow us to access the file. Then we write some default value, and then our value. By our data, I mean our Header Data, stuff like, Mono/Stereo, Samplerate, bitsPerSample, etc.

And in fourth, we write our actual sound:

  writer.Write("data"B)
  writer.Write(data.Length)
  writer.Write(data)

We first write a default value, then the length, and finally the sound itself.

G. PlaySynth

The second way to play a wave is to save it, play it and then delete it. This way is here to not use too much local storage. To play this way, it's like the first way but you define save as false.

As we are going to use SFML library in order to play sound, this part is short, and easy.

a. playSound

let playSound (name:string,save:bool,time:float32) = 
  let buffer = new SoundBuffer(name) 
  let sound = new Sound(buffer) 
  let times = Time.FromSeconds(time) 
  sound.set_PlayingOffset(times)
  sound.Play()

  while sound.Status = SoundStatus.Playing do
      Thread.Sleep(100)

  if (save=false) then System.IO.File.Delete(name)

That is code made out of documentation we found about SFML. We import the buffer and the sound, then the Time to start the sound. Set when exactly the sound have to start. And Play it.

The while loop is essentially, otherwise, the programm would close before finishing to play, So while the sound isn't finished, it idle for 100 ms.

And the last part is a trick to play without saving the file. Well not a long term save, basically the simplest way to play a sound without saving, is to actually save it, play it, then delete it. That what we are going to do.