2011-04-19 47 views
3

那麼,在下面的代碼,我從產生音符和和絃組成一個wav文件。我已經得到了它的單音符和兩個音符的和絃的工作,但對於超過2個音符的組合,我遇到的問題,因爲我不是歸一化頻率。我知道什麼我需要做的(在由音符組成它的數量每幀劃分頻率),但不一定如何做到這一點在一個優雅的方式(或以任何方式在所有)。什麼有發生的是,我需要以某種方式獲得通過notes''高達buildChord返回列表的長度,然後制定出如何將除以這個數字在整個輸入映射到buildChord正火頻率,參數傳遞

我真的不知所措,在這裏,所以任何輸入將不勝感激。

import Data.WAVE 

import Control.Applicative 
import Data.Char (isDigit) 
import Data.Function (on) 
import Data.Int (Int32) 
import Data.List (transpose, groupBy) 
import Data.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) 

回答

1

的關鍵問題是與功能:

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

transpose freqs結果是音量的對特定時間點的每一個音符正在播放列表(例如[45.2, 20, -10])。功能(:[] . round . sum)首先將它們加在一起(例如55.2),將其四捨五入(例如55),並將其包裝在列表中(例如[55])。 map (:[] . round . sum)只是做了所有的時間。

的問題是,如果你有很多注意播放一次,總和結果在一張紙條,上面是太響了。更好的是取得票據的平均值,而不是總和。這意味着10個音符同時播放不會太響。令人驚訝的是,前奏中沒有平均功能。所以我們可以寫我們自己的平均函數,或者把它嵌入到傳遞給map的函數中。我做了後者,因爲它是更少的代碼:

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

我從你的問題,你正在編寫一個音樂製作節目的方式來學習Haskell的猜測。我有一些想法可能會讓你的代碼更容易調試,而且更多的「haskell like」。在Haskell

代碼通常寫爲變換的從輸入到輸出的序列。也就是說buildChord函數是一個很好的例子 - 首先將輸入被轉置,然後被映射在與組合的多個聲音的振幅的函數。不過,你也可以用這種風格來構建你的整個程序。

該程序的目的似乎是:「以某種格式從文件中讀取筆記,然後從讀取的筆記中創建一個wav文件」。我想解決這個問題的辦法是首先要打破成不同的純轉換(即不使用的輸入或輸出),並做了閱讀和寫作的最後一步。

我會先寫一個聲波波浪變形開始。我將使用類型:

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

然後寫功能:

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

然後,我可以寫writeSoundsToWavFiletestPlaySounds功能:

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() 

一旦做到這一點,所有的WAVE代碼完成 - 其餘代碼不需要觸摸它。將它放在自己的模塊中可能是一個好主意。

之後,我會寫音樂筆記和聲音之間的轉換。我會用以下幾種類型的註釋:

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 } 

然後寫功能:

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

然後,您可以輕鬆地編寫功能musicNotesToWAVFile:

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

(功能testPlayChords可以以同樣的方式完成)。你也可以把它放在一個新的模塊中。

最後我會寫轉換音符字符串 - > [和絃]。這將只需要功能:

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

最終的方案將會進行佈線:

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