Tuesday, May 31, 2011

Metronome for Linux in Python

I changed my metronome.c in python to work with Windows. ( Initially, I made a mistake which errored in Windows.) I didn't distinguish binary/text mode in Windows, which doesn't matter in Linux. After adding sys.platform condition, it works, but looks a little clumsy.

To use this,
$ python metronome.py [sample_file] [bpm] [duration in sec]


For example, this command will generate "a.wav" which has 120 BPM beat for 15 sec. s3.wav is a simple beep wav in 44100 wav encode.
$ python metronome.py s3.wav 120 15


Here is the code:
#!/usr/bin/env python

import os
import sys
import struct

empty_sound = struct.pack('bbbbbbbb', *( 4,0,0,0,6,0,6,0 ))

class WavHeader(object):
def __init__(self, rawdata):
self.tup = struct.unpack("iiiiihhiihhii", rawdata)
self.chunk_id = self.tup[0]
self.chunk_sz = self.tup[1]
self.format = self.tup[2]
self.sub_chunk1_id = self.tup[3]
self.sub_chunk1_sz = self.tup[4]
self.audio_format = self.tup[5]
self.num_channel = self.tup[6]
self.sample_rate = self.tup[7]
self.byte_rate = self.tup[8]
self.block_align = self.tup[9]
self.bits_per_sample = self.tup[10]
self.sub_chunk2_id = self.tup[11]
self.sub_chunk2_sz = self.tup[12]

def pack(self):
return struct.pack("iiiiihhiihhii", self.chunk_id, self.chunk_sz,
self.format , self.sub_chunk1_id,
self.sub_chunk1_sz , self.audio_format,
self.num_channel , self.sample_rate ,
self.byte_rate , self.block_align ,
self.bits_per_sample, self.sub_chunk2_id,
self.sub_chunk2_sz )

ONE_SEC = 88200
def bytes_for_beat(bpm):
ratio = bpm/60.0
bytes_per_beep = ONE_SEC / ratio
return int(bytes_per_beep)


def main():
sample_fname = sys.argv[1]
tempo = int(sys.argv[2])
if tempo < 40:
print >> sys.stderr, "Invalid tempo: Make it between 40 - MAX"
print >> sys.stderr, " * longer the sample length, smaller the MAX"
sys.exit(1)
dura = int(sys.argv[3])
if dura <= 0:
print >> sys.stderr, "Invalid duration: Make it greater than 0"
sys.exit(2)

# Reading sample header
if sys.platform == 'win32':
rd = open(sample_fname, "rb")
else:
rd = open(sample_fname, "r")
sample_hdr = WavHeader(rd.read(44))
sample_data = rd.read()
rd.close()

# Generating Beat data as WAV, storing temp file 't.wav'
if sys.platform == 'win32':
bdata = open("t.wav", "wb")
else:
bdata = open("t.wav", "w")
tot_beats = dura * (tempo/60.0);
bps = bytes_for_beat(tempo);
tot_bytes = 0;
for i in range( int(tot_beats)):
bdata.write( sample_data )
tot_bytes += sample_hdr.sub_chunk2_sz;
for j in range( (bps - sample_hdr.sub_chunk2_sz)/8):
bdata.write( empty_sound )
tot_bytes += len(empty_sound)
bdata.close()

# Overwrite new size, and Generate output wav file 'a.wav'
sample_hdr.sub_chunk2_sz = tot_bytes
if sys.platform == 'win32':
outf = open("a.wav", 'wb')
else:
outf = open("a.wav", 'w')
outf.write( sample_hdr.pack() )
if sys.platform == 'win32':
outf.write( open('t.wav', 'rb').read() )
else:
outf.write( open('t.wav').read() )
outf.close()


if __name__ == '__main__':
main()

Sunday, May 29, 2011

Metronome for Linux

A solution for "Metoronome" is a very simple algorithm

 def metronome(bpm, how_long):  
while how_long > 0:
beep()
time.sleep( 60.0 / bpm )
how_long -= 60.0/bpm


Unfortunately, this wouldn't work on regular OS like Linux or Windows on PC, since they are not real time OS. To make the matter worse, sleep() operation will definitely context switch out the process, and coming back will not be in consistent interval.

I needed a metronome. I knew that there are softwares doing "Metronome", mostly commercial.

So, dig through, and found one open source solution (open metronome), written in MFC. Since my computer for music is Linux, I just read and learned its brilliant idea. It generates WAV file for beeping. I wrote a program, doing so.

 #include <stdio.h>  
