音乐推特挑战赛


37

这是Twitter图像编码挑战的音频版本。

设计一种音频压缩格式,该格式可以以140字节或更少的可打印UTF-8编码文本表示至少一分钟的音乐

通过编写一个命令行程序来实现它,该程序采用以下3个参数(在程序本身的名称之后):

  1. 字符串encodedecode
  2. 输入文件名。
  3. 输出文件名。

(如果首选的编程语言缺乏使用命令行参数的能力,则可以使用其他方法,但必须在答案中进行解释。)

encode操作将从您选择的音频格式转换为压缩的“ tweet”格式,并且该decode操作将从“ tweet”格式转换为原始音频格式。(当然,您应该实现有损压缩,因此输出文件不必与输入文件相同,只是格式相同。)

包括在您的答案中:

  • 程序的源代码完整。(如果此页面太长,则可以将其托管在其他位置并发布指向该页面的链接。)
  • 有关其工作原理的说明。
  • 至少一个示例,具有到原始音频文件的链接,将其压缩为“ tweet”文本,并通过解码tweet获得音频文件。(Answerer对版权“合理使用”主张负责。)

规则

  • 我保留随时关闭竞赛规则中任何漏洞的权利。
  • [4月24日编辑]对于encode函数的输入(和函数的输出decode),可以使用任何合理的通用音频格式,无论是:
    • 未压缩的波形,例如WAV。
    • 压缩波形,例如MP3。
    • “ Sheet music”风格,例如MIDI。
  • 压缩的“ tweet”格式必须在输入文件中实际编码声音。因此,以下类型的输出计算在内:
    • 提供实际输出存储位置的URI或文件路径。
    • 数据库表的键,实际输出存储为Blob。
    • 任何类似的东西。
  • 您的程序必须设计为压缩通用音乐文件,因此请勿执行明显与您的特定示例歌曲相关的事情。例如,如果要演示“眨眼,眨眼,小星星”,则您的压缩例程不应为do-do-so-so-la-la-so序列硬编码特定符号。
  • 您程序的输出实际上应该能够通过Twitter并毫发无损地输出。我没有支持的确切字符列表,但是请尽量使用字母,数字,符号和标点符号。并避免使用控制字符,组合字符,BIDI标记或类似的其他怪异内容。
  • 您可以提交多个条目。

评审标准

这是一次人气竞赛(即,大多数净投票获胜),但敦促选民考虑以下事项:

准确性

  • 歌曲被压缩后还能识别吗?
  • 听起来不错吗?
  • 您还能识别正在演奏的乐器吗?
  • 你还能认出歌词吗?(这可能是不可能的,但是如果有人做到这一点将给人留下深刻的印象。)

复杂

示例歌曲的选择在这里很重要。

  • [4月24日添加]对于MIDI或类似格式,此挑战最为容易。但是,如果您付出额外的努力使其与波形类型的格式一起使用,则值得您多加赞扬。
  • 有什么结构?当然,只需将相同的4个小节重复任意次数即可满足一分钟的要求。但是,更复杂的歌曲结构值得更多关注。
  • 格式可以同时处理很多音符吗?

代码

  • 使其尽可能简短。但是,这不是代码高尔夫,因此可读性比字符数更重要。
  • 只要有提高结果质量的理由,聪明,复杂的算法也可以。

9
使用MIDI与WAV是一个截然不同的挑战。我认为您应该将格式限制为仅WAV。
grovesNL 2014年

10
我渴望看到任何解决方案,但老实说:将60秒钟的声音打包为140字节意味着您每秒可用的比特数不到19位。有一些超高效的语音编码器,其工作频率为300 bps,但它们只能解码为合成音素,目的是产生可理解的语音,而不能以任何方式对音乐进行编码。
jarnbjo 2014年

