开发者

Normalizing Frequencies of Chords, Parameter Passing

开发者 https://www.devze.com 2023-02-27 02:47 出处:网络
So, in the following code, I am generating a wav file from notes and composed chords. I\'ve got it working for single notes and chords of two notes, but for combinations of more than 2 notes, I run in

So, in the following code, I am generating a wav file from notes and composed chords. I've got it working for single notes and chords of two notes, but for combinations of more than 2 notes, I run into problems because I am not normalizing the frequencies. I know what I need to do (divide the frequencies at each frame by the number of notes composing it) but not necessarily how to do it in an elegant manner (or, in any manner at all). What has to happen is, I need to somehow get the length of the list returned by notes'' up to buildChord, and then work out how to map a division by that number across the input to buildChord.

I'm really at a loss, here, so any input would be greatly appreciated.

import Data.WAVE

import Control.Applicative
import Data.Char (isDigit)
import Data.Function (on)
import Data.Int (Int32)
import Data.List (transpose, groupBy)
import D开发者_如何学运维ata.List.Split (splitOn, split, oneOf)
import System.IO (hGetContents, Handle, openFile, IOMode(..))

a4 :: Double
a4 = 440.0

frameRate :: Int 
frameRate = 32000

noteLength :: Double
noteLength = 1

volume :: Int32
volume = maxBound `div` 2

buildChord :: [[Double]] -> WAVESamples
buildChord freqs = map ((:[]) . round . sum) $ transpose freqs

generateSoundWave :: Int          -- | Samples Per Second
                  -> Double       -- | Length of Sound in Seconds
                  -> Int32        -- | Volume
                  -> Double       -- | Frequency
                  -> [Double]
generateSoundWave sPS len vol freq =
    take (round $ len * fromIntegral sPS) $
    map ((* fromIntegral vol) . sin) 
    [0.0, (freq * 2 * pi / fromIntegral sPS)..]

generateSoundWaves :: Int          -- | Samples Per Second
                   -> Double       -- | Length of Sound in Seconds
                   -> Int32        -- | Volume
                   -> [Double]     -- | Frequency
                   -> [[Double]]
generateSoundWaves sPS len vol =
    map (generateSoundWave sPS len vol)

noteToSine :: String -> WAVESamples
noteToSine chord =
    buildChord $ generateSoundWaves frameRate noteLength volume freqs
    where freqs = getFreqs $ notes chord

notes'' :: String -> [String]
notes'' = splitOn "/" 

notes' :: [String] -> [[String]]
notes' = map (split (oneOf "1234567890"))

notes :: String -> [(String, Int)]
notes chord = concatMap pair $ notes' $ notes'' chord
    where pair (x:y:ys) = (x, read y :: Int) : pair ys
          pair _           = []

notesToSines :: String -> WAVESamples
notesToSines = concatMap noteToSine . splitOn " "

getFreq :: (String, Int) -> Double
getFreq (note, octave) =
    if octave >= -1 && octave < 10 && n /= 12.0
    then a4 * 2 ** ((o - 4.0) + ((n - 9.0) / 12.0))
    else undefined
    where o = fromIntegral octave :: Double
          n = case note of
                "B#" -> 0.0
                "C"  -> 0.0
                "C#" -> 1.0
                "Db" -> 1.0
                "D"  -> 2.0
                "D#" -> 3.0
                "Eb" -> 3.0
                "E"  -> 4.0
                "Fb" -> 4.0
                "E#" -> 5.0
                "F"  -> 5.0
                "F#" -> 6.0
                "Gb" -> 6.0
                "G"  -> 7.0
                "G#" -> 8.0
                "Ab" -> 8.0
                "A"  -> 9.0
                "A#" -> 10.0
                "Bb" -> 10.0
                "B"  -> 11.0
                "Cb" -> 11.0
                _    -> 12.0

getFreqs :: [(String, Int)] -> [Double]
getFreqs = map getFreq

header :: WAVEHeader
header = WAVEHeader 1 frameRate 32 Nothing 

