Upgrade to Pro — share decks privately, control downloads, hide ads and more …

How music works, using Ruby

How music works, using Ruby

That strange phenomenon where air molecules bounce against each other in a way that somehow comforts you, makes you cry, or makes you dance all night: music. Since the advent of recorded audio, a musician doesn't even need to be present anymore for this to happen (which makes putting "I will always love you" on repeat a little less awkward).

Sound engineers have found many ways of making music sound good when played from a record. Some of their methods have become industry staples used on every recording released today.

Let's look at what they do and reproduce some of their methods in Ruby!

See https://github.com/thijsc/how_music_works for all code examples.

Thijs Cadier

May 18, 2022
Tweet

More Decks by Thijs Cadier

Other Decks in Programming

Transcript

  1. How Music Works Thijs Cadier / / How music works

    RailsConf 2022 RailsConf 2022
  2. •Get interested, don’t understand it •Write Code •Understand it a

    whole lot better •Show the outcome of this to you all! My process 00 Thijs Cadier / / How music works RailsConf 2022
  3. Thijs Cadier / / How music works RailsConf 2022 What

    we are covering today: 01 What is music made of? 02 A very brief history of recorded music 03 Digital audio 04 Ampli fi cation 05 Making sounds 06 Mixing 07 Compression 01 What is music made of? 02 A very brief history of recorded music 03 Digital audio 04 Ampli fi cation 05 Making sounds 06 Mixing 07 Compression Thijs Cadier / / How music works
  4. A waveform that we perceive as having pitch, timbre and

    a tempo. What is music made of? 01 Thijs Cadier / / How music works RailsConf 2022
  5. From a sound source to your ear Thijs Cadier /

    / How music works RailsConf 2022
  6. How does the brain map a waveform? Thijs Cadier /

    / How music works RailsConf 2022
  7. Back in the day music was always live. Thijs Cadier

    / / How music works RailsConf 2022
  8. 02 A very brief history of recorded music Thijs Cadier

    / / How music works RailsConf 2022
  9. Digital audio A lot of numbers in a sequence Thijs

    Cadier / / How music works RailsConf 2022
  10. 3 def read_wave(path) 4 # Open the input wave 5

    reader = WaveFile::Reader.new(path) 6 # Prepare a buffer for all samples 7 samples = [] 8 # Loop through all buffers in the file 9 loop do 10 begin 11 # Read part of the file 12 buffer = reader.read(8198) 13 # Add it to the samples buffer 14 samples.concat(buffer.samples) 15 rescue EOFError 16 # We're at the end, return the samples buffer 17 return samples 18 end 19 end 20 end
  11. 22 def write_wave(path, samples) 23 WaveFile::Writer.new(path, wave_format) do |writer| 24

    writer.write(WaveFile::Buffer.new(samples, wave_format)) 25 end 26 end
  12. 14 def write_points_image(path, samples) 15 image = ChunkyPNG::Image.new(WIDTH, HEIGHT, BLACK)

    16 17 samples.each_with_index do |sample, i| 20 position = if sample > 0 21 HALFWAY - offset_from_halfway(sample) 22 else 23 HALFWAY + offset_from_halfway(sample) 24 end 25 26 image.rect(i, position, i + 8, position + 8, YELLOW, YELLOW) 27 end 28 29 separator_line(image) 30 31 image.save(path) 32 end
  13. 41 slices = samples.each_slice(samples_per_pixel).map do |samples| 42 positive_average = samples.select

    do |sample| 43 sample.positive? 44 end.sum / samples_per_pixel 45 46 negative_average = samples.select do |sample| 47 sample.negative? 48 end.map do |sample| 49 sample.abs 50 end.sum / samples_per_pixel 51 52 # Return both 53 [positive_average, negative_average] 54 end
  14. 1 drum = read_wave("input/drum.wav") 2 3 def amplify(track, ratio) 4

    track.map do |sample| 5 sample * ratio 6 end 7 end 8 9 louder = amplify(drum, 2.0) 10 write_wave("output/louder.wav", louder)
  15. 1 drum = read_wave("input/drum.wav") 2 3 def amplify(track, ratio) 4

    track.map do |sample| 5 sample * ratio 6 end 7 end 8 9 clipping = amplify(drum, 4.0) 10 write_wave("output/clipping.wav", clipping)
  16. 3 # Create output array 4 output = [] 5

    6 0..SAMPLE_RATE.times do 7 output << Random.rand(-30_000..30_000) 8 end 9 10 write_wave("output/noise.wav", output)
  17. 1 class Square 2 include Enumerable 3 4 def next_sample

    5 sample = if @position < @length / 2 6 20_000 7 else 8 -20_000 9 end 10 @position += 1 11 if @position > @length 12 @position = 0 13 end 14 sample 15 end 16 17 def each 18 loop { yield next_sample } 19 end 20 end
  18. 3 # Create a new oscillator 4 oscillator = Square.new(440,

    SAMPLE_RATE) 5 6 # Create output array 7 output = [] 8 9 oscillator.each_with_index do |sample, i| 10 break if i > SAMPLE_RATE 11 output << sample 12 end 13 14 write_wave("output/square.wav", output)
  19. 1 def next_sample 2 sample = Math.sin(@position * Math::PI /

    2) * 25_000 3 4 # Increment position 5 @position += @increment_by 6 7 # Reset position if over max 8 if @position >= frequency 9 @position = 0.0 10 end 11 12 # Return the sample 13 sample 14 end
  20. 3 SAMPLE_RATE = 44_100 4 5 # Create multiple oscillators

    6 a_note = Sine.new(440, SAMPLE_RATE) 7 c_sharp_note = Sine.new(554.37, SAMPLE_RATE) 8 e_note = Sine.new(659.25, SAMPLE_RATE)
  21. 10 # Create output array 11 output = [] 12

    13 0..SAMPLE_RATE.times do |i| 14 # Get the samples for all three notes 15 sample_one = a_note.next_sample / 3 16 sample_two = c_sharp_note.next_sample / 3 17 sample_three = e_note.next_sample / 3 18 19 output << sample_one + sample_two + sample_three 20 end 21 22 write_wave("output/chord.wav", output)
  22. 4 one = Sine.new(220, SAMPLE_RATE) 5 two = Square.new(220, SAMPLE_RATE)

    6 7 # Create output array 8 output = [] 9 10 0..SAMPLE_RATE.times do |i| 11 # Get the samples for all three notes 12 sample_one = one.next_sample / 3 13 sample_two = two.next_sample / 3 14 15 output << sample_one + sample_two 16 end
  23. 1 require_relative "../helpers" 2 3 bass = read_wave("input/bass.wav") 4 drum

    = read_wave("input/drum.wav") 5 piano = read_wave("input/piano.wav")
  24. 9 def sum_tracks(*tracks) 10 [].tap do |out| 11 for i

    in 0..tracks.first.length do 12 # Start with zero 13 summed = 0 14 15 # Loop through all the tracks and increment 16 # the summed sample with its value 17 tracks.each do |track| 18 next unless track[i] 19 # Increment the summed sample, but first 20 # make it a bit less loud so we don't clip 21 summed += track[i] / 1.5 22 end 23 24 # Append the summed sample to the output 25 out << summed 26 end 27 end 28 end
  25. Thijs Cadier / / How music works RailsConf 2022 0

    1 2 3 4 5 6 7 8 9 10 4 8 12 16
  26. Apply compression with a ratio Thijs Cadier / / How

    music works RailsConf 2022 0 1 2 3 4 5 6 7 8 9 10 4 8 12 16
  27. Apply compression with a ratio Thijs Cadier / / How

    music works RailsConf 2022 0 1 2 3 4 5 6 7 8 9 10 4 8 12 16
  28. Apply make up gain Thijs Cadier / / How music

    works RailsConf 2022 0 1 2 3 4 5 6 7 8 9 10 4 8 12 16
  29. 5 def reduce_peaks(track, treshold, ratio) 6 [].tap do |out| 7

    track.each do |sample| 8 if sample > treshold or sample < -treshold 9 out << sample / ratio 10 else 11 out << sample 12 end 13 end 14 end 15 end
  30. 23 peaks_reduced = reduce_peaks(input, 1000, 8) 24 makeup_gain_applied = apply_makeup_gain(peaks_reduced,

    12.0) 25 26 # Write it to disk 27 write_wave("output/final.wav", makeup_gain_applied)