2
您所需要的软件的压缩系数比当前的技术水平高出多个数量级。如果你想明智的答案(即不涉及状组合4'33"三月葬礼为一个聋子的丧俗),我会鼓励你的时间限制下降到1秒。
娇气ossifrage

3
@squeamishossifrage他没有说听起来一定很可识别。
cjfaure 2014年

5
在聊天中(以及第二天)有一个争论是关于您实际上是指140个字节还是tweet大小的140个字符
彼得·泰勒

Answers:


26

斯卡拉

当然,对MIDI文件进行编码会更容易,但是谁能找到一堆MIDI文件呢?不是1997年!

首先,我首先决定将“ Unicode字节”解释为“ Unicode字符”并使用CJK字符,因为:

  • 它符合图像挑战
  • Twitter很酷
  • 真的需要那些

我使用一些技巧来压缩源中每最后一滴熵:

首先,音乐是由音符制成的。而且,我们通常将同一八度音阶中的同一音符视为同一音符(这就是为什么12弦吉他听起来正确的原因),因此我们只有12种可能的编码方式。(例如,当我输出B时,实际上输出的和弦仅由所有八度中的B组成,有点像12弦吉他)。

接下来,我记得在高中音乐课上,大多数音符的过渡都很小(一个音符的上或下)。跳跃不太常见。这告诉我们,跳跃大小的熵可能比音符本身的熵小。

因此,我们的方法是将源分成多个块-我发现每秒14个块效果很好(旁注,我一直想知道为什么音频以44100 Hz编码。事实证明44100有很多因素,所以我本来可以选择每秒1、2、3、4、5、6、7、9、10、12、14、15、18、20、21、25、28或30个块,并且它可以干净地划分)。然后,我们对这些块进行FFT(嗯,从技术上讲不快,因为我使用的库对于非2幂幂块不是很快。从技术上讲,我使用的是Hartley变换,而不是傅里叶变换)。

然后,我们发现声音听起来最大(我使用A加权,具有高和低截止值,主要是因为它最容易实现),然后对该声音编码或对静音进行编码(沉默检测基于SNR-低SNR)是沉默)。

然后,我们将编码后的音符转换为跳转,并将其输入到自适应算术编码器中。转换为文本的过程类似于图像压缩问题(但涉及BigInteger的某些滥用)。

到目前为止,一切都很好,但是如果样本的熵太大怎么办?我们使用粗略的心理声学模型删除了一些模型。最低的熵跳跃是“不变”,因此我们通过查找FFT数据来查找那些听众可能不会注意到我们是否继续演奏前一个音符的块,方法是查找前一个音符所在的音块几乎与最大音符一样响亮(其中“几乎”由quality参数控制)。