#include <stdlib.h>
#include <stdint.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
char empty_sound[8] = { 4, 0, 0, 0, 6, 0, 6, 0 };
struct wav_header {
// "RIFF" chunk descriptor
int32_t chunk_id;
int32_t chunk_sz;
int32_t format;
// "fmt (header)" sub-chunk
int32_t sub_chunk1_id;
int32_t sub_chunk1_sz;
int16_t audio_format;
int16_t num_channel;
int32_t sample_rate;
int32_t byte_rate;
int16_t block_align;
int16_t bits_per_sample;
// "data (easily, music)" sub-chunk
int32_t sub_chunk2_id;
int32_t sub_chunk2_sz;
char *data;
};
#define ONE_SEC 88200 // bytes
static void print_header(struct wav_header *hdr)
{
printf("(0) chunk id : %x\n", hdr->chunk_id);
printf("(4) chunk size : %d\n", hdr->chunk_sz);
printf("(8) format : %x\n\n", hdr->format);
printf("(12) sub chunk1 id : %d\n", hdr->sub_chunk1_id);
printf("(16) sub chunk1 size : %d\n", hdr->sub_chunk1_sz);
printf("(20) audio format : %d\n", hdr->audio_format);
printf("(22) num channels : %d\n", hdr->num_channel);
printf("(24) sample rate : %d\n", hdr->sample_rate);
printf("(28) byte rate : %d\n", hdr->byte_rate);
printf("(32) block align : %d\n", hdr->block_align);
printf("(34) bits per sample : %d\n\n", hdr->bits_per_sample);
printf("(36) sub chunk2 id : %d\n", hdr->sub_chunk2_id);
printf("(40) sub chunk2 size : %d\n", hdr->sub_chunk2_sz);
printf("(44 -- ) DATA\n\n");
}
static int bytes_for_sec(int bpm)
{
float ratio = bpm/60.0;
float bytes_per_beep = ONE_SEC / ratio;
return (int)bytes_per_beep;
}
int main(int argc, char **argv)
{
char *sample_fname;
int tempo, dura, bps;
int rfd; // Sample file descriptor
int wfd; // Output file descriptor
int rc, i, j, tot_beats;
char buf[512 + 1]; // Sample hdr buffer
char *sample_buf, *ptr;
struct wav_header *hdr;
struct wav_header new_hdr;
if (argc < 3) {
printf("USAGE: %s <sample_wav> <tempo in BPM> <duration in sec>\n", argv[0] );
printf(" * sample_wav must be aligned by sample. Random clip of data may make a noise.\n");
printf(" example: $ %s s3.wav 120 60\n", argv[0]);
printf(" This will make an output of 'a.wav', using s3.wav as sample, 120 bps for 60sec.\n");
exit(0);
}
sample_fname = argv[1];
tempo = atoi(argv[2]);
if (tempo < 40) {
printf("Invalid tempo: Make it between 40 - MAX, which is the fastest from sample\n");
exit(1);
}
dura = atoi(argv[3]);
if (dura == 0) {
printf("Invalid duration: Make it greater than 0.\n");
exit(2);
}
rfd = open(sample_fname, O_RDONLY);
rc = read(rfd, buf, 44);
if (rc < 0) {
perror("Sample read error\n");
exit(3);
}
hdr = (struct wav_header *)buf;
if ( NULL == (sample_buf = malloc( hdr->sub_chunk2_sz )) ) {
perror("Mem alloc failed (1)\n");
exit(4);
}
// copying sample data
ptr = sample_buf;
while ( 0 < (rc = read(rfd, ptr, 512)) ) {
ptr += rc;
}
wfd = open("t.wav" , O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
if (wfd < 0) {
perror("Error opening write file");
}
tot_beats = dura * (tempo/60.0);
bps = bytes_for_sec(tempo);
int tot_bytes = 0;
for (i =0; i<tot_beats; ++i) {
write(wfd, sample_buf, hdr->sub_chunk2_sz);
tot_bytes += hdr->sub_chunk2_sz;
for (j = 0; j< (bps - hdr->sub_chunk2_sz)/8; ++j) {
write(wfd, empty_sound, 8);
tot_bytes += 8;
}
}
close(wfd);
memcpy(&new_hdr, hdr, 44);
new_hdr.sub_chunk2_sz=tot_bytes;
wfd = open("h.wav", O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
write(wfd, &new_hdr, 44);
close(wfd);
system("/bin/cat h.wav t.wav > a.wav");
return 0;
}


Finally, I generated 30 sec of each speed (40, 44, 48, .... 208), and converted them into mp3. For example, 160 bpm wav (160.wav) is converted to 160.mp3 in 128 bit encoding.

 $ lame -h -b 128  160.wav  160.mp3



Now, if I need 5 min of 160 bpm, I just feed this 10 times in mpg123.

My favorite application of this was "speed trainer." For example, made 30 secs of 160, 161, 162, 163, 164 bpm and played it.

$ mpg123 16[0-4].mp3

Or making a 15 minute mp3 beat file isn't bad. Roughly 1 meg for 1 min, so 15 min is less than 15 meg byte. If it is encoded 64bit, the size will be even smaller.