getFileName :: IO FilePath
getFileName = putStr "Enter the name of the file: " >> getLine

getChordsAndOctaves :: IO String
getChordsAndOctaves = getFileName >>= \n ->
                      openFile n ReadMode >>=
                      hGetContents 

main :: IO ()
main = getChordsAndOctaves >>= \co ->
       putWAVEFile "out.wav" (WAVE header $ notesToSines co)


The key problem was with the function:

buildChord :: [[Double]] -> WAVESamples
buildChord freqs = map ((:[]) . round . sum) $ transpose freqs

The result of transpose freqs was a list of sound volumes for a particular point in time for each note being played (eg [45.2, 20, -10]). The function (:[] . round . sum) firstly added them together (eg 55.2), rounds it (eg to 55), and wraps it in a list (eg [55]). map (:[] . round . sum) just did that for all the instances of time.

The problem is if you have many note playing at once, the sum results in a note that is too loud. What would be better is to take the average of the notes, rather than the sum. That means 10 notes playing at the same time wont be too loud. Surprisingly, there is no average function in the prelude. So we can either write our own average function, or just embed it in the function passed to map. I did the latter as it was less code:

buildChord :: [[Double]] -> WAVESamples
buildChord freqs = map (\chord -> [round $ sum chord / genericLength chord]) $ transpose freqs

I'm guessing from your questions that you are writing a music making program as a way to learn haskell. I have a few ideas that may make your code easier to debug, and more "haskell like".

Code in haskell is often written as a sequence of transformations from input to output. That buildChord function is a good example - firstly the input was transposed, then mapped over with a function that combined the multiple sound amplitudes. However, you could also structure your whole program in this style.

The purpose of the program seems to be: "read notes from a file in some format, then create a wav file from those notes read". The way I would solve that problem would be firstly to break that up into different pure transformations (ie using no input or output), and do the reading and writing as the final step.

I would firstly start by writing a sound wave to WAVE transformation. I would use the type:

data Sound = Sound { soundFreqs :: [Double]
                   , soundVolume :: Double
                   , soundLength :: Double
                   }

Then write the function:

soundsToWAVE :: Int -> [Sound] -> WAVE
soundsToWAVE samplesPerSec sounds = undefined -- TODO

Then I could write the functions writeSoundsToWavFile and testPlaySounds:

writeSoundsToWavFile :: String -> Int -> [Sound] -> IO ()
writeSoundsToWavFile fileN samplesPerSec sounds = putWAVEFile $ soundsToWAVE fileN samplesPerSec sounds

testPlaySounds :: [Sound] -> IO ()
testPlaySounds sounds = do
  writeSoundsToWavFile "test.wav" 32000 sounds
  system("afplay test.wav") -- use aplay on linux, don't know for windows
  return ()

Once that is done, all the WAVE code is done - the rest of the code doesn't need to touch it. It may be a good idea to put that in its own module.

After that, I would write a transformation between music notes and Sounds. I would use the following types for notes:

data Note = A | B | C | D | E | F | G
data NoteAugment = None | Sharp | Flat

data MusicNote = MusicNote { note :: Note, noteAugment :: NoteAugment, noteOctave :: Int }

data Chord = Chord { notes :: [MusicNote], chordVolume :: Double }

Then write the function:

chordToSound :: Chord -> Sound
chordToSound = undefined -- TODO

You could then easily write the function musicNotesToWAVFile:

chordsToWAVFile fileName samplesPerSec notes = writeSoundsToWavFile 32000 fileName samplesPerSec (map chordToSound notes)

(the function testPlayChords can be done in the same way). You could also put this in a new module.

Finally I would write the transformation note string -> [Chord]. This would just need the function:

parseNoteFileText :: String -> [Chord]
parseNoteFileText noteText = undefined

The final program could then be wired up:

main = do
  putStrLn "Enter the name of the file: " 
  fileN <- getLine
  noteText <- readFile fileN
  chordsToWAVFile (parseNoteFileText noteText)
0

精彩评论

暂无评论...
验证码 换一张
取 消