因此,我们的目标是140个字符。我们从质量1.0(最高质量)开始编码,然后看看有多少个字符。如果太多,我们降到0.95并重复,直到达到140个字符(否则我们放弃质量0.05)。这使编码器成为n-pass编码器,其中n <= 20(尽管它在其他领域也非常低效,所以m'eh)。

编码器/解码器期望s16be单声道格式的音频。可以使用avconv实现此目的:

#decoding ogg to s16be, and keeping only the first 60s
avconv -i input.ogg -ac 1 -ar 44100 -f s16be -t 60s input.raw
#encoding s16be to mp3
avconv -f s16be -ac 1 -ar 44100 -i output.raw output.mp3

要运行编码器:

sbt "run-main com.stackexchange.codegolf.twelvestring.TwelveString encode input.raw encoded.txt"
sbt "run-main com.stackexchange.codegolf.twelvestring.TwelveString decode encoded.txt output.raw"

完整代码位于https://github.com/jamespic/twelvestring

需要注意的陷阱:您将需要nayuki的Arithmetic Coding库,该库当前没有可用的Maven工件。取而代之的是,您需要在本地构建和安装developerster的fork

这是一些示例。它们听起来很糟糕,但几乎可以识别:

  • 贝多芬的第五首:原始的编码的 -刲檩啰罓佖镱赈皌蝩蔲恁峕逊躹呯兲构摼蝺筶生物学庬一挂獴趤笲銗娵缆吃覤粠僭嫭裵狱鱨蠰儏咍箪浝姑椻赶挍呪白鸾盙宠埘謭擆闯脲誜忘椐笸囃庣稨俖咛脔湙弻籚组装鍖里桡镙訁鹻塿骱踠筟七趇杅峇敖窈裞瘫峦咰呹榇茏蛏姆臸胝娄辽憀麁黦挖憀麁黦耙毈斗俼湛营筬禴箓嬧窻丄
  • Fur Elise:原创编码 -讫忢擫镘拪纒铇鲮薯鉰孝昵埛痏絘僌莻暆雁屖鳮絒婘譮牡蛎托驺腀饚緂柤碠瞢硕胁歙棆败办冏邬酾萖苯劺誺軩忇穤锳娄伉巠桭晘酉筟緵俅怚尵鎽蝇崁饳丢弃但钤謽生成屮嘌呤俊俊鶮龚癸埙煂胭脂款骦灯菩凧咁楾媡伙俰欓焵韀册嗥燠鳢驷髉
  • 一闪一闪小星星:原始,已编码 -欠悺矜莳錥鸥谴责裴皽鳺憝浆箔皇殇鸧蜻蜓
  • 一个有趣的Chiptune:原始的,已编码的 -简诈諥尘牿灼攲陇亄鹘琾业纟鵼牤棌匩碆踬葫芦惩欃铳樯柇鋡指南衻澯伅墇搢囻荸香貱夹喉蟽藤屈锂蛉袒貊屦钛夜镤沄鍡唦魮骬乔蚖醶矕咻喸碋利褼袅匎崄窢帻六沧鼝瞮坡葍帷幔邵邵旻符琨鱴却栱烇仾椃騋荄哓统篏珆罽

更新资料

我在代码中调整了静音阈值,然后重新编码。编码已相应更新。此外,我还添加了另一首歌曲(从技术上讲不是开源的,但我怀疑原始的版权所有者会感到其IP受到威胁),只是为了好玩:

  • 帝制三月:原始编码 -岼葬湠衢妩焅喋藭霔憣嗗颟橳蕖匵腾嗅鈪芔区顕樭眀冂常僠寝萉乹俚戂阛灈蟑螂邢邢盒褈霈媬唂焰銯艉骞縩巻痭虊窻熲纡耺哅淙苉嘏庸锺禒旇蕲籷遪刨繱蕖嬯折祑仰軈牰杊瘷棏郖弘卄浕眮騜阖鏴鹡艂税寛柭烟采偋隆兎豅蚦纷襈洋折踜跅軩树爷奘庄玫亳攩猕匑仡葾昐炡瞱咏匑仡葾昐炡瞱价藭恐鹥璌榍胁樐嬨勀茌愋

更多更新

我稍微调整了编码器,这对质量产生了令人惊讶的影响(我忘记了在DHT中,异相信号实际上是负信号,因此我忽略了异相信号)。

该代码的早期版本仅采用了较大的这些异相信号,但现在采用RMS。另外,我在编码器中添加了一个相当保守的窗口函数(Tukey,alpha 0.3),以尝试消除伪影。

一切都相应更新。


1
我不能演奏“一闪一闪”和“ Chiptune”。爱丽丝(Fur Elise)距离酒店很近,而贝多芬(Bethoven)几乎无法辨认,哈哈。
justhalf 2014年

您是否想再次尝试Twinkle Twinkle和Chiptune?我认为我已经修复了网址。
James_pic 2014年

1
现在可以使用了。一闪一闪是相当下降的。但是最后发生了什么?
justhalf 2014年

是的,我不太确定最后会发生什么。我怀疑这是在算术编码中的某个地方发生的。在该代码的早期版本中,流以EOF符号终止,但在某些情况下,解码器无法读取EOF符号。我怀疑我没有正确关闭BitOutputStream,但我将对其进行调查。
James_pic 2014年

1
是的,实际上就是这样。有BitOutputStream::close一种我忘了打电话的方法。我将修复代码并更新输出。
James_pic

11

蟒蛇

对于UTF-8,我没有进行任何特殊处理,因此我的提交通过了140字节的要求。我对我的解决方案的有用性,准确性或效率不做任何声明。

我对输入和输出使用了44100 Hz的采样率。SAMPLES_PER_BYTE控制转换的质量。数值越低,声音的质量越好。我使用的值在结果部分中给出。

用法

编码

输入文件应为wav。它仅编码第一个通道。

twusic.py -e [input file] > output.b64

解码

twusic.py -d [input file] > output.raw

播放解码音乐

aplay -f U8 --rate=[rate of input file] output.raw

代码

#!/usr/bin/env python
SAMPLES_PER_BYTE = 25450

from math import sin, pi, log
from decimal import Decimal

PI_2 = Decimal(2) * Decimal(pi)

FIXED_NOTE = Decimal('220') # A
A = Decimal('2') ** (Decimal('1') / Decimal('12'))
A_LN = A.ln()

def freq(note):
    return FIXED_NOTE * (A ** Decimal(note))

def note(freq):
    return (Decimal(freq) / FIXED_NOTE).ln() / A_LN

VOLUME_MAX = Decimal('8')
def volume(level):
    return Decimal('127') * (Decimal(level+1).ln() / VOLUME_MAX.ln())

def antivolume(level):
    x = Decimal(level) / Decimal('127')
    y = VOLUME_MAX ** x
    return y - 1

NOTES = [freq(step) for step in xrange(-16, 16)]
VOLUMES = [volume(level) for level in xrange(0, VOLUME_MAX)]


def play(stream, data):
    t = 0
    for x in data:
        x = ord(x)
        w = PI_2 * NOTES[(x&0xf8) >> 3] / Decimal(16000)
        a = float(VOLUMES[x&0x07])
        for _ in xrange(0, SAMPLES_PER_BYTE):
            stream.write(chr(int(128+(a*sin(w*t)))))
            t += 1

NOTE_MAP = {'A': 0b00000000,
    'g': 0b00001000,
    'G': 0b00010000,
    'f': 0b00011000,
    'F': 0b00100000,
    'E': 0b00101000,
    'd': 0b00110000,
    'D': 0b00111000,
    'c': 0b01000000,
    'C': 0b01001000,
    'B': 0b01010000,
    'a': 0b01011000}

def convert(notes, volume):
    result = []
    for n in notes:
        if n == ' ':
            result += '\00'
        else:
            result += chr(NOTE_MAP[n] | (volume & 0x07)) * 2
    return ''.join(result)

TWINKLE = convert('C C G G A A GG' +
                    'F F E E D D CC' +
                    'G G F F E E DD' +
                    'G G F F E E DD' +
                    'C C G G A A GG' +
                    'F F E E D D CC', 0x7)

if __name__ == '__main__':
    from base64 import b64encode, b64decode
    import numpy as np
    from numpy.fft import fft, fftfreq
    import wave
    import sys

    if len(sys.argv) != 3:
        print 'must specify -e or -d plus a filename'
        sys.exit(1)

    if sys.argv[1] == '-e':
        w = wave.open(sys.argv[2], 'rb')

        try:
            output = []
            (n_channels, sampwidth, framerate, n_frames, comptype, compname) = w.getparams()
            dtype = '<i' + str(sampwidth)

            # Find max amplitude
            frames = np.abs(np.frombuffer(w.readframes(n_frames), dtype=dtype)[::n_channels])
            max_amp = np.percentile(frames, 85)

            w.rewind()

            read = 0
            while read < n_frames:
                to_read = min(n_frames-read, SAMPLES_PER_BYTE)
                raw_frames = w.readframes(to_read)
                read += to_read

                frames = np.frombuffer(raw_frames, dtype=dtype)[::n_channels]
                absolute = np.abs(frames)
                amp = np.mean(absolute)

                amp = int(round(antivolume(min((amp / max_amp) * 127, 127))))

                result = fft(frames)
                freqs = fftfreq(len(frames))

                while True:
                    idx = np.argmax(np.abs(result)**2)
                    freq = freqs[idx]
                    hz = abs(freq * framerate)
                    if hz > 0:
                        break
                    result = np.delete(result, idx)
                    if len(result) <= 0:
                        hz = 220
                        amp = 0
                        break

                n = int(round(note(hz)))
                n &= 0x1F
                n <<= 3
                n |= amp & 0x07
                output.append(chr(n))
        finally:
            w.close()
        print b64encode(''.join(output)).rstrip('=')
    else:
        with open(sys.argv[2], 'rb') as f:
            data = f.read()
        data = data + '=' * (4-len(data)%4)
        play(sys.stdout, b64decode(data))

输入

我的官方意见是Kevin MacLeod 撰写的《 Pianoforte和Beatbox即兴》。对于此文件,我使用了SAMPLES_PER_BYTE 25450。

我还自由地编码了Twinkle,Twinkle,Little Star,SAMPLES_PER_BYTE为10200。听起来好多了。

输出

即兴演奏钢琴和节拍器

aWnxQDg4mWqZWVl6W+LyOThfHOPyQThAe4x5XCqJK1EJ8Rh6jXt5XEMpk1Epe5JqTJJDSisrkkNCSqnSkkJDkiorCZHhCxsq8nlakfEp8vNb8iqLysp6MpJ7s4x7XlxdW4qKMinJKho

链接

一闪一闪亮晶晶

HBobGlJSUlJSY2FlYVNRUVFCQkJCQjs5PDksKisqGxoZGVFTUVNRREFDQjs6OjoqKykpKVRRVFJDQkJCOjs6OzksKikpGxobG1JSUlNRZWFlYVNSUVFCQkJDQTw5PDorKisqGhsZGRk

链接

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.